mirror of
https://github.com/Ocelot-Social-Community/Ocelot-Social.git
synced 2025-12-13 07:46:06 +00:00
Merge branch '5059-epic-groups' into 5140-My-Groups-Page
This commit is contained in:
commit
a9a2097557
@ -323,6 +323,7 @@ export default shield(
|
|||||||
GenerateInviteCode: isAuthenticated,
|
GenerateInviteCode: isAuthenticated,
|
||||||
switchUserRole: isAdmin,
|
switchUserRole: isAdmin,
|
||||||
markTeaserAsViewed: allow,
|
markTeaserAsViewed: allow,
|
||||||
|
saveCategorySettings: isAuthenticated,
|
||||||
},
|
},
|
||||||
User: {
|
User: {
|
||||||
email: or(isMyOwn, isAdmin),
|
email: or(isMyOwn, isAdmin),
|
||||||
|
|||||||
@ -18,19 +18,25 @@ export default {
|
|||||||
if (isMember === true) {
|
if (isMember === true) {
|
||||||
groupCypher = `
|
groupCypher = `
|
||||||
MATCH (:User {id: $userId})-[membership:MEMBER_OF]->(group:Group${groupIdCypher})
|
MATCH (:User {id: $userId})-[membership:MEMBER_OF]->(group:Group${groupIdCypher})
|
||||||
|
WITH group, membership
|
||||||
|
WHERE (group.groupType IN ['public', 'closed']) OR (group.groupType = 'hidden' AND membership.role IN ['usual', 'admin', 'owner'])
|
||||||
RETURN group {.*, myRole: membership.role}
|
RETURN group {.*, myRole: membership.role}
|
||||||
`
|
`
|
||||||
} else {
|
} else {
|
||||||
if (isMember === false) {
|
if (isMember === false) {
|
||||||
groupCypher = `
|
groupCypher = `
|
||||||
MATCH (group:Group${groupIdCypher})
|
MATCH (group:Group${groupIdCypher})
|
||||||
WHERE NOT (:User {id: $userId})-[:MEMBER_OF]->(group)
|
WHERE (NOT (:User {id: $userId})-[:MEMBER_OF]->(group))
|
||||||
|
WITH group
|
||||||
|
WHERE group.groupType IN ['public', 'closed']
|
||||||
RETURN group {.*, myRole: NULL}
|
RETURN group {.*, myRole: NULL}
|
||||||
`
|
`
|
||||||
} else {
|
} else {
|
||||||
groupCypher = `
|
groupCypher = `
|
||||||
MATCH (group:Group${groupIdCypher})
|
MATCH (group:Group${groupIdCypher})
|
||||||
OPTIONAL MATCH (:User {id: $userId})-[membership:MEMBER_OF]->(group)
|
OPTIONAL MATCH (:User {id: $userId})-[membership:MEMBER_OF]->(group)
|
||||||
|
WITH group, membership
|
||||||
|
WHERE (group.groupType IN ['public', 'closed']) OR (group.groupType = 'hidden' AND membership.role IN ['usual', 'admin', 'owner'])
|
||||||
RETURN group {.*, myRole: membership.role}
|
RETURN group {.*, myRole: membership.role}
|
||||||
`
|
`
|
||||||
}
|
}
|
||||||
|
|||||||
@ -267,6 +267,7 @@ describe('in mode', () => {
|
|||||||
|
|
||||||
describe('authenticated', () => {
|
describe('authenticated', () => {
|
||||||
let otherUser
|
let otherUser
|
||||||
|
let ownerOfHiddenGroupUser
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
otherUser = await Factory.build(
|
otherUser = await Factory.build(
|
||||||
@ -276,7 +277,18 @@ describe('in mode', () => {
|
|||||||
name: 'Other TestUser',
|
name: 'Other TestUser',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
email: 'test2@example.org',
|
email: 'other-user@example.org',
|
||||||
|
password: '1234',
|
||||||
|
},
|
||||||
|
)
|
||||||
|
ownerOfHiddenGroupUser = await Factory.build(
|
||||||
|
'user',
|
||||||
|
{
|
||||||
|
id: 'owner-of-hidden-group',
|
||||||
|
name: 'Owner Of Hidden Group',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
email: 'owner-of-hidden-group@example.org',
|
||||||
password: '1234',
|
password: '1234',
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@ -293,6 +305,59 @@ describe('in mode', () => {
|
|||||||
categoryIds,
|
categoryIds,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
authenticatedUser = await ownerOfHiddenGroupUser.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,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
await mutate({
|
||||||
|
mutation: createGroupMutation,
|
||||||
|
variables: {
|
||||||
|
id: 'second-hidden-group',
|
||||||
|
name: 'Second Investigative Journalism Group',
|
||||||
|
about: 'We will change all.',
|
||||||
|
description: 'We research …' + descriptionAdditional100,
|
||||||
|
groupType: 'hidden',
|
||||||
|
actionRadius: 'global',
|
||||||
|
categoryIds,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
await mutate({
|
||||||
|
mutation: changeGroupMemberRoleMutation,
|
||||||
|
variables: {
|
||||||
|
groupId: 'second-hidden-group',
|
||||||
|
userId: 'current-user',
|
||||||
|
roleInGroup: 'pending',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
await mutate({
|
||||||
|
mutation: createGroupMutation,
|
||||||
|
variables: {
|
||||||
|
id: 'third-hidden-group',
|
||||||
|
name: 'Third Investigative Journalism Group',
|
||||||
|
about: 'We will change all.',
|
||||||
|
description: 'We research …' + descriptionAdditional100,
|
||||||
|
groupType: 'hidden',
|
||||||
|
actionRadius: 'global',
|
||||||
|
categoryIds,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
await mutate({
|
||||||
|
mutation: changeGroupMemberRoleMutation,
|
||||||
|
variables: {
|
||||||
|
groupId: 'third-hidden-group',
|
||||||
|
userId: 'current-user',
|
||||||
|
roleInGroup: 'usual',
|
||||||
|
},
|
||||||
|
})
|
||||||
authenticatedUser = await user.toJson()
|
authenticatedUser = await user.toJson()
|
||||||
await mutate({
|
await mutate({
|
||||||
mutation: createGroupMutation,
|
mutation: createGroupMutation,
|
||||||
@ -309,117 +374,175 @@ describe('in mode', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
describe('query groups', () => {
|
describe('query groups', () => {
|
||||||
describe('without any filters', () => {
|
describe('in general finds only listed groups – no hidden groups where user is none or pending member', () => {
|
||||||
it('finds all groups', async () => {
|
describe('without any filters', () => {
|
||||||
await expect(query({ query: groupQuery, variables: {} })).resolves.toMatchObject({
|
it('finds all listed groups', async () => {
|
||||||
data: {
|
const result = await query({ query: groupQuery, variables: {} })
|
||||||
Group: expect.arrayContaining([
|
expect(result).toMatchObject({
|
||||||
expect.objectContaining({
|
|
||||||
id: 'my-group',
|
|
||||||
slug: 'the-best-group',
|
|
||||||
myRole: 'owner',
|
|
||||||
}),
|
|
||||||
expect.objectContaining({
|
|
||||||
id: 'others-group',
|
|
||||||
slug: 'uninteresting-group',
|
|
||||||
myRole: null,
|
|
||||||
}),
|
|
||||||
]),
|
|
||||||
},
|
|
||||||
errors: undefined,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('categories', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
CONFIG.CATEGORIES_ACTIVE = true
|
|
||||||
})
|
|
||||||
|
|
||||||
it('has set categories', async () => {
|
|
||||||
await expect(query({ query: groupQuery, variables: {} })).resolves.toMatchObject({
|
|
||||||
data: {
|
data: {
|
||||||
Group: expect.arrayContaining([
|
Group: expect.arrayContaining([
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
id: 'my-group',
|
id: 'my-group',
|
||||||
slug: 'the-best-group',
|
slug: 'the-best-group',
|
||||||
categories: expect.arrayContaining([
|
|
||||||
expect.objectContaining({ id: 'cat4' }),
|
|
||||||
expect.objectContaining({ id: 'cat9' }),
|
|
||||||
expect.objectContaining({ id: 'cat15' }),
|
|
||||||
]),
|
|
||||||
myRole: 'owner',
|
myRole: 'owner',
|
||||||
}),
|
}),
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
id: 'others-group',
|
id: 'others-group',
|
||||||
slug: 'uninteresting-group',
|
slug: 'uninteresting-group',
|
||||||
categories: expect.arrayContaining([
|
myRole: null,
|
||||||
expect.objectContaining({ id: 'cat4' }),
|
}),
|
||||||
expect.objectContaining({ id: 'cat9' }),
|
expect.objectContaining({
|
||||||
expect.objectContaining({ id: 'cat15' }),
|
id: 'third-hidden-group',
|
||||||
]),
|
slug: 'third-investigative-journalism-group',
|
||||||
|
myRole: 'usual',
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
},
|
||||||
|
errors: undefined,
|
||||||
|
})
|
||||||
|
expect(result.data.Group.length).toBe(3)
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('categories', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
CONFIG.CATEGORIES_ACTIVE = true
|
||||||
|
})
|
||||||
|
|
||||||
|
it('has set categories', async () => {
|
||||||
|
await expect(query({ query: groupQuery, variables: {} })).resolves.toMatchObject({
|
||||||
|
data: {
|
||||||
|
Group: expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
id: 'my-group',
|
||||||
|
slug: 'the-best-group',
|
||||||
|
categories: expect.arrayContaining([
|
||||||
|
expect.objectContaining({ id: 'cat4' }),
|
||||||
|
expect.objectContaining({ id: 'cat9' }),
|
||||||
|
expect.objectContaining({ id: 'cat15' }),
|
||||||
|
]),
|
||||||
|
myRole: 'owner',
|
||||||
|
}),
|
||||||
|
expect.objectContaining({
|
||||||
|
id: 'others-group',
|
||||||
|
slug: 'uninteresting-group',
|
||||||
|
categories: expect.arrayContaining([
|
||||||
|
expect.objectContaining({ id: 'cat4' }),
|
||||||
|
expect.objectContaining({ id: 'cat9' }),
|
||||||
|
expect.objectContaining({ id: 'cat15' }),
|
||||||
|
]),
|
||||||
|
myRole: null,
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
},
|
||||||
|
errors: undefined,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('with given id', () => {
|
||||||
|
describe("id = 'my-group'", () => {
|
||||||
|
it('finds only the listed group with this id', async () => {
|
||||||
|
const result = await query({ query: groupQuery, variables: { id: 'my-group' } })
|
||||||
|
expect(result).toMatchObject({
|
||||||
|
data: {
|
||||||
|
Group: [
|
||||||
|
expect.objectContaining({
|
||||||
|
id: 'my-group',
|
||||||
|
slug: 'the-best-group',
|
||||||
|
myRole: 'owner',
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
errors: undefined,
|
||||||
|
})
|
||||||
|
expect(result.data.Group.length).toBe(1)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("id = 'third-hidden-group'", () => {
|
||||||
|
it("finds only the hidden group where I'm 'usual' member", async () => {
|
||||||
|
const result = await query({
|
||||||
|
query: groupQuery,
|
||||||
|
variables: { id: 'third-hidden-group' },
|
||||||
|
})
|
||||||
|
expect(result).toMatchObject({
|
||||||
|
data: {
|
||||||
|
Group: expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
id: 'third-hidden-group',
|
||||||
|
slug: 'third-investigative-journalism-group',
|
||||||
|
myRole: 'usual',
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
},
|
||||||
|
errors: undefined,
|
||||||
|
})
|
||||||
|
expect(result.data.Group.length).toBe(1)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("id = 'second-hidden-group'", () => {
|
||||||
|
it("finds no hidden group where I'm 'pending' member", async () => {
|
||||||
|
const result = await query({
|
||||||
|
query: groupQuery,
|
||||||
|
variables: { id: 'second-hidden-group' },
|
||||||
|
})
|
||||||
|
expect(result.data.Group.length).toBe(0)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("id = 'hidden-group'", () => {
|
||||||
|
it("finds no hidden group where I'm not(!) a member at all", async () => {
|
||||||
|
const result = await query({
|
||||||
|
query: groupQuery,
|
||||||
|
variables: { id: 'hidden-group' },
|
||||||
|
})
|
||||||
|
expect(result.data.Group.length).toBe(0)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('isMember = true', () => {
|
||||||
|
it('finds only listed groups where user is member', async () => {
|
||||||
|
const result = await query({ query: groupQuery, variables: { isMember: true } })
|
||||||
|
expect(result).toMatchObject({
|
||||||
|
data: {
|
||||||
|
Group: expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
id: 'my-group',
|
||||||
|
slug: 'the-best-group',
|
||||||
|
myRole: 'owner',
|
||||||
|
}),
|
||||||
|
expect.objectContaining({
|
||||||
|
id: 'third-hidden-group',
|
||||||
|
slug: 'third-investigative-journalism-group',
|
||||||
|
myRole: 'usual',
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
},
|
||||||
|
errors: undefined,
|
||||||
|
})
|
||||||
|
expect(result.data.Group.length).toBe(2)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('isMember = false', () => {
|
||||||
|
it('finds only listed groups where user is not(!) member', async () => {
|
||||||
|
const result = await query({ query: groupQuery, variables: { isMember: false } })
|
||||||
|
expect(result).toMatchObject({
|
||||||
|
data: {
|
||||||
|
Group: expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
id: 'others-group',
|
||||||
|
slug: 'uninteresting-group',
|
||||||
myRole: null,
|
myRole: null,
|
||||||
}),
|
}),
|
||||||
]),
|
]),
|
||||||
},
|
},
|
||||||
errors: undefined,
|
errors: undefined,
|
||||||
})
|
})
|
||||||
})
|
expect(result.data.Group.length).toBe(1)
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe("id = 'my-group'", () => {
|
|
||||||
it('finds only the group with this id', async () => {
|
|
||||||
await expect(
|
|
||||||
query({ query: groupQuery, variables: { id: 'my-group' } }),
|
|
||||||
).resolves.toMatchObject({
|
|
||||||
data: {
|
|
||||||
Group: [
|
|
||||||
expect.objectContaining({
|
|
||||||
id: 'my-group',
|
|
||||||
slug: 'the-best-group',
|
|
||||||
myRole: 'owner',
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
},
|
|
||||||
errors: undefined,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('isMember = true', () => {
|
|
||||||
it('finds only groups where user is member', async () => {
|
|
||||||
await expect(
|
|
||||||
query({ query: groupQuery, variables: { isMember: true } }),
|
|
||||||
).resolves.toMatchObject({
|
|
||||||
data: {
|
|
||||||
Group: [
|
|
||||||
{
|
|
||||||
id: 'my-group',
|
|
||||||
slug: 'the-best-group',
|
|
||||||
myRole: 'owner',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
errors: undefined,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('isMember = false', () => {
|
|
||||||
it('finds only groups where user is not(!) member', async () => {
|
|
||||||
await expect(
|
|
||||||
query({ query: groupQuery, variables: { isMember: false } }),
|
|
||||||
).resolves.toMatchObject({
|
|
||||||
data: {
|
|
||||||
Group: expect.arrayContaining([
|
|
||||||
expect.objectContaining({
|
|
||||||
id: 'others-group',
|
|
||||||
slug: 'uninteresting-group',
|
|
||||||
myRole: null,
|
|
||||||
}),
|
|
||||||
]),
|
|
||||||
},
|
|
||||||
errors: undefined,
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@ -269,6 +269,45 @@ export default {
|
|||||||
session.close()
|
session.close()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
saveCategorySettings: async (object, args, context, resolveInfo) => {
|
||||||
|
const { activeCategories } = args
|
||||||
|
const {
|
||||||
|
user: { id },
|
||||||
|
} = context
|
||||||
|
|
||||||
|
const session = context.driver.session()
|
||||||
|
await session.writeTransaction((transaction) => {
|
||||||
|
return transaction.run(
|
||||||
|
`
|
||||||
|
MATCH (user:User { id: $id })-[previousCategories:NOT_INTERESTED_IN]->(category:Category)
|
||||||
|
DELETE previousCategories
|
||||||
|
RETURN user, category
|
||||||
|
`,
|
||||||
|
{ id },
|
||||||
|
)
|
||||||
|
})
|
||||||
|
const writeTxResultPromise = session.writeTransaction(async (transaction) => {
|
||||||
|
const saveCategorySettingsResponse = await transaction.run(
|
||||||
|
`
|
||||||
|
MATCH (category:Category) WHERE NOT category.id IN $activeCategories
|
||||||
|
MATCH (user:User { id: $id })
|
||||||
|
MERGE (user)-[r:NOT_INTERESTED_IN]->(category)
|
||||||
|
RETURN user, r, category
|
||||||
|
`,
|
||||||
|
{ id, activeCategories },
|
||||||
|
)
|
||||||
|
const [user] = await saveCategorySettingsResponse.records.map((record) =>
|
||||||
|
record.get('user'),
|
||||||
|
)
|
||||||
|
return user
|
||||||
|
})
|
||||||
|
try {
|
||||||
|
await writeTxResultPromise
|
||||||
|
return true
|
||||||
|
} finally {
|
||||||
|
session.close()
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
User: {
|
User: {
|
||||||
email: async (parent, params, context, resolveInfo) => {
|
email: async (parent, params, context, resolveInfo) => {
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import { gql } from '../../helpers/jest'
|
|||||||
import { getNeode, getDriver } from '../../db/neo4j'
|
import { getNeode, getDriver } from '../../db/neo4j'
|
||||||
import createServer from '../../server'
|
import createServer from '../../server'
|
||||||
import { createTestClient } from 'apollo-server-testing'
|
import { createTestClient } from 'apollo-server-testing'
|
||||||
|
import { categories } from '../../constants/categories'
|
||||||
|
|
||||||
const categoryIds = ['cat9']
|
const categoryIds = ['cat9']
|
||||||
let user
|
let user
|
||||||
@ -56,6 +57,12 @@ const switchUserRoleMutation = gql`
|
|||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
|
const saveCategorySettings = gql`
|
||||||
|
mutation ($activeCategories: [String]) {
|
||||||
|
saveCategorySettings(activeCategories: $activeCategories)
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
await cleanDatabase()
|
await cleanDatabase()
|
||||||
|
|
||||||
@ -544,3 +551,146 @@ describe('switch user role', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('save category settings', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
await Promise.all(
|
||||||
|
categories.map(({ icon, name }, index) => {
|
||||||
|
Factory.build('category', {
|
||||||
|
id: `cat${index + 1}`,
|
||||||
|
slug: name,
|
||||||
|
name,
|
||||||
|
icon,
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
user = await Factory.build('user', {
|
||||||
|
id: 'user',
|
||||||
|
role: 'user',
|
||||||
|
})
|
||||||
|
variables = {
|
||||||
|
activeCategories: ['cat1', 'cat3', 'cat5'],
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('not authenticated', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
authenticatedUser = undefined
|
||||||
|
})
|
||||||
|
|
||||||
|
it('throws an error', async () => {
|
||||||
|
await expect(mutate({ mutation: saveCategorySettings, variables })).resolves.toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
errors: [
|
||||||
|
expect.objectContaining({
|
||||||
|
message: 'Not Authorized!',
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('authenticated', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
authenticatedUser = await user.toJson()
|
||||||
|
})
|
||||||
|
|
||||||
|
const userQuery = gql`
|
||||||
|
query ($id: ID) {
|
||||||
|
User(id: $id) {
|
||||||
|
activeCategories {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
describe('no categories saved', () => {
|
||||||
|
it('returns true for active categories mutation', async () => {
|
||||||
|
await expect(mutate({ mutation: saveCategorySettings, variables })).resolves.toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
data: { saveCategorySettings: true },
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('query for user', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
await mutate({ mutation: saveCategorySettings, variables })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('returns the active categories when user is queried', async () => {
|
||||||
|
await expect(
|
||||||
|
query({ query: userQuery, variables: { id: authenticatedUser.id } }),
|
||||||
|
).resolves.toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
data: {
|
||||||
|
User: [
|
||||||
|
{
|
||||||
|
activeCategories: expect.arrayContaining([
|
||||||
|
{ id: 'cat1' },
|
||||||
|
{ id: 'cat3' },
|
||||||
|
{ id: 'cat5' },
|
||||||
|
]),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('categories already saved', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
variables = {
|
||||||
|
activeCategories: ['cat1', 'cat3', 'cat5'],
|
||||||
|
}
|
||||||
|
await mutate({ mutation: saveCategorySettings, variables })
|
||||||
|
variables = {
|
||||||
|
activeCategories: ['cat10', 'cat11', 'cat12', 'cat8', 'cat9'],
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it('returns true', async () => {
|
||||||
|
await expect(mutate({ mutation: saveCategorySettings, variables })).resolves.toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
data: { saveCategorySettings: true },
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('query for user', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
await mutate({ mutation: saveCategorySettings, variables })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('returns the new active categories when user is queried', async () => {
|
||||||
|
await expect(
|
||||||
|
query({ query: userQuery, variables: { id: authenticatedUser.id } }),
|
||||||
|
).resolves.toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
data: {
|
||||||
|
User: [
|
||||||
|
{
|
||||||
|
activeCategories: expect.arrayContaining([
|
||||||
|
{ id: 'cat10' },
|
||||||
|
{ id: 'cat11' },
|
||||||
|
{ id: 'cat12' },
|
||||||
|
{ id: 'cat8' },
|
||||||
|
{ id: 'cat9' },
|
||||||
|
]),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|||||||
@ -115,6 +115,14 @@ type User {
|
|||||||
|
|
||||||
emotions: [EMOTED]
|
emotions: [EMOTED]
|
||||||
|
|
||||||
|
activeCategories: [Category] @cypher(
|
||||||
|
statement: """
|
||||||
|
MATCH (category:Category)
|
||||||
|
WHERE NOT ((this)-[:NOT_INTERESTED_IN]->(category))
|
||||||
|
RETURN category
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
myRoleInGroup: GroupMemberRole
|
myRoleInGroup: GroupMemberRole
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -222,4 +230,6 @@ type Mutation {
|
|||||||
unblockUser(id: ID!): User
|
unblockUser(id: ID!): User
|
||||||
|
|
||||||
switchUserRole(role: UserRole!, id: ID!): User
|
switchUserRole(role: UserRole!, id: ID!): User
|
||||||
|
|
||||||
|
saveCategorySettings(activeCategories: [String]): Boolean
|
||||||
}
|
}
|
||||||
|
|||||||
58
webapp/components/FilterMenu/CategoriesMenu.vue
Normal file
58
webapp/components/FilterMenu/CategoriesMenu.vue
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
<template>
|
||||||
|
<dropdown ref="category-menu" placement="top-start" :offset="8" class="category-menu">
|
||||||
|
<base-button
|
||||||
|
slot="default"
|
||||||
|
:filled="filterActive"
|
||||||
|
:ghost="!filterActive"
|
||||||
|
slot-scope="{ toggleMenu }"
|
||||||
|
@click.prevent="toggleMenu()"
|
||||||
|
>
|
||||||
|
<ds-text uppercase>{{ $t('admin.categories.name') }}</ds-text>
|
||||||
|
</base-button>
|
||||||
|
<template slot="popover">
|
||||||
|
<div class="category-menu-options">
|
||||||
|
<h2 class="title">{{ $t('filter-menu.filter-by') }}</h2>
|
||||||
|
<categories-filter v-if="categoriesActive" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</dropdown>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import Dropdown from '~/components/Dropdown'
|
||||||
|
import { mapGetters } from 'vuex'
|
||||||
|
import CategoriesFilter from './CategoriesFilter'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'CategoriesMenu',
|
||||||
|
components: {
|
||||||
|
Dropdown,
|
||||||
|
CategoriesFilter,
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
placement: { type: String },
|
||||||
|
offset: { type: [String, Number] },
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
categoriesActive: this.$env.CATEGORIES_ACTIVE,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapGetters({
|
||||||
|
filterActive: 'posts/isActive',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.category-menu-options {
|
||||||
|
max-width: $size-max-width-filter-menu;
|
||||||
|
padding: $space-small $space-x-small;
|
||||||
|
|
||||||
|
> .title {
|
||||||
|
font-size: $font-size-large;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -14,7 +14,6 @@
|
|||||||
<div class="filter-menu-options">
|
<div class="filter-menu-options">
|
||||||
<h2 class="title">{{ $t('filter-menu.filter-by') }}</h2>
|
<h2 class="title">{{ $t('filter-menu.filter-by') }}</h2>
|
||||||
<following-filter />
|
<following-filter />
|
||||||
<categories-filter v-if="categoriesActive" />
|
|
||||||
</div>
|
</div>
|
||||||
<div class="filter-menu-options">
|
<div class="filter-menu-options">
|
||||||
<h2 class="title">{{ $t('filter-menu.order-by') }}</h2>
|
<h2 class="title">{{ $t('filter-menu.order-by') }}</h2>
|
||||||
@ -29,24 +28,17 @@ import Dropdown from '~/components/Dropdown'
|
|||||||
import { mapGetters } from 'vuex'
|
import { mapGetters } from 'vuex'
|
||||||
import FollowingFilter from './FollowingFilter'
|
import FollowingFilter from './FollowingFilter'
|
||||||
import OrderByFilter from './OrderByFilter'
|
import OrderByFilter from './OrderByFilter'
|
||||||
import CategoriesFilter from './CategoriesFilter'
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
Dropdown,
|
Dropdown,
|
||||||
FollowingFilter,
|
FollowingFilter,
|
||||||
CategoriesFilter,
|
|
||||||
OrderByFilter,
|
OrderByFilter,
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
placement: { type: String },
|
placement: { type: String },
|
||||||
offset: { type: [String, Number] },
|
offset: { type: [String, Number] },
|
||||||
},
|
},
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
categoriesActive: this.$env.CATEGORIES_ACTIVE,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
computed: {
|
computed: {
|
||||||
...mapGetters({
|
...mapGetters({
|
||||||
filterActive: 'posts/isActive',
|
filterActive: 'posts/isActive',
|
||||||
|
|||||||
@ -15,6 +15,15 @@
|
|||||||
>
|
>
|
||||||
<base-button icon="bars" @click="toggleMobileMenuView" circle />
|
<base-button icon="bars" @click="toggleMobileMenuView" circle />
|
||||||
</ds-flex-item>
|
</ds-flex-item>
|
||||||
|
<ds-flex-item
|
||||||
|
v-if="categoriesActive && isLoggedIn"
|
||||||
|
:class="{ 'hide-mobile-menu': !toggleMobileMenu }"
|
||||||
|
style="flex-grow: 0; flex-basis: auto"
|
||||||
|
>
|
||||||
|
<client-only>
|
||||||
|
<categories-menu></categories-menu>
|
||||||
|
</client-only>
|
||||||
|
</ds-flex-item>
|
||||||
<ds-flex-item
|
<ds-flex-item
|
||||||
:width="{ base: '45%', sm: '45%', md: '45%', lg: '50%' }"
|
:width="{ base: '45%', sm: '45%', md: '45%', lg: '50%' }"
|
||||||
:class="{ 'hide-mobile-menu': !toggleMobileMenu }"
|
:class="{ 'hide-mobile-menu': !toggleMobileMenu }"
|
||||||
@ -90,6 +99,7 @@ import FilterMenu from '~/components/FilterMenu/FilterMenu.vue'
|
|||||||
import PageFooter from '~/components/PageFooter/PageFooter'
|
import PageFooter from '~/components/PageFooter/PageFooter'
|
||||||
import AvatarMenu from '~/components/AvatarMenu/AvatarMenu'
|
import AvatarMenu from '~/components/AvatarMenu/AvatarMenu'
|
||||||
import InviteButton from '~/components/InviteButton/InviteButton'
|
import InviteButton from '~/components/InviteButton/InviteButton'
|
||||||
|
import CategoriesMenu from '~/components/FilterMenu/CategoriesMenu.vue'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
@ -102,6 +112,7 @@ export default {
|
|||||||
FilterMenu,
|
FilterMenu,
|
||||||
PageFooter,
|
PageFooter,
|
||||||
InviteButton,
|
InviteButton,
|
||||||
|
CategoriesMenu,
|
||||||
},
|
},
|
||||||
mixins: [seo],
|
mixins: [seo],
|
||||||
data() {
|
data() {
|
||||||
@ -109,6 +120,7 @@ export default {
|
|||||||
mobileSearchVisible: false,
|
mobileSearchVisible: false,
|
||||||
toggleMobileMenu: false,
|
toggleMobileMenu: false,
|
||||||
inviteRegistration: this.$env.INVITE_REGISTRATION === true, // for 'false' in .env INVITE_REGISTRATION is of type undefined and not(!) boolean false, because of internal handling,
|
inviteRegistration: this.$env.INVITE_REGISTRATION === true, // for 'false' in .env INVITE_REGISTRATION is of type undefined and not(!) boolean false, because of internal handling,
|
||||||
|
categoriesActive: this.$env.CATEGORIES_ACTIVE,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
|||||||
@ -11,7 +11,7 @@
|
|||||||
"admin": {
|
"admin": {
|
||||||
"categories": {
|
"categories": {
|
||||||
"categoryName": "Name",
|
"categoryName": "Name",
|
||||||
"name": "Kategorien",
|
"name": "Themen",
|
||||||
"postCount": "Beiträge"
|
"postCount": "Beiträge"
|
||||||
},
|
},
|
||||||
"dashboard": {
|
"dashboard": {
|
||||||
@ -98,7 +98,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"common": {
|
"common": {
|
||||||
"category": "Kategorie ::: Kategorien",
|
"category": "Thema ::: Themen",
|
||||||
"comment": "Kommentar ::: Kommentare",
|
"comment": "Kommentar ::: Kommentare",
|
||||||
"letsTalk": "Miteinander reden",
|
"letsTalk": "Miteinander reden",
|
||||||
"loading": "wird geladen",
|
"loading": "wird geladen",
|
||||||
@ -113,7 +113,7 @@
|
|||||||
"takeAction": "Aktiv werden",
|
"takeAction": "Aktiv werden",
|
||||||
"user": "Benutzer ::: Benutzer",
|
"user": "Benutzer ::: Benutzer",
|
||||||
"validations": {
|
"validations": {
|
||||||
"categories": "es müssen eine bis drei Kategorien ausgewählt werden",
|
"categories": "es müssen eine bis drei Themen ausgewählt werden",
|
||||||
"email": "muss eine gültige E-Mail-Adresse sein",
|
"email": "muss eine gültige E-Mail-Adresse sein",
|
||||||
"url": "muss eine gültige URL sein"
|
"url": "muss eine gültige URL sein"
|
||||||
},
|
},
|
||||||
@ -214,7 +214,7 @@
|
|||||||
"amount-shouts": "{amount} recommendations",
|
"amount-shouts": "{amount} recommendations",
|
||||||
"amount-views": "{amount} views",
|
"amount-views": "{amount} views",
|
||||||
"categories": {
|
"categories": {
|
||||||
"infoSelectedNoOfMaxCategories": "{chosen} von {max} Kategorien ausgewählt"
|
"infoSelectedNoOfMaxCategories": "{chosen} von {max} Themen ausgewählt"
|
||||||
},
|
},
|
||||||
"category": {
|
"category": {
|
||||||
"name": {
|
"name": {
|
||||||
@ -349,7 +349,7 @@
|
|||||||
},
|
},
|
||||||
"filter-menu": {
|
"filter-menu": {
|
||||||
"all": "Alle",
|
"all": "Alle",
|
||||||
"categories": "Themenkategorien",
|
"categories": "Themen",
|
||||||
"emotions": "Emotionen",
|
"emotions": "Emotionen",
|
||||||
"filter-by": "Filtern nach ...",
|
"filter-by": "Filtern nach ...",
|
||||||
"following": "Benutzern, denen ich folge",
|
"following": "Benutzern, denen ich folge",
|
||||||
@ -478,7 +478,7 @@
|
|||||||
"noDecision": "Keine Entscheidung!",
|
"noDecision": "Keine Entscheidung!",
|
||||||
"numberOfUsers": "{count} Nutzern",
|
"numberOfUsers": "{count} Nutzern",
|
||||||
"previousDecision": "Vorherige Entscheidung:",
|
"previousDecision": "Vorherige Entscheidung:",
|
||||||
"reasonCategory": "Kategorie",
|
"reasonCategory": "Thema",
|
||||||
"reasonDescription": "Beschreibung",
|
"reasonDescription": "Beschreibung",
|
||||||
"reportedOn": "Datum",
|
"reportedOn": "Datum",
|
||||||
"status": "Aktueller Status",
|
"status": "Aktueller Status",
|
||||||
@ -600,8 +600,8 @@
|
|||||||
},
|
},
|
||||||
"reason": {
|
"reason": {
|
||||||
"category": {
|
"category": {
|
||||||
"invalid": "Bitte wähle eine gültige Kategorie aus",
|
"invalid": "Bitte wähle ein gültiges Thema aus",
|
||||||
"label": "Wähle eine Kategorie:",
|
"label": "Wähle ein Thema:",
|
||||||
"options": {
|
"options": {
|
||||||
"advert_products_services_commercial": "Bewerben von Produkten und Dienstleistungen mit kommerzieller Absicht.",
|
"advert_products_services_commercial": "Bewerben von Produkten und Dienstleistungen mit kommerzieller Absicht.",
|
||||||
"criminal_behavior_violation_german_law": "Strafbares Verhalten bzw. Verstoß gegen deutsches Recht.",
|
"criminal_behavior_violation_german_law": "Strafbares Verhalten bzw. Verstoß gegen deutsches Recht.",
|
||||||
@ -612,7 +612,7 @@
|
|||||||
"other": "Andere …",
|
"other": "Andere …",
|
||||||
"pornographic_content_links": "Das Senden oder Verlinken eindeutig pornografischen Materials."
|
"pornographic_content_links": "Das Senden oder Verlinken eindeutig pornografischen Materials."
|
||||||
},
|
},
|
||||||
"placeholder": "Kategorie …"
|
"placeholder": "Thema …"
|
||||||
},
|
},
|
||||||
"description": {
|
"description": {
|
||||||
"label": "Bitte erkläre: Warum möchtest Du dies melden?",
|
"label": "Bitte erkläre: Warum möchtest Du dies melden?",
|
||||||
|
|||||||
@ -11,7 +11,7 @@
|
|||||||
"admin": {
|
"admin": {
|
||||||
"categories": {
|
"categories": {
|
||||||
"categoryName": "Name",
|
"categoryName": "Name",
|
||||||
"name": "Categories",
|
"name": "Topics",
|
||||||
"postCount": "Posts"
|
"postCount": "Posts"
|
||||||
},
|
},
|
||||||
"dashboard": {
|
"dashboard": {
|
||||||
@ -98,7 +98,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"common": {
|
"common": {
|
||||||
"category": "Category ::: Categories",
|
"category": "Topic ::: Topics",
|
||||||
"comment": "Comment ::: Comments",
|
"comment": "Comment ::: Comments",
|
||||||
"letsTalk": "Let`s Talk",
|
"letsTalk": "Let`s Talk",
|
||||||
"loading": "loading",
|
"loading": "loading",
|
||||||
@ -113,7 +113,7 @@
|
|||||||
"takeAction": "Take Action",
|
"takeAction": "Take Action",
|
||||||
"user": "User ::: Users",
|
"user": "User ::: Users",
|
||||||
"validations": {
|
"validations": {
|
||||||
"categories": "at least one and at most three categories must be selected",
|
"categories": "at least one and at most three topics must be selected",
|
||||||
"email": "must be a valid e-mail address",
|
"email": "must be a valid e-mail address",
|
||||||
"url": "must be a valid URL"
|
"url": "must be a valid URL"
|
||||||
},
|
},
|
||||||
@ -214,7 +214,7 @@
|
|||||||
"amount-shouts": "{amount} recommendations",
|
"amount-shouts": "{amount} recommendations",
|
||||||
"amount-views": "{amount} views",
|
"amount-views": "{amount} views",
|
||||||
"categories": {
|
"categories": {
|
||||||
"infoSelectedNoOfMaxCategories": "{chosen} of {max} categories selected"
|
"infoSelectedNoOfMaxCategories": "{chosen} of {max} topics selected"
|
||||||
},
|
},
|
||||||
"category": {
|
"category": {
|
||||||
"name": {
|
"name": {
|
||||||
@ -349,7 +349,7 @@
|
|||||||
},
|
},
|
||||||
"filter-menu": {
|
"filter-menu": {
|
||||||
"all": "All",
|
"all": "All",
|
||||||
"categories": "Categories of Content",
|
"categories": "Topics",
|
||||||
"emotions": "Emotions",
|
"emotions": "Emotions",
|
||||||
"filter-by": "Filter by ...",
|
"filter-by": "Filter by ...",
|
||||||
"following": "Users I follow",
|
"following": "Users I follow",
|
||||||
@ -478,7 +478,7 @@
|
|||||||
"noDecision": "No decision!",
|
"noDecision": "No decision!",
|
||||||
"numberOfUsers": "{count} users",
|
"numberOfUsers": "{count} users",
|
||||||
"previousDecision": "Previous decision:",
|
"previousDecision": "Previous decision:",
|
||||||
"reasonCategory": "Category",
|
"reasonCategory": "Topic",
|
||||||
"reasonDescription": "Description",
|
"reasonDescription": "Description",
|
||||||
"reportedOn": "Date",
|
"reportedOn": "Date",
|
||||||
"status": "Current status",
|
"status": "Current status",
|
||||||
@ -600,8 +600,8 @@
|
|||||||
},
|
},
|
||||||
"reason": {
|
"reason": {
|
||||||
"category": {
|
"category": {
|
||||||
"invalid": "Please select a valid category",
|
"invalid": "Please select a valid topic",
|
||||||
"label": "Select a category:",
|
"label": "Select a topic:",
|
||||||
"options": {
|
"options": {
|
||||||
"advert_products_services_commercial": "Advertising products and services with commercial intent.",
|
"advert_products_services_commercial": "Advertising products and services with commercial intent.",
|
||||||
"criminal_behavior_violation_german_law": "Criminal behavior or violation of German law.",
|
"criminal_behavior_violation_german_law": "Criminal behavior or violation of German law.",
|
||||||
@ -612,7 +612,7 @@
|
|||||||
"other": "Other …",
|
"other": "Other …",
|
||||||
"pornographic_content_links": "Posting or linking of clearly pornographic material."
|
"pornographic_content_links": "Posting or linking of clearly pornographic material."
|
||||||
},
|
},
|
||||||
"placeholder": "Category …"
|
"placeholder": "Topic …"
|
||||||
},
|
},
|
||||||
"description": {
|
"description": {
|
||||||
"label": "Please explain: Why you like to report this?",
|
"label": "Please explain: Why you like to report this?",
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user