Merge pull request #5335 from Ocelot-Social-Community/5059-groups/5318-group-profile-members-list-etc

feat: 🍰 Group Profile Members List Etc
This commit is contained in:
Wolfgang Huß 2022-09-15 11:50:22 +02:00 committed by GitHub
commit 6f59ced9eb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 1023 additions and 281 deletions

View File

@ -106,6 +106,17 @@ export const joinGroupMutation = gql`
}
`
export const leaveGroupMutation = gql`
mutation ($groupId: ID!, $userId: ID!) {
LeaveGroup(groupId: $groupId, userId: $userId) {
id
name
slug
myRoleInGroup
}
}
`
export const changeGroupMemberRoleMutation = gql`
mutation ($groupId: ID!, $userId: ID!, $roleInGroup: GroupMemberRole!) {
ChangeGroupMemberRole(groupId: $groupId, userId: $userId, roleInGroup: $roleInGroup) {
@ -149,8 +160,8 @@ export const groupQuery = gql`
`
export const groupMembersQuery = gql`
query ($id: ID!, $first: Int, $offset: Int, $orderBy: [_UserOrdering], $filter: _UserFilter) {
GroupMembers(id: $id, first: $first, offset: $offset, orderBy: $orderBy, filter: $filter) {
query ($id: ID!) {
GroupMembers(id: $id) {
id
name
slug

View File

@ -191,6 +191,36 @@ const isAllowedToJoinGroup = rule({
}
})
const isAllowedToLeaveGroup = rule({
cache: 'no_cache',
})(async (_parent, args, { user, driver }) => {
if (!(user && user.id)) return false
const { groupId, userId } = args
if (user.id !== userId) return false
const session = driver.session()
const readTxPromise = session.readTransaction(async (transaction) => {
const transactionResponse = await transaction.run(
`
MATCH (member:User {id: $userId})-[membership:MEMBER_OF]->(group:Group {id: $groupId})
RETURN group {.*}, member {.*, myRoleInGroup: membership.role}
`,
{ groupId, userId },
)
return {
group: transactionResponse.records.map((record) => record.get('group'))[0],
member: transactionResponse.records.map((record) => record.get('member'))[0],
}
})
try {
const { group, member } = await readTxPromise
return !!group && !!member && !!member.myRoleInGroup && member.myRoleInGroup !== 'owner'
} catch (error) {
throw new Error(error)
} finally {
session.close()
}
})
const isAuthor = rule({
cache: 'no_cache',
})(async (_parent, args, { user, driver }) => {
@ -284,6 +314,7 @@ export default shield(
CreateGroup: isAuthenticated,
UpdateGroup: isAllowedToChangeGroupSettings,
JoinGroup: isAllowedToJoinGroup,
LeaveGroup: isAllowedToLeaveGroup,
ChangeGroupMemberRole: isAllowedToChangeGroupMemberRole,
CreatePost: isAuthenticated,
UpdatePost: isAuthor,

View File

@ -244,6 +244,27 @@ export default {
session.close()
}
},
LeaveGroup: async (_parent, params, context, _resolveInfo) => {
const { groupId, userId } = params
const session = context.driver.session()
const writeTxResultPromise = session.writeTransaction(async (transaction) => {
const leaveGroupCypher = `
MATCH (member:User {id: $userId})-[membership:MEMBER_OF]->(group:Group {id: $groupId})
DELETE membership
RETURN member {.*, myRoleInGroup: NULL}
`
const transactionResponse = await transaction.run(leaveGroupCypher, { groupId, userId })
const [member] = await transactionResponse.records.map((record) => record.get('member'))
return member
})
try {
return await writeTxResultPromise
} catch (error) {
throw new Error(error)
} finally {
session.close()
}
},
ChangeGroupMemberRole: async (_parent, params, context, _resolveInfo) => {
const { groupId, userId, roleInGroup } = params
const session = context.driver.session()

View File

@ -4,6 +4,7 @@ import {
createGroupMutation,
updateGroupMutation,
joinGroupMutation,
leaveGroupMutation,
changeGroupMemberRoleMutation,
groupMembersQuery,
groupQuery,
@ -17,6 +18,12 @@ const neode = getNeode()
let authenticatedUser
let user
let noMemberUser
let pendingMemberUser
let usualMemberUser
let adminMemberUser
let ownerMemberUser
let secondOwnerMemberUser
const categoryIds = ['cat9', 'cat4', 'cat15']
const descriptionAdditional100 =
@ -76,6 +83,169 @@ const seedBasicsAndClearAuthentication = async () => {
authenticatedUser = null
}
const seedComplexScenarioAndClearAuthentication = async () => {
await seedBasicsAndClearAuthentication()
// create users
noMemberUser = await Factory.build(
'user',
{
id: 'none-member-user',
name: 'None Member TestUser',
},
{
email: 'none-member-user@example.org',
password: '1234',
},
)
pendingMemberUser = await Factory.build(
'user',
{
id: 'pending-member-user',
name: 'Pending Member TestUser',
},
{
email: 'pending-member-user@example.org',
password: '1234',
},
)
usualMemberUser = await Factory.build(
'user',
{
id: 'usual-member-user',
name: 'Usual Member TestUser',
},
{
email: 'usual-member-user@example.org',
password: '1234',
},
)
adminMemberUser = await Factory.build(
'user',
{
id: 'admin-member-user',
name: 'Admin Member TestUser',
},
{
email: 'admin-member-user@example.org',
password: '1234',
},
)
ownerMemberUser = await Factory.build(
'user',
{
id: 'owner-member-user',
name: 'Owner Member TestUser',
},
{
email: 'owner-member-user@example.org',
password: '1234',
},
)
secondOwnerMemberUser = await Factory.build(
'user',
{
id: 'second-owner-member-user',
name: 'Second Owner Member TestUser',
},
{
email: 'second-owner-member-user@example.org',
password: '1234',
},
)
// create groups
// public-group
authenticatedUser = await usualMemberUser.toJson()
await mutate({
mutation: createGroupMutation,
variables: {
id: 'public-group',
name: 'The Best Group',
about: 'We will change the world!',
description: 'Some description' + descriptionAdditional100,
groupType: 'public',
actionRadius: 'regional',
categoryIds,
},
})
await mutate({
mutation: joinGroupMutation,
variables: {
groupId: 'public-group',
userId: 'owner-of-closed-group',
},
})
await mutate({
mutation: joinGroupMutation,
variables: {
groupId: 'public-group',
userId: 'owner-of-hidden-group',
},
})
// closed-group
authenticatedUser = await ownerMemberUser.toJson()
await mutate({
mutation: createGroupMutation,
variables: {
id: 'closed-group',
name: 'Uninteresting Group',
about: 'We will change nothing!',
description: 'We love it like it is!?' + descriptionAdditional100,
groupType: 'closed',
actionRadius: 'national',
categoryIds,
},
})
// hidden-group
authenticatedUser = await adminMemberUser.toJson()
await mutate({
mutation: createGroupMutation,
variables: {
id: 'hidden-group',
name: 'Investigative Journalism Group',
about: 'We will change all.',
description: 'We research …' + descriptionAdditional100,
groupType: 'hidden',
actionRadius: 'global',
categoryIds,
},
})
// 'JoinGroup' mutation does not work in hidden groups so we join them by 'ChangeGroupMemberRole' through the owner
await mutate({
mutation: changeGroupMemberRoleMutation,
variables: {
groupId: 'hidden-group',
userId: 'admin-member-user',
roleInGroup: 'usual',
},
})
await mutate({
mutation: changeGroupMemberRoleMutation,
variables: {
groupId: 'hidden-group',
userId: 'second-owner-member-user',
roleInGroup: 'usual',
},
})
await mutate({
mutation: changeGroupMemberRoleMutation,
variables: {
groupId: 'hidden-group',
userId: 'admin-member-user',
roleInGroup: 'usual',
},
})
await mutate({
mutation: changeGroupMemberRoleMutation,
variables: {
groupId: 'hidden-group',
userId: 'second-owner-member-user',
roleInGroup: 'usual',
},
})
authenticatedUser = null
}
beforeAll(async () => {
await cleanDatabase()
})
@ -1032,8 +1202,8 @@ describe('in mode', () => {
})
it('finds all members', async () => {
const result = await mutate({
mutation: groupMembersQuery,
const result = await query({
query: groupMembersQuery,
variables,
})
expect(result).toMatchObject({
@ -1065,8 +1235,8 @@ describe('in mode', () => {
})
it('finds all members', async () => {
const result = await mutate({
mutation: groupMembersQuery,
const result = await query({
query: groupMembersQuery,
variables,
})
expect(result).toMatchObject({
@ -1098,8 +1268,8 @@ describe('in mode', () => {
})
it('finds all members', async () => {
const result = await mutate({
mutation: groupMembersQuery,
const result = await query({
query: groupMembersQuery,
variables,
})
expect(result).toMatchObject({
@ -1141,8 +1311,8 @@ describe('in mode', () => {
})
it('finds all members', async () => {
const result = await mutate({
mutation: groupMembersQuery,
const result = await query({
query: groupMembersQuery,
variables,
})
expect(result).toMatchObject({
@ -1174,8 +1344,8 @@ describe('in mode', () => {
})
it('finds all members', async () => {
const result = await mutate({
mutation: groupMembersQuery,
const result = await query({
query: groupMembersQuery,
variables,
})
expect(result).toMatchObject({
@ -1239,8 +1409,8 @@ describe('in mode', () => {
})
it('finds all members', async () => {
const result = await mutate({
mutation: groupMembersQuery,
const result = await query({
query: groupMembersQuery,
variables,
})
expect(result).toMatchObject({
@ -1276,8 +1446,8 @@ describe('in mode', () => {
})
it('finds all members', async () => {
const result = await mutate({
mutation: groupMembersQuery,
const result = await query({
query: groupMembersQuery,
variables,
})
expect(result).toMatchObject({
@ -1313,8 +1483,8 @@ describe('in mode', () => {
})
it('finds all members', async () => {
const result = await mutate({
mutation: groupMembersQuery,
const result = await query({
query: groupMembersQuery,
variables,
})
expect(result).toMatchObject({
@ -1371,162 +1541,8 @@ describe('in mode', () => {
})
describe('ChangeGroupMemberRole', () => {
let pendingMemberUser
let usualMemberUser
let adminMemberUser
let ownerMemberUser
let secondOwnerMemberUser
beforeAll(async () => {
await seedBasicsAndClearAuthentication()
// create users
pendingMemberUser = await Factory.build(
'user',
{
id: 'pending-member-user',
name: 'Pending Member TestUser',
},
{
email: 'pending-member-user@example.org',
password: '1234',
},
)
usualMemberUser = await Factory.build(
'user',
{
id: 'usual-member-user',
name: 'Usual Member TestUser',
},
{
email: 'usual-member-user@example.org',
password: '1234',
},
)
adminMemberUser = await Factory.build(
'user',
{
id: 'admin-member-user',
name: 'Admin Member TestUser',
},
{
email: 'admin-member-user@example.org',
password: '1234',
},
)
ownerMemberUser = await Factory.build(
'user',
{
id: 'owner-member-user',
name: 'Owner Member TestUser',
},
{
email: 'owner-member-user@example.org',
password: '1234',
},
)
secondOwnerMemberUser = await Factory.build(
'user',
{
id: 'second-owner-member-user',
name: 'Second Owner Member TestUser',
},
{
email: 'second-owner-member-user@example.org',
password: '1234',
},
)
// create groups
// public-group
authenticatedUser = await usualMemberUser.toJson()
await mutate({
mutation: createGroupMutation,
variables: {
id: 'public-group',
name: 'The Best Group',
about: 'We will change the world!',
description: 'Some description' + descriptionAdditional100,
groupType: 'public',
actionRadius: 'regional',
categoryIds,
},
})
await mutate({
mutation: joinGroupMutation,
variables: {
groupId: 'public-group',
userId: 'owner-of-closed-group',
},
})
await mutate({
mutation: joinGroupMutation,
variables: {
groupId: 'public-group',
userId: 'owner-of-hidden-group',
},
})
// closed-group
authenticatedUser = await ownerMemberUser.toJson()
await mutate({
mutation: createGroupMutation,
variables: {
id: 'closed-group',
name: 'Uninteresting Group',
about: 'We will change nothing!',
description: 'We love it like it is!?' + descriptionAdditional100,
groupType: 'closed',
actionRadius: 'national',
categoryIds,
},
})
// hidden-group
authenticatedUser = await adminMemberUser.toJson()
await mutate({
mutation: createGroupMutation,
variables: {
id: 'hidden-group',
name: 'Investigative Journalism Group',
about: 'We will change all.',
description: 'We research …' + descriptionAdditional100,
groupType: 'hidden',
actionRadius: 'global',
categoryIds,
},
})
// 'JoinGroup' mutation does not work in hidden groups so we join them by 'ChangeGroupMemberRole' through the owner
await mutate({
mutation: changeGroupMemberRoleMutation,
variables: {
groupId: 'hidden-group',
userId: 'admin-member-user',
roleInGroup: 'usual',
},
})
await mutate({
mutation: changeGroupMemberRoleMutation,
variables: {
groupId: 'hidden-group',
userId: 'second-owner-member-user',
roleInGroup: 'usual',
},
})
await mutate({
mutation: changeGroupMemberRoleMutation,
variables: {
groupId: 'hidden-group',
userId: 'admin-member-user',
roleInGroup: 'usual',
},
})
await mutate({
mutation: changeGroupMemberRoleMutation,
variables: {
groupId: 'hidden-group',
userId: 'second-owner-member-user',
roleInGroup: 'usual',
},
})
authenticatedUser = null
await seedComplexScenarioAndClearAuthentication()
})
afterAll(async () => {
@ -2330,6 +2346,241 @@ describe('in mode', () => {
})
})
describe('LeaveGroup', () => {
beforeAll(async () => {
await seedComplexScenarioAndClearAuthentication()
// closed-group
authenticatedUser = await ownerMemberUser.toJson()
await mutate({
mutation: changeGroupMemberRoleMutation,
variables: {
groupId: 'closed-group',
userId: 'pending-member-user',
roleInGroup: 'pending',
},
})
await mutate({
mutation: changeGroupMemberRoleMutation,
variables: {
groupId: 'closed-group',
userId: 'usual-member-user',
roleInGroup: 'usual',
},
})
await mutate({
mutation: changeGroupMemberRoleMutation,
variables: {
groupId: 'closed-group',
userId: 'admin-member-user',
roleInGroup: 'admin',
},
})
await mutate({
mutation: changeGroupMemberRoleMutation,
variables: {
groupId: 'closed-group',
userId: 'second-owner-member-user',
roleInGroup: 'owner',
},
})
authenticatedUser = null
})
afterAll(async () => {
await cleanDatabase()
})
describe('unauthenticated', () => {
it('throws authorization error', async () => {
const { errors } = await mutate({
mutation: leaveGroupMutation,
variables: {
groupId: 'not-existing-group',
userId: 'current-user',
},
})
expect(errors[0]).toHaveProperty('message', 'Not Authorized!')
})
})
describe('authenticated', () => {
describe('in all group types', () => {
describe('here "closed-group" for example', () => {
const memberInGroup = async (userId, groupId) => {
const result = await query({
query: groupMembersQuery,
variables: {
id: groupId,
},
})
return result.data && result.data.GroupMembers
? !!result.data.GroupMembers.find((member) => member.id === userId)
: null
}
beforeEach(async () => {
authenticatedUser = null
variables = {
groupId: 'closed-group',
}
})
describe('left by "pending-member-user"', () => {
it('has "null" as membership role, was in the group, and left the group', async () => {
authenticatedUser = await ownerMemberUser.toJson()
expect(await memberInGroup('pending-member-user', 'closed-group')).toBe(true)
authenticatedUser = await pendingMemberUser.toJson()
await expect(
mutate({
mutation: leaveGroupMutation,
variables: {
...variables,
userId: 'pending-member-user',
},
}),
).resolves.toMatchObject({
data: {
LeaveGroup: {
id: 'pending-member-user',
myRoleInGroup: null,
},
},
errors: undefined,
})
authenticatedUser = await ownerMemberUser.toJson()
expect(await memberInGroup('pending-member-user', 'closed-group')).toBe(false)
})
})
describe('left by "usual-member-user"', () => {
it('has "null" as membership role, was in the group, and left the group', async () => {
authenticatedUser = await ownerMemberUser.toJson()
expect(await memberInGroup('usual-member-user', 'closed-group')).toBe(true)
authenticatedUser = await usualMemberUser.toJson()
await expect(
mutate({
mutation: leaveGroupMutation,
variables: {
...variables,
userId: 'usual-member-user',
},
}),
).resolves.toMatchObject({
data: {
LeaveGroup: {
id: 'usual-member-user',
myRoleInGroup: null,
},
},
errors: undefined,
})
authenticatedUser = await ownerMemberUser.toJson()
expect(await memberInGroup('usual-member-user', 'closed-group')).toBe(false)
})
})
describe('left by "admin-member-user"', () => {
it('has "null" as membership role, was in the group, and left the group', async () => {
authenticatedUser = await ownerMemberUser.toJson()
expect(await memberInGroup('admin-member-user', 'closed-group')).toBe(true)
authenticatedUser = await adminMemberUser.toJson()
await expect(
mutate({
mutation: leaveGroupMutation,
variables: {
...variables,
userId: 'admin-member-user',
},
}),
).resolves.toMatchObject({
data: {
LeaveGroup: {
id: 'admin-member-user',
myRoleInGroup: null,
},
},
errors: undefined,
})
authenticatedUser = await ownerMemberUser.toJson()
expect(await memberInGroup('admin-member-user', 'closed-group')).toBe(false)
})
})
describe('left by "owner-member-user"', () => {
it('throws authorization error', async () => {
authenticatedUser = await ownerMemberUser.toJson()
const { errors } = await mutate({
mutation: leaveGroupMutation,
variables: {
...variables,
userId: 'owner-member-user',
},
})
expect(errors[0]).toHaveProperty('message', 'Not Authorized!')
})
})
describe('left by "second-owner-member-user"', () => {
it('throws authorization error', async () => {
authenticatedUser = await secondOwnerMemberUser.toJson()
const { errors } = await mutate({
mutation: leaveGroupMutation,
variables: {
...variables,
userId: 'second-owner-member-user',
},
})
expect(errors[0]).toHaveProperty('message', 'Not Authorized!')
})
})
describe('left by "none-member-user"', () => {
it('throws authorization error', async () => {
authenticatedUser = await noMemberUser.toJson()
const { errors } = await mutate({
mutation: leaveGroupMutation,
variables: {
...variables,
userId: 'none-member-user',
},
})
expect(errors[0]).toHaveProperty('message', 'Not Authorized!')
})
})
describe('as "owner-member-user" try to leave member "usual-member-user"', () => {
it('throws authorization error', async () => {
authenticatedUser = await ownerMemberUser.toJson()
const { errors } = await mutate({
mutation: leaveGroupMutation,
variables: {
...variables,
userId: 'usual-member-user',
},
})
expect(errors[0]).toHaveProperty('message', 'Not Authorized!')
})
})
describe('as "usual-member-user" try to leave member "admin-member-user"', () => {
it('throws authorization error', async () => {
authenticatedUser = await usualMemberUser.toJson()
const { errors } = await mutate({
mutation: leaveGroupMutation,
variables: {
...variables,
userId: 'admin-member-user',
},
})
expect(errors[0]).toHaveProperty('message', 'Not Authorized!')
})
})
})
})
})
})
describe('UpdateGroup', () => {
beforeAll(async () => {
await seedBasicsAndClearAuthentication()

View File

@ -117,6 +117,11 @@ type Mutation {
userId: ID!
): User
LeaveGroup(
groupId: ID!
userId: ID!
): User
ChangeGroupMemberRole(
groupId: ID!
userId: ID!

View File

@ -19,7 +19,6 @@ import { followUserMutation, unfollowUserMutation } from '~/graphql/User'
export default {
name: 'HcFollowButton',
props: {
followId: { type: String, default: null },
isFollowed: { type: Boolean, default: false },

View File

@ -0,0 +1,136 @@
<template>
<base-button
class="track-button"
:disabled="disabled"
:loading="localLoading"
:icon="icon"
:filled="isMember && !hovered"
:danger="isMember && hovered"
@mouseenter.native="onHover"
@mouseleave.native="hovered = false"
@click.prevent="toggle"
>
{{ label }}
</base-button>
</template>
<script>
import { mapMutations } from 'vuex'
import { joinGroupMutation, leaveGroupMutation } from '~/graphql/groups'
export default {
name: 'JoinLeaveButton',
props: {
group: { type: Object, required: true },
userId: { type: String, required: true },
isMember: { type: Boolean, required: true },
disabled: { type: Boolean, default: false },
loading: { type: Boolean, default: false },
},
data() {
return {
localLoading: this.loading,
hovered: false,
}
},
computed: {
icon() {
if (this.isMember && this.hovered) {
return 'close'
} else {
return this.isMember ? 'check' : 'plus'
}
},
label() {
if (this.isMember) {
return this.$t('group.joinLeaveButton.iAmMember')
} else {
return this.$t('group.joinLeaveButton.join')
}
},
},
watch: {
isMember() {
this.localLoading = false
this.hovered = false
},
loading() {
this.localLoading = this.loading
},
},
methods: {
...mapMutations({
commitModalData: 'modal/SET_OPEN',
}),
onHover() {
if (!this.disabled && !this.localLoading) {
this.hovered = true
}
},
toggle() {
if (this.isMember) {
this.openLeaveModal()
} else {
this.joinLeave()
}
},
openLeaveModal() {
this.commitModalData(this.leaveModalData())
},
leaveModalData() {
return {
name: 'confirm',
data: {
type: '',
resource: { id: '' },
modalData: {
titleIdent: 'group.leaveModal.title',
messageIdent: 'group.leaveModal.message',
messageParams: {
name: this.group.name,
},
buttons: {
confirm: {
danger: true,
icon: 'sign-out',
textIdent: 'group.leaveModal.confirmButton',
callback: this.joinLeave,
},
cancel: {
icon: 'close',
textIdent: 'actions.cancel',
callback: () => {},
},
},
},
},
}
},
async joinLeave() {
const join = !this.isMember
const mutation = join ? joinGroupMutation : leaveGroupMutation
this.hovered = false
this.$emit('prepare', join)
try {
const { data } = await this.$apollo.mutate({
mutation,
variables: { groupId: this.group.id, userId: this.userId },
})
const joinedLeftGroupResult = join ? data.JoinGroup : data.LeaveGroup
this.$emit('update', joinedLeftGroupResult)
} catch (error) {
this.$toast.error(error.message)
}
},
},
}
</script>
<style lang="scss">
.track-button {
display: block;
width: 100%;
}
</style>

View File

@ -0,0 +1,41 @@
<template>
<profile-list
:uniqueName="`${type}Filter`"
:title="$filters.truncate(userName, 15) + ' ' + $t(`profile.network.${type}`)"
:titleNobody="$filters.truncate(userName, 15) + ' ' + $t(`profile.network.${type}Nobody`)"
:allProfilesCount="allConnectionsCount"
:profiles="connections"
:loading="loading"
@fetchAllProfiles="$emit('fetchAllConnections', type)"
/>
</template>
<script>
import ProfileList, { profileListVisibleCount } from '~/components/features/ProfileList/ProfileList'
export const followListVisibleCount = profileListVisibleCount
export default {
name: 'FollowerList',
components: {
ProfileList,
},
props: {
user: { type: Object, default: null },
type: { type: String, default: 'following' },
loading: { type: Boolean, default: false },
},
computed: {
userName() {
const { name } = this.user || {}
return name || this.$t('profile.userAnonym')
},
allConnectionsCount() {
return this.user[`${this.type}Count`]
},
connections() {
return this.user[this.type]
},
},
}
</script>

View File

@ -1,10 +1,10 @@
<template>
<base-card class="follow-list">
<template v-if="connections && connections.length">
<base-card class="profile-list">
<template v-if="profiles.length">
<h5 class="title spacer-x-small">
{{ userName | truncate(15) }} {{ $t(`profile.network.${type}`) }}
{{ title }}
</h5>
<ul :class="connectionsClass">
<ul :class="profilesClass">
<li
v-for="connection in filteredConnections"
:key="connection.id"
@ -13,30 +13,30 @@
<user-teaser :user="connection" />
</li>
</ul>
<base-button
v-if="hasMore"
:loading="loading"
class="spacer-x-small"
size="small"
@click="$emit('fetchAllConnections', type)"
>
{{
$t('profile.network.andMore', {
number: allConnectionsCount - connections.length,
})
}}
</base-button>
<ds-input
v-if="!hasMore"
:name="`${type}Filter`"
v-if="isMoreAsVisible"
:name="uniqueName"
:placeholder="filter"
class="spacer-x-small"
icon="filter"
size="small"
@input.native="setFilter"
/>
<base-button
v-if="hasMore"
:loading="loading"
class="spacer-x-small"
size="small"
@click="$emit('fetchAllProfiles')"
>
{{
$t('profile.network.andMore', {
number: allProfilesCount - profiles.length,
})
}}
</base-button>
</template>
<p v-else class="nobody-message">{{ userName }} {{ $t(`profile.network.${type}Nobody`) }}</p>
<p v-else-if="titleNobody" class="nobody-message">{{ titleNobody }}</p>
</base-card>
</template>
@ -44,41 +44,40 @@
import { escape } from 'xregexp/xregexp-all.js'
import UserTeaser from '~/components/UserTeaser/UserTeaser'
export const profileListVisibleCount = 7
export default {
name: 'FollowerList',
name: 'ProfileList',
components: {
UserTeaser,
},
props: {
user: { type: Object, default: null },
type: { type: String, default: 'following' },
uniqueName: { type: String, required: true },
title: { type: String, required: true },
titleNobody: { type: String, default: null },
allProfilesCount: { type: Number, required: true },
profiles: { type: Array, required: true },
loading: { type: Boolean, default: false },
},
data() {
return {
profileListVisibleCount,
filter: null,
}
},
computed: {
userName() {
const { name } = this.user || {}
return name || this.$t('profile.userAnonym')
},
allConnectionsCount() {
return this.user[`${this.type}Count`]
},
connections() {
return this.user[this.type]
},
hasMore() {
return this.allConnectionsCount > this.connections.length
return this.allProfilesCount > this.profiles.length
},
connectionsClass() {
return `connections${this.hasMore ? '' : ' --overflow'}`
isMoreAsVisible() {
return this.profiles.length > this.profileListVisibleCount
},
profilesClass() {
return `profiles${this.isMoreAsVisible ? ' --overflow' : ''}`
},
filteredConnections() {
if (!this.filter) {
return this.connections
return this.profiles
}
// @example
@ -89,7 +88,7 @@ export default {
'i',
)
const fuzzyScores = this.connections
const fuzzyScores = this.profiles
.map((user) => {
const match = user.name.match(fuzzyExpression)
@ -122,7 +121,7 @@ export default {
</script>
<style lang="scss">
.follow-list {
.profile-list {
display: flex;
flex-direction: column;
position: relative;
@ -133,7 +132,7 @@ export default {
font-size: $font-size-base;
}
.connections {
.profiles {
height: $size-height-connections;
padding: $space-none;
list-style-type: none;

View File

@ -106,6 +106,17 @@ export const joinGroupMutation = gql`
}
`
export const leaveGroupMutation = gql`
mutation ($groupId: ID!, $userId: ID!) {
LeaveGroup(groupId: $groupId, userId: $userId) {
id
name
slug
myRoleInGroup
}
}
`
export const changeGroupMemberRoleMutation = gql`
mutation ($groupId: ID!, $userId: ID!, $roleInGroup: GroupMemberRole!) {
ChangeGroupMemberRole(groupId: $groupId, userId: $userId, roleInGroup: $roleInGroup) {
@ -149,8 +160,8 @@ export const groupQuery = gql`
`
export const groupMembersQuery = gql`
query ($id: ID!, $first: Int, $offset: Int, $orderBy: [_UserOrdering], $filter: _UserFilter) {
GroupMembers(id: $id, first: $first, offset: $offset, orderBy: $orderBy, filter: $filter) {
query ($id: ID!) {
GroupMembers(id: $id) {
id
name
slug

View File

@ -375,10 +375,44 @@
"following": "Folge Ich"
},
"group": {
"actionRadii": {
"continental": "Kontinentale Gruppe",
"global": "Globale Gruppe",
"interplanetary": "Interplanetare Gruppe",
"national": "Nationale Gruppe",
"regional": "Regionale Gruppe"
},
"actionRadius": "Aktionsradius",
"foundation": "Gründung",
"goal": "Ziel der Gruppe",
"group-created": "Die Gruppe wurde angelegt!",
"group-updated": "Die Gruppendaten wurden geändert!",
"joinLeaveButton": {
"iAmMember": "Bin Mitglied",
"join": "Beitreten"
},
"leaveModal": {
"confirmButton": "Verlassen",
"message": "Eine Gruppe zu verlassen ist möglicherweise nicht rückgängig zu machen!<br>Gruppe <b>„{name}“</b> verlassen!",
"title": "Möchtest du wirklich die Gruppe verlassen?"
},
"membersCount": "Mitglieder",
"membersListTitle": "Gruppenmitglieder",
"newGroup": "Erstelle eine neue Gruppe",
"role": "Deine Rolle in der Gruppe",
"roles": {
"admin": "Administrator",
"owner": "Inhaber",
"pending": "Ausstehendes Mitglied",
"usual": "Einfaches Mitglied"
},
"save": "Neue Gruppe anlegen",
"type": "Gruppentyp",
"types": {
"closed": "Geschlossene Gruppe",
"hidden": "Versteckte Gruppe",
"public": "Öffentliche Gruppe"
},
"update": "Änderung speichern"
},
"hashtags-filter": {
@ -538,8 +572,6 @@
"follow": "Folgen",
"followers": "Folgen",
"following": "Folge Ich",
"groupGoal": "Ziel:",
"groupSince": "Gründung",
"invites": {
"description": "Zur Einladung die E-Mail-Adresse hier eintragen.",
"emailPlaceholder": "E-Mail-Adresse für die Einladung",

View File

@ -375,10 +375,44 @@
"following": "Following"
},
"group": {
"actionRadii": {
"continental": "Continental Group",
"global": "Global Group",
"interplanetary": "Interplanetary Group",
"national": "National Group",
"regional": "Regional Group"
},
"actionRadius": "Action radius",
"foundation": "Foundation",
"goal": "Goal of group",
"group-created": "The group was created!",
"group-updated": "The group data has been changed.",
"joinLeaveButton": {
"iAmMember": "I'm a member",
"join": "Join"
},
"leaveModal": {
"confirmButton": "Leave",
"message": "Leaving a group may be irreversible!<br>Leave group <b>“{name}”</b>!",
"title": "Do you really want to leave the group?"
},
"membersCount": "Members",
"membersListTitle": "Group Members",
"newGroup": "Create a new Group",
"role": "Your role in the group",
"roles": {
"admin": "Administrator",
"owner": "Owner",
"pending": "Pending Member",
"usual": "Simple Member"
},
"save": "Create new group",
"type": "Group type",
"types": {
"closed": "Closed Group",
"hidden": "Hidden Group",
"public": "Public Group"
},
"update": "Save change"
},
"hashtags-filter": {
@ -538,8 +572,6 @@
"follow": "Follow",
"followers": "Followers",
"following": "Following",
"groupGoal": "Goal:",
"groupSince": "Foundation",
"invites": {
"description": "Enter their e-mail address for invitation.",
"emailPlaceholder": "E-mail to invite",

View File

@ -297,6 +297,16 @@
"follow": "Seguir",
"following": "Siguiendo"
},
"group": {
"foundation": null,
"goal": null,
"joinLeaveButton": {
"iAmMember": null,
"join": null
},
"membersCount": null,
"membersListTitle": null
},
"hashtags-filter": {
"clearSearch": "Borrar búsqueda",
"hashtag-search": "Buscando a #{hashtag}",

View File

@ -286,6 +286,16 @@
"follow": "Suivre",
"following": "Je suis les"
},
"group": {
"foundation": null,
"goal": null,
"joinLeaveButton": {
"iAmMember": null,
"join": null
},
"membersCount": null,
"membersListTitle": null
},
"hashtags-filter": {
"clearSearch": "Réinitialiser la recherche",
"hashtag-search": "Recherche de #{hashtag}",

View File

@ -294,6 +294,16 @@
"follow": null,
"following": null
},
"group": {
"foundation": null,
"goal": null,
"joinLeaveButton": {
"iAmMember": null,
"join": null
},
"membersCount": null,
"membersListTitle": null
},
"hashtags-filter": {
"clearSearch": null,
"hashtag-search": null,

View File

@ -82,6 +82,16 @@
"follow": "Volgen",
"following": "Volgt"
},
"group": {
"foundation": null,
"goal": null,
"joinLeaveButton": {
"iAmMember": null,
"join": null
},
"membersCount": null,
"membersListTitle": null
},
"login": {
"email": "Uw E-mail",
"hello": "Hallo",

View File

@ -166,6 +166,16 @@
"follow": "naśladować",
"following": "w skutek"
},
"group": {
"foundation": null,
"goal": null,
"joinLeaveButton": {
"iAmMember": null,
"join": null
},
"membersCount": null,
"membersListTitle": null
},
"hashtags-filter": {
"title": "Twoja bańka filtrująca"
},

View File

@ -332,6 +332,16 @@
"follow": "Seguir",
"following": "Seguindo"
},
"group": {
"foundation": null,
"goal": null,
"joinLeaveButton": {
"iAmMember": null,
"join": null
},
"membersCount": null,
"membersListTitle": null
},
"hashtags-filter": {
"clearSearch": "Limpar pesquisa",
"hashtag-search": "Procurando por #{hashtag}",

View File

@ -311,6 +311,16 @@
"follow": "Подписаться",
"following": "Вы подписаны"
},
"group": {
"foundation": null,
"goal": null,
"joinLeaveButton": {
"iAmMember": null,
"join": null
},
"membersCount": null,
"membersListTitle": null
},
"hashtags-filter": {
"clearSearch": "Очистить поиск",
"hashtag-search": "Поиск по #{hashtag}",

View File

@ -42,50 +42,98 @@
{{ user.location.name }}
</ds-text> -->
<ds-text align="center" color="soft" size="small">
{{ $t('profile.groupSince') }} {{ group.createdAt | date('MMMM yyyy') }}
{{ $t('group.foundation') }} {{ group.createdAt | date('MMMM yyyy') }}
</ds-text>
</ds-space>
<ds-flex>
<ds-flex-item>
<!-- <client-only>
<client-only>
<ds-number :label="$t('group.membersCount')">
<count-to
slot="count"
:start-val="membersCountStartValue"
:end-val="groupMembers.length"
/>
</ds-number>
</client-only>
</ds-flex-item>
<!-- <ds-flex-item>
<client-only>
<ds-number :label="$t('profile.followers')">
<hc-count-to
<count-to
slot="count"
:start-val="followedByCountStartValue"
:end-val="user.followedByCount"
/>
</ds-number>
</client-only> -->
</ds-flex-item>
<ds-flex-item>
<!-- <client-only>
</client-only>
</ds-flex-item> -->
<!-- <ds-flex-item>
<client-only>
<ds-number :label="$t('profile.following')">
<hc-count-to slot="count" :end-val="user.followingCount" />
<count-to slot="count" :end-val="user.followingCount" />
</ds-number>
</client-only> -->
</ds-flex-item>
</client-only>
</ds-flex-item> -->
</ds-flex>
<div v-if="!isGroupMember" class="action-buttons">
<div class="action-buttons">
<!-- <base-button v-if="user.isBlocked" @click="unblockUser(user)">
{{ $t('settings.blocked-users.unblock') }}
</base-button>
<base-button v-if="user.isMuted" @click="unmuteUser(user)">
{{ $t('settings.muted-users.unmute') }}
</base-button>
<hc-follow-button
<follow-button
v-if="!user.isMuted && !user.isBlocked"
:follow-id="user.id"
:is-followed="user.followedByCurrentUser"
@optimistic="optimisticFollow"
@update="updateFollow"
/> -->
<join-leave-button
:group="group || {}"
:userId="currentUser.id"
:isMember="isGroupMember"
:disabled="isGroupOwner"
:loading="$apollo.loading"
@prepare="prepareJoinLeave"
@update="updateJoinLeave"
/>
<!-- implement:
v-if="!user.isMuted && !user.isBlocked" -->
</div>
<hr />
<ds-space margin-top="small" margin-bottom="small">
<template v-if="isGroupMember">
<ds-text class="centered-text hyphenate-text" color="soft" size="small">
{{ $t('group.role') }}
</ds-text>
<div class="chip" align="center">
<ds-chip color="primary">{{ $t('group.roles.' + group.myRole) }}</ds-chip>
</div>
</template>
<ds-text class="centered-text hyphenate-text" color="soft" size="small">
{{ $t('group.type') }}
</ds-text>
<div class="chip" align="center">
<ds-chip color="primary">{{ $t('group.types.' + group.groupType) }}</ds-chip>
</div>
<ds-text class="centered-text hyphenate-text" color="soft" size="small">
{{ $t('group.actionRadius') }}
</ds-text>
<div class="chip" align="center">
<ds-chip color="primary">{{ $t('group.actionRadii.' + group.actionRadius) }}</ds-chip>
</div>
</ds-space>
<template v-if="group.about">
<hr />
<ds-space margin-top="small" margin-bottom="small">
<ds-text color="soft" size="small" class="hyphenate-text">
{{ $t('profile.groupGoal') }} {{ group.about }}
<ds-text class="centered-text hyphenate-text" color="soft" size="small">
{{ $t('group.goal') }}
</ds-text>
<div class="chip" align="center">
<ds-chip>{{ group.about }}</ds-chip>
</div>
</ds-space>
</template>
</base-card>
@ -93,7 +141,17 @@
<ds-heading tag="h3" soft style="text-align: center; margin-bottom: 10px">
{{ $t('profile.network.title') }}
</ds-heading>
<!-- <follow-list
<!-- Group members list -->
<profile-list
:uniqueName="`groupMembersFilter`"
:title="$t('group.membersListTitle')"
:allProfilesCount="groupMembers.length"
:profiles="groupMembers"
:loading="$apollo.loading"
@fetchAllProfiles="fetchAllMembers"
/>
<!-- <ds-space />
<follow-list
:loading="$apollo.loading"
:user="user"
type="followedBy"
@ -159,7 +217,7 @@
</template>
<template v-else>
<ds-grid-item column-span="fullWidth">
<hc-empty margin="xx-large" icon="file" />
<empty margin="xx-large" icon="file" />
</ds-grid-item>
</template>
</masonry-grid>
@ -173,26 +231,26 @@
<script>
import uniqBy from 'lodash/uniqBy'
import postListActions from '~/mixins/postListActions'
import PostTeaser from '~/components/PostTeaser/PostTeaser.vue'
// import HcFollowButton from '~/components/FollowButton.vue'
// import HcCountTo from '~/components/CountTo.vue'
// import HcBadges from '~/components/Badges.vue'
// import FollowList from '~/components/features/FollowList/FollowList'
import HcEmpty from '~/components/Empty/Empty'
// import ContentMenu from '~/components/ContentMenu/ContentMenu'
import AvatarUploader from '~/components/Uploader/AvatarUploader'
import ProfileAvatar from '~/components/_new/generic/ProfileAvatar/ProfileAvatar'
import MasonryGrid from '~/components/MasonryGrid/MasonryGrid.vue'
import MasonryGridItem from '~/components/MasonryGrid/MasonryGridItem.vue'
// import TabNavigation from '~/components/_new/generic/TabNavigation/TabNavigation'
// import { profilePagePosts } from '~/graphql/PostQuery'
import { groupQuery } from '~/graphql/groups'
import { updateGroupMutation } from '~/graphql/groups.js'
import { updateGroupMutation, groupQuery, groupMembersQuery } from '~/graphql/groups'
// import { muteUser, unmuteUser } from '~/graphql/settings/MutedUsers'
// import { blockUser, unblockUser } from '~/graphql/settings/BlockedUsers'
// import UpdateQuery from '~/components/utils/UpdateQuery'
import postListActions from '~/mixins/postListActions'
import AvatarUploader from '~/components/Uploader/AvatarUploader'
// import ContentMenu from '~/components/ContentMenu/ContentMenu'
import CountTo from '~/components/CountTo.vue'
import Empty from '~/components/Empty/Empty'
// import FollowButton from '~/components/Button/FollowButton'
// import FollowList from '~/components/features/ProfileList/FollowList'
import JoinLeaveButton from '~/components/Button/JoinLeaveButton'
import MasonryGrid from '~/components/MasonryGrid/MasonryGrid.vue'
import MasonryGridItem from '~/components/MasonryGrid/MasonryGridItem.vue'
import PostTeaser from '~/components/PostTeaser/PostTeaser.vue'
import ProfileAvatar from '~/components/_new/generic/ProfileAvatar/ProfileAvatar'
import ProfileList from '~/components/features/ProfileList/ProfileList'
// import SocialMedia from '~/components/SocialMedia/SocialMedia'
// import TabNavigation from '~/components/_new/generic/TabNavigation/TabNavigation'
// const tabToFilterMapping = ({ tab, id }) => {
// return {
@ -204,18 +262,19 @@ import { updateGroupMutation } from '~/graphql/groups.js'
export default {
components: {
// SocialMedia,
PostTeaser,
// HcFollowButton,
// HcCountTo,
// HcBadges,
HcEmpty,
ProfileAvatar,
// ContentMenu,
AvatarUploader,
// ContentMenu,
CountTo,
Empty,
// FollowButton,
// FollowList,
JoinLeaveButton,
PostTeaser,
ProfileAvatar,
ProfileList,
MasonryGrid,
MasonryGridItem,
// FollowList,
// SocialMedia,
// TabNavigation,
},
mixins: [postListActions],
@ -223,31 +282,45 @@ export default {
name: 'slide-up',
mode: 'out-in',
},
head() {
return {
title: this.groupName,
}
},
data() {
// const filter = tabToFilterMapping({ tab: 'post', id: this.$route.params.id })
return {
Group: [],
GroupMembers: [],
posts: [],
hasMore: true,
offset: 0,
pageSize: 6,
tabActive: 'post',
// hasMore: true,
// offset: 0,
// pageSize: 6,
// tabActive: 'post',
// filter,
followedByCountStartValue: 0,
followedByCount: 7,
followingCount: 7,
// followedByCountStartValue: 0,
// followedByCount: 7,
// followingCount: 7,
membersCountStartValue: 0,
membersCountToLoad: Infinity,
updateGroupMutation,
}
},
computed: {
isGroupOwner() {
return this.group.myRole === 'owner'
},
isGroupMember() {
return this.group.myRole
currentUser() {
return this.$store.getters['auth/user']
},
group() {
return this.Group ? this.Group[0] : {}
return this.Group[0] ? this.Group[0] : {}
},
groupMembers() {
return this.GroupMembers ? this.GroupMembers : []
},
isGroupOwner() {
return this.group ? this.group.myRole === 'owner' : false
},
isGroupMember() {
return this.group ? !!this.group.myRole : false
},
groupName() {
const { name } = this.group || {}
@ -384,10 +457,17 @@ export default {
// this.user.followedByCurrentUser = followedByCurrentUser
// this.user.followedBy = followedBy
// },
// fetchAllConnections(type) {
// if (type === 'following') this.followingCount = Infinity
// if (type === 'followedBy') this.followedByCount = Infinity
// },
prepareJoinLeave() {
// "membersCountStartValue" is updated to avoid counting from 0 when join/leave
this.membersCountStartValue = this.GroupMembers.length
},
updateJoinLeave({ myRoleInGroup }) {
this.Group[0].myRole = myRoleInGroup
this.$apollo.queries.GroupMembers.refetch()
},
fetchAllMembers() {
this.membersCountToLoad = Infinity
},
},
apollo: {
// profilePagePosts: {
@ -421,6 +501,17 @@ export default {
},
fetchPolicy: 'cache-and-network',
},
GroupMembers: {
query() {
return groupMembersQuery
},
variables() {
return {
id: this.$route.params.id,
}
},
fetchPolicy: 'cache-and-network',
},
},
}
</script>
@ -449,4 +540,11 @@ export default {
margin-bottom: $space-x-small;
}
}
.centered-text {
text-align: center;
margin-bottom: $space-xxx-small;
}
.chip {
margin-bottom: $space-x-small;
}
</style>

View File

@ -172,10 +172,10 @@
import uniqBy from 'lodash/uniqBy'
import postListActions from '~/mixins/postListActions'
import PostTeaser from '~/components/PostTeaser/PostTeaser.vue'
import HcFollowButton from '~/components/FollowButton.vue'
import HcFollowButton from '~/components/Button/FollowButton'
import HcCountTo from '~/components/CountTo.vue'
import HcBadges from '~/components/Badges.vue'
import FollowList from '~/components/features/FollowList/FollowList'
import FollowList, { followListVisibleCount } from '~/components/features/ProfileList/FollowList'
import HcEmpty from '~/components/Empty/Empty'
import ContentMenu from '~/components/ContentMenu/ContentMenu'
import AvatarUploader from '~/components/Uploader/AvatarUploader'
@ -220,6 +220,11 @@ export default {
name: 'slide-up',
mode: 'out-in',
},
head() {
return {
title: this.userName,
}
},
data() {
const filter = tabToFilterMapping({ tab: 'post', id: this.$route.params.id })
return {
@ -231,8 +236,8 @@ export default {
tabActive: 'post',
filter,
followedByCountStartValue: 0,
followedByCount: 7,
followingCount: 7,
followedByCount: followListVisibleCount,
followingCount: followListVisibleCount,
updateUserMutation: updateUserMutation(),
}
},