Prettify issue_show and jobs modules

This commit is contained in:
Mike Greiling 2018-10-10 01:23:54 -05:00
parent 550f55745a
commit 9867eaf3ef
No known key found for this signature in database
GPG key ID: 0303DF507FA67596
23 changed files with 1078 additions and 1084 deletions

View file

@ -1,281 +1,279 @@
<script>
import Visibility from 'visibilityjs';
import { visitUrl } from '../../lib/utils/url_utility';
import Poll from '../../lib/utils/poll';
import eventHub from '../event_hub';
import Service from '../services/index';
import Store from '../stores';
import titleComponent from './title.vue';
import descriptionComponent from './description.vue';
import editedComponent from './edited.vue';
import formComponent from './form.vue';
import recaptchaModalImplementor from '../../vue_shared/mixins/recaptcha_modal_implementor';
import Visibility from 'visibilityjs';
import { visitUrl } from '../../lib/utils/url_utility';
import Poll from '../../lib/utils/poll';
import eventHub from '../event_hub';
import Service from '../services/index';
import Store from '../stores';
import titleComponent from './title.vue';
import descriptionComponent from './description.vue';
import editedComponent from './edited.vue';
import formComponent from './form.vue';
import recaptchaModalImplementor from '../../vue_shared/mixins/recaptcha_modal_implementor';
export default {
components: {
descriptionComponent,
titleComponent,
editedComponent,
formComponent,
export default {
components: {
descriptionComponent,
titleComponent,
editedComponent,
formComponent,
},
mixins: [recaptchaModalImplementor],
props: {
endpoint: {
required: true,
type: String,
},
mixins: [
recaptchaModalImplementor,
],
props: {
endpoint: {
required: true,
type: String,
},
updateEndpoint: {
required: true,
type: String,
},
canUpdate: {
required: true,
type: Boolean,
},
canDestroy: {
required: true,
type: Boolean,
},
showInlineEditButton: {
type: Boolean,
required: false,
default: true,
},
showDeleteButton: {
type: Boolean,
required: false,
default: true,
},
enableAutocomplete: {
type: Boolean,
required: false,
default: true,
},
issuableRef: {
type: String,
required: true,
},
initialTitleHtml: {
type: String,
required: true,
},
initialTitleText: {
type: String,
required: true,
},
initialDescriptionHtml: {
type: String,
required: false,
default: '',
},
initialDescriptionText: {
type: String,
required: false,
default: '',
},
initialTaskStatus: {
type: String,
required: false,
default: '',
},
updatedAt: {
type: String,
required: false,
default: '',
},
updatedByName: {
type: String,
required: false,
default: '',
},
updatedByPath: {
type: String,
required: false,
default: '',
},
issuableTemplates: {
type: Array,
required: false,
default: () => [],
},
markdownPreviewPath: {
type: String,
required: true,
},
markdownDocsPath: {
type: String,
required: true,
},
markdownVersion: {
type: Number,
required: false,
default: 0,
},
projectPath: {
type: String,
required: true,
},
projectNamespace: {
type: String,
required: true,
},
issuableType: {
type: String,
required: false,
default: 'issue',
},
canAttachFile: {
type: Boolean,
required: false,
default: true,
},
updateEndpoint: {
required: true,
type: String,
},
data() {
const store = new Store({
titleHtml: this.initialTitleHtml,
titleText: this.initialTitleText,
descriptionHtml: this.initialDescriptionHtml,
descriptionText: this.initialDescriptionText,
updatedAt: this.updatedAt,
updatedByName: this.updatedByName,
updatedByPath: this.updatedByPath,
taskStatus: this.initialTaskStatus,
});
canUpdate: {
required: true,
type: Boolean,
},
canDestroy: {
required: true,
type: Boolean,
},
showInlineEditButton: {
type: Boolean,
required: false,
default: true,
},
showDeleteButton: {
type: Boolean,
required: false,
default: true,
},
enableAutocomplete: {
type: Boolean,
required: false,
default: true,
},
issuableRef: {
type: String,
required: true,
},
initialTitleHtml: {
type: String,
required: true,
},
initialTitleText: {
type: String,
required: true,
},
initialDescriptionHtml: {
type: String,
required: false,
default: '',
},
initialDescriptionText: {
type: String,
required: false,
default: '',
},
initialTaskStatus: {
type: String,
required: false,
default: '',
},
updatedAt: {
type: String,
required: false,
default: '',
},
updatedByName: {
type: String,
required: false,
default: '',
},
updatedByPath: {
type: String,
required: false,
default: '',
},
issuableTemplates: {
type: Array,
required: false,
default: () => [],
},
markdownPreviewPath: {
type: String,
required: true,
},
markdownDocsPath: {
type: String,
required: true,
},
markdownVersion: {
type: Number,
required: false,
default: 0,
},
projectPath: {
type: String,
required: true,
},
projectNamespace: {
type: String,
required: true,
},
issuableType: {
type: String,
required: false,
default: 'issue',
},
canAttachFile: {
type: Boolean,
required: false,
default: true,
},
},
data() {
const store = new Store({
titleHtml: this.initialTitleHtml,
titleText: this.initialTitleText,
descriptionHtml: this.initialDescriptionHtml,
descriptionText: this.initialDescriptionText,
updatedAt: this.updatedAt,
updatedByName: this.updatedByName,
updatedByPath: this.updatedByPath,
taskStatus: this.initialTaskStatus,
});
return {
store,
state: store.state,
showForm: false,
};
return {
store,
state: store.state,
showForm: false,
};
},
computed: {
formState() {
return this.store.formState;
},
computed: {
formState() {
return this.store.formState;
},
hasUpdated() {
return !!this.state.updatedAt;
},
issueChanged() {
const descriptionChanged =
this.initialDescriptionText !== this.store.formState.description;
const titleChanged =
this.initialTitleText !== this.store.formState.title;
return descriptionChanged || titleChanged;
},
hasUpdated() {
return !!this.state.updatedAt;
},
created() {
this.service = new Service(this.endpoint);
this.poll = new Poll({
resource: this.service,
method: 'getData',
successCallback: res => this.store.updateState(res.data),
errorCallback(err) {
throw new Error(err);
},
});
issueChanged() {
const descriptionChanged = this.initialDescriptionText !== this.store.formState.description;
const titleChanged = this.initialTitleText !== this.store.formState.title;
return descriptionChanged || titleChanged;
},
},
created() {
this.service = new Service(this.endpoint);
this.poll = new Poll({
resource: this.service,
method: 'getData',
successCallback: res => this.store.updateState(res.data),
errorCallback(err) {
throw new Error(err);
},
});
if (!Visibility.hidden()) {
this.poll.makeRequest();
}
Visibility.change(() => {
if (!Visibility.hidden()) {
this.poll.makeRequest();
this.poll.restart();
} else {
this.poll.stop();
}
});
Visibility.change(() => {
if (!Visibility.hidden()) {
this.poll.restart();
} else {
this.poll.stop();
}
});
window.addEventListener('beforeunload', this.handleBeforeUnloadEvent);
window.addEventListener('beforeunload', this.handleBeforeUnloadEvent);
eventHub.$on('delete.issuable', this.deleteIssuable);
eventHub.$on('update.issuable', this.updateIssuable);
eventHub.$on('close.form', this.closeForm);
eventHub.$on('open.form', this.openForm);
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);
window.removeEventListener('beforeunload', this.handleBeforeUnloadEvent);
},
methods: {
handleBeforeUnloadEvent(e) {
const event = e;
if (this.showForm && this.issueChanged) {
event.returnValue = 'Are you sure you want to lose your issue information?';
}
return undefined;
},
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);
window.removeEventListener('beforeunload', this.handleBeforeUnloadEvent);
},
methods: {
handleBeforeUnloadEvent(e) {
const event = e;
if (this.showForm && this.issueChanged) {
event.returnValue = 'Are you sure you want to lose your issue information?';
}
return undefined;
},
openForm() {
if (!this.showForm) {
this.showForm = true;
this.store.setFormState({
title: this.state.titleText,
description: this.state.descriptionText,
lockedWarningVisible: false,
updateLoading: false,
});
}
},
closeForm() {
this.showForm = false;
},
updateIssuable() {
return this.service.updateIssuable(this.store.formState)
.then(res => res.data)
.then(data => this.checkForSpam(data))
.then((data) => {
if (window.location.pathname !== data.web_url) {
visitUrl(data.web_url);
}
return this.service.getData();
})
.then(res => res.data)
.then((data) => {
this.store.updateState(data);
eventHub.$emit('close.form');
})
.catch((error) => {
if (error && error.name === 'SpamError') {
this.openRecaptcha();
} else {
eventHub.$emit('close.form');
window.Flash(`Error updating ${this.issuableType}`);
}
});
},
closeRecaptchaModal() {
openForm() {
if (!this.showForm) {
this.showForm = true;
this.store.setFormState({
title: this.state.titleText,
description: this.state.descriptionText,
lockedWarningVisible: false,
updateLoading: false,
});
this.closeRecaptcha();
},
deleteIssuable() {
this.service.deleteIssuable()
.then(res => res.data)
.then((data) => {
// Stop the poll so we don't get 404's with the issuable not existing
this.poll.stop();
visitUrl(data.web_url);
})
.catch(() => {
eventHub.$emit('close.form');
window.Flash(`Error deleting ${this.issuableType}`);
});
},
}
},
};
closeForm() {
this.showForm = false;
},
updateIssuable() {
return this.service
.updateIssuable(this.store.formState)
.then(res => res.data)
.then(data => this.checkForSpam(data))
.then(data => {
if (window.location.pathname !== data.web_url) {
visitUrl(data.web_url);
}
return this.service.getData();
})
.then(res => res.data)
.then(data => {
this.store.updateState(data);
eventHub.$emit('close.form');
})
.catch(error => {
if (error && error.name === 'SpamError') {
this.openRecaptcha();
} else {
eventHub.$emit('close.form');
window.Flash(`Error updating ${this.issuableType}`);
}
});
},
closeRecaptchaModal() {
this.store.setFormState({
updateLoading: false,
});
this.closeRecaptcha();
},
deleteIssuable() {
this.service
.deleteIssuable()
.then(res => res.data)
.then(data => {
// Stop the poll so we don't get 404's with the issuable not existing
this.poll.stop();
visitUrl(data.web_url);
})
.catch(() => {
eventHub.$emit('close.form');
window.Flash(`Error deleting ${this.issuableType}`);
});
},
},
};
</script>
<template>

View file

@ -1,110 +1,105 @@
<script>
import $ from 'jquery';
import animateMixin from '../mixins/animate';
import TaskList from '../../task_list';
import recaptchaModalImplementor from '../../vue_shared/mixins/recaptcha_modal_implementor';
import $ from 'jquery';
import animateMixin from '../mixins/animate';
import TaskList from '../../task_list';
import recaptchaModalImplementor from '../../vue_shared/mixins/recaptcha_modal_implementor';
export default {
mixins: [
animateMixin,
recaptchaModalImplementor,
],
export default {
mixins: [animateMixin, recaptchaModalImplementor],
props: {
canUpdate: {
type: Boolean,
required: true,
},
descriptionHtml: {
type: String,
required: true,
},
descriptionText: {
type: String,
required: true,
},
taskStatus: {
type: String,
required: false,
default: '',
},
issuableType: {
type: String,
required: false,
default: 'issue',
},
updateUrl: {
type: String,
required: false,
default: null,
},
props: {
canUpdate: {
type: Boolean,
required: true,
},
data() {
return {
preAnimation: false,
pulseAnimation: false,
};
descriptionHtml: {
type: String,
required: true,
},
watch: {
descriptionHtml() {
this.animateChange();
descriptionText: {
type: String,
required: true,
},
taskStatus: {
type: String,
required: false,
default: '',
},
issuableType: {
type: String,
required: false,
default: 'issue',
},
updateUrl: {
type: String,
required: false,
default: null,
},
},
data() {
return {
preAnimation: false,
pulseAnimation: false,
};
},
watch: {
descriptionHtml() {
this.animateChange();
this.$nextTick(() => {
this.renderGFM();
});
},
taskStatus() {
this.updateTaskStatusText();
},
this.$nextTick(() => {
this.renderGFM();
});
},
mounted() {
this.renderGFM();
taskStatus() {
this.updateTaskStatusText();
},
methods: {
renderGFM() {
$(this.$refs['gfm-content']).renderGFM();
},
mounted() {
this.renderGFM();
this.updateTaskStatusText();
},
methods: {
renderGFM() {
$(this.$refs['gfm-content']).renderGFM();
if (this.canUpdate) {
// eslint-disable-next-line no-new
new TaskList({
dataType: this.issuableType,
fieldName: 'description',
selector: '.detail-page-description',
onSuccess: this.taskListUpdateSuccess.bind(this),
});
}
},
taskListUpdateSuccess(data) {
try {
this.checkForSpam(data);
this.closeRecaptcha();
} catch (error) {
if (error && error.name === 'SpamError') this.openRecaptcha();
}
},
updateTaskStatusText() {
const taskRegexMatches = this.taskStatus.match(/(\d+) of ((?!0)\d+)/);
const $issuableHeader = $('.issuable-meta');
const $tasks = $('#task_status', $issuableHeader);
const $tasksShort = $('#task_status_short', $issuableHeader);
if (taskRegexMatches) {
$tasks.text(this.taskStatus);
$tasksShort.text(
`${taskRegexMatches[1]}/${taskRegexMatches[2]} task${taskRegexMatches[2] > 1 ?
's' :
''}`,
);
} else {
$tasks.text('');
$tasksShort.text('');
}
},
if (this.canUpdate) {
// eslint-disable-next-line no-new
new TaskList({
dataType: this.issuableType,
fieldName: 'description',
selector: '.detail-page-description',
onSuccess: this.taskListUpdateSuccess.bind(this),
});
}
},
};
taskListUpdateSuccess(data) {
try {
this.checkForSpam(data);
this.closeRecaptcha();
} catch (error) {
if (error && error.name === 'SpamError') this.openRecaptcha();
}
},
updateTaskStatusText() {
const taskRegexMatches = this.taskStatus.match(/(\d+) of ((?!0)\d+)/);
const $issuableHeader = $('.issuable-meta');
const $tasks = $('#task_status', $issuableHeader);
const $tasksShort = $('#task_status_short', $issuableHeader);
if (taskRegexMatches) {
$tasks.text(this.taskStatus);
$tasksShort.text(
`${taskRegexMatches[1]}/${taskRegexMatches[2]} task${taskRegexMatches[2] > 1 ? 's' : ''}`,
);
} else {
$tasks.text('');
$tasksShort.text('');
}
},
},
};
</script>
<template>

View file

@ -1,64 +1,64 @@
<script>
import { __, sprintf } from '~/locale';
import updateMixin from '../mixins/update';
import eventHub from '../event_hub';
import { __, sprintf } from '~/locale';
import updateMixin from '../mixins/update';
import eventHub from '../event_hub';
const issuableTypes = {
issue: __('Issue'),
epic: __('Epic'),
};
const issuableTypes = {
issue: __('Issue'),
epic: __('Epic'),
};
export default {
mixins: [updateMixin],
props: {
canDestroy: {
type: Boolean,
required: true,
},
formState: {
type: Object,
required: true,
},
showDeleteButton: {
type: Boolean,
required: false,
default: true,
},
issuableType: {
type: String,
required: true,
},
export default {
mixins: [updateMixin],
props: {
canDestroy: {
type: Boolean,
required: true,
},
data() {
return {
deleteLoading: false,
};
formState: {
type: Object,
required: true,
},
computed: {
isSubmitEnabled() {
return this.formState.title.trim() !== '';
},
shouldShowDeleteButton() {
return this.canDestroy && this.showDeleteButton;
},
showDeleteButton: {
type: Boolean,
required: false,
default: true,
},
methods: {
closeForm() {
eventHub.$emit('close.form');
},
deleteIssuable() {
const confirmMessage = sprintf(__('%{issuableType} will be removed! Are you sure?'), {
issuableType: issuableTypes[this.issuableType],
});
// eslint-disable-next-line no-alert
if (window.confirm(confirmMessage)) {
this.deleteLoading = true;
issuableType: {
type: String,
required: true,
},
},
data() {
return {
deleteLoading: false,
};
},
computed: {
isSubmitEnabled() {
return this.formState.title.trim() !== '';
},
shouldShowDeleteButton() {
return this.canDestroy && this.showDeleteButton;
},
},
methods: {
closeForm() {
eventHub.$emit('close.form');
},
deleteIssuable() {
const confirmMessage = sprintf(__('%{issuableType} will be removed! Are you sure?'), {
issuableType: issuableTypes[this.issuableType],
});
// eslint-disable-next-line no-alert
if (window.confirm(confirmMessage)) {
this.deleteLoading = true;
eventHub.$emit('delete.issuable');
}
},
eventHub.$emit('delete.issuable');
}
},
};
},
};
</script>
<template>

View file

@ -1,33 +1,33 @@
<script>
import timeAgoTooltip from '../../vue_shared/components/time_ago_tooltip.vue';
import timeAgoTooltip from '../../vue_shared/components/time_ago_tooltip.vue';
export default {
components: {
timeAgoTooltip,
export default {
components: {
timeAgoTooltip,
},
props: {
updatedAt: {
type: String,
required: false,
default: '',
},
props: {
updatedAt: {
type: String,
required: false,
default: '',
},
updatedByName: {
type: String,
required: false,
default: '',
},
updatedByPath: {
type: String,
required: false,
default: '',
},
updatedByName: {
type: String,
required: false,
default: '',
},
computed: {
hasUpdatedBy() {
return this.updatedByName && this.updatedByPath;
},
updatedByPath: {
type: String,
required: false,
default: '',
},
};
},
computed: {
hasUpdatedBy() {
return this.updatedByName && this.updatedByPath;
},
},
};
</script>
<template>
@ -53,4 +53,3 @@
</span>
</small>
</template>

View file

@ -1,45 +1,45 @@
<script>
import updateMixin from '../../mixins/update';
import markdownField from '../../../vue_shared/components/markdown/field.vue';
import updateMixin from '../../mixins/update';
import markdownField from '../../../vue_shared/components/markdown/field.vue';
export default {
components: {
markdownField,
export default {
components: {
markdownField,
},
mixins: [updateMixin],
props: {
formState: {
type: Object,
required: true,
},
mixins: [updateMixin],
props: {
formState: {
type: Object,
required: true,
},
markdownPreviewPath: {
type: String,
required: true,
},
markdownDocsPath: {
type: String,
required: true,
},
markdownVersion: {
type: Number,
required: false,
default: 0,
},
canAttachFile: {
type: Boolean,
required: false,
default: true,
},
enableAutocomplete: {
type: Boolean,
required: false,
default: true,
},
markdownPreviewPath: {
type: String,
required: true,
},
mounted() {
this.$refs.textarea.focus();
markdownDocsPath: {
type: String,
required: true,
},
};
markdownVersion: {
type: Number,
required: false,
default: 0,
},
canAttachFile: {
type: Boolean,
required: false,
default: true,
},
enableAutocomplete: {
type: Boolean,
required: false,
default: true,
},
},
mounted() {
this.$refs.textarea.focus();
},
};
</script>
<template>

View file

@ -1,46 +1,46 @@
<script>
import $ from 'jquery';
import IssuableTemplateSelectors from '../../../templates/issuable_template_selectors';
import $ from 'jquery';
import IssuableTemplateSelectors from '../../../templates/issuable_template_selectors';
export default {
props: {
formState: {
type: Object,
required: true,
},
issuableTemplates: {
type: Array,
required: false,
default: () => [],
},
projectPath: {
type: String,
required: true,
},
projectNamespace: {
type: String,
required: true,
},
export default {
props: {
formState: {
type: Object,
required: true,
},
computed: {
issuableTemplatesJson() {
return JSON.stringify(this.issuableTemplates);
},
issuableTemplates: {
type: Array,
required: false,
default: () => [],
},
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;
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 IssuableTemplateSelectors({
$dropdowns: $(this.$refs.toggle),
editor,
});
},
};
this.issuableTemplate = new IssuableTemplateSelectors({
$dropdowns: $(this.$refs.toggle),
editor,
});
},
};
</script>
<template>

View file

@ -1,15 +1,15 @@
<script>
import updateMixin from '../../mixins/update';
import updateMixin from '../../mixins/update';
export default {
mixins: [updateMixin],
props: {
formState: {
type: Object,
required: true,
},
export default {
mixins: [updateMixin],
props: {
formState: {
type: Object,
required: true,
},
};
},
};
</script>
<template>

View file

@ -1,79 +1,79 @@
<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 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';
export default {
components: {
lockedWarning,
titleField,
descriptionField,
descriptionTemplate,
editActions,
export default {
components: {
lockedWarning,
titleField,
descriptionField,
descriptionTemplate,
editActions,
},
props: {
canDestroy: {
type: Boolean,
required: true,
},
props: {
canDestroy: {
type: Boolean,
required: true,
},
formState: {
type: Object,
required: true,
},
issuableTemplates: {
type: Array,
required: false,
default: () => [],
},
issuableType: {
type: String,
required: true,
},
markdownPreviewPath: {
type: String,
required: true,
},
markdownDocsPath: {
type: String,
required: true,
},
markdownVersion: {
type: Number,
required: false,
default: 0,
},
projectPath: {
type: String,
required: true,
},
projectNamespace: {
type: String,
required: true,
},
showDeleteButton: {
type: Boolean,
required: false,
default: true,
},
canAttachFile: {
type: Boolean,
required: false,
default: true,
},
enableAutocomplete: {
type: Boolean,
required: false,
default: true,
},
formState: {
type: Object,
required: true,
},
computed: {
hasIssuableTemplates() {
return this.issuableTemplates.length;
},
issuableTemplates: {
type: Array,
required: false,
default: () => [],
},
};
issuableType: {
type: String,
required: true,
},
markdownPreviewPath: {
type: String,
required: true,
},
markdownDocsPath: {
type: String,
required: true,
},
markdownVersion: {
type: Number,
required: false,
default: 0,
},
projectPath: {
type: String,
required: true,
},
projectNamespace: {
type: String,
required: true,
},
showDeleteButton: {
type: Boolean,
required: false,
default: true,
},
canAttachFile: {
type: Boolean,
required: false,
default: true,
},
enableAutocomplete: {
type: Boolean,
required: false,
default: true,
},
},
computed: {
hasIssuableTemplates() {
return this.issuableTemplates.length;
},
},
};
</script>
<template>

View file

@ -1,11 +1,11 @@
<script>
export default {
computed: {
currentPath() {
return window.location.pathname;
},
export default {
computed: {
currentPath() {
return window.location.pathname;
},
};
},
};
</script>
<template>

View file

@ -25,8 +25,10 @@ export default class Store {
}
stateShouldUpdate(data) {
return this.state.titleText !== data.title_text ||
this.state.descriptionText !== data.description_text;
return (
this.state.titleText !== data.title_text ||
this.state.descriptionText !== data.description_text
);
}
setFormState(state) {

View file

@ -1,30 +1,28 @@
<script>
import TimeagoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import timeagoMixin from '~/vue_shared/mixins/timeago';
import TimeagoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import timeagoMixin from '~/vue_shared/mixins/timeago';
export default {
components: {
TimeagoTooltip,
export default {
components: {
TimeagoTooltip,
},
mixins: [timeagoMixin],
props: {
artifact: {
type: Object,
required: true,
},
mixins: [
timeagoMixin,
],
props: {
artifact: {
type: Object,
required: true,
},
},
computed: {
isExpired() {
return this.artifact.expired;
},
computed: {
isExpired() {
return this.artifact.expired;
},
// Only when the key is `false` we can render this block
willExpire() {
return this.artifact.expired === false;
},
// Only when the key is `false` we can render this block
willExpire() {
return this.artifact.expired === false;
},
};
},
};
</script>
<template>
<div class="block">

View file

@ -1,26 +1,26 @@
<script>
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
export default {
components: {
ClipboardButton,
export default {
components: {
ClipboardButton,
},
props: {
commit: {
type: Object,
required: true,
},
props: {
commit: {
type: Object,
required: true,
},
mergeRequest: {
type: Object,
required: false,
default: null,
},
isLastBlock: {
type: Boolean,
required: true,
},
mergeRequest: {
type: Object,
required: false,
default: null,
},
};
isLastBlock: {
type: Boolean,
required: true,
},
},
};
</script>
<template>
<div

View file

@ -1,38 +1,38 @@
<script>
export default {
props: {
illustrationPath: {
type: String,
required: true,
},
illustrationSizeClass: {
type: String,
required: true,
},
title: {
type: String,
required: true,
},
content: {
type: String,
required: false,
default: null,
},
action: {
type: Object,
required: false,
default: null,
validator(value) {
return (
value === null ||
(Object.prototype.hasOwnProperty.call(value, 'path') &&
Object.prototype.hasOwnProperty.call(value, 'method') &&
Object.prototype.hasOwnProperty.call(value, 'button_title'))
);
},
export default {
props: {
illustrationPath: {
type: String,
required: true,
},
illustrationSizeClass: {
type: String,
required: true,
},
title: {
type: String,
required: true,
},
content: {
type: String,
required: false,
default: null,
},
action: {
type: Object,
required: false,
default: null,
validator(value) {
return (
value === null ||
(Object.prototype.hasOwnProperty.call(value, 'path') &&
Object.prototype.hasOwnProperty.call(value, 'method') &&
Object.prototype.hasOwnProperty.call(value, 'button_title'))
);
},
},
};
},
};
</script>
<template>
<div class="row empty-state">

View file

@ -1,129 +1,131 @@
<script>
import _ from 'underscore';
import CiIcon from '~/vue_shared/components/ci_icon.vue';
import { sprintf, __ } from '../../locale';
import _ from 'underscore';
import CiIcon from '~/vue_shared/components/ci_icon.vue';
import { sprintf, __ } from '../../locale';
export default {
components: {
CiIcon,
export default {
components: {
CiIcon,
},
props: {
deploymentStatus: {
type: Object,
required: true,
},
props: {
deploymentStatus: {
type: Object,
required: true,
},
iconStatus: {
type: Object,
required: true,
},
iconStatus: {
type: Object,
required: true,
},
computed: {
environment() {
let environmentText;
switch (this.deploymentStatus.status) {
case 'last':
},
computed: {
environment() {
let environmentText;
switch (this.deploymentStatus.status) {
case 'last':
environmentText = sprintf(
__('This job is the most recent deployment to %{link}.'),
{ link: this.environmentLink },
false,
);
break;
case 'out_of_date':
if (this.hasLastDeployment) {
environmentText = sprintf(
__('This job is the most recent deployment to %{link}.'),
{ link: this.environmentLink },
__(
'This job is an out-of-date deployment to %{environmentLink}. View the most recent deployment %{deploymentLink}.',
),
{
environmentLink: this.environmentLink,
deploymentLink: this.deploymentLink(`#${this.lastDeployment.iid}`),
},
false,
);
break;
case 'out_of_date':
if (this.hasLastDeployment) {
environmentText = sprintf(
__(
'This job is an out-of-date deployment to %{environmentLink}. View the most recent deployment %{deploymentLink}.',
),
{
environmentLink: this.environmentLink,
deploymentLink: this.deploymentLink(`#${this.lastDeployment.iid}`),
},
false,
);
} else {
environmentText = sprintf(
__('This job is an out-of-date deployment to %{environmentLink}.'),
{ environmentLink: this.environmentLink },
false,
);
}
break;
case 'failed':
} else {
environmentText = sprintf(
__('The deployment of this job to %{environmentLink} did not succeed.'),
__('This job is an out-of-date deployment to %{environmentLink}.'),
{ environmentLink: this.environmentLink },
false,
);
break;
case 'creating':
if (this.hasLastDeployment) {
environmentText = sprintf(
__(
'This job is creating a deployment to %{environmentLink} and will overwrite the %{deploymentLink}.',
),
{
environmentLink: this.environmentLink,
deploymentLink: this.deploymentLink(__('latest deployment')),
},
false,
);
} else {
environmentText = sprintf(
__('This job is creating a deployment to %{environmentLink}.'),
{ environmentLink: this.environmentLink },
false,
);
}
break;
default:
break;
}
return environmentText;
},
environmentLink() {
if (this.hasEnvironment) {
return sprintf(
'%{startLink}%{name}%{endLink}',
{
startLink: `<a href="${
this.deploymentStatus.environment.environment_path
}" class="js-environment-link">`,
name: _.escape(this.deploymentStatus.environment.name),
endLink: '</a>',
},
}
break;
case 'failed':
environmentText = sprintf(
__('The deployment of this job to %{environmentLink} did not succeed.'),
{ environmentLink: this.environmentLink },
false,
);
}
return '';
},
hasLastDeployment() {
return this.hasEnvironment && this.deploymentStatus.environment.last_deployment;
},
lastDeployment() {
return this.hasLastDeployment ? this.deploymentStatus.environment.last_deployment : {};
},
hasEnvironment() {
return !_.isEmpty(this.deploymentStatus.environment);
},
lastDeploymentPath() {
return !_.isEmpty(this.lastDeployment.deployable) ? this.lastDeployment.deployable.build_path : '';
},
break;
case 'creating':
if (this.hasLastDeployment) {
environmentText = sprintf(
__(
'This job is creating a deployment to %{environmentLink} and will overwrite the %{deploymentLink}.',
),
{
environmentLink: this.environmentLink,
deploymentLink: this.deploymentLink(__('latest deployment')),
},
false,
);
} else {
environmentText = sprintf(
__('This job is creating a deployment to %{environmentLink}.'),
{ environmentLink: this.environmentLink },
false,
);
}
break;
default:
break;
}
return environmentText;
},
methods: {
deploymentLink(name) {
environmentLink() {
if (this.hasEnvironment) {
return sprintf(
'%{startLink}%{name}%{endLink}',
{
startLink: `<a href="${this.lastDeploymentPath}" class="js-job-deployment-link">`,
name,
startLink: `<a href="${
this.deploymentStatus.environment.environment_path
}" class="js-environment-link">`,
name: _.escape(this.deploymentStatus.environment.name),
endLink: '</a>',
},
false,
);
},
}
return '';
},
};
hasLastDeployment() {
return this.hasEnvironment && this.deploymentStatus.environment.last_deployment;
},
lastDeployment() {
return this.hasLastDeployment ? this.deploymentStatus.environment.last_deployment : {};
},
hasEnvironment() {
return !_.isEmpty(this.deploymentStatus.environment);
},
lastDeploymentPath() {
return !_.isEmpty(this.lastDeployment.deployable)
? this.lastDeployment.deployable.build_path
: '';
},
},
methods: {
deploymentLink(name) {
return sprintf(
'%{startLink}%{name}%{endLink}',
{
startLink: `<a href="${this.lastDeploymentPath}" class="js-job-deployment-link">`,
name,
endLink: '</a>',
},
false,
);
},
},
};
</script>
<template>
<div class="prepend-top-default js-environment-container">

View file

@ -1,28 +1,28 @@
<script>
import _ from 'underscore';
import TimeagoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import _ from 'underscore';
import TimeagoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
export default {
components: {
TimeagoTooltip,
export default {
components: {
TimeagoTooltip,
},
props: {
user: {
type: Object,
required: false,
default: () => ({}),
},
props: {
user: {
type: Object,
required: false,
default: () => ({}),
},
erasedAt: {
type: String,
required: true,
},
erasedAt: {
type: String,
required: true,
},
computed: {
isErasedByUser() {
return !_.isEmpty(this.user);
},
},
computed: {
isErasedByUser() {
return !_.isEmpty(this.user);
},
};
},
};
</script>
<template>
<div class="prepend-top-default js-build-erased">

View file

@ -1,17 +1,17 @@
<script>
export default {
name: 'JobLog',
props: {
trace: {
type: String,
required: true,
},
isComplete: {
type: Boolean,
required: true,
},
export default {
name: 'JobLog',
props: {
trace: {
type: String,
required: true,
},
};
isComplete: {
type: Boolean,
required: true,
},
},
};
</script>
<template>
<pre class="build-trace">

View file

@ -1,69 +1,68 @@
<script>
import { polyfillSticky } from '~/lib/utils/sticky';
import Icon from '~/vue_shared/components/icon.vue';
import tooltip from '~/vue_shared/directives/tooltip';
import { numberToHumanSize } from '~/lib/utils/number_utils';
import { sprintf } from '~/locale';
import { polyfillSticky } from '~/lib/utils/sticky';
import Icon from '~/vue_shared/components/icon.vue';
import tooltip from '~/vue_shared/directives/tooltip';
import { numberToHumanSize } from '~/lib/utils/number_utils';
import { sprintf } from '~/locale';
export default {
components: {
Icon,
export default {
components: {
Icon,
},
directives: {
tooltip,
},
props: {
erasePath: {
type: String,
required: false,
default: null,
},
directives: {
tooltip,
size: {
type: Number,
required: true,
},
props: {
erasePath: {
type: String,
required: false,
default: null,
},
size: {
type: Number,
required: true,
},
rawPath: {
type: String,
required: false,
default: null,
},
isScrollTopDisabled: {
type: Boolean,
required: true,
},
isScrollBottomDisabled: {
type: Boolean,
required: true,
},
isScrollingDown: {
type: Boolean,
required: true,
},
isTraceSizeVisible: {
type: Boolean,
required: true,
},
rawPath: {
type: String,
required: false,
default: null,
},
computed: {
jobLogSize() {
return sprintf('Showing last %{size} of log -', {
size: numberToHumanSize(this.size),
});
},
isScrollTopDisabled: {
type: Boolean,
required: true,
},
mounted() {
polyfillSticky(this.$el);
isScrollBottomDisabled: {
type: Boolean,
required: true,
},
methods: {
handleScrollToTop() {
this.$emit('scrollJobLogTop');
},
handleScrollToBottom() {
this.$emit('scrollJobLogBottom');
},
isScrollingDown: {
type: Boolean,
required: true,
},
};
isTraceSizeVisible: {
type: Boolean,
required: true,
},
},
computed: {
jobLogSize() {
return sprintf('Showing last %{size} of log -', {
size: numberToHumanSize(this.size),
});
},
},
mounted() {
polyfillSticky(this.$el);
},
methods: {
handleScrollToTop() {
this.$emit('scrollJobLogTop');
},
handleScrollToBottom() {
this.$emit('scrollJobLogBottom');
},
},
};
</script>
<template>
<div class="top-bar">

View file

@ -1,36 +1,36 @@
<script>
import _ from 'underscore';
import CiIcon from '~/vue_shared/components/ci_icon.vue';
import Icon from '~/vue_shared/components/icon.vue';
import tooltip from '~/vue_shared/directives/tooltip';
import _ from 'underscore';
import CiIcon from '~/vue_shared/components/ci_icon.vue';
import Icon from '~/vue_shared/components/icon.vue';
import tooltip from '~/vue_shared/directives/tooltip';
export default {
components: {
CiIcon,
Icon,
export default {
components: {
CiIcon,
Icon,
},
directives: {
tooltip,
},
props: {
jobs: {
type: Array,
required: true,
},
directives: {
tooltip,
jobId: {
type: Number,
required: true,
},
props: {
jobs: {
type: Array,
required: true,
},
jobId: {
type: Number,
required: true,
},
},
methods: {
isJobActive(currentJobId) {
return this.jobId === currentJobId;
},
methods: {
isJobActive(currentJobId) {
return this.jobId === currentJobId;
},
tooltipText(job) {
return `${_.escape(job.name)} - ${job.status.tooltip}`;
},
tooltipText(job) {
return `${_.escape(job.name)} - ${job.status.tooltip}`;
},
};
},
};
</script>
<template>
<div class="js-jobs-container builds-container">

View file

@ -1,112 +1,112 @@
<script>
import _ from 'underscore';
import { mapActions, mapState } from 'vuex';
import timeagoMixin from '~/vue_shared/mixins/timeago';
import { timeIntervalInWords } from '~/lib/utils/datetime_utility';
import Icon from '~/vue_shared/components/icon.vue';
import DetailRow from './sidebar_detail_row.vue';
import ArtifactsBlock from './artifacts_block.vue';
import TriggerBlock from './trigger_block.vue';
import CommitBlock from './commit_block.vue';
import StagesDropdown from './stages_dropdown.vue';
import JobsContainer from './jobs_container.vue';
import _ from 'underscore';
import { mapActions, mapState } from 'vuex';
import timeagoMixin from '~/vue_shared/mixins/timeago';
import { timeIntervalInWords } from '~/lib/utils/datetime_utility';
import Icon from '~/vue_shared/components/icon.vue';
import DetailRow from './sidebar_detail_row.vue';
import ArtifactsBlock from './artifacts_block.vue';
import TriggerBlock from './trigger_block.vue';
import CommitBlock from './commit_block.vue';
import StagesDropdown from './stages_dropdown.vue';
import JobsContainer from './jobs_container.vue';
export default {
name: 'JobSidebar',
components: {
ArtifactsBlock,
CommitBlock,
DetailRow,
Icon,
TriggerBlock,
StagesDropdown,
JobsContainer,
export default {
name: 'JobSidebar',
components: {
ArtifactsBlock,
CommitBlock,
DetailRow,
Icon,
TriggerBlock,
StagesDropdown,
JobsContainer,
},
mixins: [timeagoMixin],
props: {
runnerHelpUrl: {
type: String,
required: false,
default: '',
},
mixins: [timeagoMixin],
props: {
runnerHelpUrl: {
type: String,
required: false,
default: '',
},
terminalPath: {
type: String,
required: false,
default: null,
},
terminalPath: {
type: String,
required: false,
default: null,
},
computed: {
...mapState(['job', 'isLoading', 'stages', 'jobs']),
coverage() {
return `${this.job.coverage}%`;
},
duration() {
return timeIntervalInWords(this.job.duration);
},
queued() {
return timeIntervalInWords(this.job.queued);
},
runnerId() {
return `${this.job.runner.description} (#${this.job.runner.id})`;
},
retryButtonClass() {
let className =
'js-retry-button float-right btn btn-retry d-none d-md-block d-lg-block d-xl-block';
className +=
this.job.status && this.job.recoverable ? ' btn-primary' : ' btn-inverted-secondary';
return className;
},
hasTimeout() {
return this.job.metadata != null && this.job.metadata.timeout_human_readable !== null;
},
timeout() {
if (this.job.metadata == null) {
return '';
}
},
computed: {
...mapState(['job', 'isLoading', 'stages', 'jobs']),
coverage() {
return `${this.job.coverage}%`;
},
duration() {
return timeIntervalInWords(this.job.duration);
},
queued() {
return timeIntervalInWords(this.job.queued);
},
runnerId() {
return `${this.job.runner.description} (#${this.job.runner.id})`;
},
retryButtonClass() {
let className =
'js-retry-button float-right btn btn-retry d-none d-md-block d-lg-block d-xl-block';
className +=
this.job.status && this.job.recoverable ? ' btn-primary' : ' btn-inverted-secondary';
return className;
},
hasTimeout() {
return this.job.metadata != null && this.job.metadata.timeout_human_readable !== null;
},
timeout() {
if (this.job.metadata == null) {
return '';
}
let t = this.job.metadata.timeout_human_readable;
if (this.job.metadata.timeout_source !== '') {
t += ` (from ${this.job.metadata.timeout_source})`;
}
let t = this.job.metadata.timeout_human_readable;
if (this.job.metadata.timeout_source !== '') {
t += ` (from ${this.job.metadata.timeout_source})`;
}
return t;
},
renderBlock() {
return (
this.job.merge_request ||
this.job.duration ||
this.job.finished_data ||
this.job.erased_at ||
this.job.queued ||
this.job.runner ||
this.job.coverage ||
this.job.tags.length ||
this.job.cancel_path
);
},
hasArtifact() {
return !_.isEmpty(this.job.artifact);
},
hasTriggers() {
return !_.isEmpty(this.job.trigger);
},
hasStages() {
return (
(this.job &&
this.job.pipeline &&
this.job.pipeline.stages &&
this.job.pipeline.stages.length > 0) ||
false
);
},
commit() {
return this.job.pipeline.commit || {};
},
return t;
},
methods: {
...mapActions(['fetchJobsForStage']),
renderBlock() {
return (
this.job.merge_request ||
this.job.duration ||
this.job.finished_data ||
this.job.erased_at ||
this.job.queued ||
this.job.runner ||
this.job.coverage ||
this.job.tags.length ||
this.job.cancel_path
);
},
};
hasArtifact() {
return !_.isEmpty(this.job.artifact);
},
hasTriggers() {
return !_.isEmpty(this.job.trigger);
},
hasStages() {
return (
(this.job &&
this.job.pipeline &&
this.job.pipeline.stages &&
this.job.pipeline.stages.length > 0) ||
false
);
},
commit() {
return this.job.pipeline.commit || {};
},
},
methods: {
...mapActions(['fetchJobsForStage']),
},
};
</script>
<template>
<aside

View file

@ -1,31 +1,31 @@
<script>
export default {
name: 'SidebarDetailRow',
props: {
title: {
type: String,
required: false,
default: '',
},
value: {
type: String,
required: true,
},
helpUrl: {
type: String,
required: false,
default: '',
},
export default {
name: 'SidebarDetailRow',
props: {
title: {
type: String,
required: false,
default: '',
},
computed: {
hasTitle() {
return this.title.length > 0;
},
hasHelpURL() {
return this.helpUrl.length > 0;
},
value: {
type: String,
required: true,
},
};
helpUrl: {
type: String,
required: false,
default: '',
},
},
computed: {
hasTitle() {
return this.title.length > 0;
},
hasHelpURL() {
return this.helpUrl.length > 0;
},
},
};
</script>
<template>
<p class="build-detail-row">

View file

@ -1,50 +1,50 @@
<script>
import _ from 'underscore';
import CiIcon from '~/vue_shared/components/ci_icon.vue';
import Icon from '~/vue_shared/components/icon.vue';
import { __ } from '~/locale';
import _ from 'underscore';
import CiIcon from '~/vue_shared/components/ci_icon.vue';
import Icon from '~/vue_shared/components/icon.vue';
import { __ } from '~/locale';
export default {
components: {
CiIcon,
Icon,
export default {
components: {
CiIcon,
Icon,
},
props: {
pipeline: {
type: Object,
required: true,
},
props: {
pipeline: {
type: Object,
required: true,
},
stages: {
type: Array,
required: true,
},
stages: {
type: Array,
required: true,
},
data() {
return {
selectedStage: this.stages.length > 0 ? this.stages[0].name : __('More'),
};
},
data() {
return {
selectedStage: this.stages.length > 0 ? this.stages[0].name : __('More'),
};
},
computed: {
hasRef() {
return !_.isEmpty(this.pipeline.ref);
},
computed: {
hasRef() {
return !_.isEmpty(this.pipeline.ref);
},
},
watch: {
// When the component is initially mounted it may start with an empty stages array.
// Once the prop is updated, we set the first stage as the selected one
stages(newVal) {
if (newVal.length) {
this.selectedStage = newVal[0].name;
}
},
watch: {
// When the component is initially mounted it may start with an empty stages array.
// Once the prop is updated, we set the first stage as the selected one
stages(newVal) {
if (newVal.length) {
this.selectedStage = newVal[0].name;
}
},
},
methods: {
onStageClick(stage) {
this.$emit('requestSidebarStageDropdown', stage);
this.selectedStage = stage.name;
},
methods: {
onStageClick(stage) {
this.$emit('requestSidebarStageDropdown', stage);
this.selectedStage = stage.name;
},
},
};
},
};
</script>
<template>
<div class="block-last dropdown">

View file

@ -1,27 +1,27 @@
<script>
export default {
props: {
trigger: {
type: Object,
required: true,
},
export default {
props: {
trigger: {
type: Object,
required: true,
},
data() {
return {
areVariablesVisible: false,
};
},
data() {
return {
areVariablesVisible: false,
};
},
computed: {
hasVariables() {
return this.trigger.variables && this.trigger.variables.length > 0;
},
computed: {
hasVariables() {
return this.trigger.variables && this.trigger.variables.length > 0;
},
},
methods: {
revealVariables() {
this.areVariablesVisible = true;
},
methods: {
revealVariables() {
this.areVariablesVisible = true;
},
},
};
},
};
</script>
<template>

View file

@ -7,9 +7,10 @@ import mutations from './mutations';
Vue.use(Vuex);
export default () => new Vuex.Store({
actions,
mutations,
getters,
state: state(),
});
export default () =>
new Vuex.Store({
actions,
mutations,
getters,
state: state(),
});