Merge pull request #2700 from Human-Connection/2675-migrate-avatar-component-2

refactor(styleguide): Migrate Avatar component to monorepo
This commit is contained in:
mattwr18 2020-01-21 19:11:14 +01:00 committed by GitHub
commit 901245b718
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
31 changed files with 401 additions and 241 deletions

View File

@ -24,10 +24,10 @@ Then("my comment should be successfully created", () => {
Then("I should see my comment", () => { Then("I should see my comment", () => {
cy.get("div.comment p") cy.get("div.comment p")
.should("contain", "Human Connection rocks") .should("contain", "Human Connection rocks")
.get(".ds-avatar img") .get(".user-avatar img")
.should("have.attr", "src") .should("have.attr", "src")
.and("contain", narratorAvatar) .and("contain", narratorAvatar)
.get("div p.ds-text span") .get(".user-teaser > .info > .text")
.should("contain", "today at"); .should("contain", "today at");
}); });

View File

@ -32,5 +32,5 @@ Then("I cannot upload a picture", () => {
cy.get(".ds-card-content") cy.get(".ds-card-content")
.children() .children()
.should("not.have.id", "customdropzone") .should("not.have.id", "customdropzone")
.should("have.class", "ds-avatar"); .should("have.class", "user-avatar");
}); });

View File

@ -63,7 +63,7 @@ When('I click on "Report User" from the content menu in the user info box', () =
}) })
When('I click on the author', () => { When('I click on the author', () => {
cy.get('.username') cy.get('.user-teaser')
.click() .click()
.url().should('include', '/profile/') .url().should('include', '/profile/')
}) })

View File

@ -87,7 +87,7 @@ Then(
); );
Then("I select a user entry", () => { Then("I select a user entry", () => {
cy.get(".searchable-input .userinfo") cy.get(".searchable-input .user-teaser")
.first() .first()
.trigger("click"); .trigger("click");
}) })

View File

@ -463,7 +463,7 @@ When(
); );
When("I navigate to my {string} settings page", settingsPage => { When("I navigate to my {string} settings page", settingsPage => {
cy.get(".avatar-menu").click(); cy.get(".avatar-menu-trigger").click();
cy.get(".avatar-menu-popover") cy.get(".avatar-menu-popover")
.find("a[href]") .find("a[href]")
.contains("Settings") .contains("Settings")

View File

@ -254,8 +254,7 @@ $size-width-paginate: 100px;
$size-avatar-small: 34px; $size-avatar-small: 34px;
$size-avatar-base: 44px; $size-avatar-base: 44px;
$size-avatar-large: 64px; $size-avatar-large: 114px;
$size-avatar-x-large: 114px;
/** /**
* @tokens Size Buttons * @tokens Size Buttons

View File

@ -1,68 +0,0 @@
import { mount } from '@vue/test-utils'
import Avatar from './Avatar.vue'
const localVue = global.localVue
describe('Avatar.vue', () => {
let propsData = {}
const Wrapper = () => {
return mount(Avatar, { propsData, localVue })
}
it('renders no image', () => {
expect(
Wrapper()
.find('img')
.exists(),
).toBe(false)
})
// this is testing the style guide
it('renders an icon', () => {
expect(
Wrapper()
.find('.ds-icon')
.exists(),
).toBe(true)
})
describe('given a user', () => {
describe('with a relative avatar url', () => {
beforeEach(() => {
propsData = {
user: {
avatar: '/avatar.jpg',
},
}
})
it('adds a prefix to load the image from the uploads service', () => {
expect(
Wrapper()
.find('img')
.attributes('src'),
).toBe('/api/avatar.jpg')
})
})
describe('with an absolute avatar url', () => {
beforeEach(() => {
propsData = {
user: {
avatar: 'https://s3.amazonaws.com/uifaces/faces/twitter/sawalazar/128.jpg',
},
}
})
it('keeps the avatar URL as is', () => {
// e.g. our seeds have absolute image URLs
expect(
Wrapper()
.find('img')
.attributes('src'),
).toBe('https://s3.amazonaws.com/uifaces/faces/twitter/sawalazar/128.jpg')
})
})
})
})

View File

@ -1,28 +0,0 @@
<template>
<ds-avatar
:image="user && user.avatar | proxyApiUrl"
:name="userName"
class="avatar"
:size="size"
/>
</template>
<script>
export default {
name: 'HcAvatar',
props: {
user: { type: Object, default: null },
size: { type: String, default: 'small' },
},
computed: {
userName() {
const { name } = this.user || {}
// The name is used to display the initials in case
// the image cannot be loaded.
return name
// If the name is undefined, then our styleguide will
// display an icon for the anonymous user.
},
},
}
</script>

View File

@ -42,9 +42,9 @@ describe('AvatarMenu.vue', () => {
wrapper = Wrapper() wrapper = Wrapper()
}) })
it('renders the HcAvatar component', () => { it('renders the UserAvatar component', () => {
wrapper.find('.avatar-menu-trigger').trigger('click') wrapper.find('.avatar-menu-trigger').trigger('click')
expect(wrapper.find('.ds-avatar').exists()).toBe(true) expect(wrapper.find('.user-avatar').exists()).toBe(true)
}) })
describe('given a userName', () => { describe('given a userName', () => {

View File

@ -11,7 +11,7 @@
" "
@click.prevent="toggleMenu" @click.prevent="toggleMenu"
> >
<hc-avatar :user="user" /> <user-avatar :user="user" />
<base-icon class="dropdown-arrow" name="angle-down" /> <base-icon class="dropdown-arrow" name="angle-down" />
</a> </a>
</template> </template>
@ -49,12 +49,12 @@
<script> <script>
import { mapGetters } from 'vuex' import { mapGetters } from 'vuex'
import Dropdown from '~/components/Dropdown' import Dropdown from '~/components/Dropdown'
import HcAvatar from '~/components/Avatar/Avatar.vue' import UserAvatar from '~/components/_new/generic/UserAvatar/UserAvatar'
export default { export default {
components: { components: {
Dropdown, Dropdown,
HcAvatar, UserAvatar,
}, },
props: { props: {
placement: { type: String, default: 'top-end' }, placement: { type: String, default: 'top-end' },

View File

@ -12,13 +12,13 @@
<div v-else :class="{ comment: true, 'disabled-content': comment.deleted || comment.disabled }"> <div v-else :class="{ comment: true, 'disabled-content': comment.deleted || comment.disabled }">
<ds-card :id="anchor" :class="{ 'comment--target': isTarget }"> <ds-card :id="anchor" :class="{ 'comment--target': isTarget }">
<ds-space margin-bottom="small" margin-top="small"> <ds-space margin-bottom="small" margin-top="small">
<hc-user :user="author" :date-time="comment.createdAt"> <user-teaser :user="author" :date-time="comment.createdAt">
<template v-slot:dateTime> <template v-slot:dateTime>
<ds-text v-if="comment.createdAt !== comment.updatedAt"> <ds-text v-if="comment.createdAt !== comment.updatedAt">
({{ $t('comment.edited') }}) ({{ $t('comment.edited') }})
</ds-text> </ds-text>
</template> </template>
</hc-user> </user-teaser>
<client-only> <client-only>
<content-menu <content-menu
v-show="!openEditCommentMenu" v-show="!openEditCommentMenu"
@ -61,7 +61,7 @@
<script> <script>
import { mapGetters } from 'vuex' import { mapGetters } from 'vuex'
import { COMMENT_MAX_UNTRUNCATED_LENGTH, COMMENT_TRUNCATE_TO_LENGTH } from '~/constants/comment' import { COMMENT_MAX_UNTRUNCATED_LENGTH, COMMENT_TRUNCATE_TO_LENGTH } from '~/constants/comment'
import HcUser from '~/components/User/User' import UserTeaser from '~/components/UserTeaser/UserTeaser'
import ContentMenu from '~/components/ContentMenu/ContentMenu' import ContentMenu from '~/components/ContentMenu/ContentMenu'
import ContentViewer from '~/components/Editor/ContentViewer' import ContentViewer from '~/components/Editor/ContentViewer'
import HcCommentForm from '~/components/CommentForm/CommentForm' import HcCommentForm from '~/components/CommentForm/CommentForm'
@ -82,7 +82,7 @@ export default {
} }
}, },
components: { components: {
HcUser, UserTeaser,
ContentMenu, ContentMenu,
ContentViewer, ContentViewer,
HcCommentForm, HcCommentForm,

View File

@ -38,7 +38,7 @@
<ds-space /> <ds-space />
<client-only> <client-only>
<hc-user :user="currentUser" :trunc="35" /> <user-teaser :user="currentUser" />
</client-only> </client-only>
<ds-space /> <ds-space />
<ds-input <ds-input
@ -122,14 +122,14 @@ import locales from '~/locales'
import PostMutations from '~/graphql/PostMutations.js' import PostMutations from '~/graphql/PostMutations.js'
import HcCategoriesSelect from '~/components/CategoriesSelect/CategoriesSelect' import HcCategoriesSelect from '~/components/CategoriesSelect/CategoriesSelect'
import HcTeaserImage from '~/components/TeaserImage/TeaserImage' import HcTeaserImage from '~/components/TeaserImage/TeaserImage'
import HcUser from '~/components/User/User' import UserTeaser from '~/components/UserTeaser/UserTeaser'
export default { export default {
components: { components: {
HcEditor, HcEditor,
HcCategoriesSelect, HcCategoriesSelect,
HcTeaserImage, HcTeaserImage,
HcUser, UserTeaser,
}, },
props: { props: {
contribution: { type: Object, default: () => {} }, contribution: { type: Object, default: () => {} },

View File

@ -2,7 +2,7 @@
<ds-space :class="{ read: notification.read, notification: true }" margin-bottom="x-small"> <ds-space :class="{ read: notification.read, notification: true }" margin-bottom="x-small">
<client-only> <client-only>
<ds-space margin-bottom="x-small"> <ds-space margin-bottom="x-small">
<hc-user :user="from.author" :date-time="from.createdAt" :trunc="35" /> <user-teaser :user="from.author" :date-time="from.createdAt" />
</ds-space> </ds-space>
<ds-text class="reason-text-for-test" color="soft"> <ds-text class="reason-text-for-test" color="soft">
{{ $t(`notifications.reason.${notification.reason}`) }} {{ $t(`notifications.reason.${notification.reason}`) }}
@ -35,12 +35,12 @@
</template> </template>
<script> <script>
import HcUser from '~/components/User/User' import UserTeaser from '~/components/UserTeaser/UserTeaser'
export default { export default {
name: 'Notification', name: 'Notification',
components: { components: {
HcUser, UserTeaser,
}, },
props: { props: {
notification: { notification: {

View File

@ -89,8 +89,8 @@ describe('NotificationsTable.vue', () => {
}) })
it('renders the author', () => { it('renders the author', () => {
const username = firstRowNotification.find('.username') const userinfo = firstRowNotification.find('.user-teaser > .info')
expect(username.text()).toEqual(postNotification.from.author.name) expect(userinfo.text()).toContain(postNotification.from.author.name)
}) })
it('renders the reason for the notification', () => { it('renders the reason for the notification', () => {
@ -122,8 +122,8 @@ describe('NotificationsTable.vue', () => {
}) })
it('renders the author', () => { it('renders the author', () => {
const username = secondRowNotification.find('.username') const userinfo = secondRowNotification.find('.user-teaser > .info')
expect(username.text()).toEqual(commentNotification.from.author.name) expect(userinfo.text()).toContain(commentNotification.from.author.name)
}) })
it('renders the reason for the notification', () => { it('renders the reason for the notification', () => {

View File

@ -4,7 +4,7 @@ import { action } from '@storybook/addon-actions'
import NotificationsTable from '~/components/NotificationsTable/NotificationsTable' import NotificationsTable from '~/components/NotificationsTable/NotificationsTable'
import helpers from '~/storybook/helpers' import helpers from '~/storybook/helpers'
import { post } from '~/components/PostCard/PostCard.story.js' import { post } from '~/components/PostCard/PostCard.story.js'
import { user } from '~/components/User/User.story.js' import { user } from '~/components/UserTeaser/UserTeaser.story.js'
helpers.init() helpers.init()
export const notifications = [ export const notifications = [

View File

@ -15,10 +15,9 @@
<template #user="scope"> <template #user="scope">
<ds-space margin-bottom="base"> <ds-space margin-bottom="base">
<client-only> <client-only>
<hc-user <user-teaser
:user="scope.row.from.author" :user="scope.row.from.author"
:date-time="scope.row.from.createdAt" :date-time="scope.row.from.createdAt"
:trunc="35"
:class="{ 'notification-status': scope.row.read }" :class="{ 'notification-status': scope.row.read }"
/> />
</client-only> </client-only>
@ -50,12 +49,12 @@
<hc-empty v-else icon="alert" :message="$t('notifications.empty')" /> <hc-empty v-else icon="alert" :message="$t('notifications.empty')" />
</template> </template>
<script> <script>
import HcUser from '~/components/User/User' import UserTeaser from '~/components/UserTeaser/UserTeaser'
import HcEmpty from '~/components/Empty/Empty' import HcEmpty from '~/components/Empty/Empty'
export default { export default {
components: { components: {
HcUser, UserTeaser,
HcEmpty, HcEmpty,
}, },
props: { props: {

View File

@ -20,14 +20,14 @@
<!-- Username, Image & Date of Post --> <!-- Username, Image & Date of Post -->
<div class="user-wrapper"> <div class="user-wrapper">
<client-only> <client-only>
<hc-user :user="post.author" :trunc="35" :date-time="post.createdAt" /> <user-teaser :user="post.author" :date-time="post.createdAt" />
</client-only> </client-only>
<hc-ribbon v-if="isPinned" class="ribbon--pinned" :text="$t('post.pinned')" /> <hc-ribbon v-if="isPinned" class="ribbon--pinned" :text="$t('post.pinned')" />
<hc-ribbon v-else :text="$t('post.name')" /> <hc-ribbon v-else :text="$t('post.name')" />
</div> </div>
<ds-space margin-bottom="small" /> <ds-space margin-bottom="small" />
<!-- Post Title --> <!-- Post Title -->
<ds-heading tag="h3" no-margin class="hyphenate-text">{{ post.title }}</ds-heading> <ds-heading tag="h3" class="hyphenate-text post-title">{{ post.title }}</ds-heading>
<ds-space margin-bottom="small" /> <ds-space margin-bottom="small" />
<!-- Post Content Excerpt --> <!-- Post Content Excerpt -->
<!-- eslint-disable vue/no-v-html --> <!-- eslint-disable vue/no-v-html -->
@ -78,7 +78,7 @@
</template> </template>
<script> <script>
import HcUser from '~/components/User/User' import UserTeaser from '~/components/UserTeaser/UserTeaser'
import ContentMenu from '~/components/ContentMenu/ContentMenu' import ContentMenu from '~/components/ContentMenu/ContentMenu'
import HcCategory from '~/components/Category' import HcCategory from '~/components/Category'
import HcRibbon from '~/components/Ribbon' import HcRibbon from '~/components/Ribbon'
@ -89,7 +89,7 @@ import { postMenuModalsData, deletePostMutation } from '~/components/utils/PostH
export default { export default {
name: 'HcPostCard', name: 'HcPostCard',
components: { components: {
HcUser, UserTeaser,
HcCategory, HcCategory,
HcRibbon, HcRibbon,
ContentMenu, ContentMenu,
@ -186,7 +186,11 @@ export default {
height: 75px; height: 75px;
} }
/* workaround to avoid jumping layout when hc-user is rendered */ .post-title {
margin-top: $space-large;
}
/* workaround to avoid jumping layout when user-teaser is rendered */
.user-wrapper { .user-wrapper {
height: 36px; height: 36px;
} }

View File

@ -1,5 +1,5 @@
import { mount, RouterLinkStub } from '@vue/test-utils' import { mount, RouterLinkStub } from '@vue/test-utils'
import User from './User.vue' import UserTeaser from './UserTeaser.vue'
import Vuex from 'vuex' import Vuex from 'vuex'
const localVue = global.localVue const localVue = global.localVue
@ -7,7 +7,7 @@ const filter = jest.fn(str => str)
localVue.filter('truncate', filter) localVue.filter('truncate', filter)
describe('User', () => { describe('UserTeaser', () => {
let propsData let propsData
let mocks let mocks
let stubs let stubs
@ -35,7 +35,7 @@ describe('User', () => {
const store = new Vuex.Store({ const store = new Vuex.Store({
getters, getters,
}) })
return mount(User, { store, propsData, mocks, stubs, localVue }) return mount(UserTeaser, { store, propsData, mocks, stubs, localVue })
} }
it('renders anonymous user', () => { it('renders anonymous user', () => {

View File

@ -1,6 +1,6 @@
import { storiesOf } from '@storybook/vue' import { storiesOf } from '@storybook/vue'
import { withA11y } from '@storybook/addon-a11y' import { withA11y } from '@storybook/addon-a11y'
import User from '~/components/User/User.vue' import UserTeaser from '~/components/UserTeaser/UserTeaser.vue'
import helpers from '~/storybook/helpers' import helpers from '~/storybook/helpers'
helpers.init() helpers.init()
@ -53,25 +53,33 @@ export const user = {
socialMedia: [], socialMedia: [],
} }
storiesOf('User', module) storiesOf('UserTeaser', module)
.addDecorator(withA11y) .addDecorator(withA11y)
.addDecorator(helpers.layout) .addDecorator(helpers.layout)
.add('available', () => ({ .add('user only', () => ({
components: { User }, components: { UserTeaser },
store: helpers.store, store: helpers.store,
data: () => ({ data: () => ({
user, user,
}), }),
template: '<user :user="user" :trunc="35" :date-time="new Date()" />', template: '<user-teaser :user="user" />',
}))
.add('with Date', () => ({
components: { UserTeaser },
store: helpers.store,
data: () => ({
user,
}),
template: '<user-teaser :user="user" :date-time="new Date()" />',
})) }))
.add('has edited something', () => ({ .add('has edited something', () => ({
components: { User }, components: { UserTeaser },
store: helpers.store, store: helpers.store,
data: () => ({ data: () => ({
user, user,
}), }),
template: ` template: `
<user :user="user" :trunc="35" :date-time="new Date()"> <user-teaser :user="user" :date-time="new Date()">
<template v-slot:dateTime> <template v-slot:dateTime>
- HEY! I'm edited - HEY! I'm edited
</template> </template>
@ -79,10 +87,10 @@ storiesOf('User', module)
`, `,
})) }))
.add('anonymous', () => ({ .add('anonymous', () => ({
components: { User }, components: { UserTeaser },
store: helpers.store, store: helpers.store,
data: () => ({ data: () => ({
user: null, user: null,
}), }),
template: '<user :user="user" :trunc="35" :date-time="new Date()" />', template: '<user-teaser :user="user" :date-time="new Date()" />',
})) }))

View File

@ -1,32 +1,37 @@
<template> <template>
<div class="user" v-if="displayAnonymous"> <div class="user-teaser" v-if="displayAnonymous">
<hc-avatar v-if="showAvatar" class="avatar" /> <user-avatar v-if="showAvatar" />
<div> <span class="info anonymous">{{ $t('profile.userAnonym') }}</span>
<b class="username">{{ $t('profile.userAnonym') }}</b>
</div>
</div> </div>
<dropdown v-else :class="{ 'disabled-content': user.disabled }" placement="top-start" offset="0"> <dropdown
<template slot="default" slot-scope="{ openMenu, closeMenu, isOpen }"> v-else
<nuxt-link :to="userLink" :class="['user', isOpen && 'active']"> :class="[{ 'disabled-content': user.disabled }]"
<div @mouseover="showPopover ? openMenu(true) : () => {}" @mouseleave="closeMenu(true)"> placement="top-start"
<hc-avatar v-if="showAvatar" class="avatar" :user="user" /> offset="0"
<div> >
<ds-text class="userinfo"> <template #default="{ openMenu, closeMenu, isOpen }">
<b>{{ userSlug }}</b> <nuxt-link
</ds-text> :to="userLink"
</div> :class="['user-teaser', isOpen && 'active']"
<ds-text class="username" align="left" size="small" color="soft"> @mouseover.native="showPopover ? openMenu(true) : () => {}"
{{ userName | truncate(18) }} @mouseleave.native="closeMenu(true)"
<template v-if="dateTime"> >
<base-icon name="clock" /> <user-avatar v-if="showAvatar" :user="user" size="small" />
<hc-relative-date-time :date-time="dateTime" /> <div class="info">
<slot name="dateTime"></slot> <span class="text">
</template> <span class="slug">{{ userSlug }}</span>
</ds-text> <span v-if="dateTime">{{ userName }}</span>
</span>
<span v-if="dateTime" class="text">
<base-icon name="clock" />
<hc-relative-date-time :date-time="dateTime" />
<slot name="dateTime"></slot>
</span>
<span v-else class="text">{{ userName }}</span>
</div> </div>
</nuxt-link> </nuxt-link>
</template> </template>
<template slot="popover" v-if="showPopover"> <template #popover v-if="showPopover">
<div style="min-width: 250px"> <div style="min-width: 250px">
<hc-badges v-if="user.badges && user.badges.length" :badges="user.badges" /> <hc-badges v-if="user.badges && user.badges.length" :badges="user.badges" />
<ds-text <ds-text
@ -77,7 +82,6 @@
/> />
</ds-flex-item> </ds-flex-item>
</ds-flex> </ds-flex>
<!--<ds-space margin-bottom="x-small" />-->
</div> </div>
</template> </template>
</dropdown> </dropdown>
@ -89,22 +93,21 @@ import { mapGetters } from 'vuex'
import HcRelativeDateTime from '~/components/RelativeDateTime' import HcRelativeDateTime from '~/components/RelativeDateTime'
import HcFollowButton from '~/components/FollowButton' import HcFollowButton from '~/components/FollowButton'
import HcBadges from '~/components/Badges' import HcBadges from '~/components/Badges'
import HcAvatar from '~/components/Avatar/Avatar.vue' import UserAvatar from '~/components/_new/generic/UserAvatar/UserAvatar'
import Dropdown from '~/components/Dropdown' import Dropdown from '~/components/Dropdown'
export default { export default {
name: 'HcUser', name: 'UserTeaser',
components: { components: {
HcRelativeDateTime, HcRelativeDateTime,
HcFollowButton, HcFollowButton,
HcAvatar, UserAvatar,
HcBadges, HcBadges,
Dropdown, Dropdown,
}, },
props: { props: {
user: { type: Object, default: null }, user: { type: Object, default: null },
showAvatar: { type: Boolean, default: true }, showAvatar: { type: Boolean, default: true },
trunc: { type: Number, default: 18 }, // "-1" is no trunc
dateTime: { type: [Date, String], default: null }, dateTime: { type: [Date, String], default: null },
showPopover: { type: Boolean, default: true }, showPopover: { type: Boolean, default: true },
}, },
@ -147,38 +150,51 @@ export default {
} }
</script> </script>
<style scoped lang="scss"> <style lang="scss">
.avatar { .trigger {
float: left; max-width: 100%;
margin-right: 4px;
height: 100%;
vertical-align: middle;
} }
.userinfo { .user-teaser {
display: flex; display: flex;
align-items: center; flex-wrap: nowrap;
z-index: $z-index-post-card-link;
> .ds-text {
display: flex;
align-items: center;
margin-left: $space-xx-small;
}
}
.user {
white-space: nowrap;
position: relative; position: relative;
display: flex;
align-items: center;
&:hover, > .user-avatar {
&.active { flex-shrink: 0;
z-index: 999;
} }
}
.user-slug { > .info {
margin-bottom: $space-xx-small; display: flex;
flex-direction: column;
justify-content: center;
padding-left: $space-xx-small;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
color: $text-color-soft;
font-size: $font-size-small;
&.anonymous {
font-size: $font-size-base;
}
.slug {
color: $color-primary;
font-size: $font-size-base;
}
}
.text {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
> .ds-text {
display: inline;
}
}
} }
</style> </style>

View File

@ -0,0 +1,99 @@
import { mount } from '@vue/test-utils'
import UserAvatar from './UserAvatar.vue'
import BaseIcon from '~/components/_new/generic/BaseIcon/BaseIcon'
const localVue = global.localVue
describe('UserAvatar.vue', () => {
let propsData, wrapper
beforeEach(() => {
propsData = {}
wrapper = Wrapper()
})
const Wrapper = () => {
return mount(UserAvatar, { propsData, localVue })
}
it('renders no image', () => {
expect(wrapper.find('img').exists()).toBe(false)
})
it('renders an icon', () => {
expect(wrapper.find(BaseIcon).exists()).toBe(true)
})
describe('given a user', () => {
describe('with no image', () => {
beforeEach(() => {
propsData = {
user: {
name: 'Matt Rider',
},
}
wrapper = Wrapper()
})
describe('no user name', () => {
it('renders an icon', () => {
propsData = { user: { name: null } }
wrapper = Wrapper()
expect(wrapper.find(BaseIcon).exists()).toBe(true)
})
})
describe("user name is 'Anonymous'", () => {
it('renders an icon', () => {
propsData = { user: { name: 'Anonymous' } }
wrapper = Wrapper()
expect(wrapper.find(BaseIcon).exists()).toBe(true)
})
})
it('displays user initials', () => {
expect(wrapper.find('.initials').text()).toEqual('MR')
})
it('displays no more than 3 initials', () => {
propsData = { user: { name: 'Ana Paula Nunes Marques' } }
wrapper = Wrapper()
expect(wrapper.find('.initials').text()).toEqual('APN')
})
})
describe('with a relative avatar url', () => {
beforeEach(() => {
propsData = {
user: {
name: 'Not Anonymous',
avatar: '/avatar.jpg',
},
}
wrapper = Wrapper()
})
it('adds a prefix to load the image from the uploads service', () => {
expect(wrapper.find('.image').attributes('src')).toBe('/api/avatar.jpg')
})
})
describe('with an absolute avatar url', () => {
beforeEach(() => {
propsData = {
user: {
name: 'Not Anonymous',
avatar: 'https://s3.amazonaws.com/uifaces/faces/twitter/sawalazar/128.jpg',
},
}
wrapper = Wrapper()
})
it('keeps the avatar URL as is', () => {
// e.g. our seeds have absolute image URLs
expect(wrapper.find('.image').attributes('src')).toBe(
'https://s3.amazonaws.com/uifaces/faces/twitter/sawalazar/128.jpg',
)
})
})
})
})

View File

@ -0,0 +1,57 @@
import { storiesOf } from '@storybook/vue'
import { withA11y } from '@storybook/addon-a11y'
import StoryRouter from 'storybook-vue-router'
import UserAvatar from '~/components/_new/generic/UserAvatar/UserAvatar'
import helpers from '~/storybook/helpers'
import { user } from '~/components/UserTeaser/UserTeaser.story.js'
helpers.init()
const anonymousUser = {
...user,
name: 'Anonymous',
avatar: null,
}
const userWithoutAvatar = {
...user,
avatar: null,
name: 'Ana Paula Nunes Marques',
}
storiesOf('UserAvatar', module)
.addDecorator(withA11y)
.addDecorator(helpers.layout)
.addDecorator(StoryRouter())
.add('with image', () => ({
components: { UserAvatar },
data: () => ({
user,
}),
template: '<user-avatar :user="user" />',
}))
.add('without image, anonymous user', () => ({
components: { UserAvatar },
data: () => ({
user: anonymousUser,
}),
template: '<user-avatar :user="user" />',
}))
.add('without image, user initials', () => ({
components: { UserAvatar },
data: () => ({
user: userWithoutAvatar,
}),
template: '<user-avatar :user="user" />',
}))
.add('small', () => ({
components: { UserAvatar },
data: () => ({
user,
}),
template: '<user-avatar :user="user" size="small"/>',
}))
.add('large', () => ({
components: { UserAvatar },
data: () => ({
user,
}),
template: '<user-avatar :user="user" size="large"/>',
}))

View File

@ -0,0 +1,84 @@
<template>
<div :class="['user-avatar', size && `--${this.size}`]">
<span class="initials">{{ userInitials }}</span>
<base-icon v-if="isAnonymous" name="eye-slash" />
<img
v-else
:src="user.avatar | proxyApiUrl"
class="image"
@error="event.target.style.display = 'none'"
/>
</div>
</template>
<script>
export default {
name: 'UserAvatar',
props: {
size: {
type: String,
required: false,
validator: value => {
return value.match(/(small|large)/)
},
},
user: {
type: Object,
default: null,
},
},
computed: {
isAnonymous() {
return !this.user || !this.user.name || this.user.name.toLowerCase() === 'anonymous'
},
userInitials() {
if (this.isAnonymous) return ''
return this.user.name
.match(/\b\w/g)
.join('')
.substring(0, 3)
.toUpperCase()
},
},
}
</script>
<style lang="scss">
.user-avatar {
position: relative;
height: $size-avatar-base;
width: $size-avatar-base;
border-radius: 50%;
overflow: hidden;
background-color: $color-primary-dark;
color: $text-color-primary-inverse;
&.--small {
width: $size-avatar-small;
height: $size-avatar-small;
}
&.--large {
width: $size-avatar-large;
height: $size-avatar-large;
font-size: $font-size-xx-large;
}
> .initials,
> .base-icon {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
> .image {
position: relative;
z-index: 5;
width: 100%;
object-fit: cover;
object-position: center;
}
}
</style>

View File

@ -7,11 +7,10 @@
condensed condensed
> >
<template #submitter="scope"> <template #submitter="scope">
<hc-user <user-teaser
:user="scope.row.submitter" :user="scope.row.submitter"
:showAvatar="false" :showAvatar="false"
:showPopover="false" :showPopover="false"
:trunc="30"
data-test="filing-user" data-test="filing-user"
/> />
</template> </template>
@ -29,12 +28,12 @@
</ds-table> </ds-table>
</template> </template>
<script> <script>
import HcUser from '~/components/User/User' import UserTeaser from '~/components/UserTeaser/UserTeaser'
import HcRelativeDateTime from '~/components/RelativeDateTime' import HcRelativeDateTime from '~/components/RelativeDateTime'
export default { export default {
components: { components: {
HcUser, UserTeaser,
HcRelativeDateTime, HcRelativeDateTime,
}, },
props: { props: {

View File

@ -2,7 +2,7 @@ import { storiesOf } from '@storybook/vue'
import { withA11y } from '@storybook/addon-a11y' import { withA11y } from '@storybook/addon-a11y'
import { action } from '@storybook/addon-actions' import { action } from '@storybook/addon-actions'
import { post } from '~/components/PostCard/PostCard.story.js' import { post } from '~/components/PostCard/PostCard.story.js'
import { user } from '~/components/User/User.story.js' import { user } from '~/components/UserTeaser/UserTeaser.story.js'
import helpers from '~/storybook/helpers' import helpers from '~/storybook/helpers'
import ReportList from './ReportList' import ReportList from './ReportList'
import DropdownFilter from '~/components/DropdownFilter/DropdownFilter' import DropdownFilter from '~/components/DropdownFilter/DropdownFilter'

View File

@ -19,7 +19,7 @@
<!-- Content Column --> <!-- Content Column -->
<td class="ds-table-col" data-test="report-content"> <td class="ds-table-col" data-test="report-content">
<client-only v-if="isUser"> <client-only v-if="isUser">
<hc-user :user="report.resource" :showAvatar="false" :trunc="30" :showPopover="false" /> <user-teaser :user="report.resource" :showAvatar="false" :showPopover="false" />
</client-only> </client-only>
<nuxt-link v-else class="title" :to="linkTarget"> <nuxt-link v-else class="title" :to="linkTarget">
{{ linkText | truncate(50) }} {{ linkText | truncate(50) }}
@ -29,12 +29,7 @@
<!-- Author Column --> <!-- Author Column -->
<td class="ds-table-col" data-test="report-author"> <td class="ds-table-col" data-test="report-author">
<client-only v-if="!isUser"> <client-only v-if="!isUser">
<hc-user <user-teaser :user="report.resource.author" :showAvatar="false" :showPopover="false" />
:user="report.resource.author"
:showAvatar="false"
:trunc="30"
:showPopover="false"
/>
</client-only> </client-only>
<span v-else></span> <span v-else></span>
</td> </td>
@ -46,10 +41,9 @@
{{ statusText }} {{ statusText }}
</span> </span>
<client-only v-if="isReviewed"> <client-only v-if="isReviewed">
<hc-user <user-teaser
:user="moderatorOfLatestReview" :user="moderatorOfLatestReview"
:showAvatar="false" :showAvatar="false"
:trunc="30"
:date-time="report.updatedAt" :date-time="report.updatedAt"
:showPopover="false" :showPopover="false"
/> />
@ -85,12 +79,12 @@
<script> <script>
import FiledReportsTable from '~/components/features/FiledReportsTable/FiledReportsTable' import FiledReportsTable from '~/components/features/FiledReportsTable/FiledReportsTable'
import HcUser from '~/components/User/User' import UserTeaser from '~/components/UserTeaser/UserTeaser'
export default { export default {
components: { components: {
FiledReportsTable, FiledReportsTable,
HcUser, UserTeaser,
}, },
props: { props: {
report: { report: {

View File

@ -97,8 +97,8 @@ describe('SearchableInput.vue', () => {
it("pushes to user's profile", async () => { it("pushes to user's profile", async () => {
select.element.value = 'Bob' select.element.value = 'Bob'
select.trigger('input') select.trigger('input')
const users = wrapper.findAll('.userinfo') const users = wrapper.findAll('.slug')
const bob = users.filter(item => item.text() === '@bob-der-baumeister') const bob = users.filter(item => item.text().match(/@bob-der-baumeister/))
bob.trigger('click') bob.trigger('click')
await Vue.nextTick() await Vue.nextTick()
expect(mocks.$router.push).toHaveBeenCalledWith({ expect(mocks.$router.push).toHaveBeenCalledWith({

View File

@ -27,7 +27,7 @@
v-if="option.__typename === 'User'" v-if="option.__typename === 'User'"
:class="{ 'option-with-heading': isFirstOfType(option) }" :class="{ 'option-with-heading': isFirstOfType(option) }"
> >
<hc-user :user="option" :showPopover="false" /> <user-teaser :user="option" :showPopover="false" />
</p> </p>
<p <p
v-if="option.__typename === 'Post'" v-if="option.__typename === 'Post'"
@ -45,14 +45,14 @@
import { isEmpty } from 'lodash' import { isEmpty } from 'lodash'
import SearchHeading from '~/components/generic/SearchHeading/SearchHeading.vue' import SearchHeading from '~/components/generic/SearchHeading/SearchHeading.vue'
import SearchPost from '~/components/generic/SearchPost/SearchPost.vue' import SearchPost from '~/components/generic/SearchPost/SearchPost.vue'
import HcUser from '~/components/User/User.vue' import UserTeaser from '~/components/UserTeaser/UserTeaser.vue'
export default { export default {
name: 'SearchableInput', name: 'SearchableInput',
components: { components: {
SearchHeading, SearchHeading,
SearchPost, SearchPost,
HcUser, UserTeaser,
}, },
props: { props: {
id: { type: String }, id: { type: String },

View File

@ -19,11 +19,11 @@
@click="blurred = !blurred" @click="blurred = !blurred"
/> />
</aside> </aside>
<hc-user :user="post.author" :date-time="post.createdAt"> <user-teaser :user="post.author" :date-time="post.createdAt">
<template v-slot:dateTime> <template v-slot:dateTime>
<ds-text v-if="post.createdAt !== post.updatedAt">({{ $t('post.edited') }})</ds-text> <ds-text v-if="post.createdAt !== post.updatedAt">({{ $t('post.edited') }})</ds-text>
</template> </template>
</hc-user> </user-teaser>
<client-only> <client-only>
<content-menu <content-menu
placement="bottom-end" placement="bottom-end"
@ -101,7 +101,7 @@ import ContentViewer from '~/components/Editor/ContentViewer'
import HcCategory from '~/components/Category' import HcCategory from '~/components/Category'
import HcHashtag from '~/components/Hashtag/Hashtag' import HcHashtag from '~/components/Hashtag/Hashtag'
import ContentMenu from '~/components/ContentMenu/ContentMenu' import ContentMenu from '~/components/ContentMenu/ContentMenu'
import HcUser from '~/components/User/User' import UserTeaser from '~/components/UserTeaser/UserTeaser'
import HcShoutButton from '~/components/ShoutButton.vue' import HcShoutButton from '~/components/ShoutButton.vue'
import HcCommentForm from '~/components/CommentForm/CommentForm' import HcCommentForm from '~/components/CommentForm/CommentForm'
import HcCommentList from '~/components/CommentList/CommentList' import HcCommentList from '~/components/CommentList/CommentList'
@ -119,7 +119,7 @@ export default {
components: { components: {
HcCategory, HcCategory,
HcHashtag, HcHashtag,
HcUser, UserTeaser,
HcShoutButton, HcShoutButton,
ContentMenu, ContentMenu,
HcCommentForm, HcCommentForm,

View File

@ -11,9 +11,9 @@
style="position: relative; height: auto;" style="position: relative; height: auto;"
> >
<hc-upload v-if="myProfile" :user="user"> <hc-upload v-if="myProfile" :user="user">
<hc-avatar :user="user" class="profile-avatar" size="x-large"></hc-avatar> <user-avatar :user="user" class="profile-avatar" size="large"></user-avatar>
</hc-upload> </hc-upload>
<hc-avatar v-else :user="user" class="profile-avatar" size="x-large" /> <user-avatar v-else :user="user" class="profile-avatar" size="large" />
<!-- Menu --> <!-- Menu -->
<client-only> <client-only>
<content-menu <content-menu
@ -99,7 +99,7 @@
<ds-space v-for="follow in uniq(user.following)" :key="follow.id" margin="x-small"> <ds-space v-for="follow in uniq(user.following)" :key="follow.id" margin="x-small">
<!-- TODO: find better solution for rendering errors --> <!-- TODO: find better solution for rendering errors -->
<client-only> <client-only>
<user :user="follow" :trunc="15" /> <user-teaser :user="follow" />
</client-only> </client-only>
</ds-space> </ds-space>
<ds-space v-if="user.followingCount - user.following.length" margin="small"> <ds-space v-if="user.followingCount - user.following.length" margin="small">
@ -129,7 +129,7 @@
<ds-space v-for="follow in uniq(user.followedBy)" :key="follow.id" margin="x-small"> <ds-space v-for="follow in uniq(user.followedBy)" :key="follow.id" margin="x-small">
<!-- TODO: find better solution for rendering errors --> <!-- TODO: find better solution for rendering errors -->
<client-only> <client-only>
<user :user="follow" :trunc="15" /> <user-teaser :user="follow" />
</client-only> </client-only>
</ds-space> </ds-space>
<ds-space v-if="user.followedByCount - user.followedBy.length" margin="small"> <ds-space v-if="user.followedByCount - user.followedBy.length" margin="small">
@ -157,7 +157,7 @@
<template> <template>
<ds-space v-for="link in socialMediaLinks" :key="link.username" margin="x-small"> <ds-space v-for="link in socialMediaLinks" :key="link.username" margin="x-small">
<a :href="link.url" target="_blank"> <a :href="link.url" target="_blank">
<ds-avatar :image="link.favicon" /> <user-avatar :image="link.favicon" />
{{ link.username }} {{ link.username }}
</a> </a>
</ds-space> </ds-space>
@ -271,7 +271,7 @@
<script> <script>
import uniqBy from 'lodash/uniqBy' import uniqBy from 'lodash/uniqBy'
import User from '~/components/User/User' import UserTeaser from '~/components/UserTeaser/UserTeaser'
import HcPostCard from '~/components/PostCard/PostCard.vue' import HcPostCard from '~/components/PostCard/PostCard.vue'
import HcFollowButton from '~/components/FollowButton.vue' import HcFollowButton from '~/components/FollowButton.vue'
import HcCountTo from '~/components/CountTo.vue' import HcCountTo from '~/components/CountTo.vue'
@ -279,7 +279,7 @@ import HcBadges from '~/components/Badges.vue'
import HcEmpty from '~/components/Empty/Empty' import HcEmpty from '~/components/Empty/Empty'
import ContentMenu from '~/components/ContentMenu/ContentMenu' import ContentMenu from '~/components/ContentMenu/ContentMenu'
import HcUpload from '~/components/Upload' import HcUpload from '~/components/Upload'
import HcAvatar from '~/components/Avatar/Avatar.vue' import UserAvatar from '~/components/_new/generic/UserAvatar/UserAvatar'
import MasonryGrid from '~/components/MasonryGrid/MasonryGrid.vue' import MasonryGrid from '~/components/MasonryGrid/MasonryGrid.vue'
import MasonryGridItem from '~/components/MasonryGrid/MasonryGridItem.vue' import MasonryGridItem from '~/components/MasonryGrid/MasonryGridItem.vue'
import { profilePagePosts } from '~/graphql/PostQuery' import { profilePagePosts } from '~/graphql/PostQuery'
@ -297,15 +297,14 @@ const tabToFilterMapping = ({ tab, id }) => {
} }
export default { export default {
name: 'HcUserProfile',
components: { components: {
User, UserTeaser,
HcPostCard, HcPostCard,
HcFollowButton, HcFollowButton,
HcCountTo, HcCountTo,
HcBadges, HcBadges,
HcEmpty, HcEmpty,
HcAvatar, UserAvatar,
ContentMenu, ContentMenu,
HcUpload, HcUpload,
MasonryGrid, MasonryGrid,
@ -525,11 +524,9 @@ export default {
} }
} }
} }
.profile-avatar.ds-avatar { .profile-avatar.user-avatar {
display: block;
margin: auto; margin: auto;
margin-top: -60px; margin-top: -60px;
border: #fff 5px solid;
} }
.page-name-profile-id-slug { .page-name-profile-id-slug {
.ds-flex-item:first-child .content-menu { .ds-flex-item:first-child .content-menu {

View File

@ -24,7 +24,7 @@
params: { id: scope.row.id, slug: scope.row.slug }, params: { id: scope.row.id, slug: scope.row.slug },
}" }"
> >
<hc-avatar :user="scope.row" size="small" /> <user-avatar :user="scope.row" size="small" />
</nuxt-link> </nuxt-link>
</template> </template>
<template slot="name" slot-scope="scope"> <template slot="name" slot-scope="scope">
@ -70,11 +70,11 @@
<script> <script>
import { mutedUsers, unmuteUser } from '~/graphql/settings/MutedUsers' import { mutedUsers, unmuteUser } from '~/graphql/settings/MutedUsers'
import HcAvatar from '~/components/Avatar/Avatar.vue' import UserAvatar from '~/components/_new/generic/UserAvatar/UserAvatar'
export default { export default {
components: { components: {
HcAvatar, UserAvatar,
}, },
data() { data() {
return { return {