Commit 20bfbdf2 authored by Tim Zallmann's avatar Tim Zallmann

Merge branch 'prettify-all-the-things-2' into 'master'

Prettify all the things (part 2)

See merge request gitlab-org/gitlab-ce!22250
parents 2efbc75f 56914c1a
<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(),
});
...@@ -4,7 +4,7 @@ import Cache from './cache'; ...@@ -4,7 +4,7 @@ import Cache from './cache';
class AjaxCache extends Cache { class AjaxCache extends Cache {
constructor() { constructor() {
super(); super();
this.pendingRequests = { }; this.pendingRequests = {};
} }
override(endpoint, data) { override(endpoint, data) {
...@@ -19,12 +19,13 @@ class AjaxCache extends Cache { ...@@ -19,12 +19,13 @@ class AjaxCache extends Cache {
let pendingRequest = this.pendingRequests[endpoint]; let pendingRequest = this.pendingRequests[endpoint];
if (!pendingRequest) { if (!pendingRequest) {
pendingRequest = axios.get(endpoint) pendingRequest = axios
.get(endpoint)
.then(({ data }) => { .then(({ data }) => {
this.internalStorage[endpoint] = data; this.internalStorage[endpoint] = data;
delete this.pendingRequests[endpoint]; delete this.pendingRequests[endpoint];
}) })
.catch((e) => { .catch(e => {
const error = new Error(`${endpoint}: ${e.message}`); const error = new Error(`${endpoint}: ${e.message}`);
error.textStatus = e.message; error.textStatus = e.message;
......
...@@ -7,7 +7,7 @@ axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest'; ...@@ -7,7 +7,7 @@ axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';
// Maintain a global counter for active requests // Maintain a global counter for active requests
// see: spec/support/wait_for_requests.rb // see: spec/support/wait_for_requests.rb
axios.interceptors.request.use((config) => { axios.interceptors.request.use(config => {
window.activeVueResources = window.activeVueResources || 0; window.activeVueResources = window.activeVueResources || 0;
window.activeVueResources += 1; window.activeVueResources += 1;
...@@ -15,15 +15,18 @@ axios.interceptors.request.use((config) => { ...@@ -15,15 +15,18 @@ axios.interceptors.request.use((config) => {
}); });
// Remove the global counter // Remove the global counter
axios.interceptors.response.use((config) => { axios.interceptors.response.use(
window.activeVueResources -= 1; config => {
window.activeVueResources -= 1;
return config;
}, (e) => { return config;
window.activeVueResources -= 1; },
e => {
return Promise.reject(e); window.activeVueResources -= 1;
});
return Promise.reject(e);
},
);
export default axios; export default axios;
......
...@@ -93,9 +93,13 @@ export default class LinkedTabs { ...@@ -93,9 +93,13 @@ export default class LinkedTabs {
const newState = `${copySource}${this.currentLocation.search}${this.currentLocation.hash}`; const newState = `${copySource}${this.currentLocation.search}${this.currentLocation.hash}`;
window.history.replaceState({ window.history.replaceState(
url: newState, {
}, document.title, newState); url: newState,
},
document.title,
newState,
);
return newState; return newState;
} }
......
export default class Cache { export default class Cache {
constructor() { constructor() {
this.internalStorage = { }; this.internalStorage = {};
} }
get(key) { get(key) {
......
export const pad = (val, len = 2) => `0${val}`.slice(-len);
export const pad = (val, len = 2) => (`0${val}`).slice(-len);
/** /**
* Formats dates in Pickaday * Formats dates in Pickaday
* @param {String} dateString Date in yyyy-mm-dd format * @param {String} dateString Date in yyyy-mm-dd format
* @return {Date} UTC format * @return {Date} UTC format
*/ */
export const parsePikadayDate = (dateString) => { export const parsePikadayDate = dateString => {
const parts = dateString.split('-'); const parts = dateString.split('-');
const year = parseInt(parts[0], 10); const year = parseInt(parts[0], 10);
const month = parseInt(parts[1] - 1, 10); const month = parseInt(parts[1] - 1, 10);
...@@ -20,7 +19,7 @@ export const parsePikadayDate = (dateString) => { ...@@ -20,7 +19,7 @@ export const parsePikadayDate = (dateString) => {
* @param {Date} date UTC format * @param {Date} date UTC format
* @return {String} Date formated in yyyy-mm-dd * @return {String} Date formated in yyyy-mm-dd
*/ */
export const pikadayToString = (date) => { export const pikadayToString = date => {
const day = pad(date.getDate()); const day = pad(date.getDate());
const month = pad(date.getMonth() + 1); const month = pad(date.getMonth() + 1);
const year = date.getFullYear(); const year = date.getFullYear();
......
...@@ -8,7 +8,7 @@ function notificationGranted(message, opts, onclick) { ...@@ -8,7 +8,7 @@ function notificationGranted(message, opts, onclick) {
return notification.close(); return notification.close();
}, 8000); }, 8000);
return notification.onclick = onclick || notification.close; return (notification.onclick = onclick || notification.close);
} }
function notifyPermissions() { function notifyPermissions() {
...@@ -21,7 +21,7 @@ function notifyMe(message, body, icon, onclick) { ...@@ -21,7 +21,7 @@ function notifyMe(message, body, icon, onclick) {
var opts; var opts;
opts = { opts = {
body: body, body: body,
icon: icon icon: icon,
}; };
// Let's check if the browser supports notifications // Let's check if the browser supports notifications
if (!('Notification' in window)) { if (!('Notification' in window)) {
......
...@@ -27,10 +27,10 @@ export function parseSeconds(seconds, { daysPerWeek = 5, hoursPerDay = 8 } = {}) ...@@ -27,10 +27,10 @@ export function parseSeconds(seconds, { daysPerWeek = 5, hoursPerDay = 8 } = {})
let unorderedMinutes = Math.abs(seconds / MINUTES_PER_HOUR); let unorderedMinutes = Math.abs(seconds / MINUTES_PER_HOUR);
return _.mapObject(timePeriodConstraints, (minutesPerPeriod) => { return _.mapObject(timePeriodConstraints, minutesPerPeriod => {
const periodCount = Math.floor(unorderedMinutes / minutesPerPeriod); const periodCount = Math.floor(unorderedMinutes / minutesPerPeriod);
unorderedMinutes -= (periodCount * minutesPerPeriod); unorderedMinutes -= periodCount * minutesPerPeriod;
return periodCount; return periodCount;
}); });
...@@ -42,10 +42,14 @@ export function parseSeconds(seconds, { daysPerWeek = 5, hoursPerDay = 8 } = {}) ...@@ -42,10 +42,14 @@ export function parseSeconds(seconds, { daysPerWeek = 5, hoursPerDay = 8 } = {})
*/ */
export function stringifyTime(timeObject) { export function stringifyTime(timeObject) {
const reducedTime = _.reduce(timeObject, (memo, unitValue, unitName) => { const reducedTime = _.reduce(
const isNonZero = !!unitValue; timeObject,
return isNonZero ? `${memo} ${unitValue}${unitName.charAt(0)}` : memo; (memo, unitValue, unitName) => {
}, '').trim(); const isNonZero = !!unitValue;
return isNonZero ? `${memo} ${unitValue}${unitName.charAt(0)}` : memo;
},
'',
).trim();
return reducedTime.length ? reducedTime : '0m'; return reducedTime.length ? reducedTime : '0m';
} }
...@@ -55,7 +59,5 @@ export function stringifyTime(timeObject) { ...@@ -55,7 +59,5 @@ export function stringifyTime(timeObject) {
*/ */
export function abbreviateTime(timeStr) { export function abbreviateTime(timeStr) {
return timeStr.split(' ') return timeStr.split(' ').filter(unitStr => unitStr.charAt(0) !== '0')[0];
.filter(unitStr => unitStr.charAt(0) !== '0')[0];
} }
...@@ -5,6 +5,7 @@ ...@@ -5,6 +5,7 @@
// Inspired by https://github.com/mishoo/UglifyJS/blob/2bc1d02363db3798d5df41fb5059a19edca9b7eb/lib/parse-js.js#L203 // Inspired by https://github.com/mishoo/UglifyJS/blob/2bc1d02363db3798d5df41fb5059a19edca9b7eb/lib/parse-js.js#L203
// Unicode 6.1 // Unicode 6.1
const unicodeLetters = '\\u0041-\\u005A\\u0061-\\u007A\\u00AA\\u00B5\\u00BA\\u00C0-\\u00D6\\u00D8-\\u00F6\\u00F8-\\u02C1\\u02C6-\\u02D1\\u02E0-\\u02E4\\u02EC\\u02EE\\u0370-\\u0374\\u0376\\u0377\\u037A-\\u037D\\u0386\\u0388-\\u038A\\u038C\\u038E-\\u03A1\\u03A3-\\u03F5\\u03F7-\\u0481\\u048A-\\u0527\\u0531-\\u0556\\u0559\\u0561-\\u0587\\u05D0-\\u05EA\\u05F0-\\u05F2\\u0620-\\u064A\\u066E\\u066F\\u0671-\\u06D3\\u06D5\\u06E5\\u06E6\\u06EE\\u06EF\\u06FA-\\u06FC\\u06FF\\u0710\\u0712-\\u072F\\u074D-\\u07A5\\u07B1\\u07CA-\\u07EA\\u07F4\\u07F5\\u07FA\\u0800-\\u0815\\u081A\\u0824\\u0828\\u0840-\\u0858\\u08A0\\u08A2-\\u08AC\\u0904-\\u0939\\u093D\\u0950\\u0958-\\u0961\\u0971-\\u0977\\u0979-\\u097F\\u0985-\\u098C\\u098F\\u0990\\u0993-\\u09A8\\u09AA-\\u09B0\\u09B2\\u09B6-\\u09B9\\u09BD\\u09CE\\u09DC\\u09DD\\u09DF-\\u09E1\\u09F0\\u09F1\\u0A05-\\u0A0A\\u0A0F\\u0A10\\u0A13-\\u0A28\\u0A2A-\\u0A30\\u0A32\\u0A33\\u0A35\\u0A36\\u0A38\\u0A39\\u0A59-\\u0A5C\\u0A5E\\u0A72-\\u0A74\\u0A85-\\u0A8D\\u0A8F-\\u0A91\\u0A93-\\u0AA8\\u0AAA-\\u0AB0\\u0AB2\\u0AB3\\u0AB5-\\u0AB9\\u0ABD\\u0AD0\\u0AE0\\u0AE1\\u0B05-\\u0B0C\\u0B0F\\u0B10\\u0B13-\\u0B28\\u0B2A-\\u0B30\\u0B32\\u0B33\\u0B35-\\u0B39\\u0B3D\\u0B5C\\u0B5D\\u0B5F-\\u0B61\\u0B71\\u0B83\\u0B85-\\u0B8A\\u0B8E-\\u0B90\\u0B92-\\u0B95\\u0B99\\u0B9A\\u0B9C\\u0B9E\\u0B9F\\u0BA3\\u0BA4\\u0BA8-\\u0BAA\\u0BAE-\\u0BB9\\u0BD0\\u0C05-\\u0C0C\\u0C0E-\\u0C10\\u0C12-\\u0C28\\u0C2A-\\u0C33\\u0C35-\\u0C39\\u0C3D\\u0C58\\u0C59\\u0C60\\u0C61\\u0C85-\\u0C8C\\u0C8E-\\u0C90\\u0C92-\\u0CA8\\u0CAA-\\u0CB3\\u0CB5-\\u0CB9\\u0CBD\\u0CDE\\u0CE0\\u0CE1\\u0CF1\\u0CF2\\u0D05-\\u0D0C\\u0D0E-\\u0D10\\u0D12-\\u0D3A\\u0D3D\\u0D4E\\u0D60\\u0D61\\u0D7A-\\u0D7F\\u0D85-\\u0D96\\u0D9A-\\u0DB1\\u0DB3-\\u0DBB\\u0DBD\\u0DC0-\\u0DC6\\u0E01-\\u0E30\\u0E32\\u0E33\\u0E40-\\u0E46\\u0E81\\u0E82\\u0E84\\u0E87\\u0E88\\u0E8A\\u0E8D\\u0E94-\\u0E97\\u0E99-\\u0E9F\\u0EA1-\\u0EA3\\u0EA5\\u0EA7\\u0EAA\\u0EAB\\u0EAD-\\u0EB0\\u0EB2\\u0EB3\\u0EBD\\u0EC0-\\u0EC4\\u0EC6\\u0EDC-\\u0EDF\\u0F00\\u0F40-\\u0F47\\u0F49-\\u0F6C\\u0F88-\\u0F8C\\u1000-\\u102A\\u103F\\u1050-\\u1055\\u105A-\\u105D\\u1061\\u1065\\u1066\\u106E-\\u1070\\u1075-\\u1081\\u108E\\u10A0-\\u10C5\\u10C7\\u10CD\\u10D0-\\u10FA\\u10FC-\\u1248\\u124A-\\u124D\\u1250-\\u1256\\u1258\\u125A-\\u125D\\u1260-\\u1288\\u128A-\\u128D\\u1290-\\u12B0\\u12B2-\\u12B5\\u12B8-\\u12BE\\u12C0\\u12C2-\\u12C5\\u12C8-\\u12D6\\u12D8-\\u1310\\u1312-\\u1315\\u1318-\\u135A\\u1380-\\u138F\\u13A0-\\u13F4\\u1401-\\u166C\\u166F-\\u167F\\u1681-\\u169A\\u16A0-\\u16EA\\u16EE-\\u16F0\\u1700-\\u170C\\u170E-\\u1711\\u1720-\\u1731\\u1740-\\u1751\\u1760-\\u176C\\u176E-\\u1770\\u1780-\\u17B3\\u17D7\\u17DC\\u1820-\\u1877\\u1880-\\u18A8\\u18AA\\u18B0-\\u18F5\\u1900-\\u191C\\u1950-\\u196D\\u1970-\\u1974\\u1980-\\u19AB\\u19C1-\\u19C7\\u1A00-\\u1A16\\u1A20-\\u1A54\\u1AA7\\u1B05-\\u1B33\\u1B45-\\u1B4B\\u1B83-\\u1BA0\\u1BAE\\u1BAF\\u1BBA-\\u1BE5\\u1C00-\\u1C23\\u1C4D-\\u1C4F\\u1C5A-\\u1C7D\\u1CE9-\\u1CEC\\u1CEE-\\u1CF1\\u1CF5\\u1CF6\\u1D00-\\u1DBF\\u1E00-\\u1F15\\u1F18-\\u1F1D\\u1F20-\\u1F45\\u1F48-\\u1F4D\\u1F50-\\u1F57\\u1F59\\u1F5B\\u1F5D\\u1F5F-\\u1F7D\\u1F80-\\u1FB4\\u1FB6-\\u1FBC\\u1FBE\\u1FC2-\\u1FC4\\u1FC6-\\u1FCC\\u1FD0-\\u1FD3\\u1FD6-\\u1FDB\\u1FE0-\\u1FEC\\u1FF2-\\u1FF4\\u1FF6-\\u1FFC\\u2071\\u207F\\u2090-\\u209C\\u2102\\u2107\\u210A-\\u2113\\u2115\\u2119-\\u211D\\u2124\\u2126\\u2128\\u212A-\\u212D\\u212F-\\u2139\\u213C-\\u213F\\u2145-\\u2149\\u214E\\u2160-\\u2188\\u2C00-\\u2C2E\\u2C30-\\u2C5E\\u2C60-\\u2CE4\\u2CEB-\\u2CEE\\u2CF2\\u2CF3\\u2D00-\\u2D25\\u2D27\\u2D2D\\u2D30-\\u2D67\\u2D6F\\u2D80-\\u2D96\\u2DA0-\\u2DA6\\u2DA8-\\u2DAE\\u2DB0-\\u2DB6\\u2DB8-\\u2DBE\\u2DC0-\\u2DC6\\u2DC8-\\u2DCE\\u2DD0-\\u2DD6\\u2DD8-\\u2DDE\\u2E2F\\u3005-\\u3007\\u3021-\\u3029\\u3031-\\u3035\\u3038-\\u303C\\u3041-\\u3096\\u309D-\\u309F\\u30A1-\\u30FA\\u30FC-\\u30FF\\u3105-\\u312D\\u3131-\\u318E\\u31A0-\\u31BA\\u31F0-\\u31FF\\u3400-\\u4DB5\\u4E00-\\u9FCC\\uA000-\\uA48C\\uA4D0-\\uA4FD\\uA500-\\uA60C\\uA610-\\uA61F\\uA62A\\uA62B\\uA640-\\uA66E\\uA67F-\\uA697\\uA6A0-\\uA6EF\\uA717-\\uA71F\\uA722-\\uA788\\uA78B-\\uA78E\\uA790-\\uA793\\uA7A0-\\uA7AA\\uA7F8-\\uA801\\uA803-\\uA805\\uA807-\\uA80A\\uA80C-\\uA822\\uA840-\\uA873\\uA882-\\uA8B3\\uA8F2-\\uA8F7\\uA8FB\\uA90A-\\uA925\\uA930-\\uA946\\uA960-\\uA97C\\uA984-\\uA9B2\\uA9CF\\uAA00-\\uAA28\\uAA40-\\uAA42\\uAA44-\\uAA4B\\uAA60-\\uAA76\\uAA7A\\uAA80-\\uAAAF\\uAAB1\\uAAB5\\uAAB6\\uAAB9-\\uAABD\\uAAC0\\uAAC2\\uAADB-\\uAADD\\uAAE0-\\uAAEA\\uAAF2-\\uAAF4\\uAB01-\\uAB06\\uAB09-\\uAB0E\\uAB11-\\uAB16\\uAB20-\\uAB26\\uAB28-\\uAB2E\\uABC0-\\uABE2\\uAC00-\\uD7A3\\uD7B0-\\uD7C6\\uD7CB-\\uD7FB\\uF900-\\uFA6D\\uFA70-\\uFAD9\\uFB00-\\uFB06\\uFB13-\\uFB17\\uFB1D\\uFB1F-\\uFB28\\uFB2A-\\uFB36\\uFB38-\\uFB3C\\uFB3E\\uFB40\\uFB41\\uFB43\\uFB44\\uFB46-\\uFBB1\\uFBD3-\\uFD3D\\uFD50-\\uFD8F\\uFD92-\\uFDC7\\uFDF0-\\uFDFB\\uFE70-\\uFE74\\uFE76-\\uFEFC\\uFF21-\\uFF3A\\uFF41-\\uFF5A\\uFF66-\\uFFBE\\uFFC2-\\uFFC7\\uFFCA-\\uFFCF\\uFFD2-\\uFFD7\\uFFDA-\\uFFDC'; const unicodeLetters =
'\\u0041-\\u005A\\u0061-\\u007A\\u00AA\\u00B5\\u00BA\\u00C0-\\u00D6\\u00D8-\\u00F6\\u00F8-\\u02C1\\u02C6-\\u02D1\\u02E0-\\u02E4\\u02EC\\u02EE\\u0370-\\u0374\\u0376\\u0377\\u037A-\\u037D\\u0386\\u0388-\\u038A\\u038C\\u038E-\\u03A1\\u03A3-\\u03F5\\u03F7-\\u0481\\u048A-\\u0527\\u0531-\\u0556\\u0559\\u0561-\\u0587\\u05D0-\\u05EA\\u05F0-\\u05F2\\u0620-\\u064A\\u066E\\u066F\\u0671-\\u06D3\\u06D5\\u06E5\\u06E6\\u06EE\\u06EF\\u06FA-\\u06FC\\u06FF\\u0710\\u0712-\\u072F\\u074D-\\u07A5\\u07B1\\u07CA-\\u07EA\\u07F4\\u07F5\\u07FA\\u0800-\\u0815\\u081A\\u0824\\u0828\\u0840-\\u0858\\u08A0\\u08A2-\\u08AC\\u0904-\\u0939\\u093D\\u0950\\u0958-\\u0961\\u0971-\\u0977\\u0979-\\u097F\\u0985-\\u098C\\u098F\\u0990\\u0993-\\u09A8\\u09AA-\\u09B0\\u09B2\\u09B6-\\u09B9\\u09BD\\u09CE\\u09DC\\u09DD\\u09DF-\\u09E1\\u09F0\\u09F1\\u0A05-\\u0A0A\\u0A0F\\u0A10\\u0A13-\\u0A28\\u0A2A-\\u0A30\\u0A32\\u0A33\\u0A35\\u0A36\\u0A38\\u0A39\\u0A59-\\u0A5C\\u0A5E\\u0A72-\\u0A74\\u0A85-\\u0A8D\\u0A8F-\\u0A91\\u0A93-\\u0AA8\\u0AAA-\\u0AB0\\u0AB2\\u0AB3\\u0AB5-\\u0AB9\\u0ABD\\u0AD0\\u0AE0\\u0AE1\\u0B05-\\u0B0C\\u0B0F\\u0B10\\u0B13-\\u0B28\\u0B2A-\\u0B30\\u0B32\\u0B33\\u0B35-\\u0B39\\u0B3D\\u0B5C\\u0B5D\\u0B5F-\\u0B61\\u0B71\\u0B83\\u0B85-\\u0B8A\\u0B8E-\\u0B90\\u0B92-\\u0B95\\u0B99\\u0B9A\\u0B9C\\u0B9E\\u0B9F\\u0BA3\\u0BA4\\u0BA8-\\u0BAA\\u0BAE-\\u0BB9\\u0BD0\\u0C05-\\u0C0C\\u0C0E-\\u0C10\\u0C12-\\u0C28\\u0C2A-\\u0C33\\u0C35-\\u0C39\\u0C3D\\u0C58\\u0C59\\u0C60\\u0C61\\u0C85-\\u0C8C\\u0C8E-\\u0C90\\u0C92-\\u0CA8\\u0CAA-\\u0CB3\\u0CB5-\\u0CB9\\u0CBD\\u0CDE\\u0CE0\\u0CE1\\u0CF1\\u0CF2\\u0D05-\\u0D0C\\u0D0E-\\u0D10\\u0D12-\\u0D3A\\u0D3D\\u0D4E\\u0D60\\u0D61\\u0D7A-\\u0D7F\\u0D85-\\u0D96\\u0D9A-\\u0DB1\\u0DB3-\\u0DBB\\u0DBD\\u0DC0-\\u0DC6\\u0E01-\\u0E30\\u0E32\\u0E33\\u0E40-\\u0E46\\u0E81\\u0E82\\u0E84\\u0E87\\u0E88\\u0E8A\\u0E8D\\u0E94-\\u0E97\\u0E99-\\u0E9F\\u0EA1-\\u0EA3\\u0EA5\\u0EA7\\u0EAA\\u0EAB\\u0EAD-\\u0EB0\\u0EB2\\u0EB3\\u0EBD\\u0EC0-\\u0EC4\\u0EC6\\u0EDC-\\u0EDF\\u0F00\\u0F40-\\u0F47\\u0F49-\\u0F6C\\u0F88-\\u0F8C\\u1000-\\u102A\\u103F\\u1050-\\u1055\\u105A-\\u105D\\u1061\\u1065\\u1066\\u106E-\\u1070\\u1075-\\u1081\\u108E\\u10A0-\\u10C5\\u10C7\\u10CD\\u10D0-\\u10FA\\u10FC-\\u1248\\u124A-\\u124D\\u1250-\\u1256\\u1258\\u125A-\\u125D\\u1260-\\u1288\\u128A-\\u128D\\u1290-\\u12B0\\u12B2-\\u12B5\\u12B8-\\u12BE\\u12C0\\u12C2-\\u12C5\\u12C8-\\u12D6\\u12D8-\\u1310\\u1312-\\u1315\\u1318-\\u135A\\u1380-\\u138F\\u13A0-\\u13F4\\u1401-\\u166C\\u166F-\\u167F\\u1681-\\u169A\\u16A0-\\u16EA\\u16EE-\\u16F0\\u1700-\\u170C\\u170E-\\u1711\\u1720-\\u1731\\u1740-\\u1751\\u1760-\\u176C\\u176E-\\u1770\\u1780-\\u17B3\\u17D7\\u17DC\\u1820-\\u1877\\u1880-\\u18A8\\u18AA\\u18B0-\\u18F5\\u1900-\\u191C\\u1950-\\u196D\\u1970-\\u1974\\u1980-\\u19AB\\u19C1-\\u19C7\\u1A00-\\u1A16\\u1A20-\\u1A54\\u1AA7\\u1B05-\\u1B33\\u1B45-\\u1B4B\\u1B83-\\u1BA0\\u1BAE\\u1BAF\\u1BBA-\\u1BE5\\u1C00-\\u1C23\\u1C4D-\\u1C4F\\u1C5A-\\u1C7D\\u1CE9-\\u1CEC\\u1CEE-\\u1CF1\\u1CF5\\u1CF6\\u1D00-\\u1DBF\\u1E00-\\u1F15\\u1F18-\\u1F1D\\u1F20-\\u1F45\\u1F48-\\u1F4D\\u1F50-\\u1F57\\u1F59\\u1F5B\\u1F5D\\u1F5F-\\u1F7D\\u1F80-\\u1FB4\\u1FB6-\\u1FBC\\u1FBE\\u1FC2-\\u1FC4\\u1FC6-\\u1FCC\\u1FD0-\\u1FD3\\u1FD6-\\u1FDB\\u1FE0-\\u1FEC\\u1FF2-\\u1FF4\\u1FF6-\\u1FFC\\u2071\\u207F\\u2090-\\u209C\\u2102\\u2107\\u210A-\\u2113\\u2115\\u2119-\\u211D\\u2124\\u2126\\u2128\\u212A-\\u212D\\u212F-\\u2139\\u213C-\\u213F\\u2145-\\u2149\\u214E\\u2160-\\u2188\\u2C00-\\u2C2E\\u2C30-\\u2C5E\\u2C60-\\u2CE4\\u2CEB-\\u2CEE\\u2CF2\\u2CF3\\u2D00-\\u2D25\\u2D27\\u2D2D\\u2D30-\\u2D67\\u2D6F\\u2D80-\\u2D96\\u2DA0-\\u2DA6\\u2DA8-\\u2DAE\\u2DB0-\\u2DB6\\u2DB8-\\u2DBE\\u2DC0-\\u2DC6\\u2DC8-\\u2DCE\\u2DD0-\\u2DD6\\u2DD8-\\u2DDE\\u2E2F\\u3005-\\u3007\\u3021-\\u3029\\u3031-\\u3035\\u3038-\\u303C\\u3041-\\u3096\\u309D-\\u309F\\u30A1-\\u30FA\\u30FC-\\u30FF\\u3105-\\u312D\\u3131-\\u318E\\u31A0-\\u31BA\\u31F0-\\u31FF\\u3400-\\u4DB5\\u4E00-\\u9FCC\\uA000-\\uA48C\\uA4D0-\\uA4FD\\uA500-\\uA60C\\uA610-\\uA61F\\uA62A\\uA62B\\uA640-\\uA66E\\uA67F-\\uA697\\uA6A0-\\uA6EF\\uA717-\\uA71F\\uA722-\\uA788\\uA78B-\\uA78E\\uA790-\\uA793\\uA7A0-\\uA7AA\\uA7F8-\\uA801\\uA803-\\uA805\\uA807-\\uA80A\\uA80C-\\uA822\\uA840-\\uA873\\uA882-\\uA8B3\\uA8F2-\\uA8F7\\uA8FB\\uA90A-\\uA925\\uA930-\\uA946\\uA960-\\uA97C\\uA984-\\uA9B2\\uA9CF\\uAA00-\\uAA28\\uAA40-\\uAA42\\uAA44-\\uAA4B\\uAA60-\\uAA76\\uAA7A\\uAA80-\\uAAAF\\uAAB1\\uAAB5\\uAAB6\\uAAB9-\\uAABD\\uAAC0\\uAAC2\\uAADB-\\uAADD\\uAAE0-\\uAAEA\\uAAF2-\\uAAF4\\uAB01-\\uAB06\\uAB09-\\uAB0E\\uAB11-\\uAB16\\uAB20-\\uAB26\\uAB28-\\uAB2E\\uABC0-\\uABE2\\uAC00-\\uD7A3\\uD7B0-\\uD7C6\\uD7CB-\\uD7FB\\uF900-\\uFA6D\\uFA70-\\uFAD9\\uFB00-\\uFB06\\uFB13-\\uFB17\\uFB1D\\uFB1F-\\uFB28\\uFB2A-\\uFB36\\uFB38-\\uFB3C\\uFB3E\\uFB40\\uFB41\\uFB43\\uFB44\\uFB46-\\uFBB1\\uFBD3-\\uFD3D\\uFD50-\\uFD8F\\uFD92-\\uFDC7\\uFDF0-\\uFDFB\\uFE70-\\uFE74\\uFE76-\\uFEFC\\uFF21-\\uFF3A\\uFF41-\\uFF5A\\uFF66-\\uFFBE\\uFFC2-\\uFFC7\\uFFCA-\\uFFCF\\uFFD2-\\uFFD7\\uFFDA-\\uFFDC';
export default { unicodeLetters }; export default { unicodeLetters };
...@@ -2,7 +2,7 @@ export default (fn, interval = 2000, timeout = 60000) => { ...@@ -2,7 +2,7 @@ export default (fn, interval = 2000, timeout = 60000) => {
const startTime = Date.now(); const startTime = Date.now();
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const stop = arg => ((arg instanceof Error) ? reject(arg) : resolve(arg)); const stop = arg => (arg instanceof Error ? reject(arg) : resolve(arg));
const next = () => { const next = () => {
if (Date.now() - startTime < timeout) { if (Date.now() - startTime < timeout) {
setTimeout(fn.bind(null, next, stop), interval); setTimeout(fn.bind(null, next, stop), interval);
......
...@@ -24,7 +24,11 @@ export const isSticky = (el, scrollY, stickyTop, insertPlaceholder) => { ...@@ -24,7 +24,11 @@ export const isSticky = (el, scrollY, stickyTop, insertPlaceholder) => {
} else if (top > stickyTop && el.classList.contains('is-stuck')) { } else if (top > stickyTop && el.classList.contains('is-stuck')) {
el.classList.remove('is-stuck'); el.classList.remove('is-stuck');
if (insertPlaceholder && el.nextElementSibling && el.nextElementSibling.classList.contains('sticky-placeholder')) { if (
insertPlaceholder &&
el.nextElementSibling &&
el.nextElementSibling.classList.contains('sticky-placeholder')
) {
el.nextElementSibling.remove(); el.nextElementSibling.remove();
} }
} }
...@@ -42,11 +46,19 @@ export const isSticky = (el, scrollY, stickyTop, insertPlaceholder) => { ...@@ -42,11 +46,19 @@ export const isSticky = (el, scrollY, stickyTop, insertPlaceholder) => {
export const stickyMonitor = (el, stickyTop, insertPlaceholder = true) => { export const stickyMonitor = (el, stickyTop, insertPlaceholder = true) => {
if (!el) return; if (!el) return;
if (typeof CSS === 'undefined' || !(CSS.supports('(position: -webkit-sticky) or (position: sticky)'))) return; if (
typeof CSS === 'undefined' ||
!CSS.supports('(position: -webkit-sticky) or (position: sticky)')
)
return;
document.addEventListener('scroll', () => isSticky(el, window.scrollY, stickyTop, insertPlaceholder), { document.addEventListener(
passive: true, 'scroll',
}); () => isSticky(el, window.scrollY, stickyTop, insertPlaceholder),
{
passive: true,
},
);
}; };
/** /**
...@@ -55,6 +67,6 @@ export const stickyMonitor = (el, stickyTop, insertPlaceholder = true) => { ...@@ -55,6 +67,6 @@ export const stickyMonitor = (el, stickyTop, insertPlaceholder = true) => {
* - If the current environment supports `position: sticky`, do nothing. * - If the current environment supports `position: sticky`, do nothing.
* - Can receive an iterable element list (NodeList, jQuery collection, etc.) or single HTMLElement. * - Can receive an iterable element list (NodeList, jQuery collection, etc.) or single HTMLElement.
*/ */
export const polyfillSticky = (el) => { export const polyfillSticky = el => {
StickyFill.add(el); StickyFill.add(el);
}; };
...@@ -8,12 +8,18 @@ function selectedText(text, textarea) { ...@@ -8,12 +8,18 @@ function selectedText(text, textarea) {
function lineBefore(text, textarea) { function lineBefore(text, textarea) {
var split; var split;
split = text.substring(0, textarea.selectionStart).trim().split('\n'); split = text
.substring(0, textarea.selectionStart)
.trim()
.split('\n');
return split[split.length - 1]; return split[split.length - 1];
} }
function lineAfter(text, textarea) { function lineAfter(text, textarea) {
return text.substring(textarea.selectionEnd).trim().split('\n')[0]; return text
.substring(textarea.selectionEnd)
.trim()
.split('\n')[0];
} }
function blockTagText(text, textArea, blockTag, selected) { function blockTagText(text, textArea, blockTag, selected) {
...@@ -27,7 +33,7 @@ function blockTagText(text, textArea, blockTag, selected) { ...@@ -27,7 +33,7 @@ function blockTagText(text, textArea, blockTag, selected) {
} }
return selected; return selected;
} else { } else {
return blockTag + "\n" + selected + "\n" + blockTag; return blockTag + '\n' + selected + '\n' + blockTag;
} }
} }
...@@ -58,7 +64,14 @@ function moveCursor({ textArea, tag, wrapped, removedLastNewLine, select }) { ...@@ -58,7 +64,14 @@ function moveCursor({ textArea, tag, wrapped, removedLastNewLine, select }) {
} }
export function insertMarkdownText({ textArea, text, tag, blockTag, selected, wrap, select }) { export function insertMarkdownText({ textArea, text, tag, blockTag, selected, wrap, select }) {
var textToInsert, inserted, selectedSplit, startChar, removedLastNewLine, removedFirstNewLine, currentLineEmpty, lastNewLine; var textToInsert,
inserted,
selectedSplit,
startChar,
removedLastNewLine,
removedFirstNewLine,
currentLineEmpty,
lastNewLine;
removedLastNewLine = false; removedLastNewLine = false;
removedFirstNewLine = false; removedFirstNewLine = false;
currentLineEmpty = false; currentLineEmpty = false;
...@@ -94,21 +107,23 @@ export function insertMarkdownText({ textArea, text, tag, blockTag, selected, wr ...@@ -94,21 +107,23 @@ export function insertMarkdownText({ textArea, text, tag, blockTag, selected, wr
if (blockTag != null && blockTag !== '') { if (blockTag != null && blockTag !== '') {
textToInsert = blockTagText(text, textArea, blockTag, selected); textToInsert = blockTagText(text, textArea, blockTag, selected);
} else { } else {
textToInsert = selectedSplit.map(function(val) { textToInsert = selectedSplit
if (tag.indexOf(textPlaceholder) > -1) { .map(function(val) {
return tag.replace(textPlaceholder, val); if (tag.indexOf(textPlaceholder) > -1) {
} return tag.replace(textPlaceholder, val);
if (val.indexOf(tag) === 0) { }
return "" + (val.replace(tag, '')); if (val.indexOf(tag) === 0) {
} else { return '' + val.replace(tag, '');
return "" + tag + val; } else {
} return '' + tag + val;
}).join('\n'); }
})
.join('\n');
} }
} else if (tag.indexOf(textPlaceholder) > -1) { } else if (tag.indexOf(textPlaceholder) > -1) {
textToInsert = tag.replace(textPlaceholder, selected); textToInsert = tag.replace(textPlaceholder, selected);
} else { } else {
textToInsert = "" + startChar + tag + selected + (wrap ? tag : ' '); textToInsert = '' + startChar + tag + selected + (wrap ? tag : ' ');
} }
if (removedFirstNewLine) { if (removedFirstNewLine) {
...@@ -120,7 +135,13 @@ export function insertMarkdownText({ textArea, text, tag, blockTag, selected, wr ...@@ -120,7 +135,13 @@ export function insertMarkdownText({ textArea, text, tag, blockTag, selected, wr
} }
insertText(textArea, textToInsert); insertText(textArea, textToInsert);
return moveCursor({ textArea, tag: tag.replace(textPlaceholder, selected), wrap, removedLastNewLine, select }); return moveCursor({
textArea,
tag: tag.replace(textPlaceholder, selected),
wrap,
removedLastNewLine,
select,
});
} }
function updateText({ textArea, tag, blockTag, wrap, select }) { function updateText({ textArea, tag, blockTag, wrap, select }) {
...@@ -138,15 +159,18 @@ function replaceRange(s, start, end, substitute) { ...@@ -138,15 +159,18 @@ function replaceRange(s, start, end, substitute) {
} }
export function addMarkdownListeners(form) { export function addMarkdownListeners(form) {
return $('.js-md', form).off('click').on('click', function() { return $('.js-md', form)
const $this = $(this); .off('click')
return updateText({ .on('click', function() {
textArea: $this.closest('.md-area').find('textarea'), const $this = $(this);
tag: $this.data('mdTag'), return updateText({
blockTag: $this.data('mdBlock'), textArea: $this.closest('.md-area').find('textarea'),
wrap: !$this.data('mdPrepend'), tag: $this.data('mdTag'),
select: $this.data('mdSelect') }); blockTag: $this.data('mdBlock'),
}); wrap: !$this.data('mdPrepend'),
select: $this.data('mdSelect'),
});
});
} }
export function removeMarkdownListeners(form) { export function removeMarkdownListeners(form) {
......
...@@ -8,7 +8,7 @@ ...@@ -8,7 +8,7 @@
* @returns {String} * @returns {String}
*/ */
export const addDelimiter = text => export const addDelimiter = text =>
(text ? text.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',') : text); text ? text.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',') : text;
/** /**
* Returns '99+' for numbers bigger than 99. * Returns '99+' for numbers bigger than 99.
...@@ -94,9 +94,7 @@ export function capitalizeFirstCharacter(text) { ...@@ -94,9 +94,7 @@ export function capitalizeFirstCharacter(text) {
* @return {String} * @return {String}
*/ */
export function getFirstCharacterCapitalized(text) { export function getFirstCharacterCapitalized(text) {
return text return text ? text.charAt(0).toUpperCase() : '';
? text.charAt(0).toUpperCase()
: '';
} }
/** /**
...@@ -136,10 +134,9 @@ export const convertToSentenceCase = string => { ...@@ -136,10 +134,9 @@ export const convertToSentenceCase = string => {
* e.g. HelloWorld => Hello World * e.g. HelloWorld => Hello World
* *
* @param {*} string * @param {*} string
*/ */
export const splitCamelCase = string => ( export const splitCamelCase = string =>
string string
.replace(/([A-Z]+)([A-Z][a-z])/g, ' $1 $2') .replace(/([A-Z]+)([A-Z][a-z])/g, ' $1 $2')
.replace(/([a-z\d])([A-Z])/g, '$1 $2') .replace(/([a-z\d])([A-Z])/g, '$1 $2')
.trim() .trim();
);
...@@ -26,7 +26,7 @@ initDateFormats(); ...@@ -26,7 +26,7 @@ initDateFormats();
see also https://github.com/d3/d3-3.x-api-reference/blob/master/SVG-Axes.md#tickFormat see also https://github.com/d3/d3-3.x-api-reference/blob/master/SVG-Axes.md#tickFormat
*/ */
export const dateTickFormat = (date) => { export const dateTickFormat = date => {
if (date.getDate() !== 1) { if (date.getDate() !== 1) {
return dateTimeFormats.dayFormat.format(date); return dateTimeFormats.dayFormat.format(date);
} }
......
...@@ -7,21 +7,20 @@ class UsersCache extends Cache { ...@@ -7,21 +7,20 @@ class UsersCache extends Cache {
return Promise.resolve(this.get(username)); return Promise.resolve(this.get(username));
} }
return Api.users('', { username }) return Api.users('', { username }).then(({ data }) => {
.then(({ data }) => { if (!data.length) {
if (!data.length) { throw new Error(`User "${username}" could not be found!`);
throw new Error(`User "${username}" could not be found!`); }
}
if (data.length > 1) { if (data.length > 1) {
throw new Error(`Expected username "${username}" to be unique!`); throw new Error(`Expected username "${username}" to be unique!`);
} }
const user = data[0]; const user = data[0];
this.internalStorage[username] = user; this.internalStorage[username] = user;
return user; return user;
}); });
// missing catch is intentional, error handling depends on use case // missing catch is intentional, error handling depends on use case
} }
} }
......
...@@ -6,7 +6,7 @@ import axios from '~/lib/utils/axios_utils'; ...@@ -6,7 +6,7 @@ import axios from '~/lib/utils/axios_utils';
import flash from '~/flash'; import flash from '~/flash';
import { __ } from '~/locale'; import { __ } from '~/locale';
((global) => { (global => {
global.mergeConflicts = global.mergeConflicts || {}; global.mergeConflicts = global.mergeConflicts || {};
global.mergeConflicts.diffFileEditor = Vue.extend({ global.mergeConflicts.diffFileEditor = Vue.extend({
...@@ -35,10 +35,10 @@ import { __ } from '~/locale'; ...@@ -35,10 +35,10 @@ import { __ } from '~/locale';
computed: { computed: {
classObject() { classObject() {
return { return {
'saved': this.saved, saved: this.saved,
'is-loading': this.loading 'is-loading': this.loading,
}; };
} },
}, },
watch: { watch: {
['file.showEditor'](val) { ['file.showEditor'](val) {
...@@ -49,7 +49,7 @@ import { __ } from '~/locale'; ...@@ -49,7 +49,7 @@ import { __ } from '~/locale';
} }
this.loadEditor(); this.loadEditor();
} },
}, },
mounted() { mounted() {
if (this.file.loadEditor) { if (this.file.loadEditor) {
...@@ -60,7 +60,8 @@ import { __ } from '~/locale'; ...@@ -60,7 +60,8 @@ import { __ } from '~/locale';
loadEditor() { loadEditor() {
this.loading = true; this.loading = true;
axios.get(this.file.content_path) axios
.get(this.file.content_path)
.then(({ data }) => { .then(({ data }) => {
const content = this.$el.querySelector('pre'); const content = this.$el.querySelector('pre');
const fileContent = document.createTextNode(data.content); const fileContent = document.createTextNode(data.content);
...@@ -101,7 +102,7 @@ import { __ } from '~/locale'; ...@@ -101,7 +102,7 @@ import { __ } from '~/locale';
}, },
acceptDiscardConfirmation(file) { acceptDiscardConfirmation(file) {
this.onAcceptDiscardConfirmation(file); this.onAcceptDiscardConfirmation(file);
} },
} },
}); });
})(window.gl || (window.gl = {})); })(window.gl || (window.gl = {}));
...@@ -4,7 +4,7 @@ import Vue from 'vue'; ...@@ -4,7 +4,7 @@ import Vue from 'vue';
import actionsMixin from '../mixins/line_conflict_actions'; import actionsMixin from '../mixins/line_conflict_actions';
import utilsMixin from '../mixins/line_conflict_utils'; import utilsMixin from '../mixins/line_conflict_utils';
((global) => { (global => {
global.mergeConflicts = global.mergeConflicts || {}; global.mergeConflicts = global.mergeConflicts || {};
global.mergeConflicts.parallelConflictLines = Vue.extend({ global.mergeConflicts.parallelConflictLines = Vue.extend({
......
...@@ -4,7 +4,7 @@ import $ from 'jquery'; ...@@ -4,7 +4,7 @@ import $ from 'jquery';
import Vue from 'vue'; import Vue from 'vue';
import Cookies from 'js-cookie'; import Cookies from 'js-cookie';
((global) => { (global => {
global.mergeConflicts = global.mergeConflicts || {}; global.mergeConflicts = global.mergeConflicts || {};
const diffViewType = Cookies.get('diff_view'); const diffViewType = Cookies.get('diff_view');
...@@ -17,11 +17,11 @@ import Cookies from 'js-cookie'; ...@@ -17,11 +17,11 @@ import Cookies from 'js-cookie';
const DEFAULT_RESOLVE_MODE = INTERACTIVE_RESOLVE_MODE; const DEFAULT_RESOLVE_MODE = INTERACTIVE_RESOLVE_MODE;
const VIEW_TYPES = { const VIEW_TYPES = {
INLINE: 'inline', INLINE: 'inline',
PARALLEL: 'parallel' PARALLEL: 'parallel',
}; };
const CONFLICT_TYPES = { const CONFLICT_TYPES = {
TEXT: 'text', TEXT: 'text',
TEXT_EDITOR: 'text-editor' TEXT_EDITOR: 'text-editor',
}; };
global.mergeConflicts.mergeConflictsStore = { global.mergeConflicts.mergeConflictsStore = {
...@@ -31,7 +31,7 @@ import Cookies from 'js-cookie'; ...@@ -31,7 +31,7 @@ import Cookies from 'js-cookie';
isSubmitting: false, isSubmitting: false,
isParallel: diffViewType === VIEW_TYPES.PARALLEL, isParallel: diffViewType === VIEW_TYPES.PARALLEL,
diffViewType: diffViewType, diffViewType: diffViewType,
conflictsData: {} conflictsData: {},
}, },
setConflictsData(data) { setConflictsData(data) {
...@@ -47,7 +47,7 @@ import Cookies from 'js-cookie'; ...@@ -47,7 +47,7 @@ import Cookies from 'js-cookie';
}, },
decorateFiles(files) { decorateFiles(files) {
files.forEach((file) => { files.forEach(file => {
file.content = ''; file.content = '';
file.resolutionData = {}; file.resolutionData = {};
file.promptDiscardConfirmation = false; file.promptDiscardConfirmation = false;
...@@ -72,7 +72,7 @@ import Cookies from 'js-cookie'; ...@@ -72,7 +72,7 @@ import Cookies from 'js-cookie';
setInlineLine(file) { setInlineLine(file) {
file.inlineLines = []; file.inlineLines = [];
file.sections.forEach((section) => { file.sections.forEach(section => {
let currentLineType = 'new'; let currentLineType = 'new';
const { conflict, lines, id } = section; const { conflict, lines, id } = section;
...@@ -80,7 +80,7 @@ import Cookies from 'js-cookie'; ...@@ -80,7 +80,7 @@ import Cookies from 'js-cookie';
file.inlineLines.push(this.getHeadHeaderLine(id)); file.inlineLines.push(this.getHeadHeaderLine(id));
} }
lines.forEach((line) => { lines.forEach(line => {
const { type } = line; const { type } = line;
if ((type === 'new' || type === 'old') && currentLineType !== type) { if ((type === 'new' || type === 'old') && currentLineType !== type) {
...@@ -102,7 +102,7 @@ import Cookies from 'js-cookie'; ...@@ -102,7 +102,7 @@ import Cookies from 'js-cookie';
file.parallelLines = []; file.parallelLines = [];
const linesObj = { left: [], right: [] }; const linesObj = { left: [], right: [] };
file.sections.forEach((section) => { file.sections.forEach(section => {
const { conflict, lines, id } = section; const { conflict, lines, id } = section;
if (conflict) { if (conflict) {
...@@ -110,7 +110,7 @@ import Cookies from 'js-cookie'; ...@@ -110,7 +110,7 @@ import Cookies from 'js-cookie';
linesObj.right.push(this.getHeadHeaderLine(id)); linesObj.right.push(this.getHeadHeaderLine(id));
} }
lines.forEach((line) => { lines.forEach(line => {
const { type } = line; const { type } = line;
if (conflict) { if (conflict) {
...@@ -131,10 +131,7 @@ import Cookies from 'js-cookie'; ...@@ -131,10 +131,7 @@ import Cookies from 'js-cookie';
}); });
for (let i = 0, len = linesObj.left.length; i < len; i += 1) { for (let i = 0, len = linesObj.left.length; i < len; i += 1) {
file.parallelLines.push([ file.parallelLines.push([linesObj.right[i], linesObj.left[i]]);
linesObj.right[i],
linesObj.left[i]
]);
} }
}, },
...@@ -159,9 +156,9 @@ import Cookies from 'js-cookie'; ...@@ -159,9 +156,9 @@ import Cookies from 'js-cookie';
const { files } = this.state.conflictsData; const { files } = this.state.conflictsData;
let count = 0; let count = 0;
files.forEach((file) => { files.forEach(file => {
if (file.type === CONFLICT_TYPES.TEXT) { if (file.type === CONFLICT_TYPES.TEXT) {
file.sections.forEach((section) => { file.sections.forEach(section => {
if (section.conflict) { if (section.conflict) {
count += 1; count += 1;
} }
...@@ -198,7 +195,7 @@ import Cookies from 'js-cookie'; ...@@ -198,7 +195,7 @@ import Cookies from 'js-cookie';
isHeader: true, isHeader: true,
isHead: true, isHead: true,
isSelected: false, isSelected: false,
isUnselected: false isUnselected: false,
}; };
}, },
...@@ -229,7 +226,7 @@ import Cookies from 'js-cookie'; ...@@ -229,7 +226,7 @@ import Cookies from 'js-cookie';
section: isHead ? 'head' : 'origin', section: isHead ? 'head' : 'origin',
richText: rich_text, richText: rich_text,
isSelected: false, isSelected: false,
isUnselected: false isUnselected: false,
}; };
}, },
...@@ -243,7 +240,7 @@ import Cookies from 'js-cookie'; ...@@ -243,7 +240,7 @@ import Cookies from 'js-cookie';
isHeader: true, isHeader: true,
isOrigin: true, isOrigin: true,
isSelected: false, isSelected: false,
isUnselected: false isUnselected: false,
}; };
}, },
...@@ -290,14 +287,14 @@ import Cookies from 'js-cookie'; ...@@ -290,14 +287,14 @@ import Cookies from 'js-cookie';
}, },
restoreFileLinesState(file) { restoreFileLinesState(file) {
file.inlineLines.forEach((line) => { file.inlineLines.forEach(line => {
if (line.hasConflict || line.isHeader) { if (line.hasConflict || line.isHeader) {
line.isSelected = false; line.isSelected = false;
line.isUnselected = false; line.isUnselected = false;
} }
}); });
file.parallelLines.forEach((lines) => { file.parallelLines.forEach(lines => {
const left = lines[0]; const left = lines[0];
const right = lines[1]; const right = lines[1];
const isLeftMatch = left.hasConflict || left.isHeader; const isLeftMatch = left.hasConflict || left.isHeader;
...@@ -354,7 +351,7 @@ import Cookies from 'js-cookie'; ...@@ -354,7 +351,7 @@ import Cookies from 'js-cookie';
const initial = 'Commit to source branch'; const initial = 'Commit to source branch';
const inProgress = 'Committing...'; const inProgress = 'Committing...';
return this.state ? this.state.isSubmitting ? inProgress : initial : initial; return this.state ? (this.state.isSubmitting ? inProgress : initial) : initial;
}, },
getCommitData() { getCommitData() {
...@@ -362,13 +359,13 @@ import Cookies from 'js-cookie'; ...@@ -362,13 +359,13 @@ import Cookies from 'js-cookie';
commitData = { commitData = {
commit_message: this.state.conflictsData.commitMessage, commit_message: this.state.conflictsData.commitMessage,
files: [] files: [],
}; };
this.state.conflictsData.files.forEach((file) => { this.state.conflictsData.files.forEach(file => {
const addFile = { const addFile = {
old_path: file.old_path, old_path: file.old_path,
new_path: file.new_path new_path: file.new_path,
}; };
if (file.type === CONFLICT_TYPES.TEXT) { if (file.type === CONFLICT_TYPES.TEXT) {
...@@ -391,13 +388,13 @@ import Cookies from 'js-cookie'; ...@@ -391,13 +388,13 @@ import Cookies from 'js-cookie';
handleSelected(file, sectionId, selection) { handleSelected(file, sectionId, selection) {
Vue.set(file.resolutionData, sectionId, selection); Vue.set(file.resolutionData, sectionId, selection);
file.inlineLines.forEach((line) => { file.inlineLines.forEach(line => {
if (line.id === sectionId && (line.hasConflict || line.isHeader)) { if (line.id === sectionId && (line.hasConflict || line.isHeader)) {
this.markLine(line, selection); this.markLine(line, selection);
} }
}); });
file.parallelLines.forEach((lines) => { file.parallelLines.forEach(lines => {
const left = lines[0]; const left = lines[0];
const right = lines[1]; const right = lines[1];
const hasSameId = right.id === sectionId || left.id === sectionId; const hasSameId = right.id === sectionId || left.id === sectionId;
...@@ -430,6 +427,6 @@ import Cookies from 'js-cookie'; ...@@ -430,6 +427,6 @@ import Cookies from 'js-cookie';
fileTextTypePresent() { fileTextTypePresent() {
return this.state.conflictsData.files.some(f => f.type === CONFLICT_TYPES.TEXT); return this.state.conflictsData.files.some(f => f.type === CONFLICT_TYPES.TEXT);
} },
}; };
})(window.gl || (window.gl = {})); })(window.gl || (window.gl = {}));
...@@ -148,7 +148,7 @@ export default { ...@@ -148,7 +148,7 @@ export default {
point = point.matrixTransform(this.$refs.graphData.getScreenCTM().inverse()); point = point.matrixTransform(this.$refs.graphData.getScreenCTM().inverse());
point.x += 7; point.x += 7;
this.seriesUnderMouse = this.timeSeries.filter((series) => { this.seriesUnderMouse = this.timeSeries.filter(series => {
const mouseX = series.timeSeriesScaleX.invert(point.x); const mouseX = series.timeSeriesScaleX.invert(point.x);
let minDistance = Infinity; let minDistance = Infinity;
...@@ -221,21 +221,18 @@ export default { ...@@ -221,21 +221,18 @@ export default {
.scale(axisYScale) .scale(axisYScale)
.ticks(measurements.yTicks); .ticks(measurements.yTicks);
d3 d3.select(this.$refs.baseSvg)
.select(this.$refs.baseSvg)
.select('.x-axis') .select('.x-axis')
.call(xAxis); .call(xAxis);
const width = this.graphWidth; const width = this.graphWidth;
d3 d3.select(this.$refs.baseSvg)
.select(this.$refs.baseSvg)
.select('.y-axis') .select('.y-axis')
.call(yAxis) .call(yAxis)
.selectAll('.tick') .selectAll('.tick')
.each(function createTickLines(d, i) { .each(function createTickLines(d, i) {
if (i > 0) { if (i > 0) {
d3 d3.select(this)
.select(this)
.select('line') .select('line')
.attr('x2', width) .attr('x2', width)
.attr('class', 'axis-tick'); .attr('class', 'axis-tick');
......
...@@ -38,38 +38,25 @@ export default { ...@@ -38,38 +38,25 @@ export default {
computed: { computed: {
textTransform() { textTransform() {
const yCoordinate = const yCoordinate =
(this.graphHeight - (this.graphHeight - this.margin.top + this.measurements.axisLabelLineOffset) / 2 || 0;
this.margin.top +
this.measurements.axisLabelLineOffset) /
2 || 0;
return `translate(15, ${yCoordinate}) rotate(-90)`; return `translate(15, ${yCoordinate}) rotate(-90)`;
}, },
rectTransform() { rectTransform() {
const yCoordinate = const yCoordinate =
(this.graphHeight - (this.graphHeight - this.margin.top + this.measurements.axisLabelLineOffset) / 2 +
this.margin.top +
this.measurements.axisLabelLineOffset) /
2 +
this.yLabelWidth / 2 || 0; this.yLabelWidth / 2 || 0;
return `translate(0, ${yCoordinate}) rotate(-90)`; return `translate(0, ${yCoordinate}) rotate(-90)`;
}, },
xPosition() { xPosition() {
return ( return (this.graphWidth + this.measurements.axisLabelLineOffset) / 2 - this.margin.right || 0;
(this.graphWidth + this.measurements.axisLabelLineOffset) / 2 -
this.margin.right || 0
);
}, },
yPosition() { yPosition() {
return ( return this.graphHeight - this.margin.top + this.measurements.axisLabelLineOffset || 0;
this.graphHeight -
this.margin.top +
this.measurements.axisLabelLineOffset || 0
);
}, },
yAxisLabelSentenceCase() { yAxisLabelSentenceCase() {
......
...@@ -92,7 +92,8 @@ export default { ...@@ -92,7 +92,8 @@ export default {
methods: { methods: {
seriesMetricValue(seriesIndex, series) { seriesMetricValue(seriesIndex, series) {
const indexFromCoordinates = this.currentCoordinates[series.metricTag] const indexFromCoordinates = this.currentCoordinates[series.metricTag]
? this.currentCoordinates[series.metricTag].currentDataIndex : 0; ? this.currentCoordinates[series.metricTag].currentDataIndex
: 0;
const index = this.deploymentFlagData const index = this.deploymentFlagData
? this.deploymentFlagData.seriesIndex ? this.deploymentFlagData.seriesIndex
: indexFromCoordinates; : indexFromCoordinates;
......
...@@ -26,4 +26,3 @@ export default { ...@@ -26,4 +26,3 @@ export default {
{{ summaryMetrics }} {{ summaryMetrics }}
</span> </span>
</template> </template>
...@@ -33,4 +33,3 @@ export default { ...@@ -33,4 +33,3 @@ export default {
</svg> </svg>
</td> </td>
</template> </template>
...@@ -6,7 +6,7 @@ const mixins = { ...@@ -6,7 +6,7 @@ const mixins = {
if (!this.reducedDeploymentData) return false; if (!this.reducedDeploymentData) return false;
let dataFound = false; let dataFound = false;
this.reducedDeploymentData = this.reducedDeploymentData.map((d) => { this.reducedDeploymentData = this.reducedDeploymentData.map(d => {
const deployment = d; const deployment = d;
if (d.xPos >= mouseXPos - 10 && d.xPos <= mouseXPos + 10 && !dataFound) { if (d.xPos >= mouseXPos - 10 && d.xPos <= mouseXPos + 10 && !dataFound) {
dataFound = d.xPos + 1; dataFound = d.xPos + 1;
...@@ -61,7 +61,7 @@ const mixins = { ...@@ -61,7 +61,7 @@ const mixins = {
this.currentCoordinates = {}; this.currentCoordinates = {};
this.seriesUnderMouse.forEach((series) => { this.seriesUnderMouse.forEach(series => {
const currentDataIndex = bisectDate(series.values, this.hoverData.hoveredDate); const currentDataIndex = bisectDate(series.values, this.hoverData.hoveredDate);
const currentData = series.values[currentDataIndex]; const currentData = series.values[currentDataIndex];
const currentX = Math.floor(series.timeSeriesScaleX(currentData.time)); const currentX = Math.floor(series.timeSeriesScaleX(currentData.time));
......
...@@ -8,18 +8,20 @@ const MAX_REQUESTS = 3; ...@@ -8,18 +8,20 @@ const MAX_REQUESTS = 3;
function backOffRequest(makeRequestCallback) { function backOffRequest(makeRequestCallback) {
let requestCounter = 0; let requestCounter = 0;
return backOff((next, stop) => { return backOff((next, stop) => {
makeRequestCallback().then((resp) => { makeRequestCallback()
if (resp.status === statusCodes.NO_CONTENT) { .then(resp => {
requestCounter += 1; if (resp.status === statusCodes.NO_CONTENT) {
if (requestCounter < MAX_REQUESTS) { requestCounter += 1;
next(); if (requestCounter < MAX_REQUESTS) {
next();
} else {
stop(new Error('Failed to connect to the prometheus server'));
}
} else { } else {
stop(new Error('Failed to connect to the prometheus server')); stop(resp);
} }
} else { })
stop(resp); .catch(stop);
}
}).catch(stop);
}); });
} }
...@@ -33,7 +35,7 @@ export default class MonitoringService { ...@@ -33,7 +35,7 @@ export default class MonitoringService {
getGraphsData() { getGraphsData() {
return backOffRequest(() => axios.get(this.metricsEndpoint)) return backOffRequest(() => axios.get(this.metricsEndpoint))
.then(resp => resp.data) .then(resp => resp.data)
.then((response) => { .then(response => {
if (!response || !response.data) { if (!response || !response.data) {
throw new Error(s__('Metrics|Unexpected metrics data response from prometheus endpoint')); throw new Error(s__('Metrics|Unexpected metrics data response from prometheus endpoint'));
} }
...@@ -47,22 +49,27 @@ export default class MonitoringService { ...@@ -47,22 +49,27 @@ export default class MonitoringService {
} }
return backOffRequest(() => axios.get(this.deploymentEndpoint)) return backOffRequest(() => axios.get(this.deploymentEndpoint))
.then(resp => resp.data) .then(resp => resp.data)
.then((response) => { .then(response => {
if (!response || !response.deployments) { if (!response || !response.deployments) {
throw new Error(s__('Metrics|Unexpected deployment data response from prometheus endpoint')); throw new Error(
s__('Metrics|Unexpected deployment data response from prometheus endpoint'),
);
} }
return response.deployments; return response.deployments;
}); });
} }
getEnvironmentsData() { getEnvironmentsData() {
return axios.get(this.environmentsEndpoint) return axios
.then(resp => resp.data) .get(this.environmentsEndpoint)
.then((response) => { .then(resp => resp.data)
if (!response || !response.environments) { .then(response => {
throw new Error(s__('Metrics|There was an error fetching the environments data, please try again')); if (!response || !response.environments) {
} throw new Error(
return response.environments; s__('Metrics|There was an error fetching the environments data, please try again'),
}); );
}
return response.environments;
});
} }
} }
export default { export default {
small: { // Covers both xs and sm screen sizes small: {
// Covers both xs and sm screen sizes
margin: { margin: {
top: 40, top: 40,
right: 40, right: 40,
...@@ -18,7 +19,8 @@ export default { ...@@ -18,7 +19,8 @@ export default {
}, },
axisLabelLineOffset: -20, axisLabelLineOffset: -20,
}, },
large: { // This covers both md and lg screen sizes large: {
// This covers both md and lg screen sizes
margin: { margin: {
top: 80, top: 80,
right: 80, right: 80,
......
...@@ -66,7 +66,8 @@ function queryTimeSeries(query, graphDrawData, lineStyle) { ...@@ -66,7 +66,8 @@ function queryTimeSeries(query, graphDrawData, lineStyle) {
// offset the same amount as the original data // offset the same amount as the original data
const [minX, maxX] = graphDrawData.xDom; const [minX, maxX] = graphDrawData.xDom;
const offset = d3.timeMinute(minX) - Number(minX); const offset = d3.timeMinute(minX) - Number(minX);
const datesWithoutGaps = d3.timeSecond.every(60) const datesWithoutGaps = d3.timeSecond
.every(60)
.range(d3.timeMinute.offset(minX, -1), maxX) .range(d3.timeMinute.offset(minX, -1), maxX)
.map(d => d - offset); .map(d => d - offset);
...@@ -208,9 +209,7 @@ export default function createTimeSeries(queries, graphWidth, graphHeight, graph ...@@ -208,9 +209,7 @@ export default function createTimeSeries(queries, graphWidth, graphHeight, graph
const timeSeries = queries.reduce((series, query, index) => { const timeSeries = queries.reduce((series, query, index) => {
const lineStyle = defaultStyleOrder[index % defaultStyleOrder.length]; const lineStyle = defaultStyleOrder[index % defaultStyleOrder.length];
return series.concat( return series.concat(queryTimeSeries(query, graphDrawData, lineStyle));
queryTimeSeries(query, graphDrawData, lineStyle),
);
}, []); }, []);
return { return {
......
<script> <script>
import Prism from '../../lib/highlight'; import Prism from '../../lib/highlight';
import Prompt from '../prompt.vue'; import Prompt from '../prompt.vue';
export default { export default {
components: { components: {
prompt: Prompt, prompt: Prompt,
},
props: {
count: {
type: Number,
required: false,
default: 0,
}, },
props: { codeCssClass: {
count: { type: String,
type: Number, required: false,
required: false, default: '',
default: 0,
},
codeCssClass: {
type: String,
required: false,
default: '',
},
type: {
type: String,
required: true,
},
rawCode: {
type: String,
required: true,
},
}, },
computed: { type: {
code() { type: String,
return this.rawCode; required: true,
}, },
promptType() { rawCode: {
const type = this.type.split('put')[0]; type: String,
required: true,
return type.charAt(0).toUpperCase() + type.slice(1);
},
}, },
mounted() { },
Prism.highlightElement(this.$refs.code); computed: {
code() {
return this.rawCode;
},
promptType() {
const type = this.type.split('put')[0];
return type.charAt(0).toUpperCase() + type.slice(1);
}, },
}; },
mounted() {
Prism.highlightElement(this.$refs.code);
},
};
</script> </script>
<template> <template>
......
<script> <script>
/* global katex */ /* global katex */
import marked from 'marked'; import marked from 'marked';
import sanitize from 'sanitize-html'; import sanitize from 'sanitize-html';
import Prompt from './prompt.vue'; import Prompt from './prompt.vue';
const renderer = new marked.Renderer(); const renderer = new marked.Renderer();
/* /*
Regex to match KaTex blocks. Regex to match KaTex blocks.
Supports the following: Supports the following:
...@@ -17,7 +17,7 @@ ...@@ -17,7 +17,7 @@
The matched text then goes through the KaTex renderer & then outputs the HTML The matched text then goes through the KaTex renderer & then outputs the HTML
*/ */
const katexRegexString = `( const katexRegexString = `(
^\\\\begin{[a-zA-Z]+}\\s ^\\\\begin{[a-zA-Z]+}\\s
| |
^\\$\\$ ^\\$\\$
...@@ -32,66 +32,69 @@ ...@@ -32,66 +32,69 @@
| |
\\$ \\$
) )
`.replace(/\s/g, '').trim(); `
.replace(/\s/g, '')
.trim();
renderer.paragraph = (t) => { renderer.paragraph = t => {
let text = t; let text = t;
let inline = false; let inline = false;
if (typeof katex !== 'undefined') { if (typeof katex !== 'undefined') {
const katexString = text.replace(/&amp;/g, '&') const katexString = text
.replace(/&=&/g, '\\space=\\space') .replace(/&amp;/g, '&')
.replace(/<(\/?)em>/g, '_'); .replace(/&=&/g, '\\space=\\space')
const regex = new RegExp(katexRegexString, 'gi'); .replace(/<(\/?)em>/g, '_');
const matchLocation = katexString.search(regex); const regex = new RegExp(katexRegexString, 'gi');
const numberOfMatches = katexString.match(regex); const matchLocation = katexString.search(regex);
const numberOfMatches = katexString.match(regex);
if (numberOfMatches && numberOfMatches.length !== 0) { if (numberOfMatches && numberOfMatches.length !== 0) {
if (matchLocation > 0) { if (matchLocation > 0) {
let matches = regex.exec(katexString); let matches = regex.exec(katexString);
inline = true; inline = true;
while (matches !== null) { while (matches !== null) {
const renderedKatex = katex.renderToString(matches[0].replace(/\$/g, '')); const renderedKatex = katex.renderToString(matches[0].replace(/\$/g, ''));
text = `${text.replace(matches[0], ` ${renderedKatex}`)}`; text = `${text.replace(matches[0], ` ${renderedKatex}`)}`;
matches = regex.exec(katexString); matches = regex.exec(katexString);
}
} else {
const matches = regex.exec(katexString);
text = katex.renderToString(matches[2]);
} }
} else {
const matches = regex.exec(katexString);
text = katex.renderToString(matches[2]);
} }
} }
}
return `<p class="${inline ? 'inline-katex' : ''}">${text}</p>`; return `<p class="${inline ? 'inline-katex' : ''}">${text}</p>`;
}; };
marked.setOptions({ marked.setOptions({
sanitize: true, sanitize: true,
renderer, renderer,
}); });
export default { export default {
components: { components: {
prompt: Prompt, prompt: Prompt,
}, },
props: { props: {
cell: { cell: {
type: Object, type: Object,
required: true, required: true,
},
}, },
computed: { },
markdown() { computed: {
return sanitize(marked(this.cell.source.join('').replace(/\\/g, '\\\\')), { markdown() {
allowedTags: false, return sanitize(marked(this.cell.source.join('').replace(/\\/g, '\\\\')), {
allowedAttributes: { allowedTags: false,
'*': ['class'], allowedAttributes: {
}, '*': ['class'],
}); },
}, });
}, },
}; },
};
</script> </script>
<template> <template>
...@@ -105,13 +108,13 @@ ...@@ -105,13 +108,13 @@
</template> </template>
<style> <style>
.markdown .katex { .markdown .katex {
display: block; display: block;
text-align: center; text-align: center;
} }
.markdown .inline-katex .katex { .markdown .inline-katex .katex {
display: inline; display: inline;
text-align: initial; text-align: initial;
} }
</style> </style>
<script> <script>
import sanitize from 'sanitize-html'; import sanitize from 'sanitize-html';
import Prompt from '../prompt.vue'; import Prompt from '../prompt.vue';
export default { export default {
components: { components: {
prompt: Prompt, prompt: Prompt,
},
props: {
rawCode: {
type: String,
required: true,
}, },
props: { },
rawCode: { computed: {
type: String, sanitizedOutput() {
required: true, return sanitize(this.rawCode, {
}, allowedTags: sanitize.defaults.allowedTags.concat(['img', 'svg']),
allowedAttributes: {
img: ['src'],
},
});
}, },
computed: { },
sanitizedOutput() { };
return sanitize(this.rawCode, {
allowedTags: sanitize.defaults.allowedTags.concat([
'img', 'svg',
]),
allowedAttributes: {
img: ['src'],
},
});
},
},
};
</script> </script>
<template> <template>
......
<script> <script>
import Prompt from '../prompt.vue'; import Prompt from '../prompt.vue';
export default { export default {
components: { components: {
prompt: Prompt, prompt: Prompt,
},
props: {
outputType: {
type: String,
required: true,
}, },
props: { rawCode: {
outputType: { type: String,
type: String, required: true,
required: true,
},
rawCode: {
type: String,
required: true,
},
}, },
}; },
};
</script> </script>
<template> <template>
......
<script> <script>
import CodeCell from '../code/index.vue'; import CodeCell from '../code/index.vue';
import Html from './html.vue'; import Html from './html.vue';
import Image from './image.vue'; import Image from './image.vue';
export default { export default {
components: { components: {
'code-cell': CodeCell, 'code-cell': CodeCell,
'html-output': Html, 'html-output': Html,
'image-output': Image, 'image-output': Image,
},
props: {
codeCssClass: {
type: String,
required: false,
default: '',
}, },
props: { count: {
codeCssClass: { type: Number,
type: String, required: false,
required: false, default: 0,
default: '',
},
count: {
type: Number,
required: false,
default: 0,
},
output: {
type: Object,
requred: true,
default: () => ({}),
},
}, },
computed: { output: {
componentName() { type: Object,
if (this.output.text) { requred: true,
return 'code-cell'; default: () => ({}),
} else if (this.output.data['image/png']) { },
return 'image-output'; },
} else if (this.output.data['text/html']) { computed: {
return 'html-output'; componentName() {
} else if (this.output.data['image/svg+xml']) { if (this.output.text) {
return 'html-output';
}
return 'code-cell'; return 'code-cell';
}, } else if (this.output.data['image/png']) {
rawCode() { return 'image-output';
if (this.output.text) { } else if (this.output.data['text/html']) {
return this.output.text.join(''); return 'html-output';
} } else if (this.output.data['image/svg+xml']) {
return 'html-output';
}
return this.dataForType(this.outputType); return 'code-cell';
}, },
outputType() { rawCode() {
if (this.output.text) { if (this.output.text) {
return ''; return this.output.text.join('');
} else if (this.output.data['image/png']) { }
return 'image/png';
} else if (this.output.data['text/html']) { return this.dataForType(this.outputType);
return 'text/html'; },
} else if (this.output.data['image/svg+xml']) { outputType() {
return 'image/svg+xml'; if (this.output.text) {
} return '';
} else if (this.output.data['image/png']) {
return 'image/png';
} else if (this.output.data['text/html']) {
return 'text/html';
} else if (this.output.data['image/svg+xml']) {
return 'image/svg+xml';
}
return 'text/plain'; return 'text/plain';
},
}, },
methods: { },
dataForType(type) { methods: {
let data = this.output.data[type]; dataForType(type) {
let data = this.output.data[type];
if (typeof data === 'object') { if (typeof data === 'object') {
data = data.join(''); data = data.join('');
} }
return data; return data;
},
}, },
}; },
};
</script> </script>
<template> <template>
......
<script> <script>
export default { export default {
props: { props: {
type: { type: {
type: String, type: String,
required: false, required: false,
default: '', default: '',
},
count: {
type: Number,
required: false,
default: 0,
},
}, },
computed: { count: {
hasKeys() { type: Number,
return this.type !== '' && this.count; required: false,
}, default: 0,
}, },
}; },
computed: {
hasKeys() {
return this.type !== '' && this.count;
},
},
};
</script> </script>
<template> <template>
...@@ -29,9 +29,9 @@ ...@@ -29,9 +29,9 @@
</template> </template>
<style scoped> <style scoped>
.prompt { .prompt {
padding: 0 10px; padding: 0 10px;
min-width: 7em; min-width: 7em;
font-family: monospace; font-family: monospace;
} }
</style> </style>
<script> <script>
import { import { MarkdownCell, CodeCell } from './cells';
MarkdownCell,
CodeCell,
} from './cells';
export default { export default {
components: { components: {
'code-cell': CodeCell, 'code-cell': CodeCell,
'markdown-cell': MarkdownCell, 'markdown-cell': MarkdownCell,
},
props: {
notebook: {
type: Object,
required: true,
}, },
props: { codeCssClass: {
notebook: { type: String,
type: Object, required: false,
required: true, default: '',
},
codeCssClass: {
type: String,
required: false,
default: '',
},
}, },
computed: { },
cells() { computed: {
if (this.notebook.worksheets) { cells() {
const data = { if (this.notebook.worksheets) {
cells: [], const data = {
}; cells: [],
};
return this.notebook.worksheets.reduce((cellData, sheet) => { return this.notebook.worksheets.reduce((cellData, sheet) => {
const cellDataCopy = cellData; const cellDataCopy = cellData;
cellDataCopy.cells = cellDataCopy.cells.concat(sheet.cells); cellDataCopy.cells = cellDataCopy.cells.concat(sheet.cells);
return cellDataCopy; return cellDataCopy;
}, data).cells; }, data).cells;
} }
return this.notebook.cells; return this.notebook.cells;
},
hasNotebook() {
return Object.keys(this.notebook).length;
},
}, },
methods: { hasNotebook() {
cellType(type) { return Object.keys(this.notebook).length;
return `${type}-cell`;
},
}, },
}; },
methods: {
cellType(type) {
return `${type}-cell`;
},
},
};
</script> </script>
<template> <template>
......
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