Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Support
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
G
gitlab-ce
Project overview
Project overview
Details
Activity
Releases
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Issues
0
Issues
0
List
Boards
Labels
Milestones
Merge Requests
1
Merge Requests
1
Analytics
Analytics
Repository
Value Stream
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Create a new issue
Commits
Issue Boards
Open sidebar
nexedi
gitlab-ce
Commits
26940f36
Commit
26940f36
authored
Sep 25, 2020
by
Peter Hegman
Committed by
Kushal Pandya
Sep 25, 2020
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Add member avatar component
Adds member avatar related components to group members view
parent
9748207a
Changes
14
Show whitespace changes
Inline
Side-by-side
Showing
14 changed files
with
514 additions
and
0 deletions
+514
-0
app/assets/javascripts/vue_shared/components/members/avatars/group_avatar.vue
...ts/vue_shared/components/members/avatars/group_avatar.vue
+34
-0
app/assets/javascripts/vue_shared/components/members/avatars/invite_avatar.vue
...s/vue_shared/components/members/avatars/invite_avatar.vue
+32
-0
app/assets/javascripts/vue_shared/components/members/avatars/user_avatar.vue
...pts/vue_shared/components/members/avatars/user_avatar.vue
+55
-0
app/assets/javascripts/vue_shared/components/members/constants.js
...ts/javascripts/vue_shared/components/members/constants.js
+9
-0
app/assets/javascripts/vue_shared/components/members/table/member_avatar.vue
...pts/vue_shared/components/members/table/member_avatar.vue
+31
-0
app/assets/javascripts/vue_shared/components/members/table/members_table.vue
...pts/vue_shared/components/members/table/members_table.vue
+10
-0
app/assets/javascripts/vue_shared/components/members/table/members_table_cell.vue
...ue_shared/components/members/table/members_table_cell.vue
+40
-0
locale/gitlab.pot
locale/gitlab.pot
+3
-0
spec/frontend/vue_shared/components/members/avatars/group_avatar_spec.js
...ue_shared/components/members/avatars/group_avatar_spec.js
+46
-0
spec/frontend/vue_shared/components/members/avatars/invite_avatar_spec.js
...e_shared/components/members/avatars/invite_avatar_spec.js
+38
-0
spec/frontend/vue_shared/components/members/avatars/user_avatar_spec.js
...vue_shared/components/members/avatars/user_avatar_spec.js
+66
-0
spec/frontend/vue_shared/components/members/mock_data.js
spec/frontend/vue_shared/components/members/mock_data.js
+61
-0
spec/frontend/vue_shared/components/members/table/member_avatar_spec.js
...vue_shared/components/members/table/member_avatar_spec.js
+36
-0
spec/frontend/vue_shared/components/members/table/member_table_cell_spec.js
...shared/components/members/table/member_table_cell_spec.js
+53
-0
No files found.
app/assets/javascripts/vue_shared/components/members/avatars/group_avatar.vue
0 → 100644
View file @
26940f36
<
script
>
import
{
GlAvatarLink
,
GlAvatarLabeled
}
from
'
@gitlab/ui
'
;
import
{
AVATAR_SIZE
}
from
'
../constants
'
;
export
default
{
name
:
'
GroupAvatar
'
,
avatarSize
:
AVATAR_SIZE
,
components
:
{
GlAvatarLink
,
GlAvatarLabeled
},
props
:
{
member
:
{
type
:
Object
,
required
:
true
,
},
},
computed
:
{
group
()
{
return
this
.
member
.
sharedWithGroup
;
},
},
};
</
script
>
<
template
>
<gl-avatar-link
:href=
"group.webUrl"
>
<gl-avatar-labeled
:label=
"group.fullName"
:src=
"group.avatarUrl"
:alt=
"group.fullName"
:size=
"$options.avatarSize"
:entity-name=
"group.name"
:entity-id=
"group.id"
/>
</gl-avatar-link>
</
template
>
app/assets/javascripts/vue_shared/components/members/avatars/invite_avatar.vue
0 → 100644
View file @
26940f36
<
script
>
import
{
GlAvatarLabeled
}
from
'
@gitlab/ui
'
;
import
{
AVATAR_SIZE
}
from
'
../constants
'
;
export
default
{
name
:
'
InviteAvatar
'
,
avatarSize
:
AVATAR_SIZE
,
components
:
{
GlAvatarLabeled
},
props
:
{
member
:
{
type
:
Object
,
required
:
true
,
},
},
computed
:
{
invite
()
{
return
this
.
member
.
invite
;
},
},
};
</
script
>
<
template
>
<gl-avatar-labeled
:label=
"invite.email"
:src=
"invite.avatarUrl"
:alt=
"invite.email"
:size=
"$options.avatarSize"
:entity-name=
"invite.email"
:entity-id=
"member.id"
/>
</
template
>
app/assets/javascripts/vue_shared/components/members/avatars/user_avatar.vue
0 → 100644
View file @
26940f36
<
script
>
import
{
GlAvatarLink
,
GlAvatarLabeled
,
GlSafeHtmlDirective
as
SafeHtml
}
from
'
@gitlab/ui
'
;
import
{
__
}
from
'
~/locale
'
;
import
{
AVATAR_SIZE
}
from
'
../constants
'
;
export
default
{
name
:
'
UserAvatar
'
,
avatarSize
:
AVATAR_SIZE
,
orphanedUserLabel
:
__
(
'
Orphaned member
'
),
components
:
{
GlAvatarLink
,
GlAvatarLabeled
},
directives
:
{
SafeHtml
,
},
props
:
{
member
:
{
type
:
Object
,
required
:
true
,
},
},
computed
:
{
user
()
{
return
this
.
member
.
user
;
},
},
};
</
script
>
<
template
>
<gl-avatar-link
v-if=
"user"
class=
"js-user-link"
:href=
"user.webUrl"
:data-user-id=
"user.id"
:data-username=
"user.username"
>
<gl-avatar-labeled
:label=
"user.name"
:sub-label=
"`@$
{user.username}`"
:src="user.avatarUrl"
:alt="user.name"
:size="$options.avatarSize"
:entity-name="user.name"
:entity-id="user.id"
/>
</gl-avatar-link>
<gl-avatar-labeled
v-else
:label=
"$options.orphanedUserLabel"
:alt=
"$options.orphanedUserLabel"
:size=
"$options.avatarSize"
:entity-name=
"$options.orphanedUserLabel"
:entity-id=
"member.id"
/>
</
template
>
app/assets/javascripts/vue_shared/components/members/constants.js
View file @
26940f36
...
...
@@ -53,3 +53,12 @@ export const FIELDS = [
tdClass
:
'
col-actions
'
,
},
];
export
const
AVATAR_SIZE
=
48
;
export
const
MEMBER_TYPES
=
{
user
:
'
user
'
,
group
:
'
group
'
,
invite
:
'
invite
'
,
accessRequest
:
'
accessRequest
'
,
};
app/assets/javascripts/vue_shared/components/members/table/member_avatar.vue
0 → 100644
View file @
26940f36
<
script
>
import
{
kebabCase
}
from
'
lodash
'
;
import
UserAvatar
from
'
../avatars/user_avatar.vue
'
;
import
InviteAvatar
from
'
../avatars/invite_avatar.vue
'
;
import
GroupAvatar
from
'
../avatars/group_avatar.vue
'
;
export
default
{
name
:
'
MemberAvatar
'
,
components
:
{
UserAvatar
,
InviteAvatar
,
GroupAvatar
,
AccessRequestAvatar
:
UserAvatar
},
props
:
{
memberType
:
{
type
:
String
,
required
:
true
,
},
member
:
{
type
:
Object
,
required
:
true
,
},
},
computed
:
{
avatarComponent
()
{
// eslint-disable-next-line @gitlab/require-i18n-strings
return
`
${
kebabCase
(
this
.
memberType
)}
-avatar`
;
},
},
};
</
script
>
<
template
>
<component
:is=
"avatarComponent"
:member=
"member"
/>
</
template
>
app/assets/javascripts/vue_shared/components/members/table/members_table.vue
View file @
26940f36
...
...
@@ -3,11 +3,15 @@ import { mapState } from 'vuex';
import
{
GlTable
}
from
'
@gitlab/ui
'
;
import
{
FIELDS
}
from
'
../constants
'
;
import
initUserPopovers
from
'
~/user_popovers
'
;
import
MemberAvatar
from
'
./member_avatar.vue
'
;
import
MembersTableCell
from
'
./members_table_cell.vue
'
;
export
default
{
name
:
'
MembersTable
'
,
components
:
{
GlTable
,
MemberAvatar
,
MembersTableCell
,
},
computed
:
{
...
mapState
([
'
members
'
,
'
tableFields
'
]),
...
...
@@ -33,6 +37,12 @@ export default {
:empty-text=
"__('No members found')"
show-empty
>
<template
#cell(account)=
"
{ item: member }">
<members-table-cell
#default
="
{ memberType }" :member="member">
<member-avatar
:member-type=
"memberType"
:member=
"member"
/>
</members-table-cell>
</
template
>
<
template
#cell
(
source
)
>
<!-- Temporarily empty -->
</
template
>
...
...
app/assets/javascripts/vue_shared/components/members/table/members_table_cell.vue
0 → 100644
View file @
26940f36
<
script
>
import
{
MEMBER_TYPES
}
from
'
../constants
'
;
export
default
{
name
:
'
MembersTableCell
'
,
props
:
{
member
:
{
type
:
Object
,
required
:
true
,
},
},
computed
:
{
isGroup
()
{
return
Boolean
(
this
.
member
.
sharedWithGroup
);
},
isInvite
()
{
return
Boolean
(
this
.
member
.
invite
);
},
isAccessRequest
()
{
return
Boolean
(
this
.
member
.
requestedAt
);
},
memberType
()
{
if
(
this
.
isGroup
)
{
return
MEMBER_TYPES
.
group
;
}
else
if
(
this
.
isInvite
)
{
return
MEMBER_TYPES
.
invite
;
}
else
if
(
this
.
isAccessRequest
)
{
return
MEMBER_TYPES
.
accessRequest
;
}
return
MEMBER_TYPES
.
user
;
},
},
render
()
{
return
this
.
$scopedSlots
.
default
({
memberType
:
this
.
memberType
,
});
},
};
</
script
>
locale/gitlab.pot
View file @
26940f36
...
...
@@ -17931,6 +17931,9 @@ msgstr ""
msgid "Origin"
msgstr ""
msgid "Orphaned member"
msgstr ""
msgid "Other Labels"
msgstr ""
...
...
spec/frontend/vue_shared/components/members/avatars/group_avatar_spec.js
0 → 100644
View file @
26940f36
import
{
mount
,
createWrapper
}
from
'
@vue/test-utils
'
;
import
{
getByText
as
getByTextHelper
}
from
'
@testing-library/dom
'
;
import
{
GlAvatarLink
}
from
'
@gitlab/ui
'
;
import
{
group
as
member
}
from
'
../mock_data
'
;
import
GroupAvatar
from
'
~/vue_shared/components/members/avatars/group_avatar.vue
'
;
describe
(
'
MemberList
'
,
()
=>
{
let
wrapper
;
const
group
=
member
.
sharedWithGroup
;
const
createComponent
=
(
propsData
=
{})
=>
{
wrapper
=
mount
(
GroupAvatar
,
{
propsData
:
{
member
,
...
propsData
,
},
});
};
const
getByText
=
(
text
,
options
)
=>
createWrapper
(
getByTextHelper
(
wrapper
.
element
,
text
,
options
));
beforeEach
(()
=>
{
createComponent
();
});
afterEach
(()
=>
{
wrapper
.
destroy
();
});
it
(
'
renders link to group
'
,
()
=>
{
const
link
=
wrapper
.
find
(
GlAvatarLink
);
expect
(
link
.
exists
()).
toBe
(
true
);
expect
(
link
.
attributes
(
'
href
'
)).
toBe
(
group
.
webUrl
);
});
it
(
"
renders group's full name
"
,
()
=>
{
expect
(
getByText
(
group
.
fullName
).
exists
()).
toBe
(
true
);
});
it
(
"
renders group's avatar
"
,
()
=>
{
expect
(
wrapper
.
find
(
'
img
'
).
attributes
(
'
src
'
)).
toBe
(
group
.
avatarUrl
);
});
});
spec/frontend/vue_shared/components/members/avatars/invite_avatar_spec.js
0 → 100644
View file @
26940f36
import
{
mount
,
createWrapper
}
from
'
@vue/test-utils
'
;
import
{
getByText
as
getByTextHelper
}
from
'
@testing-library/dom
'
;
import
{
invite
as
member
}
from
'
../mock_data
'
;
import
InviteAvatar
from
'
~/vue_shared/components/members/avatars/invite_avatar.vue
'
;
describe
(
'
MemberList
'
,
()
=>
{
let
wrapper
;
const
{
invite
}
=
member
;
const
createComponent
=
(
propsData
=
{})
=>
{
wrapper
=
mount
(
InviteAvatar
,
{
propsData
:
{
member
,
...
propsData
,
},
});
};
const
getByText
=
(
text
,
options
)
=>
createWrapper
(
getByTextHelper
(
wrapper
.
element
,
text
,
options
));
beforeEach
(()
=>
{
createComponent
();
});
afterEach
(()
=>
{
wrapper
.
destroy
();
});
it
(
'
renders email as name
'
,
()
=>
{
expect
(
getByText
(
invite
.
email
).
exists
()).
toBe
(
true
);
});
it
(
'
renders avatar
'
,
()
=>
{
expect
(
wrapper
.
find
(
'
img
'
).
attributes
(
'
src
'
)).
toBe
(
invite
.
avatarUrl
);
});
});
spec/frontend/vue_shared/components/members/avatars/user_avatar_spec.js
0 → 100644
View file @
26940f36
import
{
mount
,
createWrapper
}
from
'
@vue/test-utils
'
;
import
{
getByText
as
getByTextHelper
}
from
'
@testing-library/dom
'
;
import
{
GlAvatarLink
}
from
'
@gitlab/ui
'
;
import
{
member
,
orphanedMember
}
from
'
../mock_data
'
;
import
UserAvatar
from
'
~/vue_shared/components/members/avatars/user_avatar.vue
'
;
describe
(
'
MemberList
'
,
()
=>
{
let
wrapper
;
const
{
user
}
=
member
;
const
createComponent
=
(
propsData
=
{})
=>
{
wrapper
=
mount
(
UserAvatar
,
{
propsData
:
{
member
,
...
propsData
,
},
});
};
const
getByText
=
(
text
,
options
)
=>
createWrapper
(
getByTextHelper
(
wrapper
.
element
,
text
,
options
));
afterEach
(()
=>
{
wrapper
.
destroy
();
});
it
(
"
renders link to user's profile
"
,
()
=>
{
createComponent
();
const
link
=
wrapper
.
find
(
GlAvatarLink
);
expect
(
link
.
exists
()).
toBe
(
true
);
expect
(
link
.
attributes
()).
toMatchObject
({
href
:
user
.
webUrl
,
'
data-user-id
'
:
`
${
user
.
id
}
`
,
'
data-username
'
:
user
.
username
,
});
});
it
(
"
renders user's name
"
,
()
=>
{
createComponent
();
expect
(
getByText
(
user
.
name
).
exists
()).
toBe
(
true
);
});
it
(
"
renders user's username
"
,
()
=>
{
createComponent
();
expect
(
getByText
(
`@
${
user
.
username
}
`
).
exists
()).
toBe
(
true
);
});
it
(
"
renders user's avatar
"
,
()
=>
{
createComponent
();
expect
(
wrapper
.
find
(
'
img
'
).
attributes
(
'
src
'
)).
toBe
(
user
.
avatarUrl
);
});
describe
(
'
when user property does not exist
'
,
()
=>
{
it
(
'
displays an orphaned user
'
,
()
=>
{
createComponent
({
member
:
orphanedMember
});
expect
(
getByText
(
'
Orphaned member
'
).
exists
()).
toBe
(
true
);
});
});
});
spec/frontend/vue_shared/components/members/mock_data.js
0 → 100644
View file @
26940f36
export
const
member
=
{
requestedAt
:
null
,
canUpdate
:
false
,
canRemove
:
false
,
canOverride
:
false
,
accessLevel
:
{
integerValue
:
50
,
stringValue
:
'
Owner
'
},
source
:
{
id
:
178
,
name
:
'
Foo Bar
'
,
webUrl
:
'
https://gitlab.com/groups/foo-bar
'
,
},
user
:
{
id
:
123
,
name
:
'
Administrator
'
,
username
:
'
root
'
,
webUrl
:
'
https://gitlab.com/root
'
,
avatarUrl
:
'
https://www.gravatar.com/avatar/4816142ef496f956a277bedf1a40607b?s=80&d=identicon
'
,
blocked
:
false
,
twoFactorEnabled
:
false
,
},
id
:
238
,
createdAt
:
'
2020-07-17T16:22:46.923Z
'
,
expiresAt
:
null
,
usingLicense
:
false
,
groupSso
:
false
,
groupManagedAccount
:
false
,
};
export
const
group
=
{
accessLevel
:
{
integerValue
:
10
,
stringValue
:
'
Guest
'
},
sharedWithGroup
:
{
id
:
24
,
name
:
'
Commit451
'
,
avatarUrl
:
'
/uploads/-/system/user/avatar/1/avatar.png?width=40
'
,
fullPath
:
'
parent-group/commit451
'
,
fullName
:
'
Parent group / Commit451
'
,
webUrl
:
'
https://gitlab.com/groups/parent-group/commit451
'
,
},
id
:
3
,
createdAt
:
'
2020-08-06T15:31:07.662Z
'
,
expiresAt
:
null
,
};
const
{
user
,
...
memberNoUser
}
=
member
;
export
const
invite
=
{
...
memberNoUser
,
invite
:
{
email
:
'
jewel@hudsonwalter.biz
'
,
avatarUrl
:
'
https://www.gravatar.com/avatar/cbab7510da7eec2f60f638261b05436d?s=80&d=identicon
'
,
canResend
:
true
,
},
};
export
const
orphanedMember
=
memberNoUser
;
export
const
accessRequest
=
{
...
member
,
requestedAt
:
'
2020-07-17T16:22:46.923Z
'
,
};
export
const
members
=
[
member
];
spec/frontend/vue_shared/components/members/table/member_avatar_spec.js
0 → 100644
View file @
26940f36
import
{
shallowMount
}
from
'
@vue/test-utils
'
;
import
{
MEMBER_TYPES
}
from
'
~/vue_shared/components/members/constants
'
;
import
{
member
as
memberMock
,
group
,
invite
,
accessRequest
}
from
'
../mock_data
'
;
import
MemberAvatar
from
'
~/vue_shared/components/members/table/member_avatar.vue
'
;
import
UserAvatar
from
'
~/vue_shared/components/members/avatars/user_avatar.vue
'
;
import
GroupAvatar
from
'
~/vue_shared/components/members/avatars/group_avatar.vue
'
;
import
InviteAvatar
from
'
~/vue_shared/components/members/avatars/invite_avatar.vue
'
;
describe
(
'
MemberList
'
,
()
=>
{
let
wrapper
;
const
createComponent
=
propsData
=>
{
wrapper
=
shallowMount
(
MemberAvatar
,
{
propsData
,
});
};
afterEach
(()
=>
{
wrapper
.
destroy
();
});
test
.
each
`
memberType | member | expectedComponent | expectedComponentName
${
MEMBER_TYPES
.
user
}
|
${
memberMock
}
|
${
UserAvatar
}
|
${
'
UserAvatar
'
}
${
MEMBER_TYPES
.
group
}
|
${
group
}
|
${
GroupAvatar
}
|
${
'
GroupAvatar
'
}
${
MEMBER_TYPES
.
invite
}
|
${
invite
}
|
${
InviteAvatar
}
|
${
'
InviteAvatar
'
}
${
MEMBER_TYPES
.
accessRequest
}
|
${
accessRequest
}
|
${
UserAvatar
}
|
${
'
UserAvatar
'
}
`
(
'
renders $expectedComponentName when `memberType` is $memberType
'
,
({
memberType
,
member
,
expectedComponent
})
=>
{
createComponent
({
memberType
,
member
});
expect
(
wrapper
.
find
(
expectedComponent
).
exists
()).
toBe
(
true
);
},
);
});
spec/frontend/vue_shared/components/members/table/member_table_cell_spec.js
0 → 100644
View file @
26940f36
import
{
mount
,
createLocalVue
}
from
'
@vue/test-utils
'
;
import
{
MEMBER_TYPES
}
from
'
~/vue_shared/components/members/constants
'
;
import
{
member
as
memberMock
,
group
,
invite
,
accessRequest
}
from
'
../mock_data
'
;
import
MembersTableCell
from
'
~/vue_shared/components/members/table/members_table_cell.vue
'
;
describe
(
'
MemberList
'
,
()
=>
{
const
WrappedComponent
=
{
props
:
{
memberType
:
{
type
:
String
,
required
:
true
,
},
},
render
(
createElement
)
{
return
createElement
(
'
div
'
,
this
.
memberType
);
},
};
const
localVue
=
createLocalVue
();
localVue
.
component
(
'
wrapped-component
'
,
WrappedComponent
);
let
wrapper
;
const
createComponent
=
propsData
=>
{
wrapper
=
mount
(
MembersTableCell
,
{
localVue
,
propsData
,
scopedSlots
:
{
default
:
'
<wrapped-component :member-type="props.memberType" />
'
,
},
});
};
afterEach
(()
=>
{
wrapper
.
destroy
();
wrapper
=
null
;
});
test
.
each
`
member | expectedMemberType
${
memberMock
}
|
${
MEMBER_TYPES
.
user
}
${
group
}
|
${
MEMBER_TYPES
.
group
}
${
invite
}
|
${
MEMBER_TYPES
.
invite
}
${
accessRequest
}
|
${
MEMBER_TYPES
.
accessRequest
}
`
(
'
sets scoped slot prop `memberType` to $expectedMemberType
'
,
({
member
,
expectedMemberType
})
=>
{
createComponent
({
member
});
expect
(
wrapper
.
find
(
WrappedComponent
).
props
(
'
memberType
'
)).
toBe
(
expectedMemberType
);
},
);
});
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment