Commit cdc49388 authored by Filipa Lacerda's avatar Filipa Lacerda

[ci skip] Fix more rules

parent 318d6f44
...@@ -91,18 +91,21 @@ ...@@ -91,18 +91,21 @@
<template> <template>
<div class="cell text-cell"> <div class="cell text-cell">
<prompt /> <prompt />
<div class="markdown" v-html="markdown"></div> <div
class="markdown"
v-html="markdown">
</div>
</div> </div>
</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 Prompt from '../prompt.vue'; import Prompt from '../prompt.vue';
export default { export default {
props: { components: {
rawCode: { prompt: Prompt,
type: String,
required: true,
}, },
}, props: {
components: { rawCode: {
prompt: Prompt, type: String,
}, required: true,
}; },
},
};
</script> </script>
<template> <template>
......
<script> <script>
import Prompt from '../prompt.vue'; import Prompt from '../prompt.vue';
export default { export default {
props: { components: {
outputType: { prompt: Prompt,
type: String,
required: true,
}, },
rawCode: { props: {
type: String, outputType: {
required: true, type: String,
required: true,
},
rawCode: {
type: String,
required: true,
},
}, },
}, };
components: {
prompt: Prompt,
},
};
</script> </script>
<template> <template>
<div class="output"> <div class="output">
<prompt /> <prompt />
<img <img :src="'data:' + outputType + ';base64,' + rawCode" />
:src="'data:' + outputType + ';base64,' + rawCode" />
</div> </div>
</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 {
props: { components: {
codeCssClass: { 'code-cell': CodeCell,
type: String, 'html-output': Html,
required: false, 'image-output': Image,
default: '',
}, },
count: { props: {
type: Number, codeCssClass: {
required: false, type: String,
default: 0, required: false,
default: '',
},
count: {
type: Number,
required: false,
default: 0,
},
output: {
type: Object,
requred: true,
default: () => ({}),
},
}, },
output: { data() {
type: Object, return {
requred: true, outputType: '',
};
}, },
}, computed: {
components: { componentName() {
'code-cell': CodeCell, if (this.output.text) {
'html-output': Html, return 'code-cell';
'image-output': Image, } else if (this.output.data['image/png']) {
}, this.outputType = 'image/png';
data() {
return {
outputType: '',
};
},
computed: {
componentName() {
if (this.output.text) {
return 'code-cell';
} else if (this.output.data['image/png']) {
this.outputType = 'image/png';
return 'image-output'; return 'image-output';
} else if (this.output.data['text/html']) { } else if (this.output.data['text/html']) {
this.outputType = 'text/html'; this.outputType = 'text/html';
return 'html-output'; return 'html-output';
} else if (this.output.data['image/svg+xml']) { } else if (this.output.data['image/svg+xml']) {
this.outputType = 'image/svg+xml'; this.outputType = 'image/svg+xml';
return 'html-output'; return 'html-output';
} }
this.outputType = 'text/plain'; this.outputType = 'text/plain';
return 'code-cell'; return 'code-cell';
}, },
rawCode() { rawCode() {
if (this.output.text) { if (this.output.text) {
return this.output.text.join(''); return this.output.text.join('');
} }
return this.dataForType(this.outputType); return this.dataForType(this.outputType);
},
}, },
}, methods: {
methods: { dataForType(type) {
dataForType(type) { let data = this.output.data[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>
<component :is="componentName" <component
:is="componentName"
type="output" type="output"
:outputType="outputType" :output-type="outputType"
:count="count" :count="count"
:raw-code="rawCode" :raw-code="rawCode"
:code-css-class="codeCssClass" /> :code-css-class="codeCssClass"
/>
</template> </template>
...@@ -4,10 +4,17 @@ ...@@ -4,10 +4,17 @@
type: { type: {
type: String, type: String,
required: false, required: false,
default: '',
}, },
count: { count: {
type: Number, type: Number,
required: false, required: false,
default: 0,
},
},
computed: {
hasKeys() {
return this.type !== '' && this.count;
}, },
}, },
}; };
...@@ -15,16 +22,16 @@ ...@@ -15,16 +22,16 @@
<template> <template>
<div class="prompt"> <div class="prompt">
<span v-if="type && count"> <span v-if="hasKeys">
{{ type }} [{{ count }}]: {{ type }} [{{ count }}]:
</span> </span>
</div> </div>
</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>
...@@ -20,11 +20,6 @@ ...@@ -20,11 +20,6 @@
default: '', default: '',
}, },
}, },
methods: {
cellType(type) {
return `${type}-cell`;
},
},
computed: { computed: {
cells() { cells() {
if (this.notebook.worksheets) { if (this.notebook.worksheets) {
...@@ -45,6 +40,11 @@ ...@@ -45,6 +40,11 @@
return Object.keys(this.notebook).length; return Object.keys(this.notebook).length;
}, },
}, },
methods: {
cellType(type) {
return `${type}-cell`;
},
},
}; };
</script> </script>
......
...@@ -15,7 +15,17 @@ ...@@ -15,7 +15,17 @@
import issuableStateMixin from '../mixins/issuable_state'; import issuableStateMixin from '../mixins/issuable_state';
export default { export default {
name: 'commentForm', name: 'CommentForm',
components: {
issueWarning,
noteSignedOutWidget,
discussionLockedWidget,
markdownField,
userAvatarLink,
},
mixins: [
issuableStateMixin,
],
data() { data() {
return { return {
note: '', note: '',
...@@ -27,21 +37,6 @@ ...@@ -27,21 +37,6 @@
isSubmitButtonDisabled: true, isSubmitButtonDisabled: true,
}; };
}, },
components: {
issueWarning,
noteSignedOutWidget,
discussionLockedWidget,
markdownField,
userAvatarLink,
},
watch: {
note(newNote) {
this.setIsSubmitButtonDisabled(newNote, this.isSubmitting);
},
isSubmitting(newValue) {
this.setIsSubmitButtonDisabled(this.note, newValue);
},
},
computed: { computed: {
...mapGetters([ ...mapGetters([
'getCurrentUserLastNote', 'getCurrentUserLastNote',
...@@ -99,6 +94,23 @@ ...@@ -99,6 +94,23 @@
return this.getNoteableData.create_note_path; return this.getNoteableData.create_note_path;
}, },
}, },
watch: {
note(newNote) {
this.setIsSubmitButtonDisabled(newNote, this.isSubmitting);
},
isSubmitting(newValue) {
this.setIsSubmitButtonDisabled(this.note, newValue);
},
},
mounted() {
// jQuery is needed here because it is a custom event being dispatched with jQuery.
$(document).on('issuable:change', (e, isClosed) => {
this.issueState = isClosed ? constants.CLOSED : constants.REOPENED;
});
this.initAutoSave();
this.initTaskList();
},
methods: { methods: {
...mapActions([ ...mapActions([
'saveNote', 'saveNote',
...@@ -231,18 +243,6 @@ Please check your network connection and try again.`; ...@@ -231,18 +243,6 @@ Please check your network connection and try again.`;
}); });
}, },
}, },
mixins: [
issuableStateMixin,
],
mounted() {
// jQuery is needed here because it is a custom event being dispatched with jQuery.
$(document).on('issuable:change', (e, isClosed) => {
this.issueState = isClosed ? constants.CLOSED : constants.REOPENED;
});
this.initAutoSave();
this.initTaskList();
},
}; };
</script> </script>
...@@ -266,7 +266,7 @@ Please check your network connection and try again.`; ...@@ -266,7 +266,7 @@ Please check your network connection and try again.`;
:img-src="author.avatar_url" :img-src="author.avatar_url"
:img-alt="author.name" :img-alt="author.name"
:img-size="40" :img-size="40"
/> />
</div> </div>
<div class="timeline-content timeline-content-form"> <div class="timeline-content timeline-content-form">
<form <form
...@@ -310,7 +310,7 @@ Please check your network connection and try again.`; ...@@ -310,7 +310,7 @@ Please check your network connection and try again.`;
:disabled="isSubmitButtonDisabled" :disabled="isSubmitButtonDisabled"
class="btn btn-create comment-btn js-comment-button js-comment-submit-button" class="btn btn-create comment-btn js-comment-button js-comment-submit-button"
type="submit"> type="submit">
{{commentButtonTitle}} {{ commentButtonTitle }}
</button> </button>
<button <button
:disabled="isSubmitButtonDisabled" :disabled="isSubmitButtonDisabled"
...@@ -352,7 +352,7 @@ Please check your network connection and try again.`; ...@@ -352,7 +352,7 @@ Please check your network connection and try again.`;
<i <i
aria-hidden="true" aria-hidden="true"
class="fa fa-check icon"> class="fa fa-check icon">
</i> </i>
<div class="description"> <div class="description">
<strong>Start discussion</strong> <strong>Start discussion</strong>
<p> <p>
...@@ -370,7 +370,7 @@ Please check your network connection and try again.`; ...@@ -370,7 +370,7 @@ Please check your network connection and try again.`;
:class="actionButtonClassNames" :class="actionButtonClassNames"
:disabled="isSubmitting" :disabled="isSubmitting"
class="btn btn-comment btn-comment-and-close js-action-button"> class="btn btn-comment btn-comment-and-close js-action-button">
{{issueActionButtonTitle}} {{ issueActionButtonTitle }}
</button> </button>
<button <button
type="button" type="button"
......
...@@ -3,12 +3,12 @@ ...@@ -3,12 +3,12 @@
import Issuable from '~/vue_shared/mixins/issuable'; import Issuable from '~/vue_shared/mixins/issuable';
export default { export default {
mixins: [
Issuable,
],
components: { components: {
Icon, Icon,
}, },
mixins: [
Issuable,
],
}; };
</script> </script>
...@@ -18,9 +18,11 @@ ...@@ -18,9 +18,11 @@
<icon <icon
name="lock" name="lock"
:size="16" :size="16"
class="icon"> class="icon"
</icon> />
<span>This {{ issuableDisplayName }} is locked. Only <b>project members</b> can comment.</span> <span>
</span> This {{ issuableDisplayName }} is locked. Only <b>project members</b> can comment.
</span>
</span>
</div> </div>
</template> </template>
...@@ -9,7 +9,13 @@ ...@@ -9,7 +9,13 @@
import tooltip from '~/vue_shared/directives/tooltip'; import tooltip from '~/vue_shared/directives/tooltip';
export default { export default {
name: 'noteActions', name: 'NoteActions',
directives: {
tooltip,
},
components: {
loadingIcon,
},
props: { props: {
authorId: { authorId: {
type: Number, type: Number,
...@@ -41,12 +47,6 @@ ...@@ -41,12 +47,6 @@
required: true, required: true,
}, },
}, },
directives: {
tooltip,
},
components: {
loadingIcon,
},
computed: { computed: {
...mapGetters([ ...mapGetters([
'getUserDataByProp', 'getUserDataByProp',
...@@ -98,20 +98,21 @@ ...@@ -98,20 +98,21 @@
data-placement="bottom" data-placement="bottom"
data-container="body" data-container="body"
href="#" href="#"
title="Add reaction"> title="Add reaction"
<loading-icon :inline="true" /> >
<span <loading-icon :inline="true" />
v-html="emojiSmiling" <span
class="link-highlight award-control-icon-neutral"> v-html="emojiSmiling"
</span> class="link-highlight award-control-icon-neutral">
<span </span>
v-html="emojiSmiley" <span
class="link-highlight award-control-icon-positive"> v-html="emojiSmiley"
</span> class="link-highlight award-control-icon-positive">
<span </span>
v-html="emojiSmile" <span
class="link-highlight award-control-icon-super-positive"> v-html="emojiSmile"
</span> class="link-highlight award-control-icon-super-positive">
</span>
</a> </a>
</div> </div>
<div <div
...@@ -127,7 +128,8 @@ ...@@ -127,7 +128,8 @@
data-placement="bottom"> data-placement="bottom">
<span <span
v-html="editSvg" v-html="editSvg"
class="link-highlight"></span> class="link-highlight">
</span>
</button> </button>
</div> </div>
<div <div
...@@ -143,7 +145,8 @@ ...@@ -143,7 +145,8 @@
data-placement="bottom"> data-placement="bottom">
<span <span
class="icon" class="icon"
v-html="ellipsisSvg"></span> v-html="ellipsisSvg">
</span>
</button> </button>
<ul class="dropdown-menu more-actions-dropdown dropdown-open-left"> <ul class="dropdown-menu more-actions-dropdown dropdown-open-left">
<li v-if="canReportAsAbuse"> <li v-if="canReportAsAbuse">
......
<script> <script>
export default { export default {
name: 'noteAttachment', name: 'NoteAttachment',
props: { props: {
attachment: { attachment: {
type: Object, type: Object,
...@@ -19,7 +19,8 @@ ...@@ -19,7 +19,8 @@
rel="noopener noreferrer"> rel="noopener noreferrer">
<img <img
:src="attachment.url" :src="attachment.url"
class="note-image-attach" /> class="note-image-attach"
/>
</a> </a>
<div class="attachment"> <div class="attachment">
<a <a
...@@ -29,8 +30,9 @@ ...@@ -29,8 +30,9 @@
rel="noopener noreferrer"> rel="noopener noreferrer">
<i <i
class="fa fa-paperclip" class="fa fa-paperclip"
aria-hidden="true"></i> aria-hidden="true">
{{attachment.filename}} </i>
{{ attachment.filename }}
</a> </a>
</div> </div>
</div> </div>
......
...@@ -8,6 +8,9 @@ ...@@ -8,6 +8,9 @@
import tooltip from '../../vue_shared/directives/tooltip'; import tooltip from '../../vue_shared/directives/tooltip';
export default { export default {
directives: {
tooltip,
},
props: { props: {
awards: { awards: {
type: Array, type: Array,
...@@ -26,9 +29,6 @@ ...@@ -26,9 +29,6 @@
required: true, required: true,
}, },
}, },
directives: {
tooltip,
},
computed: { computed: {
...mapGetters([ ...mapGetters([
'getUserData', 'getUserData',
...@@ -73,6 +73,11 @@ ...@@ -73,6 +73,11 @@
return this.getUserData.id; return this.getUserData.id;
}, },
}, },
created() {
this.emojiSmiling = emojiSmiling;
this.emojiSmile = emojiSmile;
this.emojiSmiley = emojiSmiley;
},
methods: { methods: {
...mapActions([ ...mapActions([
'toggleAwardRequest', 'toggleAwardRequest',
...@@ -168,11 +173,6 @@ ...@@ -168,11 +173,6 @@
.catch(() => Flash('Something went wrong on our end.')); .catch(() => Flash('Something went wrong on our end.'));
}, },
}, },
created() {
this.emojiSmiling = emojiSmiling;
this.emojiSmile = emojiSmile;
this.emojiSmiley = emojiSmiley;
},
}; };
</script> </script>
...@@ -191,7 +191,7 @@ ...@@ -191,7 +191,7 @@
type="button"> type="button">
<span v-html="getAwardHTML(awardName)"></span> <span v-html="getAwardHTML(awardName)"></span>
<span class="award-control-text js-counter"> <span class="award-control-text js-counter">
{{awardList.length}} {{ awardList.length }}
</span> </span>
</button> </button>
<div <div
......
...@@ -7,6 +7,15 @@ ...@@ -7,6 +7,15 @@
import autosave from '../mixins/autosave'; import autosave from '../mixins/autosave';
export default { export default {
components: {
noteEditedText,
noteAwardsList,
noteAttachment,
noteForm,
},
mixins: [
autosave,
],
props: { props: {
note: { note: {
type: Object, type: Object,
...@@ -22,40 +31,11 @@ ...@@ -22,40 +31,11 @@
default: false, default: false,
}, },
}, },
mixins: [
autosave,
],
components: {
noteEditedText,
noteAwardsList,
noteAttachment,
noteForm,
},
computed: { computed: {
noteBody() { noteBody() {
return this.note.note; return this.note.note;
}, },
}, },
methods: {
renderGFM() {
$(this.$refs['note-body']).renderGFM();
},
initTaskList() {
if (this.canEdit) {
this.taskList = new TaskList({
dataType: 'note',
fieldName: 'note',
selector: '.notes',
});
}
},
handleFormUpdate(note, parentElement, callback) {
this.$emit('handleFormUpdate', note, parentElement, callback);
},
formCancelHandler(shouldConfirm, isDirty) {
this.$emit('cancelFormEdition', shouldConfirm, isDirty);
},
},
mounted() { mounted() {
this.renderGFM(); this.renderGFM();
this.initTaskList(); this.initTaskList();
...@@ -76,6 +56,26 @@ ...@@ -76,6 +56,26 @@
} }
} }
}, },
methods: {
renderGFM() {
$(this.$refs['note-body']).renderGFM();
},
initTaskList() {
if (this.canEdit) {
this.taskList = new TaskList({
dataType: 'note',
fieldName: 'note',
selector: '.notes',
});
}
},
handleFormUpdate(note, parentElement, callback) {
this.$emit('handleFormUpdate', note, parentElement, callback);
},
formCancelHandler(shouldConfirm, isDirty) {
this.$emit('cancelFormEdition', shouldConfirm, isDirty);
},
},
}; };
</script> </script>
...@@ -95,7 +95,7 @@ ...@@ -95,7 +95,7 @@
:is-editing="isEditing" :is-editing="isEditing"
:note-body="noteBody" :note-body="noteBody"
:note-id="note.id" :note-id="note.id"
/> />
<textarea <textarea
v-if="canEdit" v-if="canEdit"
v-model="note.note" v-model="note.note"
...@@ -106,17 +106,17 @@ ...@@ -106,17 +106,17 @@
:edited-at="note.last_edited_at" :edited-at="note.last_edited_at"
:edited-by="note.last_edited_by" :edited-by="note.last_edited_by"
action-text="Edited" action-text="Edited"
/> />
<note-awards-list <note-awards-list
v-if="note.award_emoji.length" v-if="note.award_emoji.length"
:note-id="note.id" :note-id="note.id"
:note-author-id="note.author.id" :note-author-id="note.author.id"
:awards="note.award_emoji" :awards="note.award_emoji"
:toggle-award-path="note.toggle_award_path" :toggle-award-path="note.toggle_award_path"
/> />
<note-attachment <note-attachment
v-if="note.attachment" v-if="note.attachment"
:attachment="note.attachment" :attachment="note.attachment"
/> />
</div> </div>
</template> </template>
...@@ -2,7 +2,10 @@ ...@@ -2,7 +2,10 @@
import timeAgoTooltip from '../../vue_shared/components/time_ago_tooltip.vue'; import timeAgoTooltip from '../../vue_shared/components/time_ago_tooltip.vue';
export default { export default {
name: 'editedNoteText', name: 'EditedNoteText',
components: {
timeAgoTooltip,
},
props: { props: {
actionText: { actionText: {
type: String, type: String,
...@@ -15,6 +18,7 @@ ...@@ -15,6 +18,7 @@
editedBy: { editedBy: {
type: Object, type: Object,
required: false, required: false,
default: () => ({}),
}, },
className: { className: {
type: String, type: String,
...@@ -22,25 +26,22 @@ ...@@ -22,25 +26,22 @@
default: 'edited-text', default: 'edited-text',
}, },
}, },
components: {
timeAgoTooltip,
},
}; };
</script> </script>
<template> <template>
<div :class="className"> <div :class="className">
{{actionText}} {{ actionText }}
<time-ago-tooltip <time-ago-tooltip
:time="editedAt" :time="editedAt"
tooltip-placement="bottom" tooltip-placement="bottom"
/> />
<template v-if="editedBy"> <template v-if="editedBy">
by by
<a <a
:href="editedBy.path" :href="editedBy.path"
class="js-vue-author author_link"> class="js-vue-author author_link">
{{editedBy.name}} {{ editedBy.name }}
</a> </a>
</template> </template>
</div> </div>
......
...@@ -6,7 +6,14 @@ ...@@ -6,7 +6,14 @@
import issuableStateMixin from '../mixins/issuable_state'; import issuableStateMixin from '../mixins/issuable_state';
export default { export default {
name: 'issueNoteForm', name: 'IssueNoteForm',
components: {
issueWarning,
markdownField,
},
mixins: [
issuableStateMixin,
],
props: { props: {
noteBody: { noteBody: {
type: String, type: String,
...@@ -16,6 +23,7 @@ ...@@ -16,6 +23,7 @@
noteId: { noteId: {
type: Number, type: Number,
required: false, required: false,
default: 0,
}, },
saveButtonTitle: { saveButtonTitle: {
type: String, type: String,
...@@ -39,10 +47,6 @@ ...@@ -39,10 +47,6 @@
isSubmitting: false, isSubmitting: false,
}; };
}, },
components: {
issueWarning,
markdownField,
},
computed: { computed: {
...mapGetters([ ...mapGetters([
'getDiscussionLastNote', 'getDiscussionLastNote',
...@@ -70,6 +74,18 @@ ...@@ -70,6 +74,18 @@
return !this.note.length || this.isSubmitting; return !this.note.length || this.isSubmitting;
}, },
}, },
watch: {
noteBody() {
if (this.note === this.noteBody) {
this.note = this.noteBody;
} else {
this.conflictWhileEditing = true;
}
},
},
mounted() {
this.$refs.textarea.focus();
},
methods: { methods: {
handleUpdate() { handleUpdate() {
this.isSubmitting = true; this.isSubmitting = true;
...@@ -94,26 +110,13 @@ ...@@ -94,26 +110,13 @@
this.$emit('cancelFormEdition', shouldConfirm, this.noteBody !== this.note); this.$emit('cancelFormEdition', shouldConfirm, this.noteBody !== this.note);
}, },
}, },
mixins: [
issuableStateMixin,
],
mounted() {
this.$refs.textarea.focus();
},
watch: {
noteBody() {
if (this.note === this.noteBody) {
this.note = this.noteBody;
} else {
this.conflictWhileEditing = true;
}
},
},
}; };
</script> </script>
<template> <template>
<div ref="editNoteForm" class="note-edit-form current-note-edit-form"> <div
ref="editNoteForm"
class="note-edit-form current-note-edit-form">
<div <div
v-if="conflictWhileEditing" v-if="conflictWhileEditing"
class="js-conflict-edit-warning alert alert-danger"> class="js-conflict-edit-warning alert alert-danger">
...@@ -121,12 +124,13 @@ ...@@ -121,12 +124,13 @@
<a <a
:href="noteHash" :href="noteHash"
target="_blank" target="_blank"
rel="noopener noreferrer">updated comment</a> rel="noopener noreferrer">
to ensure information is not lost. updated comment
</a>
to ensure information is not lost.
</div> </div>
<div class="flash-container timeline-content"></div> <div class="flash-container timeline-content"></div>
<form <form class="edit-note common-note-form js-quick-submit gfm-form">
class="edit-note common-note-form js-quick-submit gfm-form">
<issue-warning <issue-warning
v-if="hasWarning(getNoteableData)" v-if="hasWarning(getNoteableData)"
...@@ -160,7 +164,7 @@ ...@@ -160,7 +164,7 @@
@click="handleUpdate()" @click="handleUpdate()"
:disabled="isDisabled" :disabled="isDisabled"
class="js-vue-issue-save btn btn-save"> class="js-vue-issue-save btn btn-save">
{{saveButtonTitle}} {{ saveButtonTitle }}
</button> </button>
<button <button
@click="cancelHandler()" @click="cancelHandler()"
......
...@@ -3,6 +3,9 @@ ...@@ -3,6 +3,9 @@
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: {
timeAgoTooltip,
},
props: { props: {
author: { author: {
type: Object, type: Object,
...@@ -37,9 +40,6 @@ ...@@ -37,9 +40,6 @@
isExpanded: true, isExpanded: true,
}; };
}, },
components: {
timeAgoTooltip,
},
computed: { computed: {
toggleChevronClass() { toggleChevronClass() {
return this.isExpanded ? 'fa-chevron-up' : 'fa-chevron-down'; return this.isExpanded ? 'fa-chevron-up' : 'fa-chevron-down';
...@@ -67,16 +67,16 @@ ...@@ -67,16 +67,16 @@
<div class="note-header-info"> <div class="note-header-info">
<a :href="author.path"> <a :href="author.path">
<span class="note-header-author-name"> <span class="note-header-author-name">
{{author.name}} {{ author.name }}
</span> </span>
<span class="note-headline-light"> <span class="note-headline-light">
@{{author.username}} @{{ author.username }}
</span> </span>
</a> </a>
<span class="note-headline-light"> <span class="note-headline-light">
<span class="note-headline-meta"> <span class="note-headline-meta">
<template v-if="actionText"> <template v-if="actionText">
{{actionText}} {{ actionText }}
</template> </template>
<span <span
v-if="actionTextHtml" v-if="actionTextHtml"
...@@ -90,12 +90,13 @@ ...@@ -90,12 +90,13 @@
<time-ago-tooltip <time-ago-tooltip
:time="createdAt" :time="createdAt"
tooltip-placement="bottom" tooltip-placement="bottom"
/> />
</a> </a>
<i <i
class="fa fa-spinner fa-spin editing-spinner" class="fa fa-spinner fa-spin editing-spinner"
aria-label="Comment is being updated" aria-label="Comment is being updated"
aria-hidden="true"> aria-hidden="true"
>
</i> </i>
</span> </span>
</span> </span>
...@@ -106,12 +107,12 @@ ...@@ -106,12 +107,12 @@
@click="handleToggle" @click="handleToggle"
class="note-action-button discussion-toggle-button js-vue-toggle-button" class="note-action-button discussion-toggle-button js-vue-toggle-button"
type="button"> type="button">
<i <i
:class="toggleChevronClass" :class="toggleChevronClass"
class="fa" class="fa"
aria-hidden="true"> aria-hidden="true">
</i> </i>
Toggle discussion Toggle discussion
</button> </button>
</div> </div>
</div> </div>
......
...@@ -13,17 +13,6 @@ ...@@ -13,17 +13,6 @@
import autosave from '../mixins/autosave'; import autosave from '../mixins/autosave';
export default { export default {
props: {
note: {
type: Object,
required: true,
},
},
data() {
return {
isReplying: false,
};
},
components: { components: {
noteableNote, noteableNote,
userAvatarLink, userAvatarLink,
...@@ -37,6 +26,17 @@ ...@@ -37,6 +26,17 @@
mixins: [ mixins: [
autosave, autosave,
], ],
props: {
note: {
type: Object,
required: true,
},
},
data() {
return {
isReplying: false,
};
},
computed: { computed: {
...mapGetters([ ...mapGetters([
'getNoteableData', 'getNoteableData',
...@@ -72,6 +72,20 @@ ...@@ -72,6 +72,20 @@
return null; return null;
}, },
}, },
mounted() {
if (this.isReplying) {
this.initAutoSave();
}
},
updated() {
if (this.isReplying) {
if (!this.autosave) {
this.initAutoSave();
} else {
this.setAutoSave();
}
}
},
methods: { methods: {
...mapActions([ ...mapActions([
'saveNote', 'saveNote',
...@@ -139,20 +153,6 @@ Please check your network connection and try again.`; ...@@ -139,20 +153,6 @@ Please check your network connection and try again.`;
}); });
}, },
}, },
mounted() {
if (this.isReplying) {
this.initAutoSave();
}
},
updated() {
if (this.isReplying) {
if (!this.autosave) {
this.initAutoSave();
} else {
this.setAutoSave();
}
}
},
}; };
</script> </script>
...@@ -165,7 +165,7 @@ Please check your network connection and try again.`; ...@@ -165,7 +165,7 @@ Please check your network connection and try again.`;
:img-src="author.avatar_url" :img-src="author.avatar_url"
:img-alt="author.name" :img-alt="author.name"
:img-size="40" :img-size="40"
/> />
</div> </div>
<div class="timeline-content"> <div class="timeline-content">
<div class="discussion"> <div class="discussion">
...@@ -185,42 +185,43 @@ Please check your network connection and try again.`; ...@@ -185,42 +185,43 @@ Please check your network connection and try again.`;
:edited-by="lastUpdatedBy" :edited-by="lastUpdatedBy"
action-text="Last updated" action-text="Last updated"
class-name="discussion-headline-light js-discussion-headline" class-name="discussion-headline-light js-discussion-headline"
/> />
</div>
</div> </div>
<div </div>
v-if="note.expanded" <div
class="discussion-body"> v-if="note.expanded"
<div class="panel panel-default"> class="discussion-body">
<div class="discussion-notes"> <div class="panel panel-default">
<ul class="notes"> <div class="discussion-notes">
<component <ul class="notes">
v-for="note in note.notes" <component
:is="componentName(note)" v-for="note in note.notes"
:note="componentData(note)" :is="componentName(note)"
:key="note.id" :note="componentData(note)"
/> :key="note.id"
</ul> />
<div </ul>
:class="{ 'is-replying': isReplying }" <div
class="discussion-reply-holder"> :class="{ 'is-replying': isReplying }"
<button class="discussion-reply-holder">
v-if="canReply && !isReplying" <button
@click="showReplyForm" v-if="canReply && !isReplying"
type="button" @click="showReplyForm"
class="js-vue-discussion-reply btn btn-text-field" type="button"
title="Add a reply">Reply...</button> class="js-vue-discussion-reply btn btn-text-field"
<note-form title="Add a reply">
v-if="isReplying" Reply...
save-button-title="Comment" </button>
:discussion="note" <note-form
:is-editing="false" v-if="isReplying"
@handleFormUpdate="saveReply" save-button-title="Comment"
@cancelFormEdition="cancelReplyForm" :discussion="note"
ref="noteForm" :is-editing="false"
/> @handleFormUpdate="saveReply"
<note-signed-out-widget v-if="!canReply" /> @cancelFormEdition="cancelReplyForm"
</div> ref="noteForm"
/>
<note-signed-out-widget v-if="!canReply" />
</div> </div>
</div> </div>
</div> </div>
......
...@@ -9,6 +9,12 @@ ...@@ -9,6 +9,12 @@
import eventHub from '../event_hub'; import eventHub from '../event_hub';
export default { export default {
components: {
userAvatarLink,
noteHeader,
noteActions,
noteBody,
},
props: { props: {
note: { note: {
type: Object, type: Object,
...@@ -22,12 +28,6 @@ ...@@ -22,12 +28,6 @@
isRequesting: false, isRequesting: false,
}; };
}, },
components: {
userAvatarLink,
noteHeader,
noteActions,
noteBody,
},
computed: { computed: {
...mapGetters([ ...mapGetters([
'targetNoteHash', 'targetNoteHash',
...@@ -51,6 +51,16 @@ ...@@ -51,6 +51,16 @@
return `note_${this.note.id}`; return `note_${this.note.id}`;
}, },
}, },
created() {
eventHub.$on('enterEditMode', ({ noteId }) => {
if (noteId === this.note.id) {
this.isEditing = true;
this.scrollToNoteIfNeeded($(this.$el));
}
});
},
methods: { methods: {
...mapActions([ ...mapActions([
'deleteNote', 'deleteNote',
...@@ -126,14 +136,6 @@ ...@@ -126,14 +136,6 @@
this.$refs.noteBody.$refs.noteForm.note = noteText; this.$refs.noteBody.$refs.noteForm.note = noteText;
}, },
}, },
created() {
eventHub.$on('enterEditMode', ({ noteId }) => {
if (noteId === this.note.id) {
this.isEditing = true;
this.scrollToNoteIfNeeded($(this.$el));
}
});
},
}; };
</script> </script>
...@@ -150,7 +152,7 @@ ...@@ -150,7 +152,7 @@
:img-src="author.avatar_url" :img-src="author.avatar_url"
:img-alt="author.name" :img-alt="author.name"
:img-size="40" :img-size="40"
/> />
</div> </div>
<div class="timeline-content"> <div class="timeline-content">
<div class="note-header"> <div class="note-header">
...@@ -159,7 +161,7 @@ ...@@ -159,7 +161,7 @@
:created-at="note.created_at" :created-at="note.created_at"
:note-id="note.id" :note-id="note.id"
action-text="commented" action-text="commented"
/> />
<note-actions <note-actions
:author-id="author.id" :author-id="author.id"
:note-id="note.id" :note-id="note.id"
...@@ -170,7 +172,7 @@ ...@@ -170,7 +172,7 @@
:report-abuse-path="note.report_abuse_path" :report-abuse-path="note.report_abuse_path"
@handleEdit="editHandler" @handleEdit="editHandler"
@handleDelete="deleteHandler" @handleDelete="deleteHandler"
/> />
</div> </div>
<note-body <note-body
:note="note" :note="note"
...@@ -179,7 +181,7 @@ ...@@ -179,7 +181,7 @@
@handleFormUpdate="formUpdateHandler" @handleFormUpdate="formUpdateHandler"
@cancelFormEdition="formCancelHandler" @cancelFormEdition="formCancelHandler"
ref="noteBody" ref="noteBody"
/> />
</div> </div>
</div> </div>
</li> </li>
......
...@@ -13,7 +13,16 @@ ...@@ -13,7 +13,16 @@
import loadingIcon from '../../vue_shared/components/loading_icon.vue'; import loadingIcon from '../../vue_shared/components/loading_icon.vue';
export default { export default {
name: 'notesApp', name: 'NotesApp',
components: {
noteableNote,
noteableDiscussion,
systemNote,
commentForm,
loadingIcon,
placeholderNote,
placeholderSystemNote,
},
props: { props: {
noteableData: { noteableData: {
type: Object, type: Object,
...@@ -26,7 +35,7 @@ ...@@ -26,7 +35,7 @@
userData: { userData: {
type: Object, type: Object,
required: false, required: false,
default: {}, default: () => ({}),
}, },
}, },
store, store,
...@@ -35,21 +44,30 @@ ...@@ -35,21 +44,30 @@
isLoading: true, isLoading: true,
}; };
}, },
components: {
noteableNote,
noteableDiscussion,
systemNote,
commentForm,
loadingIcon,
placeholderNote,
placeholderSystemNote,
},
computed: { computed: {
...mapGetters([ ...mapGetters([
'notes', 'notes',
'getNotesDataByProp', 'getNotesDataByProp',
]), ]),
}, },
created() {
this.setNotesData(this.notesData);
this.setNoteableData(this.noteableData);
this.setUserData(this.userData);
},
mounted() {
this.fetchNotes();
const parentElement = this.$el.parentElement;
if (parentElement &&
parentElement.classList.contains('js-vue-notes-event')) {
parentElement.addEventListener('toggleAward', (event) => {
const { awardName, noteId } = event.detail;
this.actionToggleAward({ awardName, noteId });
});
}
},
methods: { methods: {
...mapActions({ ...mapActions({
actionFetchNotes: 'fetchNotes', actionFetchNotes: 'fetchNotes',
...@@ -105,24 +123,6 @@ ...@@ -105,24 +123,6 @@
} }
}, },
}, },
created() {
this.setNotesData(this.notesData);
this.setNoteableData(this.noteableData);
this.setUserData(this.userData);
},
mounted() {
this.fetchNotes();
const parentElement = this.$el.parentElement;
if (parentElement &&
parentElement.classList.contains('js-vue-notes-event')) {
parentElement.addEventListener('toggleAward', (event) => {
const { awardName, noteId } = event.detail;
this.actionToggleAward({ awardName, noteId });
});
}
},
}; };
</script> </script>
...@@ -144,7 +144,7 @@ ...@@ -144,7 +144,7 @@
:is="getComponentName(note)" :is="getComponentName(note)"
:note="getComponentData(note)" :note="getComponentData(note)"
:key="note.id" :key="note.id"
/> />
</ul> </ul>
<comment-form /> <comment-form />
......
...@@ -5,6 +5,7 @@ ...@@ -5,6 +5,7 @@
import page from './page/index.vue'; import page from './page/index.vue';
export default { export default {
components: { page },
props: { props: {
pdf: { pdf: {
type: [String, Uint8Array], type: [String, Uint8Array],
...@@ -17,8 +18,6 @@ ...@@ -17,8 +18,6 @@
pages: [], pages: [],
}; };
}, },
components: { page },
watch: { pdf: 'load' },
computed: { computed: {
document() { document() {
return typeof this.pdf === 'string' ? this.pdf : { data: this.pdf }; return typeof this.pdf === 'string' ? this.pdf : { data: this.pdf };
...@@ -27,6 +26,11 @@ ...@@ -27,6 +26,11 @@
return this.pdf && this.pdf.length > 0; return this.pdf && this.pdf.length > 0;
}, },
}, },
watch: { pdf: 'load' },
mounted() {
pdfjsLib.PDFJS.workerSrc = workerSrc;
if (this.hasPDF) this.load();
},
methods: { methods: {
load() { load() {
this.pages = []; this.pages = [];
...@@ -47,20 +51,20 @@ ...@@ -47,20 +51,20 @@
return Promise.all(pagePromises); return Promise.all(pagePromises);
}, },
}, },
mounted() {
pdfjsLib.PDFJS.workerSrc = workerSrc;
if (this.hasPDF) this.load();
},
}; };
</script> </script>
<template> <template>
<div class="pdf-viewer" v-if="hasPDF"> <div
<page v-for="(page, index) in pages" class="pdf-viewer"
v-if="hasPDF">
<page
v-for="(page, index) in pages"
:key="index" :key="index"
:v-if="!loading" :v-if="!loading"
:page="page" :page="page"
:number="index + 1" /> :number="index + 1"
/>
</div> </div>
</template> </template>
......
...@@ -32,6 +32,20 @@ ...@@ -32,6 +32,20 @@
return !!(this.customInputEnabled || !this.intervalIsPreset); return !!(this.customInputEnabled || !this.intervalIsPreset);
}, },
}, },
watch: {
cronInterval() {
// updates field validation state when model changes, as
// glFieldError only updates on input.
this.$nextTick(() => {
gl.pipelineScheduleFieldErrors.updateFormValidityState();
});
},
},
created() {
if (this.intervalIsPreset) {
this.enableCustomInput = false;
}
},
methods: { methods: {
toggleCustomInput(shouldEnable) { toggleCustomInput(shouldEnable) {
this.customInputEnabled = shouldEnable; this.customInputEnabled = shouldEnable;
...@@ -43,20 +57,6 @@ ...@@ -43,20 +57,6 @@
} }
}, },
}, },
created() {
if (this.intervalIsPreset) {
this.enableCustomInput = false;
}
},
watch: {
cronInterval() {
// updates field validation state when model changes, as
// glFieldError only updates on input.
this.$nextTick(() => {
gl.pipelineScheduleFieldErrors.updateFormValidityState();
});
},
},
}; };
</script> </script>
...@@ -78,7 +78,12 @@ ...@@ -78,7 +78,12 @@
</label> </label>
<span class="cron-syntax-link-wrap"> <span class="cron-syntax-link-wrap">
(<a :href="cronSyntaxUrl" target="_blank">{{ __('Cron syntax') }}</a>) (<a
:href="cronSyntaxUrl"
target="_blank"
>
{{ __('Cron syntax') }}
</a>)
</span> </span>
</div> </div>
...@@ -93,7 +98,10 @@ ...@@ -93,7 +98,10 @@
@click="toggleCustomInput(false)" @click="toggleCustomInput(false)"
/> />
<label class="label-light" for="every-day"> <label
class="label-light"
for="every-day"
>
{{ __('Every day (at 4:00am)') }} {{ __('Every day (at 4:00am)') }}
</label> </label>
</div> </div>
...@@ -109,7 +117,10 @@ ...@@ -109,7 +117,10 @@
@click="toggleCustomInput(false)" @click="toggleCustomInput(false)"
/> />
<label class="label-light" for="every-week"> <label
class="label-light"
for="every-week"
>
{{ __('Every week (Sundays at 4:00am)') }} {{ __('Every week (Sundays at 4:00am)') }}
</label> </label>
</div> </div>
...@@ -125,7 +136,10 @@ ...@@ -125,7 +136,10 @@
@click="toggleCustomInput(false)" @click="toggleCustomInput(false)"
/> />
<label class="label-light" for="every-month"> <label
class="label-light"
for="every-month"
>
{{ __('Every month (on the 1st at 4:00am)') }} {{ __('Every month (on the 1st at 4:00am)') }}
</label> </label>
</div> </div>
......
...@@ -16,15 +16,15 @@ ...@@ -16,15 +16,15 @@
calloutDismissed: Cookies.get(cookieKey) === 'true', calloutDismissed: Cookies.get(cookieKey) === 'true',
}; };
}, },
created() {
this.illustrationSvg = illustrationSvg;
},
methods: { methods: {
dismissCallout() { dismissCallout() {
this.calloutDismissed = true; this.calloutDismissed = true;
Cookies.set(cookieKey, this.calloutDismissed, { expires: 365 }); Cookies.set(cookieKey, this.calloutDismissed, { expires: 365 });
}, },
}, },
created() {
this.illustrationSvg = illustrationSvg;
},
}; };
</script> </script>
<template> <template>
...@@ -41,17 +41,23 @@ ...@@ -41,17 +41,23 @@
class="fa fa-times"> class="fa fa-times">
</i> </i>
</button> </button>
<div class="svg-container" v-html="illustrationSvg"></div> <div
class="svg-container"
v-html="illustrationSvg">
</div>
<div class="user-callout-copy"> <div class="user-callout-copy">
<h4>{{ __('Scheduling Pipelines') }}</h4> <h4>{{ __('Scheduling Pipelines') }}</h4>
<p> <p>
{{ __('The pipelines schedule runs pipelines in the future, repeatedly, for specific branches or tags. Those scheduled pipelines will inherit limited project access based on their associated user.') }} {{ __('The pipelines schedule runs pipelines in the future, repeatedly, for specific branches or tags. Those scheduled pipelines will inherit limited project access based on their associated user.') }}
</p> </p>
<p> {{ __('Learn more in the') }} <p> {{ __('Learn more in the') }}
<a <a
:href="docsUrl" :href="docsUrl"
target="_blank" target="_blank"
rel="nofollow">{{ s__('Learn more in the|pipeline schedules documentation') }}</a>. <!-- oneline to prevent extra space before period --> rel="nofollow"
>
{{ s__('Learn more in the|pipeline schedules documentation') }}</a>.
<!-- oneline to prevent extra space before period -->
</p> </p>
</div> </div>
</div> </div>
......
<script> <script>
/* eslint-disable no-new, no-alert */ /* eslint-disable no-alert */
import eventHub from '../event_hub'; import eventHub from '../event_hub';
import loadingIcon from '../../vue_shared/components/loading_icon.vue'; import loadingIcon from '../../vue_shared/components/loading_icon.vue';
import tooltip from '../../vue_shared/directives/tooltip'; import tooltip from '../../vue_shared/directives/tooltip';
export default { export default {
props: { directives: {
endpoint: { tooltip,
type: String,
required: true,
}, },
title: { components: {
type: String, loadingIcon,
required: true,
}, },
icon: { props: {
type: String, endpoint: {
required: true, type: String,
required: true,
},
title: {
type: String,
required: true,
},
icon: {
type: String,
required: true,
},
cssClass: {
type: String,
required: true,
},
confirmActionMessage: {
type: String,
required: false,
default: '',
},
}, },
cssClass: { data() {
type: String, return {
required: true, isLoading: false,
};
}, },
confirmActionMessage: { computed: {
type: String, iconClass() {
required: false, return `fa fa-${this.icon}`;
},
buttonClass() {
return `btn ${this.cssClass}`;
},
}, },
}, methods: {
directives: { onClick() {
tooltip, if (this.confirmActionMessage !== '' && confirm(this.confirmActionMessage)) {
}, this.makeRequest();
components: { } else if (this.confirmActionMessage === '') {
loadingIcon, this.makeRequest();
}, }
data() { },
return { makeRequest() {
isLoading: false, this.isLoading = true;
};
},
computed: {
iconClass() {
return `fa fa-${this.icon}`;
},
buttonClass() {
return `btn ${this.cssClass}`;
},
},
methods: {
onClick() {
if (this.confirmActionMessage && confirm(this.confirmActionMessage)) {
this.makeRequest();
} else if (!this.confirmActionMessage) {
this.makeRequest();
}
},
makeRequest() {
this.isLoading = true;
eventHub.$emit('postAction', this.endpoint); eventHub.$emit('postAction', this.endpoint);
},
}, },
}, };
};
</script> </script>
<template> <template>
......
...@@ -32,7 +32,7 @@ ...@@ -32,7 +32,7 @@
<a <a
:href="helpPagePath" :href="helpPagePath"
class="btn btn-info" class="btn btn-info"
> >
{{ s__("Pipelines|Get started with Pipelines") }} {{ s__("Pipelines|Get started with Pipelines") }}
</a> </a>
</div> </div>
......
...@@ -7,6 +7,14 @@ ...@@ -7,6 +7,14 @@
* TODO: Remove UJS from here and use an async request instead. * TODO: Remove UJS from here and use an async request instead.
*/ */
export default { export default {
components: {
icon,
},
directives: {
tooltip,
},
props: { props: {
tooltipText: { tooltipText: {
type: String, type: String,
...@@ -29,14 +37,6 @@ ...@@ -29,14 +37,6 @@
}, },
}, },
components: {
icon,
},
directives: {
tooltip,
},
computed: { computed: {
cssClass() { cssClass() {
const actionIconDash = dasherize(this.actionIcon); const actionIconDash = dasherize(this.actionIcon);
...@@ -53,7 +53,8 @@ ...@@ -53,7 +53,8 @@
:href="link" :href="link"
class="ci-action-icon-container ci-action-icon-wrapper" class="ci-action-icon-container ci-action-icon-wrapper"
:class="cssClass" :class="cssClass"
data-container="body"> data-container="body"
<icon :name="actionIcon"/> >
<icon :name="actionIcon" />
</a> </a>
</template> </template>
...@@ -7,6 +7,13 @@ ...@@ -7,6 +7,13 @@
* TODO: Remove UJS from here and use an async request instead. * TODO: Remove UJS from here and use an async request instead.
*/ */
export default { export default {
components: {
icon,
},
directives: {
tooltip,
},
props: { props: {
tooltipText: { tooltipText: {
type: String, type: String,
...@@ -28,14 +35,6 @@ ...@@ -28,14 +35,6 @@
required: true, required: true,
}, },
}, },
components: {
icon,
},
directives: {
tooltip,
},
}; };
</script> </script>
<template> <template>
...@@ -47,7 +46,8 @@ ...@@ -47,7 +46,8 @@
rel="nofollow" rel="nofollow"
class="ci-action-icon-wrapper js-ci-status-icon" class="ci-action-icon-wrapper js-ci-status-icon"
data-container="body" data-container="body"
aria-label="Job's action"> aria-label="Job's action"
<icon :name="actionIcon"/> >
<icon :name="actionIcon" />
</a> </a>
</template> </template>
...@@ -27,13 +27,6 @@ ...@@ -27,13 +27,6 @@
* } * }
*/ */
export default { export default {
props: {
job: {
type: Object,
required: true,
},
},
directives: { directives: {
tooltip, tooltip,
}, },
...@@ -43,12 +36,23 @@ ...@@ -43,12 +36,23 @@
jobNameComponent, jobNameComponent,
}, },
props: {
job: {
type: Object,
required: true,
},
},
computed: { computed: {
tooltipText() { tooltipText() {
return `${this.job.name} - ${this.job.status.label}`; return `${this.job.name} - ${this.job.status.label}`;
}, },
}, },
mounted() {
this.stopDropdownClickPropagation();
},
methods: { methods: {
/** /**
* When the user right clicks or cmd/ctrl + click in the job name * When the user right clicks or cmd/ctrl + click in the job name
...@@ -66,10 +70,6 @@ ...@@ -66,10 +70,6 @@
}); });
}, },
}, },
mounted() {
this.stopDropdownClickPropagation();
},
}; };
</script> </script>
<template> <template>
...@@ -84,22 +84,25 @@ ...@@ -84,22 +84,25 @@
<job-name-component <job-name-component
:name="job.name" :name="job.name"
:status="job.status" /> :status="job.status"
/>
<span class="dropdown-counter-badge"> <span class="dropdown-counter-badge">
{{job.size}} {{ job.size }}
</span> </span>
</button> </button>
<ul class="dropdown-menu big-pipeline-graph-dropdown-menu js-grouped-pipeline-dropdown"> <ul class="dropdown-menu big-pipeline-graph-dropdown-menu js-grouped-pipeline-dropdown">
<li class="scrollable-menu"> <li class="scrollable-menu">
<ul> <ul>
<li v-for="item in job.jobs"> <li
v-for="(item, i) in job.jobs"
:key="i">
<job-component <job-component
:job="item" :job="item"
:is-dropdown="true" :is-dropdown="true"
css-class-job-name="mini-pipeline-graph-dropdown-item" css-class-job-name="mini-pipeline-graph-dropdown-item"
/> />
</li> </li>
</ul> </ul>
</li> </li>
......
<script> <script>
import loadingIcon from '~/vue_shared/components/loading_icon.vue'; import loadingIcon from '~/vue_shared/components/loading_icon.vue';
import '~/flash';
import stageColumnComponent from './stage_column_component.vue'; import stageColumnComponent from './stage_column_component.vue';
export default { export default {
components: {
stageColumnComponent,
loadingIcon,
},
props: { props: {
isLoading: { isLoading: {
type: Boolean, type: Boolean,
...@@ -15,11 +19,6 @@ ...@@ -15,11 +19,6 @@
}, },
}, },
components: {
stageColumnComponent,
loadingIcon,
},
computed: { computed: {
graph() { graph() {
return this.pipeline.details && this.pipeline.details.stages; return this.pipeline.details && this.pipeline.details.stages;
...@@ -58,7 +57,7 @@ ...@@ -58,7 +57,7 @@
<loading-icon <loading-icon
v-if="isLoading" v-if="isLoading"
size="3" size="3"
/> />
</div> </div>
<ul <ul
...@@ -70,7 +69,8 @@ ...@@ -70,7 +69,8 @@
:jobs="stage.groups" :jobs="stage.groups"
:key="stage.name" :key="stage.name"
:stage-connector-class="stageConnectorClass(index, stage)" :stage-connector-class="stageConnectorClass(index, stage)"
:is-first-column="isFirstColumn(index)"/> :is-first-column="isFirstColumn(index)"
/>
</ul> </ul>
</div> </div>
</div> </div>
......
...@@ -29,6 +29,15 @@ ...@@ -29,6 +29,15 @@
*/ */
export default { export default {
components: {
actionComponent,
dropdownActionComponent,
jobNameComponent,
},
directives: {
tooltip,
},
props: { props: {
job: { job: {
type: Object, type: Object,
...@@ -48,16 +57,6 @@ ...@@ -48,16 +57,6 @@
}, },
}, },
components: {
actionComponent,
dropdownActionComponent,
jobNameComponent,
},
directives: {
tooltip,
},
computed: { computed: {
status() { status() {
return this.job && this.job.status ? this.job.status : {}; return this.job && this.job.status ? this.job.status : {};
...@@ -102,12 +101,12 @@ ...@@ -102,12 +101,12 @@
:class="cssClassJobName" :class="cssClassJobName"
data-container="body" data-container="body"
class="js-pipeline-graph-job-link" class="js-pipeline-graph-job-link"
> >
<job-name-component <job-name-component
:name="job.name" :name="job.name"
:status="job.status" :status="job.status"
/> />
</a> </a>
<div <div
...@@ -117,12 +116,12 @@ ...@@ -117,12 +116,12 @@
:title="tooltipText" :title="tooltipText"
:class="cssClassJobName" :class="cssClassJobName"
data-container="body" data-container="body"
> >
<job-name-component <job-name-component
:name="job.name" :name="job.name"
:status="job.status" :status="job.status"
/> />
</div> </div>
<action-component <action-component
...@@ -131,7 +130,7 @@ ...@@ -131,7 +130,7 @@
:link="status.action.path" :link="status.action.path"
:action-icon="status.action.icon" :action-icon="status.action.icon"
:action-method="status.action.method" :action-method="status.action.method"
/> />
<dropdown-action-component <dropdown-action-component
v-if="hasAction && isDropdown" v-if="hasAction && isDropdown"
...@@ -139,6 +138,6 @@ ...@@ -139,6 +138,6 @@
:link="status.action.path" :link="status.action.path"
:action-icon="status.action.icon" :action-icon="status.action.icon"
:action-method="status.action.method" :action-method="status.action.method"
/> />
</div> </div>
</template> </template>
...@@ -8,6 +8,9 @@ ...@@ -8,6 +8,9 @@
* - Dropdown badge components * - Dropdown badge components
*/ */
export default { export default {
components: {
ciIcon,
},
props: { props: {
name: { name: {
type: String, type: String,
...@@ -19,19 +22,14 @@ ...@@ -19,19 +22,14 @@
required: true, required: true,
}, },
}, },
components: {
ciIcon,
},
}; };
</script> </script>
<template> <template>
<span class="ci-job-name-component"> <span class="ci-job-name-component">
<ci-icon <ci-icon :status="status" />
:status="status" />
<span class="ci-status-text"> <span class="ci-status-text">
{{name}} {{ name }}
</span> </span>
</span> </span>
</template> </template>
<script> <script>
import jobComponent from './job_component.vue'; import jobComponent from './job_component.vue';
import dropdownJobComponent from './dropdown_job_component.vue'; import dropdownJobComponent from './dropdown_job_component.vue';
export default { export default {
props: { components: {
title: { jobComponent,
type: String, dropdownJobComponent,
required: true,
}, },
jobs: { props: {
type: Array, title: {
required: true, type: String,
}, required: true,
},
isFirstColumn: {
type: Boolean,
required: false,
default: false,
},
stageConnectorClass: { jobs: {
type: String, type: Array,
required: false, required: true,
default: '', },
},
},
components: { isFirstColumn: {
jobComponent, type: Boolean,
dropdownJobComponent, required: false,
}, default: false,
},
methods: { stageConnectorClass: {
firstJob(list) { type: String,
return list[0]; required: false,
default: '',
},
}, },
jobId(job) { methods: {
return `ci-badge-${job.name}`; firstJob(list) {
}, return list[0];
},
jobId(job) {
return `ci-badge-${job.name}`;
},
buildConnnectorClass(index) { buildConnnectorClass(index) {
return index === 0 && !this.isFirstColumn ? 'left-connector' : ''; return index === 0 && !this.isFirstColumn ? 'left-connector' : '';
},
}, },
}, };
};
</script> </script>
<template> <template>
<li <li
class="stage-column" class="stage-column"
:class="stageConnectorClass"> :class="stageConnectorClass">
<div class="stage-name"> <div class="stage-name">
{{title}} {{ title }}
</div> </div>
<div class="builds-container"> <div class="builds-container">
<ul> <ul>
...@@ -61,7 +61,8 @@ export default { ...@@ -61,7 +61,8 @@ export default {
:key="job.id" :key="job.id"
class="build" class="build"
:class="buildConnnectorClass(index)" :class="buildConnnectorClass(index)"
:id="jobId(job)"> :id="jobId(job)"
>
<div class="curve"></div> <div class="curve"></div>
...@@ -69,12 +70,12 @@ export default { ...@@ -69,12 +70,12 @@ export default {
v-if="job.size === 1" v-if="job.size === 1"
:job="job" :job="job"
css-class-job-name="build-content" css-class-job-name="build-content"
/> />
<dropdown-job-component <dropdown-job-component
v-if="job.size > 1" v-if="job.size > 1"
:job="job" :job="job"
/> />
</li> </li>
</ul> </ul>
......
<script> <script>
import ciHeader from '../../vue_shared/components/header_ci_component.vue'; import ciHeader from '../../vue_shared/components/header_ci_component.vue';
import eventHub from '../event_hub'; import eventHub from '../event_hub';
import loadingIcon from '../../vue_shared/components/loading_icon.vue'; import loadingIcon from '../../vue_shared/components/loading_icon.vue';
export default { export default {
name: 'PipelineHeaderSection', name: 'PipelineHeaderSection',
props: { components: {
pipeline: { ciHeader,
type: Object, loadingIcon,
required: true,
}, },
isLoading: { props: {
type: Boolean, pipeline: {
required: true, type: Object,
required: true,
},
isLoading: {
type: Boolean,
required: true,
},
}, },
}, data() {
components: { return {
ciHeader, actions: this.getActions(),
loadingIcon, };
},
data() {
return {
actions: this.getActions(),
};
},
computed: {
status() {
return this.pipeline.details && this.pipeline.details.status;
}, },
shouldRenderContent() {
return !this.isLoading && Object.keys(this.pipeline).length; computed: {
status() {
return this.pipeline.details && this.pipeline.details.status;
},
shouldRenderContent() {
return !this.isLoading && Object.keys(this.pipeline).length;
},
}, },
},
methods: { watch: {
postAction(action) { pipeline() {
const index = this.actions.indexOf(action); this.actions = this.getActions();
},
},
this.$set(this.actions[index], 'isLoading', true); methods: {
postAction(action) {
const index = this.actions.indexOf(action);
eventHub.$emit('headerPostAction', action); this.$set(this.actions[index], 'isLoading', true);
},
getActions() { eventHub.$emit('headerPostAction', action);
const actions = []; },
if (this.pipeline.retry_path) { getActions() {
actions.push({ const actions = [];
label: 'Retry',
path: this.pipeline.retry_path,
cssClass: 'js-retry-button btn btn-inverted-secondary',
type: 'button',
isLoading: false,
});
}
if (this.pipeline.cancel_path) { if (this.pipeline.retry_path) {
actions.push({ actions.push({
label: 'Cancel running', label: 'Retry',
path: this.pipeline.cancel_path, path: this.pipeline.retry_path,
cssClass: 'js-btn-cancel-pipeline btn btn-danger', cssClass: 'js-retry-button btn btn-inverted-secondary',
type: 'button', type: 'button',
isLoading: false, isLoading: false,
}); });
} }
return actions; if (this.pipeline.cancel_path) {
}, actions.push({
}, label: 'Cancel running',
path: this.pipeline.cancel_path,
cssClass: 'js-btn-cancel-pipeline btn btn-danger',
type: 'button',
isLoading: false,
});
}
watch: { return actions;
pipeline() { },
this.actions = this.getActions();
}, },
}, };
};
</script> </script>
<template> <template>
<div class="pipeline-header-container"> <div class="pipeline-header-container">
...@@ -89,9 +88,10 @@ export default { ...@@ -89,9 +88,10 @@ export default {
:user="pipeline.user" :user="pipeline.user"
:actions="actions" :actions="actions"
@actionClicked="postAction" @actionClicked="postAction"
/> />
<loading-icon <loading-icon
v-if="isLoading" v-if="isLoading"
size="2"/> size="2"
/>
</div> </div>
</template> </template>
...@@ -4,6 +4,13 @@ ...@@ -4,6 +4,13 @@
import popover from '../../vue_shared/directives/popover'; import popover from '../../vue_shared/directives/popover';
export default { export default {
components: {
userAvatarLink,
},
directives: {
tooltip,
popover,
},
props: { props: {
pipeline: { pipeline: {
type: Object, type: Object,
...@@ -14,13 +21,6 @@ ...@@ -14,13 +21,6 @@
required: true, required: true,
}, },
}, },
components: {
userAvatarLink,
},
directives: {
tooltip,
popover,
},
computed: { computed: {
user() { user() {
return this.pipeline.user; return this.pipeline.user;
...@@ -50,7 +50,7 @@ ...@@ -50,7 +50,7 @@
<a <a
:href="pipeline.path" :href="pipeline.path"
class="js-pipeline-url-link"> class="js-pipeline-url-link">
<span class="pipeline-id">#{{pipeline.id}}</span> <span class="pipeline-id">#{{ pipeline.id }}</span>
</a> </a>
<span>by</span> <span>by</span>
<user-avatar-link <user-avatar-link
......
...@@ -13,6 +13,15 @@ ...@@ -13,6 +13,15 @@
import CIPaginationMixin from '../../vue_shared/mixins/ci_pagination_api_mixin'; import CIPaginationMixin from '../../vue_shared/mixins/ci_pagination_api_mixin';
export default { export default {
components: {
tablePagination,
navigationTabs,
navigationControls,
},
mixins: [
pipelinesMixin,
CIPaginationMixin,
],
props: { props: {
store: { store: {
type: Object, type: Object,
...@@ -28,15 +37,6 @@ ...@@ -28,15 +37,6 @@
default: 'root', default: 'root',
}, },
}, },
components: {
tablePagination,
navigationTabs,
navigationControls,
},
mixins: [
pipelinesMixin,
CIPaginationMixin,
],
data() { data() {
const pipelinesData = document.querySelector('#pipelines-list-vue').dataset; const pipelinesData = document.querySelector('#pipelines-list-vue').dataset;
...@@ -214,7 +214,7 @@ ...@@ -214,7 +214,7 @@
:tabs="tabs" :tabs="tabs"
@onChangeTab="onChangeTab" @onChangeTab="onChangeTab"
scope="pipelines" scope="pipelines"
/> />
<navigation-controls <navigation-controls
:new-pipeline-path="newPipelinePath" :new-pipeline-path="newPipelinePath"
...@@ -222,7 +222,7 @@ ...@@ -222,7 +222,7 @@
:help-page-path="helpPagePath" :help-page-path="helpPagePath"
:ci-lint-path="ciLintPath" :ci-lint-path="ciLintPath"
:can-create-pipeline="canCreatePipelineParsed " :can-create-pipeline="canCreatePipelineParsed "
/> />
</div> </div>
<div class="content-list pipelines"> <div class="content-list pipelines">
...@@ -232,22 +232,23 @@ ...@@ -232,22 +232,23 @@
size="3" size="3"
v-if="isLoading" v-if="isLoading"
class="prepend-top-20" class="prepend-top-20"
/> />
<empty-state <empty-state
v-if="shouldRenderEmptyState" v-if="shouldRenderEmptyState"
:help-page-path="helpPagePath" :help-page-path="helpPagePath"
:empty-state-svg-path="emptyStateSvgPath" :empty-state-svg-path="emptyStateSvgPath"
/> />
<error-state <error-state
v-if="shouldRenderErrorState" v-if="shouldRenderErrorState"
:error-state-svg-path="errorStateSvgPath" :error-state-svg-path="errorStateSvgPath"
/> />
<div <div
class="blank-state-row" class="blank-state-row"
v-if="shouldRenderNoPipelinesMessage"> v-if="shouldRenderNoPipelinesMessage"
>
<div class="blank-state-center"> <div class="blank-state-center">
<h2 class="blank-state-title js-blank-state-title">No pipelines to show.</h2> <h2 class="blank-state-title js-blank-state-title">No pipelines to show.</h2>
</div> </div>
...@@ -255,21 +256,22 @@ ...@@ -255,21 +256,22 @@
<div <div
class="table-holder" class="table-holder"
v-if="shouldRenderTable"> v-if="shouldRenderTable"
>
<pipelines-table-component <pipelines-table-component
:pipelines="state.pipelines" :pipelines="state.pipelines"
:update-graph-dropdown="updateGraphDropdown" :update-graph-dropdown="updateGraphDropdown"
:auto-devops-help-path="autoDevopsPath" :auto-devops-help-path="autoDevopsPath"
:view-type="viewType" :view-type="viewType"
/> />
</div> </div>
<table-pagination <table-pagination
v-if="shouldRenderPagination" v-if="shouldRenderPagination"
:change="onChangePage" :change="onChangePage"
:page-info="state.pageInfo" :page-info="state.pageInfo"
/> />
</div> </div>
</div> </div>
</template> </template>
...@@ -5,18 +5,18 @@ ...@@ -5,18 +5,18 @@
import tooltip from '../../vue_shared/directives/tooltip'; import tooltip from '../../vue_shared/directives/tooltip';
export default { export default {
props: {
actions: {
type: Array,
required: true,
},
},
directives: { directives: {
tooltip, tooltip,
}, },
components: { components: {
loadingIcon, loadingIcon,
}, },
props: {
actions: {
type: Array,
required: true,
},
},
data() { data() {
return { return {
playIconSvg, playIconSvg,
...@@ -50,7 +50,8 @@ ...@@ -50,7 +50,8 @@
data-toggle="dropdown" data-toggle="dropdown"
data-placement="top" data-placement="top"
aria-label="Manual job" aria-label="Manual job"
:disabled="isLoading"> :disabled="isLoading"
>
<span v-html="playIconSvg"></span> <span v-html="playIconSvg"></span>
<i <i
class="fa fa-caret-down" class="fa fa-caret-down"
...@@ -60,14 +61,18 @@ ...@@ -60,14 +61,18 @@
</button> </button>
<ul class="dropdown-menu dropdown-menu-align-right"> <ul class="dropdown-menu dropdown-menu-align-right">
<li v-for="action in actions"> <li
v-for="(action, i) in actions"
:key="i"
>
<button <button
type="button" type="button"
class="js-pipeline-action-link no-btn btn" class="js-pipeline-action-link no-btn btn"
@click="onClickAction(action.path)" @click="onClickAction(action.path)"
:class="{ disabled: isActionDisabled(action) }" :class="{ disabled: isActionDisabled(action) }"
:disabled="isActionDisabled(action)"> :disabled="isActionDisabled(action)"
{{action.name}} >
{{ action.name }}
</button> </button>
</li> </li>
</ul> </ul>
......
...@@ -3,46 +3,50 @@ ...@@ -3,46 +3,50 @@
import icon from '../../vue_shared/components/icon.vue'; import icon from '../../vue_shared/components/icon.vue';
export default { export default {
props: {
artifacts: {
type: Array,
required: true,
},
},
directives: { directives: {
tooltip, tooltip,
}, },
components: { components: {
icon, icon,
}, },
props: {
artifacts: {
type: Array,
required: true,
},
},
}; };
</script> </script>
<template> <template>
<div <div
class="btn-group" class="btn-group"
role="group"> role="group"
>
<button <button
v-tooltip v-tooltip
class="dropdown-toggle btn btn-default build-artifacts js-pipeline-dropdown-download" class="dropdown-toggle btn btn-default build-artifacts js-pipeline-dropdown-download"
title="Artifacts" title="Artifacts"
data-placement="top" data-placement="top"
data-toggle="dropdown" data-toggle="dropdown"
aria-label="Artifacts"> aria-label="Artifacts"
<icon >
name="download"> <icon name="download" />
</icon>
<i <i
class="fa fa-caret-down" class="fa fa-caret-down"
aria-hidden="true"> aria-hidden="true"
>
</i> </i>
</button> </button>
<ul class="dropdown-menu dropdown-menu-align-right"> <ul class="dropdown-menu dropdown-menu-align-right">
<li v-for="artifact in artifacts"> <li
v-for="(artifact, i) in artifacts"
:key="i">
<a <a
rel="nofollow" rel="nofollow"
download download
:href="artifact.path"> :href="artifact.path"
Download {{artifact.name}} artifacts >
Download {{ artifact.name }} artifacts
</a> </a>
</li> </li>
</ul> </ul>
......
...@@ -7,6 +7,9 @@ ...@@ -7,6 +7,9 @@
* Given an array of objects, renders a table. * Given an array of objects, renders a table.
*/ */
export default { export default {
components: {
pipelinesTableRowComponent,
},
props: { props: {
pipelines: { pipelines: {
type: Array, type: Array,
...@@ -26,34 +29,36 @@ ...@@ -26,34 +29,36 @@
required: true, required: true,
}, },
}, },
components: {
pipelinesTableRowComponent,
},
}; };
</script> </script>
<template> <template>
<div class="ci-table"> <div class="ci-table">
<div <div
class="gl-responsive-table-row table-row-header" class="gl-responsive-table-row table-row-header"
role="row"> role="row"
>
<div <div
class="table-section section-10 js-pipeline-status pipeline-status" class="table-section section-10 js-pipeline-status pipeline-status"
role="rowheader"> role="rowheader"
>
Status Status
</div> </div>
<div <div
class="table-section section-15 js-pipeline-info pipeline-info" class="table-section section-15 js-pipeline-info pipeline-info"
role="rowheader"> role="rowheader"
>
Pipeline Pipeline
</div> </div>
<div <div
class="table-section section-25 js-pipeline-commit pipeline-commit" class="table-section section-25 js-pipeline-commit pipeline-commit"
role="rowheader"> role="rowheader"
>
Commit Commit
</div> </div>
<div <div
class="table-section section-15 js-pipeline-stages pipeline-stages" class="table-section section-15 js-pipeline-stages pipeline-stages"
role="rowheader"> role="rowheader"
>
Stages Stages
</div> </div>
</div> </div>
......
<script> <script>
/* eslint-disable no-param-reassign */ /* eslint-disable no-param-reassign */
import asyncButtonComponent from './async_button.vue'; import asyncButtonComponent from './async_button.vue';
import pipelinesActionsComponent from './pipelines_actions.vue'; import pipelinesActionsComponent from './pipelines_actions.vue';
import pipelinesArtifactsComponent from './pipelines_artifacts.vue'; import pipelinesArtifactsComponent from './pipelines_artifacts.vue';
import ciBadge from '../../vue_shared/components/ci_badge_link.vue'; import ciBadge from '../../vue_shared/components/ci_badge_link.vue';
import pipelineStage from './stage.vue'; import pipelineStage from './stage.vue';
import pipelineUrl from './pipeline_url.vue'; import pipelineUrl from './pipeline_url.vue';
import pipelinesTimeago from './time_ago.vue'; import pipelinesTimeago from './time_ago.vue';
import commitComponent from '../../vue_shared/components/commit.vue'; import commitComponent from '../../vue_shared/components/commit.vue';
/** /**
* Pipeline table row. * Pipeline table row.
* *
* Given the received object renders a table row in the pipelines' table. * Given the received object renders a table row in the pipelines' table.
*/ */
export default { export default {
props: { components: {
pipeline: { asyncButtonComponent,
type: Object, pipelinesActionsComponent,
required: true, pipelinesArtifactsComponent,
commitComponent,
pipelineStage,
pipelineUrl,
ciBadge,
pipelinesTimeago,
}, },
updateGraphDropdown: { props: {
type: Boolean, pipeline: {
required: false, type: Object,
default: false, required: true,
},
updateGraphDropdown: {
type: Boolean,
required: false,
default: false,
},
autoDevopsHelpPath: {
type: String,
required: true,
},
viewType: {
type: String,
required: true,
},
}, },
autoDevopsHelpPath: { computed: {
type: String, /**
required: true, * If provided, returns the commit tag.
}, * Needed to render the commit component column.
viewType: { *
type: String, * This field needs a lot of verification, because of different possible cases:
required: true, *
}, * 1. person who is an author of a commit might be a GitLab user
}, * 2. if person who is an author of a commit is a GitLab user he/she can have a GitLab avatar
components: { * 3. If GitLab user does not have avatar he/she might have a Gravatar
asyncButtonComponent, * 4. If committer is not a GitLab User he/she can have a Gravatar
pipelinesActionsComponent, * 5. We do not have consistent API object in this case
pipelinesArtifactsComponent, * 6. We should improve API and the code
commitComponent, *
pipelineStage, * @returns {Object|Undefined}
pipelineUrl, */
ciBadge, commitAuthor() {
pipelinesTimeago, let commitAuthorInformation;
},
computed: {
/**
* If provided, returns the commit tag.
* Needed to render the commit component column.
*
* This field needs a lot of verification, because of different possible cases:
*
* 1. person who is an author of a commit might be a GitLab user
* 2. if person who is an author of a commit is a GitLab user he/she can have a GitLab avatar
* 3. If GitLab user does not have avatar he/she might have a Gravatar
* 4. If committer is not a GitLab User he/she can have a Gravatar
* 5. We do not have consistent API object in this case
* 6. We should improve API and the code
*
* @returns {Object|Undefined}
*/
commitAuthor() {
let commitAuthorInformation;
if (!this.pipeline || !this.pipeline.commit) { if (!this.pipeline || !this.pipeline.commit) {
return null; return null;
} }
// 1. person who is an author of a commit might be a GitLab user // 1. person who is an author of a commit might be a GitLab user
if (this.pipeline.commit.author) { if (this.pipeline.commit.author) {
// 2. if person who is an author of a commit is a GitLab user // 2. if person who is an author of a commit is a GitLab user
// he/she can have a GitLab avatar // he/she can have a GitLab avatar
if (this.pipeline.commit.author.avatar_url) { if (this.pipeline.commit.author.avatar_url) {
commitAuthorInformation = this.pipeline.commit.author; commitAuthorInformation = this.pipeline.commit.author;
// 3. If GitLab user does not have avatar he/she might have a Gravatar // 3. If GitLab user does not have avatar he/she might have a Gravatar
} else if (this.pipeline.commit.author_gravatar_url) { } else if (this.pipeline.commit.author_gravatar_url) {
commitAuthorInformation = Object.assign({}, this.pipeline.commit.author, { commitAuthorInformation = Object.assign({}, this.pipeline.commit.author, {
avatar_url: this.pipeline.commit.author_gravatar_url,
});
}
// 4. If committer is not a GitLab User he/she can have a Gravatar
} else {
commitAuthorInformation = {
avatar_url: this.pipeline.commit.author_gravatar_url, avatar_url: this.pipeline.commit.author_gravatar_url,
}); path: `mailto:${this.pipeline.commit.author_email}`,
username: this.pipeline.commit.author_name,
};
} }
// 4. If committer is not a GitLab User he/she can have a Gravatar
} else {
commitAuthorInformation = {
avatar_url: this.pipeline.commit.author_gravatar_url,
path: `mailto:${this.pipeline.commit.author_email}`,
username: this.pipeline.commit.author_name,
};
}
return commitAuthorInformation; return commitAuthorInformation;
}, },
/** /**
* If provided, returns the commit tag. * If provided, returns the commit tag.
* Needed to render the commit component column. * Needed to render the commit component column.
* *
* @returns {String|Undefined} * @returns {String|Undefined}
*/ */
commitTag() { commitTag() {
if (this.pipeline.ref && if (this.pipeline.ref &&
this.pipeline.ref.tag) { this.pipeline.ref.tag) {
return this.pipeline.ref.tag; return this.pipeline.ref.tag;
} }
return undefined; return undefined;
}, },
/** /**
* If provided, returns the commit ref. * If provided, returns the commit ref.
* Needed to render the commit component column. * Needed to render the commit component column.
* *
* Matches `path` prop sent in the API to `ref_url` prop needed * Matches `path` prop sent in the API to `ref_url` prop needed
* in the commit component. * in the commit component.
* *
* @returns {Object|Undefined} * @returns {Object|Undefined}
*/ */
commitRef() { commitRef() {
if (this.pipeline.ref) { if (this.pipeline.ref) {
return Object.keys(this.pipeline.ref).reduce((accumulator, prop) => { return Object.keys(this.pipeline.ref).reduce((accumulator, prop) => {
if (prop === 'path') { if (prop === 'path') {
accumulator.ref_url = this.pipeline.ref[prop]; accumulator.ref_url = this.pipeline.ref[prop];
} else { } else {
accumulator[prop] = this.pipeline.ref[prop]; accumulator[prop] = this.pipeline.ref[prop];
} }
return accumulator; return accumulator;
}, {}); }, {});
} }
return undefined; return undefined;
}, },
/** /**
* If provided, returns the commit url. * If provided, returns the commit url.
* Needed to render the commit component column. * Needed to render the commit component column.
* *
* @returns {String|Undefined} * @returns {String|Undefined}
*/ */
commitUrl() { commitUrl() {
if (this.pipeline.commit && if (this.pipeline.commit &&
this.pipeline.commit.commit_path) { this.pipeline.commit.commit_path) {
return this.pipeline.commit.commit_path; return this.pipeline.commit.commit_path;
} }
return undefined; return undefined;
}, },
/** /**
* If provided, returns the commit short sha. * If provided, returns the commit short sha.
* Needed to render the commit component column. * Needed to render the commit component column.
* *
* @returns {String|Undefined} * @returns {String|Undefined}
*/ */
commitShortSha() { commitShortSha() {
if (this.pipeline.commit && if (this.pipeline.commit &&
this.pipeline.commit.short_id) { this.pipeline.commit.short_id) {
return this.pipeline.commit.short_id; return this.pipeline.commit.short_id;
} }
return undefined; return undefined;
}, },
/** /**
* If provided, returns the commit title. * If provided, returns the commit title.
* Needed to render the commit component column. * Needed to render the commit component column.
* *
* @returns {String|Undefined} * @returns {String|Undefined}
*/ */
commitTitle() { commitTitle() {
if (this.pipeline.commit && if (this.pipeline.commit &&
this.pipeline.commit.title) { this.pipeline.commit.title) {
return this.pipeline.commit.title; return this.pipeline.commit.title;
} }
return undefined; return undefined;
}, },
/** /**
* Timeago components expects a number * Timeago components expects a number
* *
* @return {type} description * @return {type} description
*/ */
pipelineDuration() { pipelineDuration() {
if (this.pipeline.details && this.pipeline.details.duration) { if (this.pipeline.details && this.pipeline.details.duration) {
return this.pipeline.details.duration; return this.pipeline.details.duration;
} }
return 0; return 0;
}, },
/** /**
* Timeago component expects a String. * Timeago component expects a String.
* *
* @return {String} * @return {String}
*/ */
pipelineFinishedAt() { pipelineFinishedAt() {
if (this.pipeline.details && this.pipeline.details.finished_at) { if (this.pipeline.details && this.pipeline.details.finished_at) {
return this.pipeline.details.finished_at; return this.pipeline.details.finished_at;
} }
return ''; return '';
}, },
pipelineStatus() { pipelineStatus() {
if (this.pipeline.details && this.pipeline.details.status) { if (this.pipeline.details && this.pipeline.details.status) {
return this.pipeline.details.status; return this.pipeline.details.status;
} }
return {}; return {};
}, },
displayPipelineActions() { displayPipelineActions() {
return this.pipeline.flags.retryable || return this.pipeline.flags.retryable ||
this.pipeline.flags.cancelable || this.pipeline.flags.cancelable ||
this.pipeline.details.manual_actions.length || this.pipeline.details.manual_actions.length ||
this.pipeline.details.artifacts.length; this.pipeline.details.artifacts.length;
}, },
isChildView() { isChildView() {
return this.viewType === 'child'; return this.viewType === 'child';
},
}, },
}, };
};
</script> </script>
<template> <template>
<div class="commit gl-responsive-table-row"> <div class="commit gl-responsive-table-row">
<div class="table-section section-10 commit-link"> <div class="table-section section-10 commit-link">
<div class="table-mobile-header" <div
class="table-mobile-header"
role="rowheader"> role="rowheader">
Status Status
</div> </div>
...@@ -229,14 +230,14 @@ export default { ...@@ -229,14 +230,14 @@ export default {
<ci-badge <ci-badge
:status="pipelineStatus" :status="pipelineStatus"
:show-text="!isChildView" :show-text="!isChildView"
/> />
</div> </div>
</div> </div>
<pipeline-url <pipeline-url
:pipeline="pipeline" :pipeline="pipeline"
:auto-devops-help-path="autoDevopsHelpPath" :auto-devops-help-path="autoDevopsHelpPath"
/> />
<div class="table-section section-25"> <div class="table-section section-25">
<div <div
...@@ -253,7 +254,7 @@ export default { ...@@ -253,7 +254,7 @@ export default {
:title="commitTitle" :title="commitTitle"
:author="commitAuthor" :author="commitAuthor"
:show-branch="!isChildView" :show-branch="!isChildView"
/> />
</div> </div>
</div> </div>
...@@ -264,21 +265,24 @@ export default { ...@@ -264,21 +265,24 @@ export default {
Stages Stages
</div> </div>
<div class="table-mobile-content"> <div class="table-mobile-content">
<div class="stage-container dropdown js-mini-pipeline-graph" <template v-if="pipeline.details.stages.length > 0">
v-if="pipeline.details.stages.length > 0" <div
v-for="stage in pipeline.details.stages"> class="stage-container dropdown js-mini-pipeline-graph"
<pipeline-stage v-for="(stage, index) in pipeline.details.stages"
:stage="stage" :key="index">
:update-dropdown="updateGraphDropdown" <pipeline-stage
:stage="stage"
:update-dropdown="updateGraphDropdown"
/> />
</div> </div>
</template>
</div> </div>
</div> </div>
<pipelines-timeago <pipelines-timeago
:duration="pipelineDuration" :duration="pipelineDuration"
:finished-time="pipelineFinishedAt" :finished-time="pipelineFinishedAt"
/> />
<div <div
v-if="displayPipelineActions" v-if="displayPipelineActions"
...@@ -287,13 +291,13 @@ export default { ...@@ -287,13 +291,13 @@ export default {
<pipelines-actions-component <pipelines-actions-component
v-if="pipeline.details.manual_actions.length" v-if="pipeline.details.manual_actions.length"
:actions="pipeline.details.manual_actions" :actions="pipeline.details.manual_actions"
/> />
<pipelines-artifacts-component <pipelines-artifacts-component
v-if="pipeline.details.artifacts.length" v-if="pipeline.details.artifacts.length"
class="hidden-xs hidden-sm" class="hidden-xs hidden-sm"
:artifacts="pipeline.details.artifacts" :artifacts="pipeline.details.artifacts"
/> />
<async-button-component <async-button-component
v-if="pipeline.flags.retryable" v-if="pipeline.flags.retryable"
...@@ -301,7 +305,7 @@ export default { ...@@ -301,7 +305,7 @@ export default {
css-class="js-pipelines-retry-button btn-default btn-retry" css-class="js-pipelines-retry-button btn-default btn-retry"
title="Retry" title="Retry"
icon="repeat" icon="repeat"
/> />
<async-button-component <async-button-component
v-if="pipeline.flags.cancelable" v-if="pipeline.flags.cancelable"
...@@ -310,7 +314,7 @@ export default { ...@@ -310,7 +314,7 @@ export default {
title="Cancel" title="Cancel"
icon="remove" icon="remove"
confirm-action-message="Are you sure you want to cancel this pipeline?" confirm-action-message="Are you sure you want to cancel this pipeline?"
/> />
</div> </div>
</div> </div>
</div> </div>
......
<script> <script>
/** /**
* Renders each stage of the pipeline mini graph. * Renders each stage of the pipeline mini graph.
* *
* Given the provided endpoint will make a request to * Given the provided endpoint will make a request to
* fetch the dropdown data when the stage is clicked. * fetch the dropdown data when the stage is clicked.
* *
* Request is made inside this component to make it reusable between: * Request is made inside this component to make it reusable between:
* 1. Pipelines main table * 1. Pipelines main table
* 2. Pipelines table in commit and Merge request views * 2. Pipelines table in commit and Merge request views
* 3. Merge request widget * 3. Merge request widget
* 4. Commit widget * 4. Commit widget
*/ */
import Flash from '../../flash'; import Flash from '../../flash';
import icon from '../../vue_shared/components/icon.vue'; import icon from '../../vue_shared/components/icon.vue';
import loadingIcon from '../../vue_shared/components/loading_icon.vue'; import loadingIcon from '../../vue_shared/components/loading_icon.vue';
import tooltip from '../../vue_shared/directives/tooltip'; import tooltip from '../../vue_shared/directives/tooltip';
export default { export default {
props: { components: {
stage: { loadingIcon,
type: Object, icon,
required: true,
}, },
updateDropdown: { directives: {
type: Boolean, tooltip,
required: false,
default: false,
}, },
},
directives: {
tooltip,
},
data() {
return {
isLoading: false,
dropdownContent: '',
};
},
components: {
loadingIcon,
icon,
},
updated() {
if (this.dropdownContent.length > 0) {
this.stopDropdownClickPropagation();
}
},
watch: {
updateDropdown() {
if (this.updateDropdown &&
this.isDropdownOpen() &&
!this.isLoading) {
this.fetchJobs();
}
},
},
methods: { props: {
onClickStage() { stage: {
if (!this.isDropdownOpen()) { type: Object,
this.isLoading = true; required: true,
this.fetchJobs(); },
}
updateDropdown: {
type: Boolean,
required: false,
default: false,
},
}, },
fetchJobs() { data() {
this.$http.get(this.stage.dropdown_path) return {
.then(response => response.json()) isLoading: false,
.then((data) => { dropdownContent: '',
this.dropdownContent = data.html; };
this.isLoading = false;
})
.catch(() => {
this.closeDropdown();
this.isLoading = false;
const flash = new Flash('Something went wrong on our end.');
return flash;
});
}, },
/** computed: {
* When the user right clicks or cmd/ctrl + click in the job name dropdownClass() {
* the dropdown should not be closed and the link should open in another tab, return this.dropdownContent.length > 0 ?
* so we stop propagation of the click event inside the dropdown. 'js-builds-dropdown-container' :
* 'js-builds-dropdown-loading';
* Since this component is rendered multiple times per page we need to guarantee we only },
* target the click event of this component.
*/
stopDropdownClickPropagation() {
$(this.$el.querySelectorAll('.js-builds-dropdown-list a.mini-pipeline-graph-dropdown-item'))
.on('click', (e) => {
e.stopPropagation();
});
},
closeDropdown() { triggerButtonClass() {
if (this.isDropdownOpen()) { return `ci-status-icon-${this.stage.status.group}`;
$(this.$refs.dropdown).dropdown('toggle'); },
}
},
isDropdownOpen() { borderlessIcon() {
return this.$el.classList.contains('open'); return `${this.stage.status.icon}_borderless`;
},
}, },
},
computed: { watch: {
dropdownClass() { updateDropdown() {
return this.dropdownContent.length > 0 ? if (this.updateDropdown &&
'js-builds-dropdown-container' : this.isDropdownOpen() &&
'js-builds-dropdown-loading'; !this.isLoading) {
this.fetchJobs();
}
},
}, },
triggerButtonClass() { updated() {
return `ci-status-icon-${this.stage.status.group}`; if (this.dropdownContent.length > 0) {
this.stopDropdownClickPropagation();
}
}, },
borderlessIcon() { methods: {
return `${this.stage.status.icon}_borderless`; onClickStage() {
if (!this.isDropdownOpen()) {
this.isLoading = true;
this.fetchJobs();
}
},
fetchJobs() {
this.$http.get(this.stage.dropdown_path)
.then(response => response.json())
.then((data) => {
this.dropdownContent = data.html;
this.isLoading = false;
})
.catch(() => {
this.closeDropdown();
this.isLoading = false;
const flash = new Flash('Something went wrong on our end.');
return flash;
});
},
/**
* When the user right clicks or cmd/ctrl + click in the job name
* the dropdown should not be closed and the link should open in another tab,
* so we stop propagation of the click event inside the dropdown.
*
* Since this component is rendered multiple times per page we need to guarantee we only
* target the click event of this component.
*/
stopDropdownClickPropagation() {
$(this.$el.querySelectorAll('.js-builds-dropdown-list a.mini-pipeline-graph-dropdown-item'))
.on('click', (e) => {
e.stopPropagation();
});
},
closeDropdown() {
if (this.isDropdownOpen()) {
$(this.$refs.dropdown).dropdown('toggle');
}
},
isDropdownOpen() {
return this.$el.classList.contains('open');
},
}, },
}, };
};
</script> </script>
<template> <template>
...@@ -145,36 +145,41 @@ export default { ...@@ -145,36 +145,41 @@ export default {
type="button" type="button"
id="stageDropdown" id="stageDropdown"
aria-haspopup="true" aria-haspopup="true"
aria-expanded="false"> aria-expanded="false"
>
<span <span
aria-hidden="true" aria-hidden="true"
:aria-label="stage.title"> :aria-label="stage.title"
<icon >
:name="borderlessIcon"/> <icon :name="borderlessIcon" />
</span> </span>
<i <i
class="fa fa-caret-down" class="fa fa-caret-down"
aria-hidden="true"> aria-hidden="true"
>
</i> </i>
</button> </button>
<ul <ul
class="dropdown-menu mini-pipeline-graph-dropdown-menu js-builds-dropdown-container" class="dropdown-menu mini-pipeline-graph-dropdown-menu js-builds-dropdown-container"
aria-labelledby="stageDropdown"> aria-labelledby="stageDropdown"
>
<li <li
:class="dropdownClass" :class="dropdownClass"
class="js-builds-dropdown-list scrollable-menu"> class="js-builds-dropdown-list scrollable-menu"
>
<loading-icon v-if="isLoading"/> <loading-icon v-if="isLoading"/>
<ul <ul
v-else v-else
v-html="dropdownContent"> v-html="dropdownContent"
>
</ul> </ul>
</li> </li>
</ul> </ul>
</div> </div>
</script> </template>
...@@ -5,6 +5,12 @@ ...@@ -5,6 +5,12 @@
import timeagoMixin from '../../vue_shared/mixins/timeago'; import timeagoMixin from '../../vue_shared/mixins/timeago';
export default { export default {
directives: {
tooltip,
},
mixins: [
timeagoMixin,
],
props: { props: {
finishedTime: { finishedTime: {
type: String, type: String,
...@@ -15,12 +21,6 @@ ...@@ -15,12 +21,6 @@
required: true, required: true,
}, },
}, },
mixins: [
timeagoMixin,
],
directives: {
tooltip,
},
data() { data() {
return { return {
iconTimerSvg, iconTimerSvg,
...@@ -60,26 +60,29 @@ ...@@ -60,26 +60,29 @@
<div class="table-section section-15 pipelines-time-ago"> <div class="table-section section-15 pipelines-time-ago">
<div <div
class="table-mobile-header" class="table-mobile-header"
role="rowheader"> role="rowheader"
>
Duration Duration
</div> </div>
<div class="table-mobile-content"> <div class="table-mobile-content">
<p <p
class="duration" class="duration"
v-if="hasDuration"> v-if="hasDuration"
<span >
v-html="iconTimerSvg"> <span v-html="iconTimerSvg">
</span> </span>
{{durationFormated}} {{ durationFormated }}
</p> </p>
<p <p
class="finished-at hidden-xs hidden-sm" class="finished-at hidden-xs hidden-sm"
v-if="hasFinishedTime"> v-if="hasFinishedTime"
>
<i <i
class="fa fa-calendar" class="fa fa-calendar"
aria-hidden="true"> aria-hidden="true"
>
</i> </i>
<time <time
...@@ -87,9 +90,9 @@ ...@@ -87,9 +90,9 @@
data-placement="top" data-placement="top"
data-container="body" data-container="body"
:title="tooltipTitle(finishedTime)"> :title="tooltipTitle(finishedTime)">
{{timeFormated(finishedTime)}} {{ timeFormated(finishedTime) }}
</time> </time>
</p> </p>
</div> </div>
</div> </div>
</script> </template>
...@@ -4,6 +4,9 @@ ...@@ -4,6 +4,9 @@
import csrf from '../../../lib/utils/csrf'; import csrf from '../../../lib/utils/csrf';
export default { export default {
components: {
modal,
},
props: { props: {
actionUrl: { actionUrl: {
type: String, type: String,
...@@ -25,9 +28,6 @@ ...@@ -25,9 +28,6 @@
isOpen: false, isOpen: false,
}; };
}, },
components: {
modal,
},
computed: { computed: {
csrfToken() { csrfToken() {
return csrf.token; return csrf.token;
...@@ -99,7 +99,9 @@ Once you confirm %{deleteAccount}, it cannot be undone or recovered.`), ...@@ -99,7 +99,9 @@ Once you confirm %{deleteAccount}, it cannot be undone or recovered.`),
@toggle="toggleOpen" @toggle="toggleOpen"
@submit="onSubmit"> @submit="onSubmit">
<template slot="body" slot-scope="props"> <template
slot="body"
slot-scope="props">
<p v-html="props.text"></p> <p v-html="props.text"></p>
<form <form
...@@ -110,13 +112,19 @@ Once you confirm %{deleteAccount}, it cannot be undone or recovered.`), ...@@ -110,13 +112,19 @@ Once you confirm %{deleteAccount}, it cannot be undone or recovered.`),
<input <input
type="hidden" type="hidden"
name="_method" name="_method"
value="delete" /> value="delete"
/>
<input <input
type="hidden" type="hidden"
name="authenticity_token" name="authenticity_token"
:value="csrfToken" /> :value="csrfToken"
/>
<p id="input-label" v-html="inputLabel"></p> <p
id="input-label"
v-html="inputLabel"
>
</p>
<input <input
v-if="confirmWithPassword" v-if="confirmWithPassword"
...@@ -124,14 +132,16 @@ Once you confirm %{deleteAccount}, it cannot be undone or recovered.`), ...@@ -124,14 +132,16 @@ Once you confirm %{deleteAccount}, it cannot be undone or recovered.`),
class="form-control" class="form-control"
type="password" type="password"
v-model="enteredPassword" v-model="enteredPassword"
aria-labelledby="input-label" /> aria-labelledby="input-label"
/>
<input <input
v-else v-else
name="username" name="username"
class="form-control" class="form-control"
type="text" type="text"
v-model="enteredUsername" v-model="enteredUsername"
aria-labelledby="input-label" /> aria-labelledby="input-label"
/>
</form> </form>
</template> </template>
...@@ -140,7 +150,8 @@ Once you confirm %{deleteAccount}, it cannot be undone or recovered.`), ...@@ -140,7 +150,8 @@ Once you confirm %{deleteAccount}, it cannot be undone or recovered.`),
<button <button
type="button" type="button"
class="btn btn-danger" class="btn btn-danger"
@click="toggleOpen(true)"> @click="toggleOpen(true)"
>
{{ s__('Profiles|Delete account') }} {{ s__('Profiles|Delete account') }}
</button> </button>
</div> </div>
......
<script> <script>
import projectFeatureToggle from '../../../vue_shared/components/toggle_button.vue'; import projectFeatureToggle from '../../../vue_shared/components/toggle_button.vue';
export default { export default {
props: { components: {
name: { projectFeatureToggle,
type: String,
required: false,
default: '',
}, },
options: {
type: Array,
required: false,
default: () => [],
},
value: {
type: Number,
required: false,
default: 0,
},
disabledInput: {
type: Boolean,
required: false,
default: false,
},
},
components: {
projectFeatureToggle,
},
computed: { model: {
featureEnabled() { prop: 'value',
return this.value !== 0; event: 'change',
}, },
displayOptions() { props: {
if (this.featureEnabled) { name: {
return this.options; type: String,
} required: false,
return [ default: '',
[0, 'Enable feature to choose access level'], },
]; options: {
type: Array,
required: false,
default: () => [],
},
value: {
type: Number,
required: false,
default: 0,
},
disabledInput: {
type: Boolean,
required: false,
default: false,
},
}, },
displaySelectInput() { computed: {
return this.disabledInput || !this.featureEnabled || this.displayOptions.length < 2; featureEnabled() {
}, return this.value !== 0;
}, },
model: { displayOptions() {
prop: 'value', if (this.featureEnabled) {
event: 'change', return this.options;
}, }
return [
[0, 'Enable feature to choose access level'],
];
},
methods: { displaySelectInput() {
toggleFeature(featureEnabled) { return this.disabledInput || !this.featureEnabled || this.displayOptions.length < 2;
if (featureEnabled === false || this.options.length < 1) { },
this.$emit('change', 0);
} else {
const [firstOptionValue] = this.options[this.options.length - 1];
this.$emit('change', firstOptionValue);
}
}, },
selectOption(e) { methods: {
this.$emit('change', Number(e.target.value)); toggleFeature(featureEnabled) {
if (featureEnabled === false || this.options.length < 1) {
this.$emit('change', 0);
} else {
const [firstOptionValue] = this.options[this.options.length - 1];
this.$emit('change', firstOptionValue);
}
},
selectOption(e) {
this.$emit('change', Number(e.target.value));
},
}, },
}, };
};
</script> </script>
<template> <template>
<div class="project-feature-controls" :data-for="name"> <div
class="project-feature-controls"
:data-for="name"
>
<input <input
v-if="name" v-if="name"
type="hidden" type="hidden"
...@@ -81,7 +84,7 @@ export default { ...@@ -81,7 +84,7 @@ export default {
<project-feature-toggle <project-feature-toggle
:value="featureEnabled" :value="featureEnabled"
@change="toggleFeature" @change="toggleFeature"
:disabledInput="disabledInput" :disabled-input="disabledInput"
/> />
<div class="select-wrapper"> <div class="select-wrapper">
<select <select
...@@ -95,10 +98,14 @@ export default { ...@@ -95,10 +98,14 @@ export default {
:value="optionValue" :value="optionValue"
:selected="optionValue === value" :selected="optionValue === value"
> >
{{optionName}} {{ optionName }}
</option> </option>
</select> </select>
<i aria-hidden="true" class="fa fa-chevron-down"></i> <i
aria-hidden="true"
class="fa fa-chevron-down"
>
</i>
</div> </div>
</div> </div>
</template> </template>
<script> <script>
export default { export default {
props: { props: {
label: { label: {
type: String, type: String,
required: false, required: false,
default: null, default: null,
},
helpPath: {
type: String,
required: false,
default: null,
},
helpText: {
type: String,
required: false,
default: null,
},
}, },
helpPath: { };
type: String,
required: false,
default: null,
},
helpText: {
type: String,
required: false,
default: null,
},
},
};
</script> </script>
<template> <template>
<div class="project-feature-row"> <div class="project-feature-row">
<label v-if="label" class="label-light"> <label
{{label}} v-if="label"
<a v-if="helpPath" :href="helpPath" target="_blank"> class="label-light"
<i aria-hidden="true" data-hidden="true" class="fa fa-question-circle"></i> >
{{ label }}
<a
v-if="helpPath"
:href="helpPath"
target="_blank"
>
<i
aria-hidden="true"
data-hidden="true"
class="fa fa-question-circle"
>
</i>
</a> </a>
</label> </label>
<span v-if="helpText" class="help-block"> <span
{{helpText}} v-if="helpText"
class="help-block"
>
{{ helpText }}
</span> </span>
<slot /> <slot />
</div> </div>
......
<script> <script>
import projectFeatureSetting from './project_feature_setting.vue'; import projectFeatureSetting from './project_feature_setting.vue';
import projectFeatureToggle from '../../../vue_shared/components/toggle_button.vue'; import projectFeatureToggle from '../../../vue_shared/components/toggle_button.vue';
import projectSettingRow from './project_setting_row.vue'; import projectSettingRow from './project_setting_row.vue';
import { visibilityOptions, visibilityLevelDescriptions } from '../constants'; import { visibilityOptions, visibilityLevelDescriptions } from '../constants';
import { toggleHiddenClassBySelector } from '../external'; import { toggleHiddenClassBySelector } from '../external';
export default { export default {
props: { components: {
currentSettings: { projectFeatureSetting,
type: Object, projectFeatureToggle,
required: true, projectSettingRow,
}, },
canChangeVisibilityLevel: {
type: Boolean,
required: false,
default: false,
},
allowedVisibilityOptions: {
type: Array,
required: false,
default: () => [0, 10, 20],
},
lfsAvailable: {
type: Boolean,
required: false,
default: false,
},
registryAvailable: {
type: Boolean,
required: false,
default: false,
},
visibilityHelpPath: {
type: String,
required: false,
},
lfsHelpPath: {
type: String,
required: false,
},
registryHelpPath: {
type: String,
required: false,
},
},
data() { props: {
const defaults = { currentSettings: {
visibilityOptions, type: Object,
visibilityLevel: visibilityOptions.PUBLIC, required: true,
issuesAccessLevel: 20, },
repositoryAccessLevel: 20, canChangeVisibilityLevel: {
mergeRequestsAccessLevel: 20, type: Boolean,
buildsAccessLevel: 20, required: false,
wikiAccessLevel: 20, default: false,
snippetsAccessLevel: 20, },
containerRegistryEnabled: true, allowedVisibilityOptions: {
lfsEnabled: true, type: Array,
requestAccessEnabled: true, required: false,
highlightChangesClass: false, default: () => [0, 10, 20],
}; },
lfsAvailable: {
return { ...defaults, ...this.currentSettings }; type: Boolean,
}, required: false,
default: false,
components: { },
projectFeatureSetting, registryAvailable: {
projectFeatureToggle, type: Boolean,
projectSettingRow, required: false,
}, default: false,
},
computed: { visibilityHelpPath: {
featureAccessLevelOptions() { type: String,
const options = [ required: false,
[10, 'Only Project Members'], },
]; lfsHelpPath: {
if (this.visibilityLevel !== visibilityOptions.PRIVATE) { type: String,
options.push([20, 'Everyone With Access']); required: false,
} },
return options; registryHelpPath: {
type: String,
required: false,
},
}, },
repoFeatureAccessLevelOptions() { data() {
return this.featureAccessLevelOptions.filter( const defaults = {
([value]) => value <= this.repositoryAccessLevel, visibilityOptions,
); visibilityLevel: visibilityOptions.PUBLIC,
}, issuesAccessLevel: 20,
repositoryAccessLevel: 20,
mergeRequestsAccessLevel: 20,
buildsAccessLevel: 20,
wikiAccessLevel: 20,
snippetsAccessLevel: 20,
containerRegistryEnabled: true,
lfsEnabled: true,
requestAccessEnabled: true,
highlightChangesClass: false,
};
repositoryEnabled() { return { ...defaults, ...this.currentSettings };
return this.repositoryAccessLevel > 0;
}, },
visibilityLevelDescription() { computed: {
return visibilityLevelDescriptions[this.visibilityLevel]; featureAccessLevelOptions() {
}, const options = [
}, [10, 'Only Project Members'],
];
if (this.visibilityLevel !== visibilityOptions.PRIVATE) {
options.push([20, 'Everyone With Access']);
}
return options;
},
methods: { repoFeatureAccessLevelOptions() {
highlightChanges() { return this.featureAccessLevelOptions.filter(
this.highlightChangesClass = true; ([value]) => value <= this.repositoryAccessLevel,
this.$nextTick(() => { );
this.highlightChangesClass = false; },
});
},
visibilityAllowed(option) { repositoryEnabled() {
return this.allowedVisibilityOptions.includes(option); return this.repositoryAccessLevel > 0;
}, },
},
watch: { visibilityLevelDescription() {
visibilityLevel(value, oldValue) { return visibilityLevelDescriptions[this.visibilityLevel];
if (value === visibilityOptions.PRIVATE) { },
// when private, features are restricted to "only team members"
this.issuesAccessLevel = Math.min(10, this.issuesAccessLevel);
this.repositoryAccessLevel = Math.min(10, this.repositoryAccessLevel);
this.mergeRequestsAccessLevel = Math.min(10, this.mergeRequestsAccessLevel);
this.buildsAccessLevel = Math.min(10, this.buildsAccessLevel);
this.wikiAccessLevel = Math.min(10, this.wikiAccessLevel);
this.snippetsAccessLevel = Math.min(10, this.snippetsAccessLevel);
this.highlightChanges();
} else if (oldValue === visibilityOptions.PRIVATE) {
// if changing away from private, make enabled features more permissive
if (this.issuesAccessLevel > 0) this.issuesAccessLevel = 20;
if (this.repositoryAccessLevel > 0) this.repositoryAccessLevel = 20;
if (this.mergeRequestsAccessLevel > 0) this.mergeRequestsAccessLevel = 20;
if (this.buildsAccessLevel > 0) this.buildsAccessLevel = 20;
if (this.wikiAccessLevel > 0) this.wikiAccessLevel = 20;
if (this.snippetsAccessLevel > 0) this.snippetsAccessLevel = 20;
this.highlightChanges();
}
}, },
repositoryAccessLevel(value, oldValue) { watch: {
if (value < oldValue) { visibilityLevel(value, oldValue) {
// sub-features cannot have more premissive access level if (value === visibilityOptions.PRIVATE) {
this.mergeRequestsAccessLevel = Math.min(this.mergeRequestsAccessLevel, value); // when private, features are restricted to "only team members"
this.buildsAccessLevel = Math.min(this.buildsAccessLevel, value); this.issuesAccessLevel = Math.min(10, this.issuesAccessLevel);
this.repositoryAccessLevel = Math.min(10, this.repositoryAccessLevel);
this.mergeRequestsAccessLevel = Math.min(10, this.mergeRequestsAccessLevel);
this.buildsAccessLevel = Math.min(10, this.buildsAccessLevel);
this.wikiAccessLevel = Math.min(10, this.wikiAccessLevel);
this.snippetsAccessLevel = Math.min(10, this.snippetsAccessLevel);
this.highlightChanges();
} else if (oldValue === visibilityOptions.PRIVATE) {
// if changing away from private, make enabled features more permissive
if (this.issuesAccessLevel > 0) this.issuesAccessLevel = 20;
if (this.repositoryAccessLevel > 0) this.repositoryAccessLevel = 20;
if (this.mergeRequestsAccessLevel > 0) this.mergeRequestsAccessLevel = 20;
if (this.buildsAccessLevel > 0) this.buildsAccessLevel = 20;
if (this.wikiAccessLevel > 0) this.wikiAccessLevel = 20;
if (this.snippetsAccessLevel > 0) this.snippetsAccessLevel = 20;
this.highlightChanges();
}
},
repositoryAccessLevel(value, oldValue) {
if (value < oldValue) {
// sub-features cannot have more premissive access level
this.mergeRequestsAccessLevel = Math.min(this.mergeRequestsAccessLevel, value);
this.buildsAccessLevel = Math.min(this.buildsAccessLevel, value);
if (value === 0) { if (value === 0) {
this.containerRegistryEnabled = false; this.containerRegistryEnabled = false;
this.lfsEnabled = false; this.lfsEnabled = false;
}
} else if (oldValue === 0) {
this.mergeRequestsAccessLevel = value;
this.buildsAccessLevel = value;
this.containerRegistryEnabled = true;
this.lfsEnabled = true;
} }
} else if (oldValue === 0) { },
this.mergeRequestsAccessLevel = value;
this.buildsAccessLevel = value;
this.containerRegistryEnabled = true;
this.lfsEnabled = true;
}
},
issuesAccessLevel(value, oldValue) { issuesAccessLevel(value, oldValue) {
if (value === 0) toggleHiddenClassBySelector('.issues-feature', true); if (value === 0) toggleHiddenClassBySelector('.issues-feature', true);
else if (oldValue === 0) toggleHiddenClassBySelector('.issues-feature', false); else if (oldValue === 0) toggleHiddenClassBySelector('.issues-feature', false);
}, },
mergeRequestsAccessLevel(value, oldValue) { mergeRequestsAccessLevel(value, oldValue) {
if (value === 0) toggleHiddenClassBySelector('.merge-requests-feature', true); if (value === 0) toggleHiddenClassBySelector('.merge-requests-feature', true);
else if (oldValue === 0) toggleHiddenClassBySelector('.merge-requests-feature', false); else if (oldValue === 0) toggleHiddenClassBySelector('.merge-requests-feature', false);
}, },
buildsAccessLevel(value, oldValue) { buildsAccessLevel(value, oldValue) {
if (value === 0) toggleHiddenClassBySelector('.builds-feature', true); if (value === 0) toggleHiddenClassBySelector('.builds-feature', true);
else if (oldValue === 0) toggleHiddenClassBySelector('.builds-feature', false); else if (oldValue === 0) toggleHiddenClassBySelector('.builds-feature', false);
},
}, },
},
};
methods: {
highlightChanges() {
this.highlightChangesClass = true;
this.$nextTick(() => {
this.highlightChangesClass = false;
});
},
visibilityAllowed(option) {
return this.allowedVisibilityOptions.includes(option);
},
},
};
</script> </script>
<template> <template>
...@@ -203,22 +202,36 @@ export default { ...@@ -203,22 +202,36 @@ export default {
Public Public
</option> </option>
</select> </select>
<i aria-hidden="true" data-hidden="true" class="fa fa-chevron-down"></i> <i
aria-hidden="true"
data-hidden="true"
class="fa fa-chevron-down"
>
</i>
</div> </div>
</div> </div>
<span class="help-block">{{ visibilityLevelDescription }}</span> <span class="help-block">{{ visibilityLevelDescription }}</span>
<label v-if="visibilityLevel !== visibilityOptions.PUBLIC" class="request-access"> <label
v-if="visibilityLevel !== visibilityOptions.PUBLIC"
class="request-access"
>
<input <input
type="hidden" type="hidden"
name="project[request_access_enabled]" name="project[request_access_enabled]"
:value="requestAccessEnabled" :value="requestAccessEnabled"
/> />
<input type="checkbox" v-model="requestAccessEnabled" /> <input
type="checkbox"
v-model="requestAccessEnabled"
/>
Allow users to request access Allow users to request access
</label> </label>
</project-setting-row> </project-setting-row>
</div> </div>
<div class="project-feature-settings" :class="{ 'highlight-changes': highlightChangesClass }"> <div
class="project-feature-settings"
:class="{ 'highlight-changes': highlightChangesClass }"
>
<project-setting-row <project-setting-row
label="Issues" label="Issues"
help-text="Lightweight issue tracking system for this project" help-text="Lightweight issue tracking system for this project"
...@@ -248,7 +261,7 @@ export default { ...@@ -248,7 +261,7 @@ export default {
name="project[project_feature_attributes][merge_requests_access_level]" name="project[project_feature_attributes][merge_requests_access_level]"
:options="repoFeatureAccessLevelOptions" :options="repoFeatureAccessLevelOptions"
v-model="mergeRequestsAccessLevel" v-model="mergeRequestsAccessLevel"
:disabledInput="!repositoryEnabled" :disabled-input="!repositoryEnabled"
/> />
</project-setting-row> </project-setting-row>
<project-setting-row <project-setting-row
...@@ -259,7 +272,7 @@ export default { ...@@ -259,7 +272,7 @@ export default {
name="project[project_feature_attributes][builds_access_level]" name="project[project_feature_attributes][builds_access_level]"
:options="repoFeatureAccessLevelOptions" :options="repoFeatureAccessLevelOptions"
v-model="buildsAccessLevel" v-model="buildsAccessLevel"
:disabledInput="!repositoryEnabled" :disabled-input="!repositoryEnabled"
/> />
</project-setting-row> </project-setting-row>
<project-setting-row <project-setting-row
...@@ -271,7 +284,7 @@ export default { ...@@ -271,7 +284,7 @@ export default {
<project-feature-toggle <project-feature-toggle
name="project[container_registry_enabled]" name="project[container_registry_enabled]"
v-model="containerRegistryEnabled" v-model="containerRegistryEnabled"
:disabledInput="!repositoryEnabled" :disabled-input="!repositoryEnabled"
/> />
</project-setting-row> </project-setting-row>
<project-setting-row <project-setting-row
...@@ -283,7 +296,7 @@ export default { ...@@ -283,7 +296,7 @@ export default {
<project-feature-toggle <project-feature-toggle
name="project[lfs_enabled]" name="project[lfs_enabled]"
v-model="lfsEnabled" v-model="lfsEnabled"
:disabledInput="!repositoryEnabled" :disabled-input="!repositoryEnabled"
/> />
</project-setting-row> </project-setting-row>
</div> </div>
......
...@@ -47,6 +47,22 @@ export default { ...@@ -47,6 +47,22 @@ export default {
return this.store.getSearchedProjects(); return this.store.getSearchedProjects();
}, },
}, },
created() {
if (this.currentProject.id) {
this.logCurrentProjectAccess();
}
eventHub.$on('dropdownOpen', this.fetchFrequentProjects);
eventHub.$on('searchProjects', this.fetchSearchedProjects);
eventHub.$on('searchCleared', this.handleSearchClear);
eventHub.$on('searchFailed', this.handleSearchFailure);
},
beforeDestroy() {
eventHub.$off('dropdownOpen', this.fetchFrequentProjects);
eventHub.$off('searchProjects', this.fetchSearchedProjects);
eventHub.$off('searchCleared', this.handleSearchClear);
eventHub.$off('searchFailed', this.handleSearchFailure);
},
methods: { methods: {
toggleFrequentProjectsList(state) { toggleFrequentProjectsList(state) {
this.isLoadingProjects = !state; this.isLoadingProjects = !state;
...@@ -108,22 +124,6 @@ export default { ...@@ -108,22 +124,6 @@ export default {
this.toggleSearchProjectsList(true); this.toggleSearchProjectsList(true);
}, },
}, },
created() {
if (this.currentProject.id) {
this.logCurrentProjectAccess();
}
eventHub.$on('dropdownOpen', this.fetchFrequentProjects);
eventHub.$on('searchProjects', this.fetchSearchedProjects);
eventHub.$on('searchCleared', this.handleSearchClear);
eventHub.$on('searchFailed', this.handleSearchFailure);
},
beforeDestroy() {
eventHub.$off('dropdownOpen', this.fetchFrequentProjects);
eventHub.$off('searchProjects', this.fetchSearchedProjects);
eventHub.$off('searchCleared', this.handleSearchClear);
eventHub.$off('searchFailed', this.handleSearchFailure);
},
}; };
</script> </script>
......
<script> <script>
import { s__ } from '../../locale'; import { s__ } from '../../locale';
import projectsListItem from './projects_list_item.vue'; import projectsListItem from './projects_list_item.vue';
export default { export default {
components: { components: {
projectsListItem, projectsListItem,
},
props: {
projects: {
type: Array,
required: true,
}, },
localStorageFailed: { props: {
type: Boolean, projects: {
required: true, type: Array,
required: true,
},
localStorageFailed: {
type: Boolean,
required: true,
},
}, },
}, computed: {
computed: { isListEmpty() {
isListEmpty() { return this.projects.length === 0;
return this.projects.length === 0; },
listEmptyMessage() {
return this.localStorageFailed ?
s__('ProjectsDropdown|This feature requires browser localStorage support') :
s__('ProjectsDropdown|Projects you visit often will appear here');
},
}, },
listEmptyMessage() { };
return this.localStorageFailed ?
s__('ProjectsDropdown|This feature requires browser localStorage support') :
s__('ProjectsDropdown|Projects you visit often will appear here');
},
},
};
</script> </script>
<template> <template>
...@@ -40,7 +40,7 @@ export default { ...@@ -40,7 +40,7 @@ export default {
class="section-empty" class="section-empty"
v-if="isListEmpty" v-if="isListEmpty"
> >
{{listEmptyMessage}} {{ listEmptyMessage }}
</li> </li>
<projects-list-item <projects-list-item
v-else v-else
......
<script> <script>
import identicon from '../../vue_shared/components/identicon.vue'; import identicon from '../../vue_shared/components/identicon.vue';
export default { export default {
components: { components: {
identicon, identicon,
},
props: {
matcher: {
type: String,
required: false,
}, },
projectId: { props: {
type: Number, matcher: {
required: true, type: String,
}, required: false,
projectName: { },
type: String, projectId: {
required: true, type: Number,
}, required: true,
namespace: { },
type: String, projectName: {
required: true, type: String,
}, required: true,
webUrl: { },
type: String, namespace: {
required: true, type: String,
}, required: true,
avatarUrl: { },
required: true, webUrl: {
validator(value) { type: String,
return value === null || typeof value === 'string'; required: true,
},
avatarUrl: {
required: true,
validator(value) {
return value === null || typeof value === 'string';
},
}, },
}, },
}, computed: {
computed: { hasAvatar() {
hasAvatar() { return this.avatarUrl !== null;
return this.avatarUrl !== null; },
}, highlightedProjectName() {
highlightedProjectName() { if (this.matcher) {
if (this.matcher) { const matcherRegEx = new RegExp(this.matcher, 'gi');
const matcherRegEx = new RegExp(this.matcher, 'gi'); const matches = this.projectName.match(matcherRegEx);
const matches = this.projectName.match(matcherRegEx);
if (matches && matches.length > 0) { if (matches && matches.length > 0) {
return this.projectName.replace(matches[0], `<b>${matches[0]}</b>`); return this.projectName.replace(matches[0], `<b>${matches[0]}</b>`);
}
} }
} return this.projectName;
return this.projectName; },
}, /**
/** * Smartly truncates project namespace by doing two things;
* Smartly truncates project namespace by doing two things; * 1. Only include Group names in path by removing project name
* 1. Only include Group names in path by removing project name * 2. Only include first and last group names in the path
* 2. Only include first and last group names in the path * when namespace has more than 2 groups present
* when namespace has more than 2 groups present *
* * First part (removal of project name from namespace) can be
* First part (removal of project name from namespace) can be * done from backend but doing so involves migration of
* done from backend but doing so involves migration of * existing project namespaces which is not wise thing to do.
* existing project namespaces which is not wise thing to do. */
*/ truncatedNamespace() {
truncatedNamespace() { const namespaceArr = this.namespace.split(' / ');
const namespaceArr = this.namespace.split(' / '); namespaceArr.splice(-1, 1);
namespaceArr.splice(-1, 1); let namespace = namespaceArr.join(' / ');
let namespace = namespaceArr.join(' / ');
if (namespaceArr.length > 2) { if (namespaceArr.length > 2) {
namespace = `${namespaceArr[0]} / ... / ${namespaceArr.pop()}`; namespace = `${namespaceArr[0]} / ... / ${namespaceArr.pop()}`;
} }
return namespace; return namespace;
},
}, },
}, };
};
</script> </script>
<template> <template>
...@@ -92,7 +92,7 @@ export default { ...@@ -92,7 +92,7 @@ export default {
<identicon <identicon
v-else v-else
size-class="s32" size-class="s32"
:entity-id=projectId :entity-id="projectId"
:entity-name="projectName" :entity-name="projectName"
/> />
</div> </div>
...@@ -108,7 +108,7 @@ export default { ...@@ -108,7 +108,7 @@ export default {
<div <div
class="project-namespace" class="project-namespace"
:title="namespace" :title="namespace"
>{{truncatedNamespace}}</div> >{{ truncatedNamespace }}</div>
</div> </div>
</a> </a>
</li> </li>
......
<script> <script>
import _ from 'underscore'; import _ from 'underscore';
import eventHub from '../event_hub'; import eventHub from '../event_hub';
export default { export default {
data() { data() {
return { return {
searchQuery: '', searchQuery: '',
}; };
},
watch: {
searchQuery() {
this.handleInput();
}, },
}, watch: {
methods: { searchQuery() {
setFocus() { this.handleInput();
this.$refs.search.focus(); },
}, },
emitSearchEvents() { mounted() {
if (this.searchQuery) { eventHub.$on('dropdownOpen', this.setFocus);
eventHub.$emit('searchProjects', this.searchQuery);
} else {
eventHub.$emit('searchCleared');
}
}, },
/** beforeDestroy() {
* Callback function within _.debounce is intentionally eventHub.$off('dropdownOpen', this.setFocus);
* kept as ES5 `function() {}` instead of ES6 `() => {}` },
* as it otherwise messes up function context methods: {
* and component reference is no longer accessible via `this` setFocus() {
*/ this.$refs.search.focus();
// eslint-disable-next-line func-names },
handleInput: _.debounce(function () { emitSearchEvents() {
this.emitSearchEvents(); if (this.searchQuery) {
}, 500), eventHub.$emit('searchProjects', this.searchQuery);
}, } else {
mounted() { eventHub.$emit('searchCleared');
eventHub.$on('dropdownOpen', this.setFocus); }
}, },
beforeDestroy() { /**
eventHub.$off('dropdownOpen', this.setFocus); * Callback function within _.debounce is intentionally
}, * kept as ES5 `function() {}` instead of ES6 `() => {}`
}; * as it otherwise messes up function context
* and component reference is no longer accessible via `this`
*/
// eslint-disable-next-line func-names
handleInput: _.debounce(function () {
this.emitSearchEvents();
}, 500),
},
};
</script> </script>
<template> <template>
......
<script> <script>
/* globals Flash */
import { mapGetters, mapActions } from 'vuex'; import { mapGetters, mapActions } from 'vuex';
import '../../flash'; import Flash from '../../flash';
import loadingIcon from '../../vue_shared/components/loading_icon.vue'; import loadingIcon from '../../vue_shared/components/loading_icon.vue';
import store from '../stores'; import store from '../stores';
import collapsibleContainer from './collapsible_container.vue'; import collapsibleContainer from './collapsible_container.vue';
import { errorMessages, errorMessagesTypes } from '../constants'; import { errorMessages, errorMessagesTypes } from '../constants';
export default { export default {
name: 'registryListApp', name: 'RegistryListApp',
components: {
collapsibleContainer,
loadingIcon,
},
props: { props: {
endpoint: { endpoint: {
type: String, type: String,
...@@ -16,22 +19,12 @@ ...@@ -16,22 +19,12 @@
}, },
}, },
store, store,
components: {
collapsibleContainer,
loadingIcon,
},
computed: { computed: {
...mapGetters([ ...mapGetters([
'isLoading', 'isLoading',
'repos', 'repos',
]), ]),
}, },
methods: {
...mapActions([
'setMainEndpoint',
'fetchRepos',
]),
},
created() { created() {
this.setMainEndpoint(this.endpoint); this.setMainEndpoint(this.endpoint);
}, },
...@@ -39,6 +32,12 @@ ...@@ -39,6 +32,12 @@
this.fetchRepos() this.fetchRepos()
.catch(() => Flash(errorMessages[errorMessagesTypes.FETCH_REPOS])); .catch(() => Flash(errorMessages[errorMessagesTypes.FETCH_REPOS]));
}, },
methods: {
...mapActions([
'setMainEndpoint',
'fetchRepos',
]),
},
}; };
</script> </script>
<template> <template>
...@@ -46,17 +45,17 @@ ...@@ -46,17 +45,17 @@
<loading-icon <loading-icon
v-if="isLoading" v-if="isLoading"
size="3" size="3"
/> />
<collapsible-container <collapsible-container
v-else-if="!isLoading && repos.length" v-else-if="!isLoading && repos.length"
v-for="(item, index) in repos" v-for="(item, index) in repos"
:key="index" :key="index"
:repo="item" :repo="item"
/> />
<p v-else-if="!isLoading && !repos.length"> <p v-else-if="!isLoading && !repos.length">
{{__("No container images stored for this project. Add one by following the instructions above.")}} {{ __("No container images stored for this project. Add one by following the instructions above.") }}
</p> </p>
</div> </div>
</template> </template>
<script> <script>
/* globals Flash */
import { mapActions } from 'vuex'; import { mapActions } from 'vuex';
import '../../flash'; import Flash from '../../flash';
import clipboardButton from '../../vue_shared/components/clipboard_button.vue'; import clipboardButton from '../../vue_shared/components/clipboard_button.vue';
import loadingIcon from '../../vue_shared/components/loading_icon.vue'; import loadingIcon from '../../vue_shared/components/loading_icon.vue';
import tooltip from '../../vue_shared/directives/tooltip'; import tooltip from '../../vue_shared/directives/tooltip';
...@@ -9,13 +8,7 @@ ...@@ -9,13 +8,7 @@
import { errorMessages, errorMessagesTypes } from '../constants'; import { errorMessages, errorMessagesTypes } from '../constants';
export default { export default {
name: 'collapsibeContainerRegisty', name: 'CollapsibeContainerRegisty',
props: {
repo: {
type: Object,
required: true,
},
},
components: { components: {
clipboardButton, clipboardButton,
loadingIcon, loadingIcon,
...@@ -24,6 +17,12 @@ ...@@ -24,6 +17,12 @@
directives: { directives: {
tooltip, tooltip,
}, },
props: {
repo: {
type: Object,
required: true,
},
},
data() { data() {
return { return {
isOpen: false, isOpen: false,
...@@ -65,28 +64,29 @@ ...@@ -65,28 +64,29 @@
<template> <template>
<div class="container-image"> <div class="container-image">
<div <div class="container-image-head">
class="container-image-head">
<button <button
type="button" type="button"
@click="toggleRepo" @click="toggleRepo"
class="js-toggle-repo btn-link"> class="js-toggle-repo btn-link"
>
<i <i
class="fa" class="fa"
:class="{ :class="{
'fa-chevron-right': !isOpen, 'fa-chevron-right': !isOpen,
'fa-chevron-up': isOpen, 'fa-chevron-up': isOpen,
}" }"
aria-hidden="true"> aria-hidden="true"
>
</i> </i>
{{repo.name}} {{ repo.name }}
</button> </button>
<clipboard-button <clipboard-button
v-if="repo.location" v-if="repo.location"
:text="clipboardText" :text="clipboardText"
:title="repo.location" :title="repo.location"
/> />
<div class="controls hidden-xs pull-right"> <div class="controls hidden-xs pull-right">
<button <button
...@@ -96,35 +96,38 @@ ...@@ -96,35 +96,38 @@
:title="s__('ContainerRegistry|Remove repository')" :title="s__('ContainerRegistry|Remove repository')"
:aria-label="s__('ContainerRegistry|Remove repository')" :aria-label="s__('ContainerRegistry|Remove repository')"
v-tooltip v-tooltip
@click="handleDeleteRepository"> @click="handleDeleteRepository"
>
<i <i
class="fa fa-trash" class="fa fa-trash"
aria-hidden="true"> aria-hidden="true"
>
</i> </i>
</button> </button>
</div> </div>
</div> </div>
<loading-icon <loading-icon
v-if="repo.isLoading" v-if="repo.isLoading"
class="append-bottom-20" class="append-bottom-20"
size="2" size="2"
/> />
<div <div
v-else-if="!repo.isLoading && isOpen" v-else-if="!repo.isLoading && isOpen"
class="container-image-tags"> class="container-image-tags"
>
<table-registry <table-registry
v-if="repo.list.length" v-if="repo.list.length"
:repo="repo" :repo="repo"
/> />
<div <div
v-else v-else
class="nothing-here-block"> class="nothing-here-block"
{{s__("ContainerRegistry|No tags in Container Registry for this container image.")}} >
{{ s__("ContainerRegistry|No tags in Container Registry for this container image.") }}
</div> </div>
</div> </div>
</div> </div>
......
<script> <script>
/* globals Flash */
import { mapActions } from 'vuex'; import { mapActions } from 'vuex';
import { n__ } from '../../locale'; import { n__ } from '../../locale';
import '../../flash'; import Flash from '../../flash';
import clipboardButton from '../../vue_shared/components/clipboard_button.vue'; import clipboardButton from '../../vue_shared/components/clipboard_button.vue';
import tablePagination from '../../vue_shared/components/table_pagination.vue'; import tablePagination from '../../vue_shared/components/table_pagination.vue';
import tooltip from '../../vue_shared/directives/tooltip'; import tooltip from '../../vue_shared/directives/tooltip';
...@@ -11,21 +10,21 @@ ...@@ -11,21 +10,21 @@
import { numberToHumanSize } from '../../lib/utils/number_utils'; import { numberToHumanSize } from '../../lib/utils/number_utils';
export default { export default {
props: {
repo: {
type: Object,
required: true,
},
},
components: { components: {
clipboardButton, clipboardButton,
tablePagination, tablePagination,
}, },
directives: {
tooltip,
},
mixins: [ mixins: [
timeagoMixin, timeagoMixin,
], ],
directives: { props: {
tooltip, repo: {
type: Object,
required: true,
},
}, },
computed: { computed: {
shouldRenderPagination() { shouldRenderPagination() {
...@@ -68,75 +67,78 @@ ...@@ -68,75 +67,78 @@
}; };
</script> </script>
<template> <template>
<div> <div>
<table class="table tags"> <table class="table tags">
<thead> <thead>
<tr> <tr>
<th>{{s__('ContainerRegistry|Tag')}}</th> <th>{{ s__('ContainerRegistry|Tag') }}</th>
<th>{{s__('ContainerRegistry|Tag ID')}}</th> <th>{{ s__('ContainerRegistry|Tag ID') }}</th>
<th>{{s__("ContainerRegistry|Size")}}</th> <th>{{ s__("ContainerRegistry|Size") }}</th>
<th>{{s__("ContainerRegistry|Created")}}</th> <th>{{ s__("ContainerRegistry|Created") }}</th>
<th></th> <th></th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr <tr
v-for="(item, i) in repo.list" v-for="(item, i) in repo.list"
:key="i"> :key="i">
<td> <td>
{{item.tag}} {{ item.tag }}
<clipboard-button <clipboard-button
v-if="item.location" v-if="item.location"
:title="item.location" :title="item.location"
:text="clipboardText(item.location)" :text="clipboardText(item.location)"
/> />
</td> </td>
<td> <td>
<span <span
v-tooltip v-tooltip
:title="item.revision" :title="item.revision"
data-placement="bottom"> data-placement="bottom"
{{item.shortRevision}} >
{{ item.shortRevision }}
</span> </span>
</td> </td>
<td> <td>
{{formatSize(item.size)}} {{ formatSize(item.size) }}
<template v-if="item.size && item.layers"> <template v-if="item.size && item.layers">
&middot; &middot;
</template> </template>
{{layers(item)}} {{ layers(item) }}
</td> </td>
<td> <td>
{{timeFormated(item.createdAt)}} {{ timeFormated(item.createdAt) }}
</td> </td>
<td class="content"> <td class="content">
<button <button
v-if="item.canDelete" v-if="item.canDelete"
type="button" type="button"
class="js-delete-registry btn btn-danger hidden-xs pull-right" class="js-delete-registry btn btn-danger hidden-xs pull-right"
:title="s__('ContainerRegistry|Remove tag')" :title="s__('ContainerRegistry|Remove tag')"
:aria-label="s__('ContainerRegistry|Remove tag')" :aria-label="s__('ContainerRegistry|Remove tag')"
data-container="body" data-container="body"
v-tooltip v-tooltip
@click="handleDeleteRegistry(item)"> @click="handleDeleteRegistry(item)"
<i >
class="fa fa-trash" <i
aria-hidden="true"> class="fa fa-trash"
</i> aria-hidden="true"
</button> >
</td> </i>
</tr> </button>
</tbody> </td>
</table> </tr>
</tbody>
</table>
<table-pagination <table-pagination
v-if="shouldRenderPagination" v-if="shouldRenderPagination"
:change="onPageChange" :change="onPageChange"
:page-info="repo.pagination" :page-info="repo.pagination"
/> />
</div> </div>
</template> </template>
<script> <script>
import Flash from '../../../flash'; import Flash from '../../../flash';
import editForm from './edit_form.vue'; import editForm from './edit_form.vue';
import Icon from '../../../vue_shared/components/icon.vue'; import Icon from '../../../vue_shared/components/icon.vue';
export default { export default {
components: { components: {
editForm, editForm,
Icon, Icon,
},
props: {
isConfidential: {
required: true,
type: Boolean,
}, },
isEditable: { props: {
required: true, isConfidential: {
type: Boolean, required: true,
type: Boolean,
},
isEditable: {
required: true,
type: Boolean,
},
service: {
required: true,
type: Object,
},
}, },
service: { data() {
required: true, return {
type: Object, edit: false,
};
}, },
}, computed: {
data() { confidentialityIcon() {
return { return this.isConfidential ? 'eye-slash' : 'eye';
edit: false, },
};
},
computed: {
confidentialityIcon() {
return this.isConfidential ? 'eye-slash' : 'eye';
}, },
}, methods: {
methods: { toggleForm() {
toggleForm() { this.edit = !this.edit;
this.edit = !this.edit; },
updateConfidentialAttribute(confidential) {
this.service.update('issue', { confidential })
.then(() => location.reload())
.catch(() => {
Flash(`Something went wrong trying to
change the confidentiality of this issue`);
});
},
}, },
updateConfidentialAttribute(confidential) { };
this.service.update('issue', { confidential })
.then(() => location.reload())
.catch(() => {
Flash(`Something went wrong trying to
change the confidentiality of this issue`);
});
},
},
};
</script> </script>
<template> <template>
...@@ -54,8 +54,8 @@ change the confidentiality of this issue`); ...@@ -54,8 +54,8 @@ change the confidentiality of this issue`);
<icon <icon
:name="confidentialityIcon" :name="confidentialityIcon"
:size="16" :size="16"
aria-hidden="true"> aria-hidden="true"
</icon> />
</div> </div>
<div class="title hide-collapsed"> <div class="title hide-collapsed">
Confidentiality Confidentiality
...@@ -75,22 +75,26 @@ change the confidentiality of this issue`); ...@@ -75,22 +75,26 @@ change the confidentiality of this issue`);
:is-confidential="isConfidential" :is-confidential="isConfidential"
:update-confidential-attribute="updateConfidentialAttribute" :update-confidential-attribute="updateConfidentialAttribute"
/> />
<div v-if="!isConfidential" class="no-value sidebar-item-value"> <div
v-if="!isConfidential"
class="no-value sidebar-item-value">
<icon <icon
name="eye" name="eye"
:size="16" :size="16"
aria-hidden="true" aria-hidden="true"
class="sidebar-item-icon inline"> class="sidebar-item-icon inline"
</icon> />
Not confidential Not confidential
</div> </div>
<div v-else class="value sidebar-item-value hide-collapsed"> <div
v-else
class="value sidebar-item-value hide-collapsed">
<icon <icon
name="eye-slash" name="eye-slash"
:size="16" :size="16"
aria-hidden="true" aria-hidden="true"
class="sidebar-item-icon inline is-active"> class="sidebar-item-icon inline is-active"
</icon> />
This issue is confidential This issue is confidential
</div> </div>
</div> </div>
......
<script> <script>
import editFormButtons from './edit_form_buttons.vue'; import editFormButtons from './edit_form_buttons.vue';
export default { export default {
props: { components: {
isConfidential: { editFormButtons,
required: true,
type: Boolean,
}, },
toggleForm: { props: {
required: true, isConfidential: {
type: Function, required: true,
type: Boolean,
},
toggleForm: {
required: true,
type: Function,
},
updateConfidentialAttribute: {
required: true,
type: Function,
},
}, },
updateConfidentialAttribute: { };
required: true,
type: Function,
},
},
components: {
editFormButtons,
},
};
</script> </script>
<template> <template>
......
<script> <script>
import editFormButtons from './edit_form_buttons.vue'; import editFormButtons from './edit_form_buttons.vue';
import issuableMixin from '../../../vue_shared/mixins/issuable'; import issuableMixin from '../../../vue_shared/mixins/issuable';
export default { export default {
props: { components: {
isLocked: { editFormButtons,
required: true,
type: Boolean,
}, },
mixins: [
toggleForm: { issuableMixin,
required: true, ],
type: Function, props: {
}, isLocked: {
required: true,
updateLockedAttribute: { type: Boolean,
required: true, },
type: Function,
toggleForm: {
required: true,
type: Function,
},
updateLockedAttribute: {
required: true,
type: Function,
},
}, },
}, };
mixins: [
issuableMixin,
],
components: {
editFormButtons,
},
};
</script> </script>
<template> <template>
<div class="dropdown open"> <div class="dropdown open">
<div class="dropdown-menu sidebar-item-warning-message"> <div class="dropdown-menu sidebar-item-warning-message">
<p class="text" v-if="isLocked"> <p
class="text"
v-if="isLocked">
Unlock this {{ issuableDisplayName }}? Unlock this {{ issuableDisplayName }}?
<strong>Everyone</strong> <strong>Everyone</strong>
will be able to comment. will be able to comment.
</p> </p>
<p class="text" v-else> <p
class="text"
v-else>
Lock this {{ issuableDisplayName }}? Lock this {{ issuableDisplayName }}?
Only Only
<strong>project members</strong> <strong>project members</strong>
......
<script> <script>
/* global Flash */ import Flash from '../../../flash';
import editForm from './edit_form.vue'; import editForm from './edit_form.vue';
import issuableMixin from '../../../vue_shared/mixins/issuable'; import issuableMixin from '../../../vue_shared/mixins/issuable';
import Icon from '../../../vue_shared/components/icon.vue'; import Icon from '../../../vue_shared/components/icon.vue';
export default { export default {
props: { components: {
isLocked: { editForm,
required: true, Icon,
type: Boolean,
}, },
mixins: [
issuableMixin,
],
isEditable: { props: {
required: true, isLocked: {
type: Boolean, required: true,
}, type: Boolean,
mediator: {
required: true,
type: Object,
validator(mediatorObject) {
return mediatorObject.service && mediatorObject.service.update && mediatorObject.store;
}, },
},
},
mixins: [
issuableMixin,
],
components: { isEditable: {
editForm, required: true,
Icon, type: Boolean,
}, },
computed: { mediator: {
lockIcon() { required: true,
return this.isLocked ? 'lock' : 'lock-open'; type: Object,
validator(mediatorObject) {
return mediatorObject.service && mediatorObject.service.update && mediatorObject.store;
},
},
}, },
isLockDialogOpen() { computed: {
return this.mediator.store.isLockDialogOpen; lockIcon() {
}, return this.isLocked ? 'lock' : 'lock-open';
}, },
methods: { isLockDialogOpen() {
toggleForm() { return this.mediator.store.isLockDialogOpen;
this.mediator.store.isLockDialogOpen = !this.mediator.store.isLockDialogOpen; },
}, },
updateLockedAttribute(locked) { methods: {
this.mediator.service.update(this.issuableType, { toggleForm() {
discussion_locked: locked, this.mediator.store.isLockDialogOpen = !this.mediator.store.isLockDialogOpen;
}) },
.then(() => location.reload())
.catch(() => Flash(this.__(`Something went wrong trying to updateLockedAttribute(locked) {
change the locked state of this ${this.issuableDisplayName}`))); this.mediator.service.update(this.issuableType, {
discussion_locked: locked,
})
.then(() => location.reload())
.catch(() => Flash(this.__(`Something went wrong trying to
change the locked state of this ${this.issuableDisplayName}`)));
},
}, },
}, };
};
</script> </script>
<template> <template>
...@@ -68,8 +67,8 @@ change the locked state of this ${this.issuableDisplayName}`))); ...@@ -68,8 +67,8 @@ change the locked state of this ${this.issuableDisplayName}`)));
:name="lockIcon" :name="lockIcon"
:size="16" :size="16"
aria-hidden="true" aria-hidden="true"
class="sidebar-item-icon is-active"> class="sidebar-item-icon is-active"
</icon> />
</div> </div>
<div class="title hide-collapsed"> <div class="title hide-collapsed">
...@@ -101,8 +100,8 @@ change the locked state of this ${this.issuableDisplayName}`))); ...@@ -101,8 +100,8 @@ change the locked state of this ${this.issuableDisplayName}`)));
name="lock" name="lock"
:size="16" :size="16"
aria-hidden="true" aria-hidden="true"
class="sidebar-item-icon inline is-active"> class="sidebar-item-icon inline is-active"
</icon> />
{{ __('Locked') }} {{ __('Locked') }}
</div> </div>
...@@ -114,8 +113,8 @@ change the locked state of this ${this.issuableDisplayName}`))); ...@@ -114,8 +113,8 @@ change the locked state of this ${this.issuableDisplayName}`)));
name="lock-open" name="lock-open"
:size="16" :size="16"
aria-hidden="true" aria-hidden="true"
class="sidebar-item-icon inline"> class="sidebar-item-icon inline"
</icon> />
{{ __('Unlocked') }} {{ __('Unlocked') }}
</div> </div>
</div> </div>
......
<script> <script>
import { __, n__, sprintf } from '../../../locale'; import { __, n__, sprintf } from '../../../locale';
import loadingIcon from '../../../vue_shared/components/loading_icon.vue'; import loadingIcon from '../../../vue_shared/components/loading_icon.vue';
import userAvatarImage from '../../../vue_shared/components/user_avatar/user_avatar_image.vue'; import userAvatarImage from '../../../vue_shared/components/user_avatar/user_avatar_image.vue';
export default { export default {
props: { components: {
loading: { loadingIcon,
type: Boolean, userAvatarImage,
required: false,
default: false,
}, },
participants: { props: {
type: Array, loading: {
required: false, type: Boolean,
default: () => [], required: false,
default: false,
},
participants: {
type: Array,
required: false,
default: () => [],
},
numberOfLessParticipants: {
type: Number,
required: false,
default: 7,
},
}, },
numberOfLessParticipants: { data() {
type: Number, return {
required: false, isShowingMoreParticipants: false,
default: 7, };
}, },
}, computed: {
data() { lessParticipants() {
return { return this.participants.slice(0, this.numberOfLessParticipants);
isShowingMoreParticipants: false, },
}; visibleParticipants() {
}, return this.isShowingMoreParticipants ? this.participants : this.lessParticipants;
components: { },
loadingIcon, hasMoreParticipants() {
userAvatarImage, return this.participants.length > this.numberOfLessParticipants;
}, },
computed: { toggleLabel() {
lessParticipants() { let label = '';
return this.participants.slice(0, this.numberOfLessParticipants); if (this.isShowingMoreParticipants) {
}, label = __('- show less');
visibleParticipants() { } else {
return this.isShowingMoreParticipants ? this.participants : this.lessParticipants; label = sprintf(__('+ %{moreCount} more'), {
}, moreCount: this.participants.length - this.numberOfLessParticipants,
hasMoreParticipants() { });
return this.participants.length > this.numberOfLessParticipants; }
},
toggleLabel() {
let label = '';
if (this.isShowingMoreParticipants) {
label = __('- show less');
} else {
label = sprintf(__('+ %{moreCount} more'), {
moreCount: this.participants.length - this.numberOfLessParticipants,
});
}
return label; return label;
}, },
participantLabel() { participantLabel() {
return sprintf( return sprintf(
n__('%{count} participant', '%{count} participants', this.participants.length), n__('%{count} participant', '%{count} participants', this.participants.length),
{ count: this.loading ? '' : this.participantCount }, { count: this.loading ? '' : this.participantCount },
); );
}, },
participantCount() { participantCount() {
return this.participants.length; return this.participants.length;
},
}, },
}, methods: {
methods: { toggleMoreParticipants() {
toggleMoreParticipants() { this.isShowingMoreParticipants = !this.isShowingMoreParticipants;
this.isShowingMoreParticipants = !this.isShowingMoreParticipants; },
}, },
}, };
};
</script> </script>
<template> <template>
...@@ -75,14 +75,17 @@ export default { ...@@ -75,14 +75,17 @@ export default {
<div class="sidebar-collapsed-icon"> <div class="sidebar-collapsed-icon">
<i <i
class="fa fa-users" class="fa fa-users"
aria-hidden="true"> aria-hidden="true"
>
</i> </i>
<loading-icon <loading-icon
v-if="loading" v-if="loading"
class="js-participants-collapsed-loading-icon" /> class="js-participants-collapsed-loading-icon"
/>
<span <span
v-else v-else
class="js-participants-collapsed-count"> class="js-participants-collapsed-count"
>
{{ participantCount }} {{ participantCount }}
</span> </span>
</div> </div>
...@@ -90,34 +93,40 @@ export default { ...@@ -90,34 +93,40 @@ export default {
<loading-icon <loading-icon
v-if="loading" v-if="loading"
:inline="true" :inline="true"
class="js-participants-expanded-loading-icon" /> class="js-participants-expanded-loading-icon"
/>
{{ participantLabel }} {{ participantLabel }}
</div> </div>
<div class="participants-list hide-collapsed"> <div class="participants-list hide-collapsed">
<div <div
v-for="participant in visibleParticipants" v-for="participant in visibleParticipants"
:key="participant.id" :key="participant.id"
class="participants-author js-participants-author"> class="participants-author js-participants-author"
>
<a <a
class="author_link" class="author_link"
:href="participant.web_url"> :href="participant.web_url"
>
<user-avatar-image <user-avatar-image
:lazy="true" :lazy="true"
:img-src="participant.avatar_url" :img-src="participant.avatar_url"
css-classes="avatar-inline" css-classes="avatar-inline"
:size="24" :size="24"
:tooltip-text="participant.name" :tooltip-text="participant.name"
tooltip-placement="bottom" /> tooltip-placement="bottom"
/>
</a> </a>
</div> </div>
</div> </div>
<div <div
v-if="hasMoreParticipants" v-if="hasMoreParticipants"
class="participants-more hide-collapsed"> class="participants-more hide-collapsed"
>
<button <button
type="button" type="button"
class="btn-transparent btn-blank js-toggle-participants-button" class="btn-transparent btn-blank js-toggle-participants-button"
@click="toggleMoreParticipants"> @click="toggleMoreParticipants"
>
{{ toggleLabel }} {{ toggleLabel }}
</button> </button>
</div> </div>
......
<script> <script>
import Store from '../../stores/sidebar_store'; import Store from '../../stores/sidebar_store';
import participants from './participants.vue'; import participants from './participants.vue';
export default { export default {
data() { components: {
return { participants,
store: new Store(),
};
},
props: {
mediator: {
type: Object,
required: true,
}, },
}, props: {
components: { mediator: {
participants, type: Object,
}, required: true,
}; },
},
data() {
return {
store: new Store(),
};
},
};
</script> </script>
<template> <template>
......
...@@ -6,10 +6,8 @@ import { __ } from '../../../locale'; ...@@ -6,10 +6,8 @@ import { __ } from '../../../locale';
import subscriptions from './subscriptions.vue'; import subscriptions from './subscriptions.vue';
export default { export default {
data() { components: {
return { subscriptions,
store: new Store(),
};
}, },
props: { props: {
mediator: { mediator: {
...@@ -17,10 +15,17 @@ export default { ...@@ -17,10 +15,17 @@ export default {
required: true, required: true,
}, },
}, },
components: { data() {
subscriptions, return {
store: new Store(),
};
},
created() {
eventHub.$on('toggleSubscription', this.onToggleSubscription);
},
beforeDestroy() {
eventHub.$off('toggleSubscription', this.onToggleSubscription);
}, },
methods: { methods: {
onToggleSubscription() { onToggleSubscription() {
this.mediator.toggleSubscription() this.mediator.toggleSubscription()
...@@ -29,14 +34,6 @@ export default { ...@@ -29,14 +34,6 @@ export default {
}); });
}, },
}, },
created() {
eventHub.$on('toggleSubscription', this.onToggleSubscription);
},
beforeDestroy() {
eventHub.$off('toggleSubscription', this.onToggleSubscription);
},
}; };
</script> </script>
...@@ -44,6 +41,7 @@ export default { ...@@ -44,6 +41,7 @@ export default {
<div class="block subscriptions"> <div class="block subscriptions">
<subscriptions <subscriptions
:loading="store.isFetching.subscriptions" :loading="store.isFetching.subscriptions"
:subscribed="store.subscribed" /> :subscribed="store.subscribed"
/>
</div> </div>
</template> </template>
<script> <script>
import { __ } from '../../../locale'; import { __ } from '../../../locale';
import eventHub from '../../event_hub'; import eventHub from '../../event_hub';
import loadingButton from '../../../vue_shared/components/loading_button.vue'; import loadingButton from '../../../vue_shared/components/loading_button.vue';
export default { export default {
props: { components: {
loading: { loadingButton,
type: Boolean,
required: false,
default: false,
}, },
subscribed: { props: {
type: Boolean, loading: {
required: false, type: Boolean,
required: false,
default: false,
},
subscribed: {
type: Boolean,
required: false,
default: false,
},
id: {
type: Number,
required: false,
default: 0,
},
}, },
id: { computed: {
type: Number, buttonLabel() {
required: false, let label;
}, if (this.subscribed === false) {
}, label = __('Subscribe');
components: { } else if (this.subscribed === true) {
loadingButton, label = __('Unsubscribe');
}, }
computed: {
buttonLabel() {
let label;
if (this.subscribed === false) {
label = __('Subscribe');
} else if (this.subscribed === true) {
label = __('Unsubscribe');
}
return label; return label;
},
}, },
}, methods: {
methods: { toggleSubscription() {
toggleSubscription() { eventHub.$emit('toggleSubscription', this.id);
eventHub.$emit('toggleSubscription', this.id); },
}, },
}, };
};
</script> </script>
<template> <template>
...@@ -47,7 +49,8 @@ export default { ...@@ -47,7 +49,8 @@ export default {
<div class="sidebar-collapsed-icon"> <div class="sidebar-collapsed-icon">
<i <i
class="fa fa-rss" class="fa fa-rss"
aria-hidden="true"> aria-hidden="true"
>
</i> </i>
</div> </div>
<span class="issuable-header-text hide-collapsed pull-left"> <span class="issuable-header-text hide-collapsed pull-left">
......
...@@ -5,6 +5,11 @@ ...@@ -5,6 +5,11 @@
export default { export default {
name: 'MRWidgetPipeline', name: 'MRWidgetPipeline',
components: {
pipelineStage,
ciIcon,
icon,
},
props: { props: {
pipeline: { pipeline: {
type: Object, type: Object,
...@@ -21,11 +26,6 @@ ...@@ -21,11 +26,6 @@
required: false, required: false,
}, },
}, },
components: {
pipelineStage,
ciIcon,
icon,
},
computed: { computed: {
hasPipeline() { hasPipeline() {
return this.pipeline && Object.keys(this.pipeline).length > 0; return this.pipeline && Object.keys(this.pipeline).length > 0;
...@@ -62,7 +62,8 @@ ...@@ -62,7 +62,8 @@
<template v-else-if="hasPipeline"> <template v-else-if="hasPipeline">
<a <a
class="append-right-10" class="append-right-10"
:href="this.status.details_path"> :href="status.details_path"
>
<ci-icon :status="status" /> <ci-icon :status="status" />
</a> </a>
...@@ -70,33 +71,37 @@ ...@@ -70,33 +71,37 @@
Pipeline Pipeline
<a <a
:href="pipeline.path" :href="pipeline.path"
class="pipeline-id"> class="pipeline-id"
#{{pipeline.id}} >
#{{ pipeline.id }}
</a> </a>
{{pipeline.details.status.label}} for {{ pipeline.details.status.label }} for
<a <a
:href="pipeline.commit.commit_path" :href="pipeline.commit.commit_path"
class="commit-sha js-commit-link"> class="commit-sha js-commit-link"
{{pipeline.commit.short_id}}</a>. >
{{ pipeline.commit.short_id }}</a>.
<span class="mr-widget-pipeline-graph"> <span class="mr-widget-pipeline-graph">
<span class="stage-cell"> <span
class="stage-cell"
v-if="hasStages"
>
<div <div
v-if="hasStages"
v-for="(stage, i) in pipeline.details.stages" v-for="(stage, i) in pipeline.details.stages"
:key="i" :key="i"
class="stage-container dropdown js-mini-pipeline-graph"> class="stage-container dropdown js-mini-pipeline-graph"
>
<pipeline-stage :stage="stage" /> <pipeline-stage :stage="stage" />
</div> </div>
</span> </span>
</span> </span>
<template v-if="pipeline.coverage"> <template v-if="pipeline.coverage">
Coverage {{pipeline.coverage}}% Coverage {{ pipeline.coverage }}%
</template> </template>
</div> </div>
</template> </template>
</div> </div>
......
...@@ -23,6 +23,12 @@ ...@@ -23,6 +23,12 @@
*/ */
export default { export default {
components: {
ciIcon,
},
directives: {
tooltip,
},
props: { props: {
status: { status: {
type: Object, type: Object,
...@@ -34,12 +40,6 @@ ...@@ -34,12 +40,6 @@
default: true, default: true,
}, },
}, },
components: {
ciIcon,
},
directives: {
tooltip,
},
computed: { computed: {
cssClass() { cssClass() {
const className = this.status.group; const className = this.status.group;
...@@ -53,11 +53,12 @@ ...@@ -53,11 +53,12 @@
:href="status.details_path" :href="status.details_path"
:class="cssClass" :class="cssClass"
v-tooltip v-tooltip
:title="!showText ? status.text : ''"> :title="!showText ? status.text : ''"
>
<ci-icon :status="status" /> <ci-icon :status="status" />
<template v-if="showText"> <template v-if="showText">
{{status.text}} {{ status.text }}
</template> </template>
</a> </a>
</template> </template>
...@@ -23,6 +23,9 @@ ...@@ -23,6 +23,9 @@
* - Jobs show view sidebar * - Jobs show view sidebar
*/ */
export default { export default {
components: {
icon,
},
props: { props: {
status: { status: {
type: Object, type: Object,
...@@ -30,10 +33,6 @@ ...@@ -30,10 +33,6 @@
}, },
}, },
components: {
icon,
},
computed: { computed: {
cssClass() { cssClass() {
const status = this.status.group; const status = this.status.group;
...@@ -43,9 +42,7 @@ ...@@ -43,9 +42,7 @@
}; };
</script> </script>
<template> <template>
<span <span :class="cssClass">
:class="cssClass"> <icon :name="status.icon" />
<icon
:name="status.icon"/>
</span> </span>
</template> </template>
...@@ -4,7 +4,7 @@ ...@@ -4,7 +4,7 @@
*/ */
export default { export default {
name: 'clipboardButton', name: 'ClipboardButton',
props: { props: {
text: { text: {
type: String, type: String,
...@@ -23,10 +23,12 @@ ...@@ -23,10 +23,12 @@
type="button" type="button"
class="btn btn-transparent btn-clipboard" class="btn btn-transparent btn-clipboard"
:data-title="title" :data-title="title"
:data-clipboard-text="text"> :data-clipboard-text="text"
<i >
aria-hidden="true" <i
class="fa fa-clipboard"> aria-hidden="true"
</i> class="fa fa-clipboard"
>
</i>
</button> </button>
</template> </template>
...@@ -2,9 +2,16 @@ ...@@ -2,9 +2,16 @@
import commitIconSvg from 'icons/_icon_commit.svg'; import commitIconSvg from 'icons/_icon_commit.svg';
import userAvatarLink from './user_avatar/user_avatar_link.vue'; import userAvatarLink from './user_avatar/user_avatar_link.vue';
import tooltip from '../directives/tooltip'; import tooltip from '../directives/tooltip';
import Icon from '../../vue_shared/components/icon.vue'; import icon from '../../vue_shared/components/icon.vue';
export default { export default {
directives: {
tooltip,
},
components: {
userAvatarLink,
icon,
},
props: { props: {
/** /**
* Indicates the existance of a tag. * Indicates the existance of a tag.
...@@ -103,13 +110,6 @@ ...@@ -103,13 +110,6 @@
this.author.username ? `${this.author.username}'s avatar` : null; this.author.username ? `${this.author.username}'s avatar` : null;
}, },
}, },
directives: {
tooltip,
},
components: {
userAvatarLink,
Icon,
},
created() { created() {
this.commitIconSvg = commitIconSvg; this.commitIconSvg = commitIconSvg;
}, },
...@@ -118,17 +118,17 @@ ...@@ -118,17 +118,17 @@
<template> <template>
<div class="branch-commit"> <div class="branch-commit">
<template v-if="hasCommitRef && showBranch"> <template v-if="hasCommitRef && showBranch">
<div <div class="icon-container hidden-xs">
class="icon-container hidden-xs">
<i <i
v-if="tag" v-if="tag"
class="fa fa-tag" class="fa fa-tag"
aria-hidden="true"> aria-hidden="true"
>
</i> </i>
<icon <icon
v-if="!tag" v-if="!tag"
name="fork"> name="fork"
</icon> />
</div> </div>
<a <a
...@@ -136,25 +136,29 @@ ...@@ -136,25 +136,29 @@
:href="commitRef.ref_url" :href="commitRef.ref_url"
v-tooltip v-tooltip
data-container="body" data-container="body"
:title="commitRef.name"> :title="commitRef.name"
{{commitRef.name}} >
{{ commitRef.name }}
</a> </a>
</template> </template>
<div <div
v-html="commitIconSvg" v-html="commitIconSvg"
class="commit-icon js-commit-icon"> class="commit-icon js-commit-icon"
>
</div> </div>
<a <a
class="commit-sha" class="commit-sha"
:href="commitUrl"> :href="commitUrl"
{{shortSha}} >
{{ shortSha }}
</a> </a>
<div class="commit-title flex-truncate-parent"> <div class="commit-title flex-truncate-parent">
<span <span
v-if="title" v-if="title"
class="flex-truncate-child"> class="flex-truncate-child"
>
<user-avatar-link <user-avatar-link
v-if="hasAuthor" v-if="hasAuthor"
class="avatar-image-container" class="avatar-image-container"
...@@ -165,8 +169,9 @@ ...@@ -165,8 +169,9 @@
/> />
<a <a
class="commit-row-message" class="commit-row-message"
:href="commitUrl"> :href="commitUrl"
{{title}} >
{{ title }}
</a> </a>
</span> </span>
<span v-else> <span v-else>
......
...@@ -16,6 +16,10 @@ ...@@ -16,6 +16,10 @@
*/ */
export default { export default {
components: {
loadingIcon,
icon,
},
props: { props: {
fileName: { fileName: {
type: String, type: String,
...@@ -52,10 +56,6 @@ ...@@ -52,10 +56,6 @@
default: '', default: '',
}, },
}, },
components: {
loadingIcon,
icon,
},
computed: { computed: {
spriteHref() { spriteHref() {
const iconName = getIconForFile(this.fileName) || 'file'; const iconName = getIconForFile(this.fileName) || 'file';
...@@ -75,9 +75,9 @@ ...@@ -75,9 +75,9 @@
<span> <span>
<svg <svg
:class="[iconSizeClass, cssClasses]" :class="[iconSizeClass, cssClasses]"
v-if="!loading && !folder"> v-if="!loading && !folder"
<use >
v-bind="{'xlink:href':spriteHref}"/> <use v-bind="{ 'xlink:href':spriteHref }" />
</svg> </svg>
<icon <icon
v-if="!loading && folder" v-if="!loading && folder"
......
<script> <script>
import ciIconBadge from './ci_badge_link.vue'; import ciIconBadge from './ci_badge_link.vue';
import loadingIcon from './loading_icon.vue'; import loadingIcon from './loading_icon.vue';
import timeagoTooltip from './time_ago_tooltip.vue'; import timeagoTooltip from './time_ago_tooltip.vue';
import tooltip from '../directives/tooltip'; import tooltip from '../directives/tooltip';
import userAvatarImage from './user_avatar/user_avatar_image.vue'; import userAvatarImage from './user_avatar/user_avatar_image.vue';
/** /**
* Renders header component for job and pipeline page based on UI mockups * Renders header component for job and pipeline page based on UI mockups
* *
* Used in: * Used in:
* - job show page * - job show page
* - pipeline show page * - pipeline show page
*/ */
export default { export default {
props: { directives: {
status: { tooltip,
type: Object,
required: true,
}, },
itemName: {
type: String, components: {
required: true, ciIconBadge,
}, loadingIcon,
itemId: { timeagoTooltip,
type: Number, userAvatarImage,
required: true,
},
time: {
type: String,
required: true,
},
user: {
type: Object,
required: false,
default: () => ({}),
},
actions: {
type: Array,
required: false,
default: () => [],
}, },
hasSidebarButton: { props: {
type: Boolean, status: {
required: false, type: Object,
default: false, required: true,
},
itemName: {
type: String,
required: true,
},
itemId: {
type: Number,
required: true,
},
time: {
type: String,
required: true,
},
user: {
type: Object,
required: false,
default: () => ({}),
},
actions: {
type: Array,
required: false,
default: () => [],
},
hasSidebarButton: {
type: Boolean,
required: false,
default: false,
},
}, },
},
computed: {
directives: { userAvatarAltText() {
tooltip, return `${this.user.name}'s avatar`;
}, },
components: {
ciIconBadge,
loadingIcon,
timeagoTooltip,
userAvatarImage,
},
computed: {
userAvatarAltText() {
return `${this.user.name}'s avatar`;
}, },
},
methods: { methods: {
onClickAction(action) { onClickAction(action) {
this.$emit('actionClicked', action); this.$emit('actionClicked', action);
},
}, },
}, };
};
</script> </script>
<template> <template>
...@@ -79,7 +78,7 @@ export default { ...@@ -79,7 +78,7 @@ export default {
<ci-icon-badge :status="status" /> <ci-icon-badge :status="status" />
<strong> <strong>
{{itemName}} #{{itemId}} {{ itemName }} #{{ itemId }}
</strong> </strong>
triggered triggered
...@@ -93,30 +92,35 @@ export default { ...@@ -93,30 +92,35 @@ export default {
v-tooltip v-tooltip
:href="user.path" :href="user.path"
:title="user.email" :title="user.email"
class="js-user-link commit-committer-link"> class="js-user-link commit-committer-link"
>
<user-avatar-image <user-avatar-image
:img-src="user.avatar_url" :img-src="user.avatar_url"
:img-alt="userAvatarAltText" :img-alt="userAvatarAltText"
:tooltip-text="user.name" :tooltip-text="user.name"
:img-size="24" :img-size="24"
/> />
{{user.name}} {{ user.name }}
</a> </a>
</template> </template>
</section> </section>
<section <section
class="header-action-buttons" class="header-action-buttons"
v-if="actions.length"> v-if="actions.length"
>
<template <template
v-for="action in actions"> v-for="(action, i) in actions"
>
<a <a
v-if="action.type === 'link'" v-if="action.type === 'link'"
:href="action.path" :href="action.path"
:class="action.cssClass"> :class="action.cssClass"
{{action.label}} :key="i"
>
{{ action.label }}
</a> </a>
<a <a
...@@ -124,8 +128,10 @@ export default { ...@@ -124,8 +128,10 @@ export default {
:href="action.path" :href="action.path"
data-method="post" data-method="post"
rel="nofollow" rel="nofollow"
:class="action.cssClass"> :class="action.cssClass"
{{action.label}} :key="i"
>
{{ action.label }}
</a> </a>
<button <button
...@@ -133,12 +139,15 @@ export default { ...@@ -133,12 +139,15 @@ export default {
@click="onClickAction(action)" @click="onClickAction(action)"
:disabled="action.isLoading" :disabled="action.isLoading"
:class="action.cssClass" :class="action.cssClass"
type="button"> type="button"
{{action.label}} :key="i"
>
{{ action.label }}
<i <i
v-show="action.isLoading" v-show="action.isLoading"
class="fa fa-spin fa-spinner" class="fa fa-spin fa-spinner"
aria-hidden="true"> aria-hidden="true"
>
</i> </i>
</button> </button>
</template> </template>
...@@ -147,11 +156,13 @@ export default { ...@@ -147,11 +156,13 @@ export default {
type="button" type="button"
class="btn btn-default visible-xs-block visible-sm-block sidebar-toggle-btn js-sidebar-build-toggle js-sidebar-build-toggle-header" class="btn btn-default visible-xs-block visible-sm-block sidebar-toggle-btn js-sidebar-build-toggle js-sidebar-build-toggle-header"
aria-label="Toggle Sidebar" aria-label="Toggle Sidebar"
id="toggleSidebar"> id="toggleSidebar"
>
<i <i
class="fa fa-angle-double-left" class="fa fa-angle-double-left"
aria-hidden="true" aria-hidden="true"
aria-labelledby="toggleSidebar"> aria-labelledby="toggleSidebar"
>
</i> </i>
</button> </button>
</section> </section>
......
<script> <script>
/* This is a re-usable vue component for rendering a svg sprite /* This is a re-usable vue component for rendering a svg sprite
icon icon
Sample configuration: Sample configuration:
<icon <icon
name="retry" name="retry"
:size="32" :size="32"
css-classes="top" css-classes="top"
/> />
*/ */
// only allow classes in images.scss e.g. s12 // only allow classes in images.scss e.g. s12
const validSizes = [8, 12, 16, 18, 24, 32, 48, 72]; const validSizes = [8, 12, 16, 18, 24, 32, 48, 72];
...@@ -80,7 +80,6 @@ ...@@ -80,7 +80,6 @@
:height="height" :height="height"
:x="x" :x="x"
:y="y"> :y="y">
<use <use v-bind="{ 'xlink:href':spriteHref }" />
v-bind="{'xlink:href':spriteHref}"/>
</svg> </svg>
</template> </template>
...@@ -46,6 +46,6 @@ export default { ...@@ -46,6 +46,6 @@ export default {
class="avatar identicon" class="avatar identicon"
:class="sizeClass" :class="sizeClass"
:style="identiconStyles"> :style="identiconStyles">
{{identiconTitle}} {{ identiconTitle }}
</div> </div>
</template> </template>
<script> <script>
import Icon from '../../../vue_shared/components/icon.vue'; import icon from '../../../vue_shared/components/icon.vue';
export default { export default {
components: {
icon,
},
props: { props: {
isLocked: { isLocked: {
type: Boolean, type: Boolean,
...@@ -16,10 +19,6 @@ ...@@ -16,10 +19,6 @@
}, },
}, },
components: {
Icon,
},
computed: { computed: {
warningIcon() { warningIcon() {
if (this.isConfidential) return 'eye-slash'; if (this.isConfidential) return 'eye-slash';
...@@ -37,12 +36,12 @@ ...@@ -37,12 +36,12 @@
<template> <template>
<div class="issuable-note-warning"> <div class="issuable-note-warning">
<icon <icon
:name="warningIcon" :name="warningIcon"
:size="16" :size="16"
class="icon inline" class="icon inline"
aria-hidden="true" aria-hidden="true"
v-if="!isLockedAndConfidential"> v-if="!isLockedAndConfidential"
</icon> />
<span v-if="isLockedAndConfidential"> <span v-if="isLockedAndConfidential">
{{ __('This issue is confidential and locked.') }} {{ __('This issue is confidential and locked.') }}
......
<script> <script>
/* This is a re-usable vue component for rendering a button /* This is a re-usable vue component for rendering a button
that will probably be sending off ajax requests and need that will probably be sending off ajax requests and need
to show the loading status by setting the `loading` option. to show the loading status by setting the `loading` option.
This can also be used for initial page load when you don't This can also be used for initial page load when you don't
know the action of the button yet by setting know the action of the button yet by setting
`loading: true, label: undefined`. `loading: true, label: undefined`.
Sample configuration: Sample configuration:
<loading-button <loading-button
:loading="true" :loading="true"
:label="Hello" :label="Hello"
@click="..." @click="..."
/> />
*/ */
import loadingIcon from './loading_icon.vue'; import loadingIcon from './loading_icon.vue';
export default { export default {
props: { components: {
loading: { loadingIcon,
type: Boolean,
required: false,
default: false,
}, },
disabled: { props: {
type: Boolean, loading: {
required: false, type: Boolean,
default: false, required: false,
default: false,
},
disabled: {
type: Boolean,
required: false,
default: false,
},
label: {
type: String,
required: false,
default: '',
},
containerClass: {
type: String,
required: false,
default: 'btn btn-align-content',
},
}, },
label: { computed: {
type: String, hasLabel() {
required: false, return this.label !== '';
},
}, },
containerClass: { methods: {
type: String, onClick(e) {
required: false, this.$emit('click', e);
default: 'btn btn-align-content', },
}, },
}, };
components: {
loadingIcon,
},
methods: {
onClick(e) {
this.$emit('click', e);
},
},
};
</script> </script>
<template> <template>
...@@ -59,23 +65,23 @@ export default { ...@@ -59,23 +65,23 @@ export default {
:class="containerClass" :class="containerClass"
:disabled="loading || disabled" :disabled="loading || disabled"
> >
<transition name="fade"> <transition name="fade">
<loading-icon <loading-icon
v-if="loading" v-if="loading"
:inline="true" :inline="true"
class="js-loading-button-icon" class="js-loading-button-icon"
:class="{ :class="{
'append-right-5': label 'append-right-5': label
}" }"
/> />
</transition> </transition>
<transition name="fade"> <transition name="fade">
<span <span
v-if="label" v-if="hasLabel"
class="js-loading-button-label" class="js-loading-button-label"
> >
{{ label }} {{ label }}
</span> </span>
</transition> </transition>
</button> </button>
</template> </template>
...@@ -38,7 +38,8 @@ ...@@ -38,7 +38,8 @@
class="fa fa-spin fa-spinner" class="fa fa-spin fa-spinner"
:class="cssClass" :class="cssClass"
aria-hidden="true" aria-hidden="true"
:aria-label="label"> :aria-label="label"
>
</i> </i>
</component> </component>
</template> </template>
...@@ -6,6 +6,11 @@ ...@@ -6,6 +6,11 @@
import icon from '../icon.vue'; import icon from '../icon.vue';
export default { export default {
components: {
markdownHeader,
markdownToolbar,
icon,
},
props: { props: {
markdownPreviewPath: { markdownPreviewPath: {
type: String, type: String,
...@@ -24,6 +29,7 @@ ...@@ -24,6 +29,7 @@
quickActionsDocsPath: { quickActionsDocsPath: {
type: String, type: String,
required: false, required: false,
default: '',
}, },
canAttachFile: { canAttachFile: {
type: Boolean, type: Boolean,
...@@ -45,17 +51,24 @@ ...@@ -45,17 +51,24 @@
previewMarkdown: false, previewMarkdown: false,
}; };
}, },
components: {
markdownHeader,
markdownToolbar,
icon,
},
computed: { computed: {
shouldShowReferencedUsers() { shouldShowReferencedUsers() {
const referencedUsersThreshold = 10; const referencedUsersThreshold = 10;
return this.referencedUsers.length >= referencedUsersThreshold; return this.referencedUsers.length >= referencedUsersThreshold;
}, },
}, },
mounted() {
/*
GLForm class handles all the toolbar buttons
*/
return new GLForm($(this.$refs['gl-form']), this.enableAutocomplete);
},
beforeDestroy() {
const glForm = $(this.$refs['gl-form']).data('gl-form');
if (glForm) {
glForm.destroy();
}
},
methods: { methods: {
showPreviewTab() { showPreviewTab() {
if (this.previewMarkdown) return; if (this.previewMarkdown) return;
...@@ -98,18 +111,6 @@ ...@@ -98,18 +111,6 @@
}); });
}, },
}, },
mounted() {
/*
GLForm class handles all the toolbar buttons
*/
return new GLForm($(this.$refs['gl-form']), this.enableAutocomplete);
},
beforeDestroy() {
const glForm = $(this.$refs['gl-form']).data('gl-form');
if (glForm) {
glForm.destroy();
}
},
}; };
</script> </script>
...@@ -121,34 +122,39 @@ ...@@ -121,34 +122,39 @@
<markdown-header <markdown-header
:preview-markdown="previewMarkdown" :preview-markdown="previewMarkdown"
@preview-markdown="showPreviewTab" @preview-markdown="showPreviewTab"
@write-markdown="showWriteTab" /> @write-markdown="showWriteTab"
/>
<div <div
class="md-write-holder" class="md-write-holder"
v-show="!previewMarkdown"> v-show="!previewMarkdown"
>
<div class="zen-backdrop"> <div class="zen-backdrop">
<slot name="textarea"></slot> <slot name="textarea"></slot>
<a <a
class="zen-control zen-control-leave js-zen-leave" class="zen-control zen-control-leave js-zen-leave"
href="#" href="#"
aria-label="Enter zen mode"> aria-label="Enter zen mode"
>
<icon <icon
name="screen-normal" name="screen-normal"
:size="32"> :size="32"
</icon> />
</a> </a>
<markdown-toolbar <markdown-toolbar
:markdown-docs-path="markdownDocsPath" :markdown-docs-path="markdownDocsPath"
:quick-actions-docs-path="quickActionsDocsPath" :quick-actions-docs-path="quickActionsDocsPath"
:can-attach-file="canAttachFile" :can-attach-file="canAttachFile"
/> />
</div> </div>
</div> </div>
<div <div
class="md md-preview-holder md-preview" class="md md-preview-holder md-preview"
v-show="previewMarkdown"> v-show="previewMarkdown"
>
<div <div
ref="markdown-preview" ref="markdown-preview"
v-html="markdownPreview"> v-html="markdownPreview"
>
</div> </div>
<span v-if="markdownPreviewLoading"> <span v-if="markdownPreviewLoading">
Loading... Loading...
...@@ -158,23 +164,27 @@ ...@@ -158,23 +164,27 @@
<div <div
v-if="referencedCommands" v-if="referencedCommands"
v-html="referencedCommands" v-html="referencedCommands"
class="referenced-commands"></div> class="referenced-commands"
>
</div>
<div <div
v-if="shouldShowReferencedUsers" v-if="shouldShowReferencedUsers"
class="referenced-users"> class="referenced-users"
<span> >
<i <span>
class="fa fa-exclamation-triangle" <i
aria-hidden="true"> class="fa fa-exclamation-triangle"
</i> aria-hidden="true"
You are about to add >
<strong> </i>
<span class="js-referenced-users-count"> You are about to add
{{referencedUsers.length}} <strong>
</span> <span class="js-referenced-users-count">
</strong> people to the discussion. Proceed with caution. {{ referencedUsers.length }}
</span> </span>
</div> </strong> people to the discussion. Proceed with caution.
</span>
</div>
</template> </template>
</div> </div>
</template> </template>
...@@ -4,18 +4,26 @@ ...@@ -4,18 +4,26 @@
import icon from '../icon.vue'; import icon from '../icon.vue';
export default { export default {
directives: {
tooltip,
},
components: {
toolbarButton,
icon,
},
props: { props: {
previewMarkdown: { previewMarkdown: {
type: Boolean, type: Boolean,
required: true, required: true,
}, },
}, },
directives: { mounted() {
tooltip, $(document).on('markdown-preview:show.vue', this.previewMarkdownTab);
$(document).on('markdown-preview:hide.vue', this.writeMarkdownTab);
}, },
components: { beforeDestroy() {
toolbarButton, $(document).off('markdown-preview:show.vue', this.previewMarkdownTab);
icon, $(document).off('markdown-preview:hide.vue', this.writeMarkdownTab);
}, },
methods: { methods: {
isMarkdownForm(form) { isMarkdownForm(form) {
...@@ -36,14 +44,6 @@ ...@@ -36,14 +44,6 @@
this.$emit('write-markdown'); this.$emit('write-markdown');
}, },
}, },
mounted() {
$(document).on('markdown-preview:show.vue', this.previewMarkdownTab);
$(document).on('markdown-preview:hide.vue', this.writeMarkdownTab);
},
beforeDestroy() {
$(document).off('markdown-preview:show.vue', this.previewMarkdownTab);
$(document).off('markdown-preview:hide.vue', this.writeMarkdownTab);
},
}; };
</script> </script>
...@@ -52,12 +52,14 @@ ...@@ -52,12 +52,14 @@
<ul class="nav-links clearfix"> <ul class="nav-links clearfix">
<li <li
class="md-header-tab" class="md-header-tab"
:class="{ active: !previewMarkdown }"> :class="{ active: !previewMarkdown }"
>
<a <a
class="js-write-link" class="js-write-link"
href="#md-write-holder" href="#md-write-holder"
tabindex="-1" tabindex="-1"
@click.prevent="writeMarkdownTab($event)"> @click.prevent="writeMarkdownTab($event)"
>
Write Write
</a> </a>
</li> </li>
...@@ -68,46 +70,55 @@ ...@@ -68,46 +70,55 @@
class="js-preview-link" class="js-preview-link"
href="#md-preview-holder" href="#md-preview-holder"
tabindex="-1" tabindex="-1"
@click.prevent="previewMarkdownTab($event)"> @click.prevent="previewMarkdownTab($event)"
>
Preview Preview
</a> </a>
</li> </li>
<li <li
class="md-header-toolbar" class="md-header-toolbar"
:class="{ active: !previewMarkdown }"> :class="{ active: !previewMarkdown }"
>
<toolbar-button <toolbar-button
tag="**" tag="**"
button-title="Add bold text" button-title="Add bold text"
icon="bold" /> icon="bold"
/>
<toolbar-button <toolbar-button
tag="*" tag="*"
button-title="Add italic text" button-title="Add italic text"
icon="italic" /> icon="italic"
/>
<toolbar-button <toolbar-button
tag="> " tag="> "
:prepend="true" :prepend="true"
button-title="Insert a quote" button-title="Insert a quote"
icon="quote" /> icon="quote"
/>
<toolbar-button <toolbar-button
tag="`" tag="`"
tag-block="```" tag-block="```"
button-title="Insert code" button-title="Insert code"
icon="code" /> icon="code"
/>
<toolbar-button <toolbar-button
tag="* " tag="* "
:prepend="true" :prepend="true"
button-title="Add a bullet list" button-title="Add a bullet list"
icon="list-bulleted" /> icon="list-bulleted"
/>
<toolbar-button <toolbar-button
tag="1. " tag="1. "
:prepend="true" :prepend="true"
button-title="Add a numbered list" button-title="Add a numbered list"
icon="list-numbered" /> icon="list-numbered"
/>
<toolbar-button <toolbar-button
tag="* [ ] " tag="* [ ] "
:prepend="true" :prepend="true"
button-title="Add a task list" button-title="Add a task list"
icon="task-done" /> icon="task-done"
/>
<button <button
v-tooltip v-tooltip
aria-label="Go full screen" aria-label="Go full screen"
...@@ -115,10 +126,11 @@ ...@@ -115,10 +126,11 @@
data-container="body" data-container="body"
tabindex="-1" tabindex="-1"
title="Go full screen" title="Go full screen"
type="button"> type="button"
>
<icon <icon
name="screen-full"> name="screen-full"
</icon> />
</button> </button>
</li> </li>
</ul> </ul>
......
...@@ -8,6 +8,7 @@ ...@@ -8,6 +8,7 @@
quickActionsDocsPath: { quickActionsDocsPath: {
type: String, type: String,
required: false, required: false,
default: '',
}, },
canAttachFile: { canAttachFile: {
type: Boolean, type: Boolean,
...@@ -15,32 +16,40 @@ ...@@ -15,32 +16,40 @@
default: true, default: true,
}, },
}, },
computed: {
hasQuickActionsDocsPath() {
return this.quickActionsDocsPath !== '';
},
},
}; };
</script> </script>
<template> <template>
<div class="comment-toolbar clearfix"> <div class="comment-toolbar clearfix">
<div class="toolbar-text"> <div class="toolbar-text">
<template v-if="!quickActionsDocsPath && markdownDocsPath"> <template v-if="!hasQuickActionsDocsPath && markdownDocsPath">
<a <a
:href="markdownDocsPath" :href="markdownDocsPath"
target="_blank" target="_blank"
tabindex="-1"> tabindex="-1"
>
Markdown is supported Markdown is supported
</a> </a>
</template> </template>
<template v-if="quickActionsDocsPath && markdownDocsPath"> <template v-if="hasQuickActionsDocsPath && markdownDocsPath">
<a <a
:href="markdownDocsPath" :href="markdownDocsPath"
target="_blank" target="_blank"
tabindex="-1"> tabindex="-1"
>
Markdown Markdown
</a> </a>
and and
<a <a
:href="quickActionsDocsPath" :href="quickActionsDocsPath"
target="_blank" target="_blank"
tabindex="-1"> tabindex="-1"
>
quick actions quick actions
</a> </a>
are supported are supported
...@@ -53,46 +62,58 @@ ...@@ -53,46 +62,58 @@
<span class="uploading-progress-container hide"> <span class="uploading-progress-container hide">
<i <i
class="fa fa-file-image-o toolbar-button-icon" class="fa fa-file-image-o toolbar-button-icon"
aria-hidden="true"></i> aria-hidden="true"
>
</i>
<span class="attaching-file-message"></span> <span class="attaching-file-message"></span>
<span class="uploading-progress">0%</span> <span class="uploading-progress">0%</span>
<span class="uploading-spinner"> <span class="uploading-spinner">
<i <i
class="fa fa-spinner fa-spin toolbar-button-icon" class="fa fa-spinner fa-spin toolbar-button-icon"
aria-hidden="true"></i> aria-hidden="true"
>
</i>
</span> </span>
</span> </span>
<span class="uploading-error-container hide"> <span class="uploading-error-container hide">
<span class="uploading-error-icon"> <span class="uploading-error-icon">
<i <i
class="fa fa-file-image-o toolbar-button-icon" class="fa fa-file-image-o toolbar-button-icon"
aria-hidden="true"></i> aria-hidden="true"
>
</i>
</span> </span>
<span class="uploading-error-message"></span> <span class="uploading-error-message"></span>
<button <button
class="retry-uploading-link" class="retry-uploading-link"
type="button"> type="button"
Try again >
Try again
</button> </button>
or or
<button <button
class="attach-new-file markdown-selector" class="attach-new-file markdown-selector"
type="button"> type="button"
>
attach a new file attach a new file
</button> </button>
</span> </span>
<button <button
class="markdown-selector button-attach-file" class="markdown-selector button-attach-file"
tabindex="-1" tabindex="-1"
type="button"> type="button"
>
<i <i
class="fa fa-file-image-o toolbar-button-icon" class="fa fa-file-image-o toolbar-button-icon"
aria-hidden="true"></i> aria-hidden="true"
>
</i>
Attach a file Attach a file
</button> </button>
<button <button
class="btn btn-default btn-xs hide button-cancel-uploading-files" class="btn btn-default btn-xs hide button-cancel-uploading-files"
type="button"> type="button"
>
Cancel Cancel
</button> </button>
</span> </span>
......
...@@ -3,6 +3,12 @@ ...@@ -3,6 +3,12 @@
import icon from '../icon.vue'; import icon from '../icon.vue';
export default { export default {
components: {
icon,
},
directives: {
tooltip,
},
props: { props: {
buttonTitle: { buttonTitle: {
type: String, type: String,
...@@ -27,12 +33,6 @@ ...@@ -27,12 +33,6 @@
default: false, default: false,
}, },
}, },
components: {
icon,
},
directives: {
tooltip,
},
}; };
</script> </script>
...@@ -47,9 +47,10 @@ ...@@ -47,9 +47,10 @@
:data-md-block="tagBlock" :data-md-block="tagBlock"
:data-md-prepend="prepend" :data-md-prepend="prepend"
:title="buttonTitle" :title="buttonTitle"
:aria-label="buttonTitle"> :aria-label="buttonTitle"
>
<icon <icon
:name="icon"> :name="icon"
</icon> />
</button> </button>
</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