Commit b0642299 authored by Phil Hughes's avatar Phil Hughes

Merge branch 'user-avatar-vue-ce' into 'master'

Consolidate user avatar Vue logic

Closes #31017

See merge request !10718
parents 01a7f333 55737682
import Vue from 'vue'; import Vue from 'vue';
import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
import eventHub from '../eventhub'; import eventHub from '../eventhub';
const Store = gl.issueBoards.BoardsStore; const Store = gl.issueBoards.BoardsStore;
...@@ -38,6 +39,9 @@ gl.issueBoards.IssueCardInner = Vue.extend({ ...@@ -38,6 +39,9 @@ gl.issueBoards.IssueCardInner = Vue.extend({
maxCounter: 99, maxCounter: 99,
}; };
}, },
components: {
userAvatarLink,
},
computed: { computed: {
numberOverLimit() { numberOverLimit() {
return this.issue.assignees.length - this.limitBeforeCounter; return this.issue.assignees.length - this.limitBeforeCounter;
...@@ -146,23 +150,16 @@ gl.issueBoards.IssueCardInner = Vue.extend({ ...@@ -146,23 +150,16 @@ gl.issueBoards.IssueCardInner = Vue.extend({
</span> </span>
</h4> </h4>
<div class="card-assignee"> <div class="card-assignee">
<a <user-avatar-link
class="has-tooltip js-no-trigger"
:href="assigneeUrl(assignee)"
:title="assigneeUrlTitle(assignee)"
v-for="(assignee, index) in issue.assignees" v-for="(assignee, index) in issue.assignees"
v-if="shouldRenderAssignee(index)" v-if="shouldRenderAssignee(index)"
data-container="body" class="js-no-trigger"
data-placement="bottom" :link-href="assigneeUrl(assignee)"
> :img-alt="avatarUrlTitle(assignee)"
<img :img-src="assignee.avatar"
class="avatar avatar-inline s20" :tooltip-text="assigneeUrlTitle(assignee)"
:src="assignee.avatar" tooltip-placement="bottom"
width="20"
height="20"
:alt="avatarUrlTitle(assignee)"
/> />
</a>
<span <span
class="avatar-counter has-tooltip" class="avatar-counter has-tooltip"
:title="assigneeCounterTooltip" :title="assigneeCounterTooltip"
......
/* eslint-disable no-param-reassign */ /* eslint-disable no-param-reassign */
import Vue from 'vue'; import Vue from 'vue';
import userAvatarImage from '../../vue_shared/components/user_avatar/user_avatar_image.vue';
const global = window.gl || (window.gl = {}); const global = window.gl || (window.gl = {});
global.cycleAnalytics = global.cycleAnalytics || {}; global.cycleAnalytics = global.cycleAnalytics || {};
...@@ -10,6 +11,9 @@ global.cycleAnalytics.StageCodeComponent = Vue.extend({ ...@@ -10,6 +11,9 @@ global.cycleAnalytics.StageCodeComponent = Vue.extend({
items: Array, items: Array,
stage: Object, stage: Object,
}, },
components: {
userAvatarImage,
},
template: ` template: `
<div> <div>
<div class="events-description"> <div class="events-description">
...@@ -19,7 +23,8 @@ global.cycleAnalytics.StageCodeComponent = Vue.extend({ ...@@ -19,7 +23,8 @@ global.cycleAnalytics.StageCodeComponent = Vue.extend({
<ul class="stage-event-list"> <ul class="stage-event-list">
<li v-for="mergeRequest in items" class="stage-event-item"> <li v-for="mergeRequest in items" class="stage-event-item">
<div class="item-details"> <div class="item-details">
<img class="avatar" :src="mergeRequest.author.avatarUrl"> <!-- FIXME: Pass an alt attribute here for accessibility -->
<user-avatar-image :img-src="mergeRequest.author.avatarUrl"/>
<h5 class="item-title merge-merquest-title"> <h5 class="item-title merge-merquest-title">
<a :href="mergeRequest.url"> <a :href="mergeRequest.url">
{{ mergeRequest.title }} {{ mergeRequest.title }}
......
/* eslint-disable no-param-reassign */ /* eslint-disable no-param-reassign */
import Vue from 'vue'; import Vue from 'vue';
import userAvatarImage from '../../vue_shared/components/user_avatar/user_avatar_image.vue';
const global = window.gl || (window.gl = {}); const global = window.gl || (window.gl = {});
global.cycleAnalytics = global.cycleAnalytics || {}; global.cycleAnalytics = global.cycleAnalytics || {};
...@@ -10,6 +10,9 @@ global.cycleAnalytics.StageIssueComponent = Vue.extend({ ...@@ -10,6 +10,9 @@ global.cycleAnalytics.StageIssueComponent = Vue.extend({
items: Array, items: Array,
stage: Object, stage: Object,
}, },
components: {
userAvatarImage,
},
template: ` template: `
<div> <div>
<div class="events-description"> <div class="events-description">
...@@ -19,7 +22,8 @@ global.cycleAnalytics.StageIssueComponent = Vue.extend({ ...@@ -19,7 +22,8 @@ global.cycleAnalytics.StageIssueComponent = Vue.extend({
<ul class="stage-event-list"> <ul class="stage-event-list">
<li v-for="issue in items" class="stage-event-item"> <li v-for="issue in items" class="stage-event-item">
<div class="item-details"> <div class="item-details">
<img class="avatar" :src="issue.author.avatarUrl"> <!-- FIXME: Pass an alt attribute here for accessibility -->
<user-avatar-image :img-src="issue.author.avatarUrl"/>
<h5 class="item-title issue-title"> <h5 class="item-title issue-title">
<a class="issue-title" :href="issue.url"> <a class="issue-title" :href="issue.url">
{{ issue.title }} {{ issue.title }}
......
/* eslint-disable no-param-reassign */ /* eslint-disable no-param-reassign */
import Vue from 'vue'; import Vue from 'vue';
import userAvatarImage from '../../vue_shared/components/user_avatar/user_avatar_image.vue';
import iconCommit from '../svg/icon_commit.svg'; import iconCommit from '../svg/icon_commit.svg';
const global = window.gl || (window.gl = {}); const global = window.gl || (window.gl = {});
...@@ -10,11 +11,12 @@ global.cycleAnalytics.StagePlanComponent = Vue.extend({ ...@@ -10,11 +11,12 @@ global.cycleAnalytics.StagePlanComponent = Vue.extend({
items: Array, items: Array,
stage: Object, stage: Object,
}, },
components: {
userAvatarImage,
},
data() { data() {
return { iconCommit }; return { iconCommit };
}, },
template: ` template: `
<div> <div>
<div class="events-description"> <div class="events-description">
...@@ -24,7 +26,8 @@ global.cycleAnalytics.StagePlanComponent = Vue.extend({ ...@@ -24,7 +26,8 @@ global.cycleAnalytics.StagePlanComponent = Vue.extend({
<ul class="stage-event-list"> <ul class="stage-event-list">
<li v-for="commit in items" class="stage-event-item"> <li v-for="commit in items" class="stage-event-item">
<div class="item-details item-conmmit-component"> <div class="item-details item-conmmit-component">
<img class="avatar" :src="commit.author.avatarUrl"> <!-- FIXME: Pass an alt attribute here for accessibility -->
<user-avatar-image :img-src="commit.author.avatarUrl"/>
<h5 class="item-title commit-title"> <h5 class="item-title commit-title">
<a :href="commit.commitUrl"> <a :href="commit.commitUrl">
{{ commit.title }} {{ commit.title }}
......
/* eslint-disable no-param-reassign */ /* eslint-disable no-param-reassign */
import Vue from 'vue'; import Vue from 'vue';
import userAvatarImage from '../../vue_shared/components/user_avatar/user_avatar_image.vue';
const global = window.gl || (window.gl = {}); const global = window.gl || (window.gl = {});
global.cycleAnalytics = global.cycleAnalytics || {}; global.cycleAnalytics = global.cycleAnalytics || {};
...@@ -10,6 +10,9 @@ global.cycleAnalytics.StageProductionComponent = Vue.extend({ ...@@ -10,6 +10,9 @@ global.cycleAnalytics.StageProductionComponent = Vue.extend({
items: Array, items: Array,
stage: Object, stage: Object,
}, },
components: {
userAvatarImage,
},
template: ` template: `
<div> <div>
<div class="events-description"> <div class="events-description">
...@@ -19,7 +22,8 @@ global.cycleAnalytics.StageProductionComponent = Vue.extend({ ...@@ -19,7 +22,8 @@ global.cycleAnalytics.StageProductionComponent = Vue.extend({
<ul class="stage-event-list"> <ul class="stage-event-list">
<li v-for="issue in items" class="stage-event-item"> <li v-for="issue in items" class="stage-event-item">
<div class="item-details"> <div class="item-details">
<img class="avatar" :src="issue.author.avatarUrl"> <!-- FIXME: Pass an alt attribute here for accessibility -->
<user-avatar-image :img-src="issue.author.avatarUrl"/>
<h5 class="item-title issue-title"> <h5 class="item-title issue-title">
<a class="issue-title" :href="issue.url"> <a class="issue-title" :href="issue.url">
{{ issue.title }} {{ issue.title }}
......
/* eslint-disable no-param-reassign */ /* eslint-disable no-param-reassign */
import Vue from 'vue'; import Vue from 'vue';
import userAvatarImage from '../../vue_shared/components/user_avatar/user_avatar_image.vue';
const global = window.gl || (window.gl = {}); const global = window.gl || (window.gl = {});
global.cycleAnalytics = global.cycleAnalytics || {}; global.cycleAnalytics = global.cycleAnalytics || {};
...@@ -10,6 +10,9 @@ global.cycleAnalytics.StageReviewComponent = Vue.extend({ ...@@ -10,6 +10,9 @@ global.cycleAnalytics.StageReviewComponent = Vue.extend({
items: Array, items: Array,
stage: Object, stage: Object,
}, },
components: {
userAvatarImage,
},
template: ` template: `
<div> <div>
<div class="events-description"> <div class="events-description">
...@@ -19,7 +22,8 @@ global.cycleAnalytics.StageReviewComponent = Vue.extend({ ...@@ -19,7 +22,8 @@ global.cycleAnalytics.StageReviewComponent = Vue.extend({
<ul class="stage-event-list"> <ul class="stage-event-list">
<li v-for="mergeRequest in items" class="stage-event-item"> <li v-for="mergeRequest in items" class="stage-event-item">
<div class="item-details"> <div class="item-details">
<img class="avatar" :src="mergeRequest.author.avatarUrl"> <!-- FIXME: Pass an alt attribute here for accessibility -->
<user-avatar-image :img-src="mergeRequest.author.avatarUrl"/>
<h5 class="item-title merge-merquest-title"> <h5 class="item-title merge-merquest-title">
<a :href="mergeRequest.url"> <a :href="mergeRequest.url">
{{ mergeRequest.title }} {{ mergeRequest.title }}
......
/* eslint-disable no-param-reassign */ /* eslint-disable no-param-reassign */
import Vue from 'vue'; import Vue from 'vue';
import userAvatarImage from '../../vue_shared/components/user_avatar/user_avatar_image.vue';
import iconBranch from '../svg/icon_branch.svg'; import iconBranch from '../svg/icon_branch.svg';
const global = window.gl || (window.gl = {}); const global = window.gl || (window.gl = {});
...@@ -13,6 +14,9 @@ global.cycleAnalytics.StageStagingComponent = Vue.extend({ ...@@ -13,6 +14,9 @@ global.cycleAnalytics.StageStagingComponent = Vue.extend({
data() { data() {
return { iconBranch }; return { iconBranch };
}, },
components: {
userAvatarImage,
},
template: ` template: `
<div> <div>
<div class="events-description"> <div class="events-description">
...@@ -22,7 +26,8 @@ global.cycleAnalytics.StageStagingComponent = Vue.extend({ ...@@ -22,7 +26,8 @@ global.cycleAnalytics.StageStagingComponent = Vue.extend({
<ul class="stage-event-list"> <ul class="stage-event-list">
<li v-for="build in items" class="stage-event-item item-build-component"> <li v-for="build in items" class="stage-event-item item-build-component">
<div class="item-details"> <div class="item-details">
<img class="avatar" :src="build.author.avatarUrl"> <!-- FIXME: Pass an alt attribute here for accessibility -->
<user-avatar-image :img-src="build.author.avatarUrl"/>
<h5 class="item-title"> <h5 class="item-title">
<a :href="build.url" class="pipeline-id">#{{ build.id }}</a> <a :href="build.url" class="pipeline-id">#{{ build.id }}</a>
<i class="fa fa-code-fork"></i> <i class="fa fa-code-fork"></i>
......
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
import Vue from 'vue'; import Vue from 'vue';
import collapseIcon from '../icons/collapse_icon.svg'; import collapseIcon from '../icons/collapse_icon.svg';
import userAvatarImage from '../../vue_shared/components/user_avatar/user_avatar_image.vue';
const DiffNoteAvatars = Vue.extend({ const DiffNoteAvatars = Vue.extend({
props: ['discussionId'], props: ['discussionId'],
...@@ -15,22 +16,24 @@ const DiffNoteAvatars = Vue.extend({ ...@@ -15,22 +16,24 @@ const DiffNoteAvatars = Vue.extend({
collapseIcon, collapseIcon,
}; };
}, },
components: {
userAvatarImage,
},
template: ` template: `
<div class="diff-comment-avatar-holders" <div class="diff-comment-avatar-holders"
v-show="notesCount !== 0"> v-show="notesCount !== 0">
<div v-if="!isVisible"> <div v-if="!isVisible">
<img v-for="note in notesSubset" <!-- FIXME: Pass an alt attribute here for accessibility -->
class="avatar diff-comment-avatar has-tooltip js-diff-comment-avatar" <user-avatar-image
width="19" v-for="note in notesSubset"
height="19" class="diff-comment-avatar js-diff-comment-avatar"
role="button" @click.native="clickedAvatar($event)"
data-container="body" :img-src="note.authorAvatar"
data-placement="top" :tooltip-text="getTooltipText(note)"
data-html="true"
:data-line-type="lineType" :data-line-type="lineType"
:title="note.authorName + ': ' + note.noteTruncated" :size="19"
:src="note.authorAvatar" data-html="true"
@click="clickedAvatar($event)" /> />
<span v-if="notesCount > shownAvatars" <span v-if="notesCount > shownAvatars"
class="diff-comments-more-count has-tooltip js-diff-comment-avatar" class="diff-comments-more-count has-tooltip js-diff-comment-avatar"
data-container="body" data-container="body"
...@@ -150,6 +153,9 @@ const DiffNoteAvatars = Vue.extend({ ...@@ -150,6 +153,9 @@ const DiffNoteAvatars = Vue.extend({
setDiscussionVisible() { setDiscussionVisible() {
this.isVisible = $(`.diffs .notes[data-discussion-id="${this.discussion.id}"]`).is(':visible'); this.isVisible = $(`.diffs .notes[data-discussion-id="${this.discussion.id}"]`).is(':visible');
}, },
getTooltipText(note) {
return `${note.authorName}: ${note.noteTruncated}`;
},
}, },
}); });
......
<script> <script>
import Timeago from 'timeago.js'; import Timeago from 'timeago.js';
import _ from 'underscore'; import _ from 'underscore';
import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
import '../../lib/utils/text_utility'; import '../../lib/utils/text_utility';
import ActionsComponent from './environment_actions.vue'; import ActionsComponent from './environment_actions.vue';
import ExternalUrlComponent from './environment_external_url.vue'; import ExternalUrlComponent from './environment_external_url.vue';
...@@ -20,6 +21,7 @@ const timeagoInstance = new Timeago(); ...@@ -20,6 +21,7 @@ const timeagoInstance = new Timeago();
export default { export default {
components: { components: {
userAvatarLink,
'commit-component': CommitComponent, 'commit-component': CommitComponent,
'actions-component': ActionsComponent, 'actions-component': ActionsComponent,
'external-url-component': ExternalUrlComponent, 'external-url-component': ExternalUrlComponent,
...@@ -468,15 +470,13 @@ export default { ...@@ -468,15 +470,13 @@ export default {
<span v-if="!model.isFolder && deploymentHasUser"> <span v-if="!model.isFolder && deploymentHasUser">
by by
<a <user-avatar-link
:href="deploymentUser.web_url" class="js-deploy-user-container"
class="js-deploy-user-container"> :link-href="deploymentUser.web_url"
<img :img-src="deploymentUser.avatar_url"
class="avatar has-tooltip s20" :img-alt="userImageAltDescription"
:src="deploymentUser.avatar_url" :tooltip-text="deploymentUser.username"
:alt="userImageAltDescription" />
:title="deploymentUser.username" />
</a>
</span> </span>
</td> </td>
......
import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
export default { export default {
props: [ props: [
'pipeline', 'pipeline',
...@@ -7,6 +9,9 @@ export default { ...@@ -7,6 +9,9 @@ export default {
return !!this.pipeline.user; return !!this.pipeline.user;
}, },
}, },
components: {
userAvatarLink,
},
template: ` template: `
<td> <td>
<a <a
...@@ -15,18 +20,13 @@ export default { ...@@ -15,18 +20,13 @@ export default {
<span class="pipeline-id">#{{pipeline.id}}</span> <span class="pipeline-id">#{{pipeline.id}}</span>
</a> </a>
<span>by</span> <span>by</span>
<a <user-avatar-link
class="js-pipeline-url-user"
v-if="user" v-if="user"
:href="pipeline.user.web_url"> class="js-pipeline-url-user"
<img :link-href="pipeline.user.web_url"
v-if="user" :img-src="pipeline.user.avatar_url"
class="avatar has-tooltip s20 " :tooltip-text="pipeline.user.name"
:title="pipeline.user.name" />
data-container="body"
:src="pipeline.user.avatar_url"
>
</a>
<span <span
v-if="!user" v-if="!user"
class="js-pipeline-url-api api"> class="js-pipeline-url-api api">
......
import commitIconSvg from 'icons/_icon_commit.svg'; import commitIconSvg from 'icons/_icon_commit.svg';
import userAvatarLink from './user_avatar/user_avatar_link.vue';
export default { export default {
props: { props: {
...@@ -110,6 +111,9 @@ export default { ...@@ -110,6 +111,9 @@ export default {
return { commitIconSvg }; return { commitIconSvg };
}, },
components: {
userAvatarLink,
},
template: ` template: `
<div class="branch-commit"> <div class="branch-commit">
...@@ -133,16 +137,14 @@ export default { ...@@ -133,16 +137,14 @@ export default {
<p class="commit-title"> <p class="commit-title">
<span v-if="title"> <span v-if="title">
<a v-if="hasAuthor" <user-avatar-link
v-if="hasAuthor"
class="avatar-image-container" class="avatar-image-container"
:href="author.web_url"> :link-href="author.web_url"
<img :img-src="author.avatar_url"
class="avatar has-tooltip s20" :img-alt="userImageAltDescription"
:src="author.avatar_url" :tooltip-text="author.username"
:alt="userImageAltDescription" />
:title="author.username" />
</a>
<a class="commit-row-message" <a class="commit-row-message"
:href="commitUrl"> :href="commitUrl">
{{title}} {{title}}
......
<script>
/* This is a re-usable vue component for rendering a user avatar that
does not need to link to the user's profile. The image and an optional
tooltip can be configured by props passed to this component.
Sample configuration:
<user-avatar-image
:img-src="userAvatarSrc"
:img-alt="tooltipText"
:tooltip-text="tooltipText"
tooltip-placement="top"
/>
*/
import defaultAvatarUrl from 'images/no_avatar.png';
import TooltipMixin from '../../mixins/tooltip';
export default {
name: 'UserAvatarImage',
mixins: [TooltipMixin],
props: {
imgSrc: {
type: String,
required: false,
default: defaultAvatarUrl,
},
cssClasses: {
type: String,
required: false,
default: '',
},
imgAlt: {
type: String,
required: false,
default: 'user avatar',
},
size: {
type: Number,
required: false,
default: 20,
},
tooltipText: {
type: String,
required: false,
default: '',
},
tooltipPlacement: {
type: String,
required: false,
default: 'top',
},
},
computed: {
tooltipContainer() {
return this.tooltipText ? 'body' : null;
},
avatarSizeClass() {
return `s${this.size}`;
},
},
};
</script>
<template>
<img
class="avatar"
:class="[avatarSizeClass, cssClasses]"
:src="imgSrc"
:width="size"
:height="size"
:alt="imgAlt"
:data-container="tooltipContainer"
:data-placement="tooltipPlacement"
:title="tooltipText"
ref="tooltip"
/>
</template>
<script>
/* This is a re-usable vue component for rendering a user avatar wrapped in
a clickable link (likely to the user's profile). The link, image, and
tooltip can be configured by props passed to this component.
Sample configuration:
<user-avatar-link
:link-href="userProfileUrl"
:img-src="userAvatarSrc"
:img-alt="tooltipText"
:img-size="20"
:tooltip-text="tooltipText"
tooltip-placement="top"
/>
*/
import userAvatarImage from './user_avatar_image.vue';
export default {
name: 'UserAvatarLink',
components: {
userAvatarImage,
},
props: {
linkHref: {
type: String,
required: false,
default: '',
},
imgSrc: {
type: String,
required: false,
default: '',
},
imgAlt: {
type: String,
required: false,
default: '',
},
imgCssClasses: {
type: String,
required: false,
default: '',
},
imgSize: {
type: Number,
required: false,
default: 20,
},
tooltipText: {
type: String,
required: false,
default: '',
},
tooltipPlacement: {
type: String,
required: false,
default: 'top',
},
},
};
</script>
<template>
<a
class="user-avatar-link"
:href="linkHref">
<user-avatar-image
:img-src="imgSrc"
:img-alt="imgAlt"
:css-classes="imgCssClasses"
:size="imgSize"
:tooltip-text="tooltipText"
:tooltip-placement="tooltipPlacement"
/>
</a>
</template>
<script>
/* This is a re-usable vue component for rendering a user avatar svg (typically
for a blank state). It will receive styles comparable to the user avatar,
but no image is loaded, it isn't wrapped in a link, and tooltips aren't supported.
The svg and avatar size can be configured by props passed to this component.
Sample configuration:
<user-avatar-svg
:svg="potentialApproverSvg"
:size="20"
/>
*/
export default {
props: {
svg: {
type: String,
required: true,
},
size: {
type: Number,
required: false,
default: 20,
},
},
computed: {
avatarSizeClass() {
return `s${this.size}`;
},
},
};
</script>
<template>
<svg
:class="avatarSizeClass"
:height="size"
:width="size"
v-html="svg">
</svg>
</template>
...@@ -10,6 +10,8 @@ ...@@ -10,6 +10,8 @@
border-radius: $avatar_radius; border-radius: $avatar_radius;
border: 1px solid $avatar-border; border: 1px solid $avatar-border;
&.s16 { @include avatar-size(16px, 6px); } &.s16 { @include avatar-size(16px, 6px); }
&.s18 { @include avatar-size(18px, 6px); }
&.s19 { @include avatar-size(19px, 6px); }
&.s20 { @include avatar-size(20px, 7px); } &.s20 { @include avatar-size(20px, 7px); }
&.s24 { @include avatar-size(24px, 8px); } &.s24 { @include avatar-size(24px, 8px); }
&.s26 { @include avatar-size(26px, 8px); } &.s26 { @include avatar-size(26px, 8px); }
......
...@@ -65,3 +65,7 @@ ...@@ -65,3 +65,7 @@
text-decoration: none; text-decoration: none;
} }
} }
.user-avatar-link {
text-decoration: none;
}
...@@ -68,10 +68,6 @@ ...@@ -68,10 +68,6 @@
margin: 0; margin: 0;
} }
.avatar-image-container {
text-decoration: none;
}
.icon-play { .icon-play {
height: 13px; height: 13px;
width: 12px; width: 12px;
......
...@@ -90,9 +90,9 @@ var config = { ...@@ -90,9 +90,9 @@ var config = {
loader: 'raw-loader', loader: 'raw-loader',
}, },
{ {
test: /\.gif$/, test: /\.(gif|png)$/,
loader: 'url-loader', loader: 'url-loader',
query: { mimetype: 'image/gif' }, options: { limit: 2048 },
}, },
{ {
test: /\.(worker\.js|pdf|bmpr)$/, test: /\.(worker\.js|pdf|bmpr)$/,
...@@ -190,6 +190,7 @@ var config = { ...@@ -190,6 +190,7 @@ var config = {
'emojis': path.join(ROOT_PATH, 'fixtures/emojis'), 'emojis': path.join(ROOT_PATH, 'fixtures/emojis'),
'empty_states': path.join(ROOT_PATH, 'app/views/shared/empty_states'), 'empty_states': path.join(ROOT_PATH, 'app/views/shared/empty_states'),
'icons': path.join(ROOT_PATH, 'app/views/shared/icons'), 'icons': path.join(ROOT_PATH, 'app/views/shared/icons'),
'images': path.join(ROOT_PATH, 'app/assets/images'),
'vendor': path.join(ROOT_PATH, 'vendor/assets/javascripts'), 'vendor': path.join(ROOT_PATH, 'vendor/assets/javascripts'),
'vue$': 'vue/dist/vue.esm.js', 'vue$': 'vue/dist/vue.esm.js',
} }
......
...@@ -91,7 +91,7 @@ feature 'Diff note avatars', feature: true, js: true do ...@@ -91,7 +91,7 @@ feature 'Diff note avatars', feature: true, js: true do
page.within find("[id='#{position.line_code(project.repository)}']") do page.within find("[id='#{position.line_code(project.repository)}']") do
find('.diff-notes-collapse').click find('.diff-notes-collapse').click
expect(first('img.js-diff-comment-avatar')["title"]).to eq("#{note.author.name}: #{note.note.truncate(17)}") expect(first('img.js-diff-comment-avatar')["data-original-title"]).to eq("#{note.author.name}: #{note.note.truncate(17)}")
end end
end end
......
...@@ -129,7 +129,7 @@ describe('Issue card component', () => { ...@@ -129,7 +129,7 @@ describe('Issue card component', () => {
it('sets title', () => { it('sets title', () => {
expect( expect(
component.$el.querySelector('.card-assignee a').getAttribute('title'), component.$el.querySelector('.card-assignee img').getAttribute('data-original-title'),
).toContain(`Assigned to ${user.name}`); ).toContain(`Assigned to ${user.name}`);
}); });
......
...@@ -60,7 +60,7 @@ describe('Pipeline Url Component', () => { ...@@ -60,7 +60,7 @@ describe('Pipeline Url Component', () => {
expect( expect(
component.$el.querySelector('.js-pipeline-url-user').getAttribute('href'), component.$el.querySelector('.js-pipeline-url-user').getAttribute('href'),
).toEqual(mockData.pipeline.user.web_url); ).toEqual(mockData.pipeline.user.web_url);
expect(image.getAttribute('title')).toEqual(mockData.pipeline.user.name); expect(image.getAttribute('data-original-title')).toEqual(mockData.pipeline.user.name);
expect(image.getAttribute('src')).toEqual(mockData.pipeline.user.avatar_url); expect(image.getAttribute('src')).toEqual(mockData.pipeline.user.avatar_url);
}); });
......
...@@ -86,7 +86,7 @@ describe('Commit component', () => { ...@@ -86,7 +86,7 @@ describe('Commit component', () => {
it('Should render the author avatar with title and alt attributes', () => { it('Should render the author avatar with title and alt attributes', () => {
expect( expect(
component.$el.querySelector('.commit-title .avatar-image-container img').getAttribute('title'), component.$el.querySelector('.commit-title .avatar-image-container img').getAttribute('data-original-title'),
).toContain(props.author.username); ).toContain(props.author.username);
expect( expect(
component.$el.querySelector('.commit-title .avatar-image-container img').getAttribute('alt'), component.$el.querySelector('.commit-title .avatar-image-container img').getAttribute('alt'),
......
...@@ -79,7 +79,7 @@ describe('Pipelines Table Row', () => { ...@@ -79,7 +79,7 @@ describe('Pipelines Table Row', () => {
).toEqual(pipeline.user.web_url); ).toEqual(pipeline.user.web_url);
expect( expect(
component.$el.querySelector('td:nth-child(2) img').getAttribute('title'), component.$el.querySelector('td:nth-child(2) img').getAttribute('data-original-title'),
).toEqual(pipeline.user.name); ).toEqual(pipeline.user.name);
}); });
}); });
...@@ -102,7 +102,7 @@ describe('Pipelines Table Row', () => { ...@@ -102,7 +102,7 @@ describe('Pipelines Table Row', () => {
} }
const commitAuthorLink = commitAuthorElement.getAttribute('href'); const commitAuthorLink = commitAuthorElement.getAttribute('href');
const commitAuthorName = commitAuthorElement.querySelector('img.avatar').getAttribute('title'); const commitAuthorName = commitAuthorElement.querySelector('img.avatar').getAttribute('data-original-title');
return { commitAuthorElement, commitAuthorLink, commitAuthorName }; return { commitAuthorElement, commitAuthorLink, commitAuthorName };
}; };
......
import Vue from 'vue';
import UserAvatarImage from '~/vue_shared/components/user_avatar/user_avatar_image.vue';
const UserAvatarImageComponent = Vue.extend(UserAvatarImage);
describe('User Avatar Image Component', function () {
describe('Initialization', function () {
beforeEach(function () {
this.propsData = {
size: 99,
imgSrc: 'myavatarurl.com',
imgAlt: 'mydisplayname',
cssClasses: 'myextraavatarclass',
tooltipText: 'tooltip text',
tooltipPlacement: 'bottom',
};
this.userAvatarImage = new UserAvatarImageComponent({
propsData: this.propsData,
}).$mount();
});
it('should return a defined Vue component', function () {
expect(this.userAvatarImage).toBeDefined();
});
it('should have <img> as a child element', function () {
expect(this.userAvatarImage.$el.tagName).toBe('IMG');
});
it('should properly compute tooltipContainer', function () {
expect(this.userAvatarImage.tooltipContainer).toBe('body');
});
it('should properly render tooltipContainer', function () {
expect(this.userAvatarImage.$el.getAttribute('data-container')).toBe('body');
});
it('should properly compute avatarSizeClass', function () {
expect(this.userAvatarImage.avatarSizeClass).toBe('s99');
});
it('should properly render img css', function () {
const classList = this.userAvatarImage.$el.classList;
const containsAvatar = classList.contains('avatar');
const containsSizeClass = classList.contains('s99');
const containsCustomClass = classList.contains('myextraavatarclass');
expect(containsAvatar).toBe(true);
expect(containsSizeClass).toBe(true);
expect(containsCustomClass).toBe(true);
});
});
});
import Vue from 'vue';
import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
describe('User Avatar Link Component', function () {
beforeEach(function () {
this.propsData = {
linkHref: 'myavatarurl.com',
imgSize: 99,
imgSrc: 'myavatarurl.com',
imgAlt: 'mydisplayname',
imgCssClasses: 'myextraavatarclass',
tooltipText: 'tooltip text',
tooltipPlacement: 'bottom',
};
const UserAvatarLinkComponent = Vue.extend(UserAvatarLink);
this.userAvatarLink = new UserAvatarLinkComponent({
propsData: this.propsData,
}).$mount();
this.userAvatarImage = this.userAvatarLink.$children[0];
});
it('should return a defined Vue component', function () {
expect(this.userAvatarLink).toBeDefined();
});
it('should have user-avatar-image registered as child component', function () {
expect(this.userAvatarLink.$options.components.userAvatarImage).toBeDefined();
});
it('user-avatar-link should have user-avatar-image as child component', function () {
expect(this.userAvatarImage).toBeDefined();
});
it('should render <a> as a child element', function () {
expect(this.userAvatarLink.$el.tagName).toBe('A');
});
it('should have <img> as a child element', function () {
expect(this.userAvatarLink.$el.querySelector('img')).not.toBeNull();
});
it('should return neccessary props as defined', function () {
_.each(this.propsData, (val, key) => {
expect(this.userAvatarLink[key]).toBeDefined();
});
});
});
import Vue from 'vue';
import UserAvatarSvg from '~/vue_shared/components/user_avatar/user_avatar_svg.vue';
import avatarSvg from 'icons/_icon_random.svg';
const UserAvatarSvgComponent = Vue.extend(UserAvatarSvg);
describe('User Avatar Svg Component', function () {
describe('Initialization', function () {
beforeEach(function () {
this.propsData = {
size: 99,
svg: avatarSvg,
};
this.userAvatarSvg = new UserAvatarSvgComponent({
propsData: this.propsData,
}).$mount();
});
it('should return a defined Vue component', function () {
expect(this.userAvatarSvg).toBeDefined();
});
it('should have <svg> as a child element', function () {
expect(this.userAvatarSvg.$el.tagName).toEqual('svg');
expect(this.userAvatarSvg.$el.innerHTML).toContain('<path');
});
});
});
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