Commit 9867eaf3 authored by Mike Greiling's avatar Mike Greiling

Prettify issue_show and jobs modules

parent 550f5574
<script> <script>
import Visibility from 'visibilityjs'; import Visibility from 'visibilityjs';
import { visitUrl } from '../../lib/utils/url_utility'; import { visitUrl } from '../../lib/utils/url_utility';
import Poll from '../../lib/utils/poll'; import Poll from '../../lib/utils/poll';
import eventHub from '../event_hub'; 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 editedComponent from './edited.vue'; import editedComponent from './edited.vue';
import formComponent from './form.vue'; import formComponent from './form.vue';
import recaptchaModalImplementor from '../../vue_shared/mixins/recaptcha_modal_implementor'; import recaptchaModalImplementor from '../../vue_shared/mixins/recaptcha_modal_implementor';
export default { export default {
components: { components: {
descriptionComponent, descriptionComponent,
titleComponent, titleComponent,
editedComponent, editedComponent,
formComponent, formComponent,
}, },
mixins: [ mixins: [recaptchaModalImplementor],
recaptchaModalImplementor, props: {
], endpoint: {
props: { required: true,
endpoint: { type: String,
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,
},
}, },
data() { updateEndpoint: {
const store = new Store({ required: true,
titleHtml: this.initialTitleHtml, type: String,
titleText: this.initialTitleText, },
descriptionHtml: this.initialDescriptionHtml, canUpdate: {
descriptionText: this.initialDescriptionText, required: true,
updatedAt: this.updatedAt, type: Boolean,
updatedByName: this.updatedByName, },
updatedByPath: this.updatedByPath, canDestroy: {
taskStatus: this.initialTaskStatus, 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 { return {
store, store,
state: store.state, state: store.state,
showForm: false, showForm: false,
}; };
}, },
computed: { computed: {
formState() { formState() {
return this.store.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;
},
}, },
created() { hasUpdated() {
this.service = new Service(this.endpoint); return !!this.state.updatedAt;
this.poll = new Poll({ },
resource: this.service, issueChanged() {
method: 'getData', const descriptionChanged = this.initialDescriptionText !== this.store.formState.description;
successCallback: res => this.store.updateState(res.data), const titleChanged = this.initialTitleText !== this.store.formState.title;
errorCallback(err) { return descriptionChanged || titleChanged;
throw new Error(err); },
}, },
}); 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()) { if (!Visibility.hidden()) {
this.poll.makeRequest(); this.poll.restart();
} else {
this.poll.stop();
} }
});
Visibility.change(() => { window.addEventListener('beforeunload', this.handleBeforeUnloadEvent);
if (!Visibility.hidden()) {
this.poll.restart();
} else {
this.poll.stop();
}
});
window.addEventListener('beforeunload', this.handleBeforeUnloadEvent);
eventHub.$on('delete.issuable', this.deleteIssuable); eventHub.$on('delete.issuable', this.deleteIssuable);
eventHub.$on('update.issuable', this.updateIssuable); eventHub.$on('update.issuable', this.updateIssuable);
eventHub.$on('close.form', this.closeForm); eventHub.$on('close.form', this.closeForm);
eventHub.$on('open.form', this.openForm); eventHub.$on('open.form', this.openForm);
}, },
beforeDestroy() { beforeDestroy() {
eventHub.$off('delete.issuable', this.deleteIssuable); eventHub.$off('delete.issuable', this.deleteIssuable);
eventHub.$off('update.issuable', this.updateIssuable); eventHub.$off('update.issuable', this.updateIssuable);
eventHub.$off('close.form', this.closeForm); eventHub.$off('close.form', this.closeForm);
eventHub.$off('open.form', this.openForm); eventHub.$off('open.form', this.openForm);
window.removeEventListener('beforeunload', this.handleBeforeUnloadEvent); window.removeEventListener('beforeunload', this.handleBeforeUnloadEvent);
}, },
methods: { methods: {
handleBeforeUnloadEvent(e) { handleBeforeUnloadEvent(e) {
const event = e; const event = e;
if (this.showForm && this.issueChanged) { if (this.showForm && this.issueChanged) {
event.returnValue = 'Are you sure you want to lose your issue information?'; event.returnValue = 'Are you sure you want to lose your issue information?';
} }
return undefined; return undefined;
}, },
openForm() { openForm() {
if (!this.showForm) { if (!this.showForm) {
this.showForm = true; this.showForm = true;
this.store.setFormState({ this.store.setFormState({
title: this.state.titleText, title: this.state.titleText,
description: this.state.descriptionText, description: this.state.descriptionText,
lockedWarningVisible: false, lockedWarningVisible: false,
updateLoading: false, updateLoading: false,
}); });
} }
}, },
closeForm() { closeForm() {
this.showForm = false; this.showForm = false;
}, },
updateIssuable() { updateIssuable() {
return this.service.updateIssuable(this.store.formState) return this.service
.then(res => res.data) .updateIssuable(this.store.formState)
.then(data => this.checkForSpam(data)) .then(res => res.data)
.then((data) => { .then(data => this.checkForSpam(data))
if (window.location.pathname !== data.web_url) { .then(data => {
visitUrl(data.web_url); if (window.location.pathname !== data.web_url) {
} visitUrl(data.web_url);
}
return this.service.getData(); return this.service.getData();
}) })
.then(res => res.data) .then(res => res.data)
.then((data) => { .then(data => {
this.store.updateState(data); this.store.updateState(data);
eventHub.$emit('close.form');
})
.catch(error => {
if (error && error.name === 'SpamError') {
this.openRecaptcha();
} else {
eventHub.$emit('close.form'); eventHub.$emit('close.form');
}) window.Flash(`Error updating ${this.issuableType}`);
.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(); closeRecaptchaModal() {
}, this.store.setFormState({
updateLoading: false,
});
deleteIssuable() { this.closeRecaptcha();
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); deleteIssuable() {
}) this.service
.catch(() => { .deleteIssuable()
eventHub.$emit('close.form'); .then(res => res.data)
window.Flash(`Error deleting ${this.issuableType}`); .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> </script>
<template> <template>
......
<script> <script>
import $ from 'jquery'; import $ from 'jquery';
import animateMixin from '../mixins/animate'; import animateMixin from '../mixins/animate';
import TaskList from '../../task_list'; import TaskList from '../../task_list';
import recaptchaModalImplementor from '../../vue_shared/mixins/recaptcha_modal_implementor'; import recaptchaModalImplementor from '../../vue_shared/mixins/recaptcha_modal_implementor';
export default { export default {
mixins: [ mixins: [animateMixin, recaptchaModalImplementor],
animateMixin,
recaptchaModalImplementor,
],
props: { props: {
canUpdate: { canUpdate: {
type: Boolean, type: Boolean,
required: true, 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,
},
}, },
data() { descriptionHtml: {
return { type: String,
preAnimation: false, required: true,
pulseAnimation: false,
};
}, },
watch: { descriptionText: {
descriptionHtml() { type: String,
this.animateChange(); 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.$nextTick(() => {
this.renderGFM(); this.renderGFM();
}); });
},
taskStatus() {
this.updateTaskStatusText();
},
}, },
mounted() { taskStatus() {
this.renderGFM();
this.updateTaskStatusText(); this.updateTaskStatusText();
}, },
methods: { },
renderGFM() { mounted() {
$(this.$refs['gfm-content']).renderGFM(); this.renderGFM();
this.updateTaskStatusText();
},
methods: {
renderGFM() {
$(this.$refs['gfm-content']).renderGFM();
if (this.canUpdate) { if (this.canUpdate) {
// eslint-disable-next-line no-new // eslint-disable-next-line no-new
new TaskList({ new TaskList({
dataType: this.issuableType, dataType: this.issuableType,
fieldName: 'description', fieldName: 'description',
selector: '.detail-page-description', selector: '.detail-page-description',
onSuccess: this.taskListUpdateSuccess.bind(this), onSuccess: this.taskListUpdateSuccess.bind(this),
}); });
} }
}, },
taskListUpdateSuccess(data) { taskListUpdateSuccess(data) {
try { try {
this.checkForSpam(data); this.checkForSpam(data);
this.closeRecaptcha(); this.closeRecaptcha();
} catch (error) { } catch (error) {
if (error && error.name === 'SpamError') this.openRecaptcha(); if (error && error.name === 'SpamError') this.openRecaptcha();
} }
}, },
updateTaskStatusText() { updateTaskStatusText() {
const taskRegexMatches = this.taskStatus.match(/(\d+) of ((?!0)\d+)/); const taskRegexMatches = this.taskStatus.match(/(\d+) of ((?!0)\d+)/);
const $issuableHeader = $('.issuable-meta'); const $issuableHeader = $('.issuable-meta');
const $tasks = $('#task_status', $issuableHeader); const $tasks = $('#task_status', $issuableHeader);
const $tasksShort = $('#task_status_short', $issuableHeader); const $tasksShort = $('#task_status_short', $issuableHeader);
if (taskRegexMatches) { if (taskRegexMatches) {
$tasks.text(this.taskStatus); $tasks.text(this.taskStatus);
$tasksShort.text( $tasksShort.text(
`${taskRegexMatches[1]}/${taskRegexMatches[2]} task${taskRegexMatches[2] > 1 ? `${taskRegexMatches[1]}/${taskRegexMatches[2]} task${taskRegexMatches[2] > 1 ? 's' : ''}`,
's' : );
''}`, } else {
); $tasks.text('');
} else { $tasksShort.text('');
$tasks.text(''); }
$tasksShort.text('');
}
},
}, },
}; },
};
</script> </script>
<template> <template>
......
<script> <script>
import { __, sprintf } from '~/locale'; import { __, sprintf } from '~/locale';
import updateMixin from '../mixins/update'; import updateMixin from '../mixins/update';
import eventHub from '../event_hub'; import eventHub from '../event_hub';
const issuableTypes = { const issuableTypes = {
issue: __('Issue'), issue: __('Issue'),
epic: __('Epic'), epic: __('Epic'),
}; };
export default { export default {
mixins: [updateMixin], mixins: [updateMixin],
props: { props: {
canDestroy: { canDestroy: {
type: Boolean, type: Boolean,
required: true, required: true,
},
formState: {
type: Object,
required: true,
},
showDeleteButton: {
type: Boolean,
required: false,
default: true,
},
issuableType: {
type: String,
required: true,
},
}, },
data() { formState: {
return { type: Object,
deleteLoading: false, required: true,
};
}, },
computed: { showDeleteButton: {
isSubmitEnabled() { type: Boolean,
return this.formState.title.trim() !== ''; required: false,
}, default: true,
shouldShowDeleteButton() {
return this.canDestroy && this.showDeleteButton;
},
}, },
methods: { issuableType: {
closeForm() { type: String,
eventHub.$emit('close.form'); required: true,
}, },
deleteIssuable() { },
const confirmMessage = sprintf(__('%{issuableType} will be removed! Are you sure?'), { data() {
issuableType: issuableTypes[this.issuableType], return {
}); deleteLoading: false,
// eslint-disable-next-line no-alert };
if (window.confirm(confirmMessage)) { },
this.deleteLoading = true; 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> </script>
<template> <template>
......
<script> <script>
import timeAgoTooltip from '../../vue_shared/components/time_ago_tooltip.vue'; import timeAgoTooltip from '../../vue_shared/components/time_ago_tooltip.vue';
export default { export default {
components: { components: {
timeAgoTooltip, timeAgoTooltip,
},
props: {
updatedAt: {
type: String,
required: false,
default: '',
}, },
props: { updatedByName: {
updatedAt: { type: String,
type: String, required: false,
required: false, default: '',
default: '',
},
updatedByName: {
type: String,
required: false,
default: '',
},
updatedByPath: {
type: String,
required: false,
default: '',
},
}, },
computed: { updatedByPath: {
hasUpdatedBy() { type: String,
return this.updatedByName && this.updatedByPath; required: false,
}, default: '',
}, },
}; },
computed: {
hasUpdatedBy() {
return this.updatedByName && this.updatedByPath;
},
},
};
</script> </script>
<template> <template>
...@@ -53,4 +53,3 @@ ...@@ -53,4 +53,3 @@
</span> </span>
</small> </small>
</template> </template>
<script> <script>
import updateMixin from '../../mixins/update'; import updateMixin from '../../mixins/update';
import markdownField from '../../../vue_shared/components/markdown/field.vue'; import markdownField from '../../../vue_shared/components/markdown/field.vue';
export default { export default {
components: { components: {
markdownField, markdownField,
},
mixins: [updateMixin],
props: {
formState: {
type: Object,
required: true,
}, },
mixins: [updateMixin], markdownPreviewPath: {
props: { type: String,
formState: { required: true,
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,
},
}, },
mounted() { markdownDocsPath: {
this.$refs.textarea.focus(); 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> </script>
<template> <template>
......
<script> <script>
import $ from 'jquery'; import $ from 'jquery';
import IssuableTemplateSelectors from '../../../templates/issuable_template_selectors'; import IssuableTemplateSelectors from '../../../templates/issuable_template_selectors';
export default { export default {
props: { props: {
formState: { formState: {
type: Object, type: Object,
required: true, required: true,
},
issuableTemplates: {
type: Array,
required: false,
default: () => [],
},
projectPath: {
type: String,
required: true,
},
projectNamespace: {
type: String,
required: true,
},
}, },
computed: { issuableTemplates: {
issuableTemplatesJson() { type: Array,
return JSON.stringify(this.issuableTemplates); required: false,
}, default: () => [],
}, },
mounted() { projectPath: {
// Create the editor for the template type: String,
const editor = document.querySelector('.detail-page-description .note-textarea') || {}; required: true,
editor.setValue = (val) => { },
this.formState.description = val; projectNamespace: {
}; type: String,
editor.getValue = () => this.formState.description; required: true,
this.issuableTemplate = new IssuableTemplateSelectors({
$dropdowns: $(this.$refs.toggle),
editor,
});
}, },
}; },
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,
});
},
};
</script> </script>
<template> <template>
......
<script> <script>
import updateMixin from '../../mixins/update'; import updateMixin from '../../mixins/update';
export default { export default {
mixins: [updateMixin], mixins: [updateMixin],
props: { props: {
formState: { formState: {
type: Object, type: Object,
required: true, required: true,
},
}, },
}; },
};
</script> </script>
<template> <template>
......
<script> <script>
import lockedWarning from './locked_warning.vue'; import lockedWarning from './locked_warning.vue';
import titleField from './fields/title.vue'; import titleField from './fields/title.vue';
import descriptionField from './fields/description.vue'; import descriptionField from './fields/description.vue';
import editActions from './edit_actions.vue'; import editActions from './edit_actions.vue';
import descriptionTemplate from './fields/description_template.vue'; import descriptionTemplate from './fields/description_template.vue';
export default { export default {
components: { components: {
lockedWarning, lockedWarning,
titleField, titleField,
descriptionField, descriptionField,
descriptionTemplate, descriptionTemplate,
editActions, editActions,
},
props: {
canDestroy: {
type: Boolean,
required: true,
}, },
props: { formState: {
canDestroy: { type: Object,
type: Boolean, required: true,
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,
},
}, },
computed: { issuableTemplates: {
hasIssuableTemplates() { type: Array,
return this.issuableTemplates.length; 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> </script>
<template> <template>
......
<script> <script>
export default { export default {
computed: { computed: {
currentPath() { currentPath() {
return window.location.pathname; return window.location.pathname;
},
}, },
}; },
};
</script> </script>
<template> <template>
......
...@@ -25,8 +25,10 @@ export default class Store { ...@@ -25,8 +25,10 @@ export default class Store {
} }
stateShouldUpdate(data) { stateShouldUpdate(data) {
return this.state.titleText !== data.title_text || return (
this.state.descriptionText !== data.description_text; this.state.titleText !== data.title_text ||
this.state.descriptionText !== data.description_text
);
} }
setFormState(state) { setFormState(state) {
......
<script> <script>
import TimeagoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; import TimeagoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import timeagoMixin from '~/vue_shared/mixins/timeago'; import timeagoMixin from '~/vue_shared/mixins/timeago';
export default { export default {
components: { components: {
TimeagoTooltip, TimeagoTooltip,
},
mixins: [timeagoMixin],
props: {
artifact: {
type: Object,
required: true,
}, },
mixins: [ },
timeagoMixin, computed: {
], isExpired() {
props: { return this.artifact.expired;
artifact: {
type: Object,
required: true,
},
}, },
computed: { // Only when the key is `false` we can render this block
isExpired() { willExpire() {
return this.artifact.expired; return this.artifact.expired === false;
},
// Only when the key is `false` we can render this block
willExpire() {
return this.artifact.expired === false;
},
}, },
}; },
};
</script> </script>
<template> <template>
<div class="block"> <div class="block">
......
<script> <script>
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue'; import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
export default { export default {
components: { components: {
ClipboardButton, ClipboardButton,
},
props: {
commit: {
type: Object,
required: true,
}, },
props: { mergeRequest: {
commit: { type: Object,
type: Object, required: false,
required: true, default: null,
},
mergeRequest: {
type: Object,
required: false,
default: null,
},
isLastBlock: {
type: Boolean,
required: true,
},
}, },
}; isLastBlock: {
type: Boolean,
required: true,
},
},
};
</script> </script>
<template> <template>
<div <div
......
<script> <script>
export default { export default {
props: { props: {
illustrationPath: { illustrationPath: {
type: String, type: String,
required: true, required: true,
}, },
illustrationSizeClass: { illustrationSizeClass: {
type: String, type: String,
required: true, required: true,
}, },
title: { title: {
type: String, type: String,
required: true, required: true,
}, },
content: { content: {
type: String, type: String,
required: false, required: false,
default: null, default: null,
}, },
action: { action: {
type: Object, type: Object,
required: false, required: false,
default: null, default: null,
validator(value) { validator(value) {
return ( return (
value === null || value === null ||
(Object.prototype.hasOwnProperty.call(value, 'path') && (Object.prototype.hasOwnProperty.call(value, 'path') &&
Object.prototype.hasOwnProperty.call(value, 'method') && Object.prototype.hasOwnProperty.call(value, 'method') &&
Object.prototype.hasOwnProperty.call(value, 'button_title')) Object.prototype.hasOwnProperty.call(value, 'button_title'))
); );
},
}, },
}, },
}; },
};
</script> </script>
<template> <template>
<div class="row empty-state"> <div class="row empty-state">
......
<script> <script>
import _ from 'underscore'; import _ from 'underscore';
import CiIcon from '~/vue_shared/components/ci_icon.vue'; import CiIcon from '~/vue_shared/components/ci_icon.vue';
import { sprintf, __ } from '../../locale'; import { sprintf, __ } from '../../locale';
export default { export default {
components: { components: {
CiIcon, CiIcon,
},
props: {
deploymentStatus: {
type: Object,
required: true,
}, },
props: { iconStatus: {
deploymentStatus: { type: Object,
type: Object, required: true,
required: true,
},
iconStatus: {
type: Object,
required: true,
},
}, },
computed: { },
environment() { computed: {
let environmentText; environment() {
switch (this.deploymentStatus.status) { let environmentText;
case 'last': 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( 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, false,
); );
break; } else {
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':
environmentText = sprintf( 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 }, { environmentLink: this.environmentLink },
false, false,
); );
break; }
case 'creating':
if (this.hasLastDeployment) { break;
environmentText = sprintf( case 'failed':
__( environmentText = sprintf(
'This job is creating a deployment to %{environmentLink} and will overwrite the %{deploymentLink}.', __('The deployment of this job to %{environmentLink} did not succeed.'),
), { environmentLink: this.environmentLink },
{
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>',
},
false, false,
); );
} break;
return ''; case 'creating':
}, if (this.hasLastDeployment) {
hasLastDeployment() { environmentText = sprintf(
return this.hasEnvironment && this.deploymentStatus.environment.last_deployment; __(
}, 'This job is creating a deployment to %{environmentLink} and will overwrite the %{deploymentLink}.',
lastDeployment() { ),
return this.hasLastDeployment ? this.deploymentStatus.environment.last_deployment : {}; {
}, environmentLink: this.environmentLink,
hasEnvironment() { deploymentLink: this.deploymentLink(__('latest deployment')),
return !_.isEmpty(this.deploymentStatus.environment); },
}, false,
lastDeploymentPath() { );
return !_.isEmpty(this.lastDeployment.deployable) ? this.lastDeployment.deployable.build_path : ''; } else {
}, environmentText = sprintf(
__('This job is creating a deployment to %{environmentLink}.'),
{ environmentLink: this.environmentLink },
false,
);
}
break;
default:
break;
}
return environmentText;
}, },
methods: { environmentLink() {
deploymentLink(name) { if (this.hasEnvironment) {
return sprintf( return sprintf(
'%{startLink}%{name}%{endLink}', '%{startLink}%{name}%{endLink}',
{ {
startLink: `<a href="${this.lastDeploymentPath}" class="js-job-deployment-link">`, startLink: `<a href="${
name, this.deploymentStatus.environment.environment_path
}" class="js-environment-link">`,
name: _.escape(this.deploymentStatus.environment.name),
endLink: '</a>', endLink: '</a>',
}, },
false, 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> </script>
<template> <template>
<div class="prepend-top-default js-environment-container"> <div class="prepend-top-default js-environment-container">
......
<script> <script>
import _ from 'underscore'; import _ from 'underscore';
import TimeagoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; import TimeagoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
export default { export default {
components: { components: {
TimeagoTooltip, TimeagoTooltip,
},
props: {
user: {
type: Object,
required: false,
default: () => ({}),
}, },
props: { erasedAt: {
user: { type: String,
type: Object, required: true,
required: false,
default: () => ({}),
},
erasedAt: {
type: String,
required: true,
},
}, },
computed: { },
isErasedByUser() { computed: {
return !_.isEmpty(this.user); isErasedByUser() {
}, return !_.isEmpty(this.user);
}, },
}; },
};
</script> </script>
<template> <template>
<div class="prepend-top-default js-build-erased"> <div class="prepend-top-default js-build-erased">
......
<script> <script>
export default { export default {
name: 'JobLog', name: 'JobLog',
props: { props: {
trace: { trace: {
type: String, type: String,
required: true, required: true,
},
isComplete: {
type: Boolean,
required: true,
},
}, },
}; isComplete: {
type: Boolean,
required: true,
},
},
};
</script> </script>
<template> <template>
<pre class="build-trace"> <pre class="build-trace">
......
<script> <script>
import { polyfillSticky } from '~/lib/utils/sticky'; import { polyfillSticky } from '~/lib/utils/sticky';
import Icon from '~/vue_shared/components/icon.vue'; import Icon from '~/vue_shared/components/icon.vue';
import tooltip from '~/vue_shared/directives/tooltip'; import tooltip from '~/vue_shared/directives/tooltip';
import { numberToHumanSize } from '~/lib/utils/number_utils'; import { numberToHumanSize } from '~/lib/utils/number_utils';
import { sprintf } from '~/locale'; import { sprintf } from '~/locale';
export default { export default {
components: { components: {
Icon, Icon,
},
directives: {
tooltip,
},
props: {
erasePath: {
type: String,
required: false,
default: null,
}, },
directives: { size: {
tooltip, type: Number,
required: true,
}, },
props: { rawPath: {
erasePath: { type: String,
type: String, required: false,
required: false, default: null,
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,
},
}, },
computed: { isScrollTopDisabled: {
jobLogSize() { type: Boolean,
return sprintf('Showing last %{size} of log -', { required: true,
size: numberToHumanSize(this.size),
});
},
}, },
mounted() { isScrollBottomDisabled: {
polyfillSticky(this.$el); type: Boolean,
required: true,
}, },
methods: { isScrollingDown: {
handleScrollToTop() { type: Boolean,
this.$emit('scrollJobLogTop'); required: true,
},
handleScrollToBottom() {
this.$emit('scrollJobLogBottom');
},
}, },
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> </script>
<template> <template>
<div class="top-bar"> <div class="top-bar">
......
<script> <script>
import _ from 'underscore'; import _ from 'underscore';
import CiIcon from '~/vue_shared/components/ci_icon.vue'; import CiIcon from '~/vue_shared/components/ci_icon.vue';
import Icon from '~/vue_shared/components/icon.vue'; import Icon from '~/vue_shared/components/icon.vue';
import tooltip from '~/vue_shared/directives/tooltip'; import tooltip from '~/vue_shared/directives/tooltip';
export default { export default {
components: { components: {
CiIcon, CiIcon,
Icon, Icon,
},
directives: {
tooltip,
},
props: {
jobs: {
type: Array,
required: true,
}, },
directives: { jobId: {
tooltip, type: Number,
required: true,
}, },
props: { },
jobs: { methods: {
type: Array, isJobActive(currentJobId) {
required: true, return this.jobId === currentJobId;
},
jobId: {
type: Number,
required: true,
},
}, },
methods: { tooltipText(job) {
isJobActive(currentJobId) { return `${_.escape(job.name)} - ${job.status.tooltip}`;
return this.jobId === currentJobId;
},
tooltipText(job) {
return `${_.escape(job.name)} - ${job.status.tooltip}`;
},
}, },
}; },
};
</script> </script>
<template> <template>
<div class="js-jobs-container builds-container"> <div class="js-jobs-container builds-container">
......
<script> <script>
import _ from 'underscore'; import _ from 'underscore';
import { mapActions, mapState } from 'vuex'; import { mapActions, mapState } from 'vuex';
import timeagoMixin from '~/vue_shared/mixins/timeago'; import timeagoMixin from '~/vue_shared/mixins/timeago';
import { timeIntervalInWords } from '~/lib/utils/datetime_utility'; import { timeIntervalInWords } from '~/lib/utils/datetime_utility';
import Icon from '~/vue_shared/components/icon.vue'; import Icon from '~/vue_shared/components/icon.vue';
import DetailRow from './sidebar_detail_row.vue'; import DetailRow from './sidebar_detail_row.vue';
import ArtifactsBlock from './artifacts_block.vue'; import ArtifactsBlock from './artifacts_block.vue';
import TriggerBlock from './trigger_block.vue'; import TriggerBlock from './trigger_block.vue';
import CommitBlock from './commit_block.vue'; import CommitBlock from './commit_block.vue';
import StagesDropdown from './stages_dropdown.vue'; import StagesDropdown from './stages_dropdown.vue';
import JobsContainer from './jobs_container.vue'; import JobsContainer from './jobs_container.vue';
export default { export default {
name: 'JobSidebar', name: 'JobSidebar',
components: { components: {
ArtifactsBlock, ArtifactsBlock,
CommitBlock, CommitBlock,
DetailRow, DetailRow,
Icon, Icon,
TriggerBlock, TriggerBlock,
StagesDropdown, StagesDropdown,
JobsContainer, JobsContainer,
},
mixins: [timeagoMixin],
props: {
runnerHelpUrl: {
type: String,
required: false,
default: '',
}, },
mixins: [timeagoMixin], terminalPath: {
props: { type: String,
runnerHelpUrl: { required: false,
type: String, default: null,
required: false,
default: '',
},
terminalPath: {
type: String,
required: false,
default: null,
},
}, },
computed: { },
...mapState(['job', 'isLoading', 'stages', 'jobs']), computed: {
coverage() { ...mapState(['job', 'isLoading', 'stages', 'jobs']),
return `${this.job.coverage}%`; coverage() {
}, return `${this.job.coverage}%`;
duration() { },
return timeIntervalInWords(this.job.duration); duration() {
}, return timeIntervalInWords(this.job.duration);
queued() { },
return timeIntervalInWords(this.job.queued); queued() {
}, return timeIntervalInWords(this.job.queued);
runnerId() { },
return `${this.job.runner.description} (#${this.job.runner.id})`; runnerId() {
}, return `${this.job.runner.description} (#${this.job.runner.id})`;
retryButtonClass() { },
let className = retryButtonClass() {
'js-retry-button float-right btn btn-retry d-none d-md-block d-lg-block d-xl-block'; let className =
className += 'js-retry-button float-right btn btn-retry d-none d-md-block d-lg-block d-xl-block';
this.job.status && this.job.recoverable ? ' btn-primary' : ' btn-inverted-secondary'; className +=
return 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; hasTimeout() {
}, return this.job.metadata != null && this.job.metadata.timeout_human_readable !== null;
timeout() { },
if (this.job.metadata == null) { timeout() {
return ''; if (this.job.metadata == null) {
} return '';
}
let t = this.job.metadata.timeout_human_readable; let t = this.job.metadata.timeout_human_readable;
if (this.job.metadata.timeout_source !== '') { if (this.job.metadata.timeout_source !== '') {
t += ` (from ${this.job.metadata.timeout_source})`; t += ` (from ${this.job.metadata.timeout_source})`;
} }
return t; return t;
}, },
renderBlock() { renderBlock() {
return ( return (
this.job.merge_request || this.job.merge_request ||
this.job.duration || this.job.duration ||
this.job.finished_data || this.job.finished_data ||
this.job.erased_at || this.job.erased_at ||
this.job.queued || this.job.queued ||
this.job.runner || this.job.runner ||
this.job.coverage || this.job.coverage ||
this.job.tags.length || this.job.tags.length ||
this.job.cancel_path this.job.cancel_path
); );
}, },
hasArtifact() { hasArtifact() {
return !_.isEmpty(this.job.artifact); return !_.isEmpty(this.job.artifact);
}, },
hasTriggers() { hasTriggers() {
return !_.isEmpty(this.job.trigger); return !_.isEmpty(this.job.trigger);
}, },
hasStages() { hasStages() {
return ( return (
(this.job && (this.job &&
this.job.pipeline && this.job.pipeline &&
this.job.pipeline.stages && this.job.pipeline.stages &&
this.job.pipeline.stages.length > 0) || this.job.pipeline.stages.length > 0) ||
false false
); );
},
commit() {
return this.job.pipeline.commit || {};
},
}, },
methods: { commit() {
...mapActions(['fetchJobsForStage']), return this.job.pipeline.commit || {};
}, },
}; },
methods: {
...mapActions(['fetchJobsForStage']),
},
};
</script> </script>
<template> <template>
<aside <aside
......
<script> <script>
export default { export default {
name: 'SidebarDetailRow', name: 'SidebarDetailRow',
props: { props: {
title: { title: {
type: String, type: String,
required: false, required: false,
default: '', default: '',
},
value: {
type: String,
required: true,
},
helpUrl: {
type: String,
required: false,
default: '',
},
}, },
computed: { value: {
hasTitle() { type: String,
return this.title.length > 0; required: true,
},
hasHelpURL() {
return this.helpUrl.length > 0;
},
}, },
}; helpUrl: {
type: String,
required: false,
default: '',
},
},
computed: {
hasTitle() {
return this.title.length > 0;
},
hasHelpURL() {
return this.helpUrl.length > 0;
},
},
};
</script> </script>
<template> <template>
<p class="build-detail-row"> <p class="build-detail-row">
......
<script> <script>
import _ from 'underscore'; import _ from 'underscore';
import CiIcon from '~/vue_shared/components/ci_icon.vue'; import CiIcon from '~/vue_shared/components/ci_icon.vue';
import Icon from '~/vue_shared/components/icon.vue'; import Icon from '~/vue_shared/components/icon.vue';
import { __ } from '~/locale'; import { __ } from '~/locale';
export default { export default {
components: { components: {
CiIcon, CiIcon,
Icon, Icon,
},
props: {
pipeline: {
type: Object,
required: true,
}, },
props: { stages: {
pipeline: { type: Array,
type: Object, required: true,
required: true,
},
stages: {
type: Array,
required: true,
},
}, },
data() { },
return { data() {
selectedStage: this.stages.length > 0 ? this.stages[0].name : __('More'), return {
}; selectedStage: this.stages.length > 0 ? this.stages[0].name : __('More'),
};
},
computed: {
hasRef() {
return !_.isEmpty(this.pipeline.ref);
}, },
computed: { },
hasRef() { watch: {
return !_.isEmpty(this.pipeline.ref); // 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. methods: {
// Once the prop is updated, we set the first stage as the selected one onStageClick(stage) {
stages(newVal) { this.$emit('requestSidebarStageDropdown', stage);
if (newVal.length) { this.selectedStage = stage.name;
this.selectedStage = newVal[0].name;
}
},
}, },
methods: { },
onStageClick(stage) { };
this.$emit('requestSidebarStageDropdown', stage);
this.selectedStage = stage.name;
},
},
};
</script> </script>
<template> <template>
<div class="block-last dropdown"> <div class="block-last dropdown">
......
<script> <script>
export default { export default {
props: { props: {
trigger: { trigger: {
type: Object, type: Object,
required: true, required: true,
},
}, },
data() { },
return { data() {
areVariablesVisible: false, return {
}; areVariablesVisible: false,
};
},
computed: {
hasVariables() {
return this.trigger.variables && this.trigger.variables.length > 0;
}, },
computed: { },
hasVariables() { methods: {
return this.trigger.variables && this.trigger.variables.length > 0; revealVariables() {
}, this.areVariablesVisible = true;
}, },
methods: { },
revealVariables() { };
this.areVariablesVisible = true;
},
},
};
</script> </script>
<template> <template>
......
...@@ -7,9 +7,10 @@ import mutations from './mutations'; ...@@ -7,9 +7,10 @@ import mutations from './mutations';
Vue.use(Vuex); Vue.use(Vuex);
export default () => new Vuex.Store({ export default () =>
actions, new Vuex.Store({
mutations, actions,
getters, mutations,
state: state(), getters,
}); state: state(),
});
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