Commit cdc49388 authored by Filipa Lacerda's avatar Filipa Lacerda

[ci skip] Fix more rules

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