summaryrefslogtreecommitdiff
path: root/app/assets/javascripts/vue_shared
diff options
context:
space:
mode:
Diffstat (limited to 'app/assets/javascripts/vue_shared')
-rw-r--r--app/assets/javascripts/vue_shared/components/commit.js4
-rw-r--r--app/assets/javascripts/vue_shared/components/header_ci_component.vue133
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/field.vue107
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/header.vue113
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/toolbar.vue33
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/toolbar_button.vue58
-rw-r--r--app/assets/javascripts/vue_shared/components/pipelines_table_row.js4
-rw-r--r--app/assets/javascripts/vue_shared/components/time_ago_tooltip.vue58
-rw-r--r--app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_image.vue8
-rw-r--r--app/assets/javascripts/vue_shared/mixins/timeago.js18
-rw-r--r--app/assets/javascripts/vue_shared/mixins/tooltip.js4
-rw-r--r--app/assets/javascripts/vue_shared/vue_resource_interceptor.js2
12 files changed, 536 insertions, 6 deletions
diff --git a/app/assets/javascripts/vue_shared/components/commit.js b/app/assets/javascripts/vue_shared/components/commit.js
index 23bc5fbc034..8e22057e2e9 100644
--- a/app/assets/javascripts/vue_shared/components/commit.js
+++ b/app/assets/javascripts/vue_shared/components/commit.js
@@ -91,7 +91,7 @@ export default {
hasAuthor() {
return this.author &&
this.author.avatar_url &&
- this.author.web_url &&
+ this.author.path &&
this.author.username;
},
@@ -140,7 +140,7 @@ export default {
<user-avatar-link
v-if="hasAuthor"
class="avatar-image-container"
- :link-href="author.web_url"
+ :link-href="author.path"
:img-src="author.avatar_url"
:img-alt="userImageAltDescription"
:tooltip-text="author.username"
diff --git a/app/assets/javascripts/vue_shared/components/header_ci_component.vue b/app/assets/javascripts/vue_shared/components/header_ci_component.vue
new file mode 100644
index 00000000000..fe6d6a792e7
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/header_ci_component.vue
@@ -0,0 +1,133 @@
+<script>
+import ciIconBadge from './ci_badge_link.vue';
+import loadingIcon from './loading_icon.vue';
+import timeagoTooltip from './time_ago_tooltip.vue';
+import tooltipMixin from '../mixins/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,
+ },
+ 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: () => [],
+ },
+ },
+
+ mixins: [
+ tooltipMixin,
+ ],
+
+ components: {
+ ciIconBadge,
+ loadingIcon,
+ timeagoTooltip,
+ userAvatarImage,
+ },
+
+ computed: {
+ userAvatarAltText() {
+ return `${this.user.name}'s avatar`;
+ },
+ },
+
+ methods: {
+ onClickAction(action) {
+ this.$emit('actionClicked', action);
+ },
+ },
+};
+</script>
+<template>
+ <header class="page-content-header">
+ <section class="header-main-content">
+
+ <ci-icon-badge :status="status" />
+
+ <strong>
+ {{itemName}} #{{itemId}}
+ </strong>
+
+ triggered
+
+ <timeago-tooltip :time="time" />
+
+ by
+
+ <template v-if="user">
+ <a
+ :href="user.path"
+ :title="user.email"
+ class="js-user-link commit-committer-link"
+ ref="tooltip">
+
+ <user-avatar-image
+ :img-src="user.avatar_url"
+ :img-alt="userAvatarAltText"
+ :tooltip-text="user.name"
+ :img-size="24"
+ />
+
+ {{user.name}}
+ </a>
+ </template>
+ </section>
+
+ <section
+ class="header-action-button nav-controls"
+ v-if="actions.length">
+ <template
+ v-for="action in actions">
+ <a
+ v-if="action.type === 'link'"
+ :href="action.path"
+ :class="action.cssClass">
+ {{action.label}}
+ </a>
+
+ <button
+ v-else="action.type === 'button'"
+ @click="onClickAction(action)"
+ :disabled="action.isLoading"
+ :class="action.cssClass"
+ type="button">
+ {{action.label}}
+
+ <i
+ v-show="action.isLoading"
+ class="fa fa-spin fa-spinner"
+ aria-hidden="true">
+ </i>
+ </button>
+ </template>
+ </section>
+ </header>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/markdown/field.vue b/app/assets/javascripts/vue_shared/components/markdown/field.vue
new file mode 100644
index 00000000000..e6977681e96
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/markdown/field.vue
@@ -0,0 +1,107 @@
+<script>
+ /* global Flash */
+ import markdownHeader from './header.vue';
+ import markdownToolbar from './toolbar.vue';
+
+ export default {
+ props: {
+ markdownPreviewUrl: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ markdownDocs: {
+ type: String,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ markdownPreview: '',
+ markdownPreviewLoading: false,
+ previewMarkdown: false,
+ };
+ },
+ components: {
+ markdownHeader,
+ markdownToolbar,
+ },
+ methods: {
+ toggleMarkdownPreview() {
+ this.previewMarkdown = !this.previewMarkdown;
+
+ if (!this.previewMarkdown) {
+ this.markdownPreview = '';
+ } else {
+ this.markdownPreviewLoading = true;
+ this.$http.post(
+ this.markdownPreviewUrl,
+ {
+ /*
+ Can't use `$refs` as the component is technically in the parent component
+ so we access the VNode & then get the element
+ */
+ text: this.$slots.textarea[0].elm.value,
+ },
+ )
+ .then((res) => {
+ const data = res.json();
+
+ this.markdownPreviewLoading = false;
+ this.markdownPreview = data.body;
+
+ this.$nextTick(() => {
+ $(this.$refs['markdown-preview']).renderGFM();
+ });
+ })
+ .catch(() => new Flash('Error loading markdown preview'));
+ }
+ },
+ },
+ mounted() {
+ /*
+ GLForm class handles all the toolbar buttons
+ */
+ return new gl.GLForm($(this.$refs['gl-form']), true);
+ },
+ };
+</script>
+
+<template>
+ <div
+ class="md-area prepend-top-default append-bottom-default js-vue-markdown-field"
+ ref="gl-form">
+ <markdown-header
+ :preview-markdown="previewMarkdown"
+ @toggle-markdown="toggleMarkdownPreview" />
+ <div
+ class="md-write-holder"
+ 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">
+ <i
+ class="fa fa-compress"
+ aria-hidden="true">
+ </i>
+ </a>
+ <markdown-toolbar
+ :markdown-docs="markdownDocs" />
+ </div>
+ </div>
+ <div
+ class="md md-preview-holder md-preview"
+ v-show="previewMarkdown">
+ <div
+ ref="markdown-preview"
+ v-html="markdownPreview">
+ </div>
+ <span v-if="markdownPreviewLoading">
+ Loading...
+ </span>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/markdown/header.vue b/app/assets/javascripts/vue_shared/components/markdown/header.vue
new file mode 100644
index 00000000000..1a11f493b7f
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/markdown/header.vue
@@ -0,0 +1,113 @@
+<script>
+ import tooltipMixin from '../../mixins/tooltip';
+ import toolbarButton from './toolbar_button.vue';
+
+ export default {
+ mixins: [
+ tooltipMixin,
+ ],
+ props: {
+ previewMarkdown: {
+ type: Boolean,
+ required: true,
+ },
+ },
+ components: {
+ toolbarButton,
+ },
+ methods: {
+ toggleMarkdownPreview(e, form) {
+ if (form && !form.find('.js-vue-markdown-field').length) {
+ return;
+ } else if (e.target.blur) {
+ e.target.blur();
+ }
+
+ this.$emit('toggle-markdown');
+ },
+ },
+ mounted() {
+ $(document).on('markdown-preview:show.vue', this.toggleMarkdownPreview);
+ $(document).on('markdown-preview:hide.vue', this.toggleMarkdownPreview);
+ },
+ beforeDestroy() {
+ $(document).on('markdown-preview:show.vue', this.toggleMarkdownPreview);
+ $(document).off('markdown-preview:hide.vue', this.toggleMarkdownPreview);
+ },
+ };
+</script>
+
+<template>
+ <div class="md-header">
+ <ul class="nav-links clearfix">
+ <li :class="{ active: !previewMarkdown }">
+ <a
+ href="#md-write-holder"
+ tabindex="-1"
+ @click.prevent="toggleMarkdownPreview($event)">
+ Write
+ </a>
+ </li>
+ <li :class="{ active: previewMarkdown }">
+ <a
+ href="#md-preview-holder"
+ tabindex="-1"
+ @click.prevent="toggleMarkdownPreview($event)">
+ Preview
+ </a>
+ </li>
+ <li class="pull-right">
+ <div class="toolbar-group">
+ <toolbar-button
+ tag="**"
+ button-title="Add bold text"
+ icon="bold" />
+ <toolbar-button
+ tag="*"
+ button-title="Add italic text"
+ icon="italic" />
+ <toolbar-button
+ tag="> "
+ :prepend="true"
+ button-title="Insert a quote"
+ icon="quote-right" />
+ <toolbar-button
+ tag="`"
+ tag-block="```"
+ button-title="Insert code"
+ icon="code" />
+ <toolbar-button
+ tag="* "
+ :prepend="true"
+ button-title="Add a bullet list"
+ icon="list-ul" />
+ <toolbar-button
+ tag="1. "
+ :prepend="true"
+ button-title="Add a numbered list"
+ icon="list-ol" />
+ <toolbar-button
+ tag="* [ ] "
+ :prepend="true"
+ button-title="Add a task list"
+ icon="check-square-o" />
+ </div>
+ <div class="toolbar-group">
+ <button
+ aria-label="Go full screen"
+ class="toolbar-btn js-zen-enter"
+ data-container="body"
+ tabindex="-1"
+ title="Go full screen"
+ type="button"
+ ref="tooltip">
+ <i
+ aria-hidden="true"
+ class="fa fa-arrows-alt fa-fw">
+ </i>
+ </button>
+ </div>
+ </li>
+ </ul>
+ </div>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue b/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue
new file mode 100644
index 00000000000..93252293ba6
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue
@@ -0,0 +1,33 @@
+<script>
+ export default {
+ props: {
+ markdownDocs: {
+ type: String,
+ required: true,
+ },
+ },
+ };
+</script>
+
+<template>
+ <div class="comment-toolbar clearfix">
+ <div class="toolbar-text">
+ <a
+ :href="markdownDocs"
+ target="_blank"
+ tabindex="-1">
+ Markdown is supported
+ </a>
+ </div>
+ <button
+ class="toolbar-button markdown-selector"
+ type="button"
+ tabindex="-1">
+ <i
+ class="fa fa-file-image-o toolbar-button-icon"
+ aria-hidden="true">
+ </i>
+ Attach a file
+ </button>
+ </div>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/markdown/toolbar_button.vue b/app/assets/javascripts/vue_shared/components/markdown/toolbar_button.vue
new file mode 100644
index 00000000000..096be507625
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/markdown/toolbar_button.vue
@@ -0,0 +1,58 @@
+<script>
+ import tooltipMixin from '../../mixins/tooltip';
+
+ export default {
+ mixins: [
+ tooltipMixin,
+ ],
+ props: {
+ buttonTitle: {
+ type: String,
+ required: true,
+ },
+ icon: {
+ type: String,
+ required: true,
+ },
+ tag: {
+ type: String,
+ required: true,
+ },
+ tagBlock: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ prepend: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+ computed: {
+ iconClass() {
+ return `fa-${this.icon}`;
+ },
+ },
+ };
+</script>
+
+<template>
+ <button
+ type="button"
+ class="toolbar-btn js-md hidden-xs"
+ tabindex="-1"
+ ref="tooltip"
+ data-container="body"
+ :data-md-tag="tag"
+ :data-md-block="tagBlock"
+ :data-md-prepend="prepend"
+ :title="buttonTitle"
+ :aria-label="buttonTitle">
+ <i
+ aria-hidden="true"
+ class="fa fa-fw"
+ :class="iconClass">
+ </i>
+ </button>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/pipelines_table_row.js b/app/assets/javascripts/vue_shared/components/pipelines_table_row.js
index 30d16e4ed3e..f60f8eeb43d 100644
--- a/app/assets/javascripts/vue_shared/components/pipelines_table_row.js
+++ b/app/assets/javascripts/vue_shared/components/pipelines_table_row.js
@@ -4,7 +4,7 @@ import PipelinesActionsComponent from '../../pipelines/components/pipelines_acti
import PipelinesArtifactsComponent from '../../pipelines/components/pipelines_artifacts';
import ciBadge from './ci_badge_link.vue';
import PipelinesStageComponent from '../../pipelines/components/stage.vue';
-import PipelinesUrlComponent from '../../pipelines/components/pipeline_url';
+import PipelinesUrlComponent from '../../pipelines/components/pipeline_url.vue';
import PipelinesTimeagoComponent from '../../pipelines/components/time_ago';
import CommitComponent from './commit';
@@ -83,7 +83,7 @@ export default {
} else {
commitAuthorInformation = {
avatar_url: this.pipeline.commit.author_gravatar_url,
- web_url: `mailto:${this.pipeline.commit.author_email}`,
+ path: `mailto:${this.pipeline.commit.author_email}`,
username: this.pipeline.commit.author_name,
};
}
diff --git a/app/assets/javascripts/vue_shared/components/time_ago_tooltip.vue b/app/assets/javascripts/vue_shared/components/time_ago_tooltip.vue
new file mode 100644
index 00000000000..af2b4c6786e
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/time_ago_tooltip.vue
@@ -0,0 +1,58 @@
+<script>
+import tooltipMixin from '../mixins/tooltip';
+import timeagoMixin from '../mixins/timeago';
+import '../../lib/utils/datetime_utility';
+
+/**
+ * Port of ruby helper time_ago_with_tooltip
+ */
+
+export default {
+ props: {
+ time: {
+ type: String,
+ required: true,
+ },
+
+ tooltipPlacement: {
+ type: String,
+ required: false,
+ default: 'top',
+ },
+
+ shortFormat: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+
+ cssClass: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ },
+
+ mixins: [
+ tooltipMixin,
+ timeagoMixin,
+ ],
+
+ computed: {
+ timeagoCssClass() {
+ return this.shortFormat ? 'js-short-timeago' : 'js-timeago';
+ },
+ },
+};
+</script>
+<template>
+ <time
+ :class="[timeagoCssClass, cssClass]"
+ class="js-timeago js-timeago-render"
+ :title="tooltipTitle(time)"
+ :data-placement="tooltipPlacement"
+ data-container="body"
+ ref="tooltip">
+ {{timeFormated(time)}}
+ </time>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_image.vue b/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_image.vue
index b8db6afda12..cd6f8c7aee4 100644
--- a/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_image.vue
+++ b/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_image.vue
@@ -60,6 +60,12 @@ export default {
avatarSizeClass() {
return `s${this.size}`;
},
+ // API response sends null when gravatar is disabled and
+ // we provide an empty string when we use it inside user avatar link.
+ // In both cases we should render the defaultAvatarUrl
+ imageSource() {
+ return this.imgSrc === '' || this.imgSrc === null ? defaultAvatarUrl : this.imgSrc;
+ },
},
};
</script>
@@ -68,7 +74,7 @@ export default {
<img
class="avatar"
:class="[avatarSizeClass, cssClasses]"
- :src="imgSrc"
+ :src="imageSource"
:width="size"
:height="size"
:alt="imgAlt"
diff --git a/app/assets/javascripts/vue_shared/mixins/timeago.js b/app/assets/javascripts/vue_shared/mixins/timeago.js
new file mode 100644
index 00000000000..20f63ab663c
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/mixins/timeago.js
@@ -0,0 +1,18 @@
+import '../../lib/utils/datetime_utility';
+
+/**
+ * Mixin with time ago methods used in some vue components
+ */
+export default {
+ methods: {
+ timeFormated(time) {
+ const timeago = gl.utils.getTimeago();
+
+ return timeago.format(time);
+ },
+
+ tooltipTitle(time) {
+ return gl.utils.formatDate(time);
+ },
+ },
+};
diff --git a/app/assets/javascripts/vue_shared/mixins/tooltip.js b/app/assets/javascripts/vue_shared/mixins/tooltip.js
index 9bb948bff66..995c0c98505 100644
--- a/app/assets/javascripts/vue_shared/mixins/tooltip.js
+++ b/app/assets/javascripts/vue_shared/mixins/tooltip.js
@@ -6,4 +6,8 @@ export default {
updated() {
$(this.$refs.tooltip).tooltip('fixTitle');
},
+
+ beforeDestroy() {
+ $(this.$refs.tooltip).tooltip('destroy');
+ },
};
diff --git a/app/assets/javascripts/vue_shared/vue_resource_interceptor.js b/app/assets/javascripts/vue_shared/vue_resource_interceptor.js
index d5f87588c28..740930dce5b 100644
--- a/app/assets/javascripts/vue_shared/vue_resource_interceptor.js
+++ b/app/assets/javascripts/vue_shared/vue_resource_interceptor.js
@@ -4,7 +4,7 @@ import VueResource from 'vue-resource';
Vue.use(VueResource);
// Maintain a global counter for active requests
-// see: spec/support/wait_for_vue_resource.rb
+// see: spec/support/wait_for_requests.rb
Vue.http.interceptors.push((request, next) => {
window.activeVueResources = window.activeVueResources || 0;
window.activeVueResources += 1;