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>
/**
* 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>
......
......@@ -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 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>
......
......@@ -46,6 +46,6 @@ export default {
class="avatar identicon"
:class="sizeClass"
:style="identiconStyles">
{{identiconTitle}}
{{ identiconTitle }}
</div>
</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>
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