Merge branch '5059-groups/5131-implement-update-group-resolver' of github.com:Ocelot-Social-Community/Ocelot-Social into 5059-groups/5190-group-profile

# Conflicts:
#	backend/src/schema/resolvers/groups.spec.js
This commit is contained in:
Wolfgang Huß 2022-09-05 10:59:21 +02:00
commit b96dfa84e0
29 changed files with 3326 additions and 411 deletions

View File

@ -1,3 +1,102 @@
// this file is duplicated in `backend/src/constants/metadata.js` and `webapp/constants/metadata.js`
export const CATEGORIES_MIN = 1
export const CATEGORIES_MAX = 3
export const categories = [
{
icon: 'users',
name: 'networking',
description: 'Kooperation, Aktionsbündnisse, Solidarität, Hilfe',
},
{
icon: 'home',
name: 'home',
description: 'Bauen, Lebensgemeinschaften, Tiny Houses, Gemüsegarten',
},
{
icon: 'lightbulb',
name: 'energy',
description: 'Öl, Gas, Kohle, Wind, Wasserkraft, Biogas, Atomenergie, ...',
},
{
icon: 'smile',
name: 'psyche',
description: 'Seele, Gefühle, Glück',
},
{
icon: 'movement',
name: 'body-and-excercise',
description: 'Sport, Yoga, Massage, Tanzen, Entspannung',
},
{
icon: 'balance-scale',
name: 'law',
description: 'Menschenrechte, Gesetze, Verordnungen',
},
{
icon: 'money',
name: 'finance',
description: 'Geld, Finanzsystem, Alternativwährungen, ...',
},
{
icon: 'child',
name: 'children',
description: 'Familie, Pädagogik, Schule, Prägung',
},
{
icon: 'suitcase',
name: 'mobility',
description: 'Reise, Verkehr, Elektromobilität',
},
{
icon: 'shopping-cart',
name: 'economy',
description: 'Handel, Konsum, Marketing, Lebensmittel, Lieferketten, ...',
},
{
icon: 'angellist',
name: 'peace',
description: 'Krieg, Militär, soziale Verteidigung, Waffen, Cyberattacken',
},
{
icon: 'university',
name: 'politics',
description: 'Demokratie, Mitbestimmung, Wahlen, Korruption, Parteien',
},
{
icon: 'tree',
name: 'nature',
description: 'Tiere, Pflanzen, Landwirtschaft, Ökologie, Artenvielfalt',
},
{
icon: 'graduation-cap',
name: 'science',
description: 'Bildung, Hochschule, Publikationen, ...',
},
{
icon: 'medkit',
name: 'health',
description: 'Medizin, Ernährung, WHO, Impfungen, Schadstoffe, ...',
},
{
icon: 'desktop',
name: 'it-and-media',
description:
'Nachrichten, Manipulation, Datenschutz, Überwachung, Datenkraken, AI, Software, Apps',
},
{
icon: 'heart-o',
name: 'spirituality',
description: 'Religion, Werte, Ethik',
},
{
icon: 'music',
name: 'culture',
description: 'Kunst, Theater, Musik, Fotografie, Film',
},
{
icon: 'ellipsis-h',
name: 'miscellaneous',
description: '',
},
]

View File

@ -1,2 +1,3 @@
// this file is duplicated in `backend/src/constants/group.js` and `webapp/constants/group.js`
export const DESCRIPTION_WITHOUT_HTML_LENGTH_MIN = 100 // with removed HTML tags
export const DESCRIPTION_EXCERPT_HTML_LENGTH = 120 // with removed HTML tags

View File

@ -12,6 +12,7 @@ export const createGroupMutation = gql`
$groupType: GroupType!
$actionRadius: GroupActionRadius!
$categoryIds: [ID]
$locationName: String
) {
CreateGroup(
id: $id
@ -22,6 +23,7 @@ export const createGroupMutation = gql`
groupType: $groupType
actionRadius: $actionRadius
categoryIds: $categoryIds
locationName: $locationName
) {
id
name
@ -34,11 +36,87 @@ export const createGroupMutation = gql`
description
groupType
actionRadius
categories {
id
slug
name
icon
}
# locationName # test this as result
myRole
}
}
`
export const updateGroupMutation = gql`
mutation (
$id: ID!
$name: String
$slug: String
$about: String
$description: String
$actionRadius: GroupActionRadius
$categoryIds: [ID]
$avatar: ImageInput
$locationName: String
) {
UpdateGroup(
id: $id
name: $name
slug: $slug
about: $about
description: $description
actionRadius: $actionRadius
categoryIds: $categoryIds
avatar: $avatar
locationName: $locationName
) {
id
name
slug
createdAt
updatedAt
disabled
deleted
about
description
groupType
actionRadius
categories {
id
slug
name
icon
}
# avatar # test this as result
# locationName # test this as result
myRole
}
}
`
export const joinGroupMutation = gql`
mutation ($groupId: ID!, $userId: ID!) {
JoinGroup(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) {
id
name
slug
myRoleInGroup
}
}
`
// ------ queries
export const groupQuery = gql`
@ -90,6 +168,19 @@ export const groupQuery = gql`
name
icon
}
# avatar # test this as result
# locationName # test this as result
}
}
`
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) {
id
name
slug
myRoleInGroup
}
}
`

View File

@ -1,6 +1,8 @@
import { getDriver, getNeode } from '../../db/neo4j'
import { hashSync } from 'bcryptjs'
import { v4 as uuid } from 'uuid'
import { categories } from '../../constants/categories'
import CONFIG from '../../config'
const defaultAdmin = {
email: 'admin@example.org',
@ -10,6 +12,29 @@ const defaultAdmin = {
slug: 'admin',
}
const createCategories = async (session) => {
const createCategoriesTxResultPromise = session.writeTransaction(async (txc) => {
categories.forEach(({ icon, name }, index) => {
const id = `cat${index + 1}`
txc.run(
`MERGE (c:Category {
icon: "${icon}",
slug: "${name}",
name: "${name}",
id: "${id}",
createdAt: toString(datetime())
})`,
)
})
})
try {
await createCategoriesTxResultPromise
console.log('Successfully created categories!') // eslint-disable-line no-console
} catch (error) {
console.log(`Error creating categories: ${error}`) // eslint-disable-line no-console
}
}
const createDefaultAdminUser = async (session) => {
const readTxResultPromise = session.readTransaction(async (txc) => {
const result = await txc.run('MATCH (user:User) RETURN count(user) AS userCount')
@ -45,7 +70,7 @@ const createDefaultAdminUser = async (session) => {
})
try {
await createAdminTxResultPromise
console.log('Successfully created default admin user') // eslint-disable-line no-console
console.log('Successfully created default admin user!') // eslint-disable-line no-console
} catch (error) {
console.log(error) // eslint-disable-line no-console
}
@ -58,6 +83,7 @@ class Store {
const { driver } = neode
const session = driver.session()
await createDefaultAdminUser(session)
if (CONFIG.CATEGORIES_ACTIVE) await createCategories(session)
const writeTxResultPromise = session.writeTransaction(async (txc) => {
await txc.run('CALL apoc.schema.assert({},{},true)') // drop all indices and contraints
return Promise.all(

View File

@ -5,9 +5,14 @@ import createServer from '../server'
import faker from '@faker-js/faker'
import Factory from '../db/factories'
import { getNeode, getDriver } from '../db/neo4j'
import { createGroupMutation } from './graphql/groups'
import {
createGroupMutation,
joinGroupMutation,
changeGroupMemberRoleMutation,
} from './graphql/groups'
import { createPostMutation } from './graphql/posts'
import { createCommentMutation } from './graphql/comments'
import { categories } from '../constants/categories'
if (CONFIG.PRODUCTION && !CONFIG.PRODUCTION_DB_CLEAN_ALLOW) {
throw new Error(`You cannot seed the database in a non-staging and real production environment!`)
@ -269,104 +274,16 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl']
dagobert.relateTo(louie, 'blocked'),
])
await Promise.all([
Factory.build('category', {
id: 'cat1',
name: 'Just For Fun',
slug: 'just-for-fun',
icon: 'smile',
await Promise.all(
categories.map(({ icon, name }, index) => {
Factory.build('category', {
id: `cat${index + 1}`,
slug: name,
name,
icon,
})
}),
Factory.build('category', {
id: 'cat2',
name: 'Happiness & Values',
slug: 'happiness-values',
icon: 'heart-o',
}),
Factory.build('category', {
id: 'cat3',
name: 'Health & Wellbeing',
slug: 'health-wellbeing',
icon: 'medkit',
}),
Factory.build('category', {
id: 'cat4',
name: 'Environment & Nature',
slug: 'environment-nature',
icon: 'tree',
}),
Factory.build('category', {
id: 'cat5',
name: 'Animal Protection',
slug: 'animal-protection',
icon: 'paw',
}),
Factory.build('category', {
id: 'cat6',
name: 'Human Rights & Justice',
slug: 'human-rights-justice',
icon: 'balance-scale',
}),
Factory.build('category', {
id: 'cat7',
name: 'Education & Sciences',
slug: 'education-sciences',
icon: 'graduation-cap',
}),
Factory.build('category', {
id: 'cat8',
name: 'Cooperation & Development',
slug: 'cooperation-development',
icon: 'users',
}),
Factory.build('category', {
id: 'cat9',
name: 'Democracy & Politics',
slug: 'democracy-politics',
icon: 'university',
}),
Factory.build('category', {
id: 'cat10',
name: 'Economy & Finances',
slug: 'economy-finances',
icon: 'money',
}),
Factory.build('category', {
id: 'cat11',
name: 'Energy & Technology',
slug: 'energy-technology',
icon: 'flash',
}),
Factory.build('category', {
id: 'cat12',
name: 'IT, Internet & Data Privacy',
slug: 'it-internet-data-privacy',
icon: 'mouse-pointer',
}),
Factory.build('category', {
id: 'cat13',
name: 'Art, Culture & Sport',
slug: 'art-culture-sport',
icon: 'paint-brush',
}),
Factory.build('category', {
id: 'cat14',
name: 'Freedom of Speech',
slug: 'freedom-of-speech',
icon: 'bullhorn',
}),
Factory.build('category', {
id: 'cat15',
name: 'Consumption & Sustainability',
slug: 'consumption-sustainability',
icon: 'shopping-cart',
}),
Factory.build('category', {
id: 'cat16',
name: 'Global Peace & Nonviolence',
slug: 'global-peace-nonviolence',
icon: 'angellist',
}),
])
)
const [environment, nature, democracy, freedom] = await Promise.all([
Factory.build('tag', {
@ -400,6 +317,62 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl']
},
}),
])
await Promise.all([
mutate({
mutation: joinGroupMutation,
variables: {
groupId: 'g0',
userId: 'u2',
},
}),
mutate({
mutation: joinGroupMutation,
variables: {
groupId: 'g0',
userId: 'u3',
},
}),
mutate({
mutation: joinGroupMutation,
variables: {
groupId: 'g0',
userId: 'u4',
},
}),
mutate({
mutation: joinGroupMutation,
variables: {
groupId: 'g0',
userId: 'u6',
},
}),
])
await Promise.all([
mutate({
mutation: changeGroupMemberRoleMutation,
variables: {
groupId: 'g0',
userId: 'u2',
roleInGroup: 'usual',
},
}),
mutate({
mutation: changeGroupMemberRoleMutation,
variables: {
groupId: 'g0',
userId: 'u4',
roleInGroup: 'admin',
},
}),
mutate({
mutation: changeGroupMemberRoleMutation,
variables: {
groupId: 'g0',
userId: 'u3',
roleInGroup: 'owner',
},
}),
])
authenticatedUser = await jennyRostock.toJson()
await Promise.all([
@ -416,6 +389,77 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl']
},
}),
])
await Promise.all([
mutate({
mutation: joinGroupMutation,
variables: {
groupId: 'g1',
userId: 'u1',
},
}),
mutate({
mutation: joinGroupMutation,
variables: {
groupId: 'g1',
userId: 'u2',
},
}),
mutate({
mutation: joinGroupMutation,
variables: {
groupId: 'g1',
userId: 'u5',
},
}),
mutate({
mutation: joinGroupMutation,
variables: {
groupId: 'g1',
userId: 'u6',
},
}),
mutate({
mutation: joinGroupMutation,
variables: {
groupId: 'g1',
userId: 'u7',
},
}),
])
await Promise.all([
mutate({
mutation: changeGroupMemberRoleMutation,
variables: {
groupId: 'g0',
userId: 'u1',
roleInGroup: 'usual',
},
}),
mutate({
mutation: changeGroupMemberRoleMutation,
variables: {
groupId: 'g0',
userId: 'u2',
roleInGroup: 'usual',
},
}),
mutate({
mutation: changeGroupMemberRoleMutation,
variables: {
groupId: 'g0',
userId: 'u5',
roleInGroup: 'admin',
},
}),
mutate({
mutation: changeGroupMemberRoleMutation,
variables: {
groupId: 'g0',
userId: 'u6',
roleInGroup: 'owner',
},
}),
])
authenticatedUser = await bobDerBaumeister.toJson()
await Promise.all([
@ -432,6 +476,62 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl']
},
}),
])
await Promise.all([
mutate({
mutation: joinGroupMutation,
variables: {
groupId: 'g2',
userId: 'u4',
},
}),
mutate({
mutation: joinGroupMutation,
variables: {
groupId: 'g2',
userId: 'u5',
},
}),
mutate({
mutation: joinGroupMutation,
variables: {
groupId: 'g2',
userId: 'u6',
},
}),
mutate({
mutation: joinGroupMutation,
variables: {
groupId: 'g2',
userId: 'u7',
},
}),
])
await Promise.all([
mutate({
mutation: changeGroupMemberRoleMutation,
variables: {
groupId: 'g0',
userId: 'u4',
roleInGroup: 'usual',
},
}),
mutate({
mutation: changeGroupMemberRoleMutation,
variables: {
groupId: 'g0',
userId: 'u5',
roleInGroup: 'usual',
},
}),
mutate({
mutation: changeGroupMemberRoleMutation,
variables: {
groupId: 'g0',
userId: 'u6',
roleInGroup: 'usual',
},
}),
])
// Create Posts

View File

@ -7,3 +7,12 @@
export function gql(strings) {
return strings.join('')
}
// sometime we have to wait to check a db state by having a look into the db in a certain moment
// or we wait a bit to check if we missed to set an await somewhere
// see: https://www.sitepoint.com/delay-sleep-pause-wait/
export function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms))
}
// usage 4 seconds for example
// await sleep(4 * 1000)

View File

@ -1,9 +1,14 @@
import trunc from 'trunc-html'
import { DESCRIPTION_EXCERPT_HTML_LENGTH } from '../constants/groups'
export default {
Mutation: {
CreateGroup: async (resolve, root, args, context, info) => {
args.descriptionExcerpt = trunc(args.description, 120).html
args.descriptionExcerpt = trunc(args.description, DESCRIPTION_EXCERPT_HTML_LENGTH).html
return resolve(root, args, context, info)
},
UpdateGroup: async (resolve, root, args, context, info) => {
args.descriptionExcerpt = trunc(args.description, DESCRIPTION_EXCERPT_HTML_LENGTH).html
return resolve(root, args, context, info)
},
CreatePost: async (resolve, root, args, context, info) => {

View File

@ -52,6 +52,145 @@ const isMySocialMedia = rule({
return socialMedia.ownedBy.node.id === user.id
})
const isAllowedToChangeGroupSettings = rule({
cache: 'no_cache',
})(async (_parent, args, { user, driver }) => {
if (!(user && user.id)) return false
const ownerId = user.id
const { id: groupId } = args
const session = driver.session()
const readTxPromise = session.readTransaction(async (transaction) => {
const transactionResponse = await transaction.run(
`
MATCH (owner:User {id: $ownerId})-[membership:MEMBER_OF]->(group:Group {id: $groupId})
RETURN group {.*}, owner {.*, myRoleInGroup: membership.role}
`,
{ groupId, ownerId },
)
return {
owner: transactionResponse.records.map((record) => record.get('owner'))[0],
group: transactionResponse.records.map((record) => record.get('group'))[0],
}
})
try {
const { owner, group } = await readTxPromise
return !!group && !!owner && ['owner'].includes(owner.myRoleInGroup)
} catch (error) {
throw new Error(error)
} finally {
session.close()
}
})
const isAllowedSeeingMembersOfGroup = rule({
cache: 'no_cache',
})(async (_parent, args, { user, driver }) => {
if (!(user && user.id)) return false
const { id: groupId } = args
const session = driver.session()
const readTxPromise = session.readTransaction(async (transaction) => {
const transactionResponse = await transaction.run(
`
MATCH (group:Group {id: $groupId})
OPTIONAL MATCH (member:User {id: $userId})-[membership:MEMBER_OF]->(group)
RETURN group {.*}, member {.*, myRoleInGroup: membership.role}
`,
{ groupId, userId: user.id },
)
return {
member: transactionResponse.records.map((record) => record.get('member'))[0],
group: transactionResponse.records.map((record) => record.get('group'))[0],
}
})
try {
const { member, group } = await readTxPromise
return (
!!group &&
(group.groupType === 'public' ||
(['closed', 'hidden'].includes(group.groupType) &&
!!member &&
['usual', 'admin', 'owner'].includes(member.myRoleInGroup)))
)
} catch (error) {
throw new Error(error)
} finally {
session.close()
}
})
const isAllowedToChangeGroupMemberRole = rule({
cache: 'no_cache',
})(async (_parent, args, { user, driver }) => {
if (!(user && user.id)) return false
const adminId = user.id
const { groupId, userId, roleInGroup } = args
if (adminId === userId) return false
const session = driver.session()
const readTxPromise = session.readTransaction(async (transaction) => {
const transactionResponse = await transaction.run(
`
MATCH (admin:User {id: $adminId})-[adminMembership:MEMBER_OF]->(group:Group {id: $groupId})
OPTIONAL MATCH (group)<-[userMembership:MEMBER_OF]-(member:User {id: $userId})
RETURN group {.*}, admin {.*, myRoleInGroup: adminMembership.role}, member {.*, myRoleInGroup: userMembership.role}
`,
{ groupId, adminId, userId },
)
return {
admin: transactionResponse.records.map((record) => record.get('admin'))[0],
group: transactionResponse.records.map((record) => record.get('group'))[0],
member: transactionResponse.records.map((record) => record.get('member'))[0],
}
})
try {
const { admin, group, member } = await readTxPromise
return (
!!group &&
!!admin &&
(!member ||
(!!member &&
(member.myRoleInGroup === roleInGroup || !['owner'].includes(member.myRoleInGroup)))) &&
((['admin'].includes(admin.myRoleInGroup) &&
['pending', 'usual', 'admin'].includes(roleInGroup)) ||
(['owner'].includes(admin.myRoleInGroup) &&
['pending', 'usual', 'admin', 'owner'].includes(roleInGroup)))
)
} catch (error) {
throw new Error(error)
} finally {
session.close()
}
})
const isAllowedToJoinGroup = rule({
cache: 'no_cache',
})(async (_parent, args, { user, driver }) => {
if (!(user && user.id)) return false
const { groupId, userId } = args
const session = driver.session()
const readTxPromise = session.readTransaction(async (transaction) => {
const transactionResponse = await transaction.run(
`
MATCH (group:Group {id: $groupId})
OPTIONAL MATCH (group)<-[membership:MEMBER_OF]-(member:User {id: $userId})
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 && (group.groupType !== 'hidden' || (!!member && !!member.myRoleInGroup))
} catch (error) {
throw new Error(error)
} finally {
session.close()
}
})
const isAuthor = rule({
cache: 'no_cache',
})(async (_parent, args, { user, driver }) => {
@ -78,7 +217,7 @@ const isAuthor = rule({
const isDeletingOwnAccount = rule({
cache: 'no_cache',
})(async (parent, args, context, info) => {
})(async (parent, args, context, _info) => {
return context.user.id === args.id
})
@ -115,6 +254,7 @@ export default shield(
statistics: allow,
currentUser: allow,
Group: isAuthenticated,
GroupMembers: isAllowedSeeingMembersOfGroup,
Post: allow,
profilePagePosts: allow,
Comment: allow,
@ -142,6 +282,9 @@ export default shield(
SignupVerification: allow,
UpdateUser: onlyYourself,
CreateGroup: isAuthenticated,
UpdateGroup: isAllowedToChangeGroupSettings,
JoinGroup: isAllowedToJoinGroup,
ChangeGroupMemberRole: isAllowedToChangeGroupMemberRole,
CreatePost: isAuthenticated,
UpdatePost: isAuthor,
DeletePost: isAuthor,

View File

@ -30,6 +30,10 @@ export default {
args.slug = args.slug || (await uniqueSlug(args.name, isUniqueFor(context, 'Group')))
return resolve(root, args, context, info)
},
UpdateGroup: async (resolve, root, args, context, info) => {
args.slug = args.slug || (await uniqueSlug(args.name, isUniqueFor(context, 'Group')))
return resolve(root, args, context, info)
},
CreatePost: async (resolve, root, args, context, info) => {
args.slug = args.slug || (await uniqueSlug(args.title, isUniqueFor(context, 'Post')))
return resolve(root, args, context, info)

View File

@ -2,12 +2,13 @@ import { getNeode, getDriver } from '../db/neo4j'
import createServer from '../server'
import { createTestClient } from 'apollo-server-testing'
import Factory, { cleanDatabase } from '../db/factories'
import { createGroupMutation } from '../db/graphql/groups'
import { createGroupMutation, updateGroupMutation } from '../db/graphql/groups'
import { createPostMutation } from '../db/graphql/posts'
import { signupVerificationMutation } from '../db/graphql/authentications'
let authenticatedUser
let variables
const categoryIds = ['cat9']
const driver = getDriver()
const neode = getNeode()
@ -62,8 +63,6 @@ afterEach(async () => {
describe('slugifyMiddleware', () => {
describe('CreateGroup', () => {
const categoryIds = ['cat9']
beforeEach(() => {
variables = {
...variables,
@ -130,15 +129,14 @@ describe('slugifyMiddleware', () => {
})
it('chooses another slug', async () => {
variables = {
...variables,
name: 'Pre-Existing Group',
about: 'As an about',
}
await expect(
mutate({
mutation: createGroupMutation,
variables,
variables: {
...variables,
name: 'Pre-Existing Group',
about: 'As an about',
},
}),
).resolves.toMatchObject({
data: {
@ -151,15 +149,17 @@ describe('slugifyMiddleware', () => {
describe('but if the client specifies a slug', () => {
it('rejects CreateGroup', async (done) => {
variables = {
...variables,
name: 'Pre-Existing Group',
about: 'As an about',
slug: 'pre-existing-group',
}
try {
await expect(
mutate({ mutation: createGroupMutation, variables }),
mutate({
mutation: createGroupMutation,
variables: {
...variables,
name: 'Pre-Existing Group',
about: 'As an about',
slug: 'pre-existing-group',
},
}),
).resolves.toMatchObject({
errors: [
{
@ -189,9 +189,163 @@ describe('slugifyMiddleware', () => {
})
})
describe('CreatePost', () => {
const categoryIds = ['cat9']
describe('UpdateGroup', () => {
let createGroupResult
beforeEach(async () => {
createGroupResult = await mutate({
mutation: createGroupMutation,
variables: {
name: 'The Best Group',
slug: 'the-best-group',
about: 'Some about',
description: 'Some description' + descriptionAdditional100,
groupType: 'closed',
actionRadius: 'national',
categoryIds,
},
})
})
describe('if group exists', () => {
describe('if new slug not(!) exists', () => {
describe('setting slug by group name', () => {
it('has the new slug', async () => {
await expect(
mutate({
mutation: updateGroupMutation,
variables: {
id: createGroupResult.data.CreateGroup.id,
name: 'My Best Group',
},
}),
).resolves.toMatchObject({
data: {
UpdateGroup: {
name: 'My Best Group',
slug: 'my-best-group',
about: 'Some about',
description: 'Some description' + descriptionAdditional100,
groupType: 'closed',
actionRadius: 'national',
myRole: 'owner',
},
},
})
})
})
describe('setting slug explicitly', () => {
it('has the new slug', async () => {
await expect(
mutate({
mutation: updateGroupMutation,
variables: {
id: createGroupResult.data.CreateGroup.id,
slug: 'my-best-group',
},
}),
).resolves.toMatchObject({
data: {
UpdateGroup: {
name: 'The Best Group',
slug: 'my-best-group',
about: 'Some about',
description: 'Some description' + descriptionAdditional100,
groupType: 'closed',
actionRadius: 'national',
myRole: 'owner',
},
},
})
})
})
})
describe('if new slug exists in another group', () => {
beforeEach(async () => {
await mutate({
mutation: createGroupMutation,
variables: {
name: 'Pre-Existing Group',
slug: 'pre-existing-group',
about: 'Some about',
description: 'Some description' + descriptionAdditional100,
groupType: 'closed',
actionRadius: 'national',
categoryIds,
},
})
})
describe('setting slug by group name', () => {
it('has unique slug "*-1"', async () => {
await expect(
mutate({
mutation: updateGroupMutation,
variables: {
id: createGroupResult.data.CreateGroup.id,
name: 'Pre-Existing Group',
},
}),
).resolves.toMatchObject({
data: {
UpdateGroup: {
name: 'Pre-Existing Group',
slug: 'pre-existing-group-1',
about: 'Some about',
description: 'Some description' + descriptionAdditional100,
groupType: 'closed',
actionRadius: 'national',
myRole: 'owner',
},
},
})
})
})
describe('setting slug explicitly', () => {
it('rejects UpdateGroup', async (done) => {
try {
await expect(
mutate({
mutation: updateGroupMutation,
variables: {
id: createGroupResult.data.CreateGroup.id,
slug: 'pre-existing-group',
},
}),
).resolves.toMatchObject({
errors: [
{
message: 'Group with this slug already exists!',
},
],
})
done()
} catch (error) {
throw new Error(`
${error}
Probably your database has no unique constraints!
To see all constraints go to http://localhost:7474/browser/ and
paste the following:
\`\`\`
CALL db.constraints();
\`\`\`
Learn how to setup the database here:
https://github.com/Ocelot-Social-Community/Ocelot-Social/blob/master/backend/README.md#database-indices-and-constraints
`)
}
})
})
})
})
})
describe('CreatePost', () => {
beforeEach(() => {
variables = {
...variables,
@ -252,16 +406,15 @@ describe('slugifyMiddleware', () => {
})
it('chooses another slug', async () => {
variables = {
...variables,
title: 'Pre-existing post',
content: 'Some content',
categoryIds,
}
await expect(
mutate({
mutation: createPostMutation,
variables,
variables: {
...variables,
title: 'Pre-existing post',
content: 'Some content',
categoryIds,
},
}),
).resolves.toMatchObject({
data: {
@ -274,16 +427,18 @@ describe('slugifyMiddleware', () => {
describe('but if the client specifies a slug', () => {
it('rejects CreatePost', async (done) => {
variables = {
...variables,
title: 'Pre-existing post',
content: 'Some content',
slug: 'pre-existing-post',
categoryIds,
}
try {
await expect(
mutate({ mutation: createPostMutation, variables }),
mutate({
mutation: createPostMutation,
variables: {
...variables,
title: 'Pre-existing post',
content: 'Some content',
slug: 'pre-existing-post',
categoryIds,
},
}),
).resolves.toMatchObject({
errors: [
{
@ -313,6 +468,8 @@ describe('slugifyMiddleware', () => {
})
})
it.todo('UpdatePost')
describe('SignupVerification', () => {
beforeEach(() => {
variables = {

View File

@ -9,8 +9,7 @@ export default {
updatedAt: {
type: 'string',
isoDate: true,
required: true,
default: () => new Date().toISOString(),
required: false,
},
post: {
type: 'relationship',

View File

@ -5,6 +5,7 @@ import { CATEGORIES_MIN, CATEGORIES_MAX } from '../../constants/categories'
import { DESCRIPTION_WITHOUT_HTML_LENGTH_MIN } from '../../constants/groups'
import { removeHtmlTags } from '../../middleware/helpers/cleanHtml.js'
import Resolver from './helpers/Resolver'
import { mergeImage } from './images/images'
export default {
Query: {
@ -34,10 +35,31 @@ export default {
`
}
}
const result = await txc.run(groupCypher, {
const transactionResponse = await txc.run(groupCypher, {
userId: context.user.id,
})
return result.records.map((record) => record.get('group'))
return transactionResponse.records.map((record) => record.get('group'))
})
try {
return await readTxResultPromise
} catch (error) {
throw new Error(error)
} finally {
session.close()
}
},
GroupMembers: async (_object, params, context, _resolveInfo) => {
const { id: groupId } = params
const session = context.driver.session()
const readTxResultPromise = session.readTransaction(async (txc) => {
const groupMemberCypher = `
MATCH (user:User)-[membership:MEMBER_OF]->(:Group {id: $groupId})
RETURN user {.*, myRoleInGroup: membership.role}
`
const transactionResponse = await txc.run(groupMemberCypher, {
groupId,
})
return transactionResponse.records.map((record) => record.get('user'))
})
try {
return await readTxResultPromise
@ -87,9 +109,10 @@ export default {
MATCH (owner:User {id: $userId})
MERGE (owner)-[:CREATED]->(group)
MERGE (owner)-[membership:MEMBER_OF]->(group)
SET membership.createdAt = toString(datetime())
SET membership.updatedAt = toString(datetime())
SET membership.role = 'owner'
SET
membership.createdAt = toString(datetime()),
membership.updatedAt = null,
membership.role = 'owner'
${categoriesCypher}
RETURN group {.*, myRole: membership.role}
`,
@ -101,8 +124,7 @@ export default {
return group
})
try {
const group = await writeTxResultPromise
return group
return await writeTxResultPromise
} catch (error) {
if (error.code === 'Neo.ClientError.Schema.ConstraintValidationFailed')
throw new UserInputError('Group with this slug already exists!')
@ -111,6 +133,137 @@ export default {
session.close()
}
},
UpdateGroup: async (_parent, params, context, _resolveInfo) => {
const { categoryIds } = params
const { id: groupId, avatar: avatarInput } = params
delete params.categoryIds
if (CONFIG.CATEGORIES_ACTIVE && categoryIds) {
if (categoryIds.length < CATEGORIES_MIN) {
throw new UserInputError('Too view categories!')
}
if (categoryIds.length > CATEGORIES_MAX) {
throw new UserInputError('Too many categories!')
}
}
if (
params.description &&
removeHtmlTags(params.description).length < DESCRIPTION_WITHOUT_HTML_LENGTH_MIN
) {
throw new UserInputError('Description too short!')
}
const session = context.driver.session()
if (CONFIG.CATEGORIES_ACTIVE && categoryIds && categoryIds.length) {
const cypherDeletePreviousRelations = `
MATCH (group:Group {id: $groupId})-[previousRelations:CATEGORIZED]->(category:Category)
DELETE previousRelations
RETURN group, category
`
await session.writeTransaction((transaction) => {
return transaction.run(cypherDeletePreviousRelations, { groupId })
})
}
const writeTxResultPromise = session.writeTransaction(async (transaction) => {
let updateGroupCypher = `
MATCH (group:Group {id: $groupId})
SET group += $params
SET group.updatedAt = toString(datetime())
WITH group
`
if (CONFIG.CATEGORIES_ACTIVE && categoryIds && categoryIds.length) {
updateGroupCypher += `
UNWIND $categoryIds AS categoryId
MATCH (category:Category {id: categoryId})
MERGE (group)-[:CATEGORIZED]->(category)
WITH group
`
}
updateGroupCypher += `
OPTIONAL MATCH (:User {id: $userId})-[membership:MEMBER_OF]->(group)
RETURN group {.*, myRole: membership.role}
`
const transactionResponse = await transaction.run(updateGroupCypher, {
groupId,
userId: context.user.id,
categoryIds,
params,
})
const [group] = await transactionResponse.records.map((record) => record.get('group'))
if (avatarInput) {
await mergeImage(group, 'AVATAR_IMAGE', avatarInput, { transaction })
}
return group
})
try {
return await writeTxResultPromise
} catch (error) {
if (error.code === 'Neo.ClientError.Schema.ConstraintValidationFailed')
throw new UserInputError('Group with this slug already exists!')
throw new Error(error)
} finally {
session.close()
}
},
JoinGroup: async (_parent, params, context, _resolveInfo) => {
const { groupId, userId } = params
const session = context.driver.session()
const writeTxResultPromise = session.writeTransaction(async (transaction) => {
const joinGroupCypher = `
MATCH (member:User {id: $userId}), (group:Group {id: $groupId})
MERGE (member)-[membership:MEMBER_OF]->(group)
ON CREATE SET
membership.createdAt = toString(datetime()),
membership.updatedAt = null,
membership.role =
CASE WHEN group.groupType = 'public'
THEN 'usual'
ELSE 'pending'
END
RETURN member {.*, myRoleInGroup: membership.role}
`
const transactionResponse = await transaction.run(joinGroupCypher, { 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()
const writeTxResultPromise = session.writeTransaction(async (transaction) => {
const joinGroupCypher = `
MATCH (member:User {id: $userId}), (group:Group {id: $groupId})
MERGE (member)-[membership:MEMBER_OF]->(group)
ON CREATE SET
membership.createdAt = toString(datetime()),
membership.updatedAt = null,
membership.role = $roleInGroup
ON MATCH SET
membership.updatedAt = toString(datetime()),
membership.role = $roleInGroup
RETURN member {.*, myRoleInGroup: membership.role}
`
const transactionResponse = await transaction.run(joinGroupCypher, {
groupId,
userId,
roleInGroup,
})
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()
}
},
},
Group: {
...Resolver('Group', {

File diff suppressed because it is too large Load Diff

View File

@ -131,11 +131,11 @@ export default {
delete params.image
const session = context.driver.session()
let updatePostCypher = `
MATCH (post:Post {id: $params.id})
SET post += $params
SET post.updatedAt = toString(datetime())
WITH post
`
MATCH (post:Post {id: $params.id})
SET post += $params
SET post.updatedAt = toString(datetime())
WITH post
`
if (CONFIG.CATEGORIES_ACTIVE && categoryIds && categoryIds.length) {
const cypherDeletePreviousRelations = `
@ -358,7 +358,7 @@ export default {
undefinedToNull: ['activityId', 'objectId', 'language', 'pinnedAt', 'pinned'],
hasMany: {
tags: '-[:TAGGED]->(related:Tag)',
// categories: '-[:CATEGORIZED]->(related:Category)',
categories: '-[:CATEGORIZED]->(related:Category)',
comments: '<-[:COMMENTS]-(related:Comment)',
shoutedBy: '<-[:SHOUTED]-(related:User)',
emotions: '<-[related:EMOTED]',

View File

@ -368,7 +368,7 @@ describe('UpdatePost', () => {
describe('unauthenticated', () => {
it('throws authorization error', async () => {
authenticatedUser = null
expect(mutate({ mutation: updatePostMutation, variables })).resolves.toMatchObject({
await expect(mutate({ mutation: updatePostMutation, variables })).resolves.toMatchObject({
errors: [{ message: 'Not Authorized!' }],
data: { UpdatePost: null },
})

View File

@ -59,7 +59,7 @@ input _GroupFilter {
type Query {
Group(
isMember: Boolean # if 'undefined' or 'null' then all groups
isMember: Boolean # if 'undefined' or 'null' then get all groups
id: ID
name: String
slug: String
@ -67,18 +67,28 @@ type Query {
updatedAt: String
about: String
description: String
# groupType: GroupType # test this
# actionRadius: GroupActionRadius # test this
# avatar: ImageInput # test this
locationName: String
first: Int
offset: Int
orderBy: [_GroupOrdering]
filter: _GroupFilter
): [Group]
AvailableGroupTypes: [GroupType]!
GroupMembers(
id: ID!
first: Int
offset: Int
orderBy: [_UserOrdering]
filter: _UserFilter
): [User]
AvailableGroupActionRadii: [GroupActionRadius]!
# AvailableGroupTypes: [GroupType]!
AvailableGroupMemberRoles: [GroupMemberRole]!
# AvailableGroupActionRadii: [GroupActionRadius]!
# AvailableGroupMemberRoles: [GroupMemberRole]!
}
type Mutation {
@ -86,24 +96,38 @@ type Mutation {
id: ID
name: String!
slug: String
avatar: ImageInput
about: String
description: String!
groupType: GroupType!
actionRadius: GroupActionRadius!
categoryIds: [ID]
locationName: String
# avatar: ImageInput # a group can not be created with an avatar
locationName: String # test this as result
): Group
UpdateGroup(
id: ID!
name: String
slug: String
avatar: ImageInput
locationName: String
about: String
description: String
# groupType: GroupType # is not possible at the moment and has to be discussed. may be in the stronger direction: public → closed → hidden
actionRadius: GroupActionRadius
categoryIds: [ID]
avatar: ImageInput # test this as result
locationName: String # test this as result
): Group
DeleteGroup(id: ID!): Group
# DeleteGroup(id: ID!): Group
JoinGroup(
groupId: ID!
userId: ID!
): User
ChangeGroupMemberRole(
groupId: ID!
userId: ID!
roleInGroup: GroupMemberRole!
): User
}

View File

@ -114,6 +114,8 @@ type User {
badgesCount: Int! @cypher(statement: "MATCH (this)<-[:REWARDED]-(r:Badge) RETURN COUNT(r)")
emotions: [EMOTED]
myRoleInGroup: GroupMemberRole
}

View File

@ -0,0 +1,5 @@
<!-- Generated by IcoMoon.io -->
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
<title>child</title>
<path d="M12 3c2.202 0 3.791 1.007 4.531 2.313 0.026-0.041 0.034-0.084 0.063-0.125 0.453-0.641 1.315-1.188 2.406-1.188v2c-0.453 0-0.588 0.111-0.719 0.281 3.845 0.921 6.812 4.105 7.563 8.063 1.193 0.397 2.156 1.337 2.156 2.656 0 1.365-1.024 2.33-2.281 2.688-0.816 4.701-4.82 8.313-9.719 8.313s-8.903-3.611-9.719-8.313c-1.257-0.357-2.281-1.323-2.281-2.688s1.024-2.33 2.281-2.688c0.741-4.271 4.122-7.637 8.406-8.219-0.39-0.574-1.192-1.094-2.688-1.094v-2zM16 8c-4.093 0-7.461 3.121-7.906 7.125l-0.094 0.875h-1c-0.555 0-1 0.445-1 1s0.445 1 1 1h1l0.094 0.875c0.445 4.004 3.813 7.125 7.906 7.125s7.461-3.121 7.906-7.125l0.094-0.875h1c0.555 0 1-0.445 1-1s-0.445-1-1-1h-0.875l-0.125-0.875c-0.536-4.019-3.907-7.125-8-7.125zM12.5 16c0.828 0 1.5 0.672 1.5 1.5s-0.672 1.5-1.5 1.5-1.5-0.672-1.5-1.5 0.672-1.5 1.5-1.5zM19.5 16c0.828 0 1.5 0.672 1.5 1.5s-0.672 1.5-1.5 1.5-1.5-0.672-1.5-1.5 0.672-1.5 1.5-1.5z"></path>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@ -0,0 +1,5 @@
<!-- Generated by IcoMoon.io -->
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
<title>desktop</title>
<path d="M2 6h28v18h-13v2h5v2h-12v-2h5v-2h-13v-18zM4 8v14h24v-14h-24z"></path>
</svg>

After

Width:  |  Height:  |  Size: 240 B

View File

@ -0,0 +1,5 @@
<!-- Generated by IcoMoon.io -->
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
<title>ellipsis-h</title>
<path d="M6 14c1.105 0 2 0.895 2 2s-0.895 2-2 2-2-0.895-2-2 0.895-2 2-2zM16 14c1.105 0 2 0.895 2 2s-0.895 2-2 2-2-0.895-2-2 0.895-2 2-2zM26 14c1.105 0 2 0.895 2 2s-0.895 2-2 2-2-0.895-2-2 0.895-2 2-2z"></path>
</svg>

After

Width:  |  Height:  |  Size: 374 B

View File

@ -0,0 +1,5 @@
<!-- Generated by IcoMoon.io -->
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
<title>home</title>
<path d="M16 2.594l0.719 0.688 13 13-1.438 1.438-1.281-1.281v11.563h-9v-10h-4v10h-9v-11.563l-1.281 1.281-1.438-1.438 13-13zM16 5.438l-9 9v11.563h5v-10h8v10h5v-11.563z"></path>
</svg>

After

Width:  |  Height:  |  Size: 334 B

View File

@ -0,0 +1,5 @@
<!-- Generated by IcoMoon.io -->
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
<title>lightbulb-o</title>
<path d="M6.813 2.406l2.094 2.094-1.406 1.406-2.094-2.094zM25.188 2.406l1.406 1.406-2.094 2.094-1.406-1.406zM16 3.031c4.934-0.047 9 4.027 9 8.969 0 2.706-1.249 5.062-2.906 6.719-1.238 1.15-2 2.627-2 4.094v1.188h-0.094v4h-2.281c-0.347 0.597-0.982 1-1.719 1s-1.372-0.403-1.719-1h-2.281v-6c-0.203-1.117-0.794-2.212-1.75-3.031-2.233-1.898-3.573-4.845-3.125-8.094 0.561-4.039 3.789-7.316 7.844-7.781 0.011-0.001 0.020 0.001 0.031 0 0.336-0.041 0.671-0.059 1-0.063zM16 5.031c-0.258 0.004-0.518 0.030-0.781 0.063-3.131 0.348-5.687 2.881-6.125 6.031-0.352 2.552 0.702 4.811 2.469 6.313 1.388 1.19 2.124 2.848 2.344 4.563h4.375c0.236-1.792 1.094-3.434 2.438-4.688l-0.031-0.031c1.343-1.343 2.313-3.187 2.313-5.281 0-3.861-3.135-7.024-7-6.969zM2 12h3v2h-3v-2zM27 12h3v2h-3v-2zM7.5 20.094l1.406 1.406-2.094 2.094-1.406-1.406zM24.5 20.094l2.094 2.094-1.406 1.406-2.094-2.094zM14 24v2h4v-2h-4z"></path>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 26.4.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
width="24px" height="24.3px" viewBox="0 0 24 24.3" style="enable-background:new 0 0 24 24.3;" xml:space="preserve">
<style type="text/css">
.st0{fill:#333333;}
</style>
<g>
<circle class="st0" cx="17.1" cy="2.9" r="2.9"/>
<path class="st0" d="M23.5,5.6L23.5,5.6c-0.6-0.5-1.4-0.5-2,0.1l-2.7,3l-2.2-1.4c0.1,0.3,0.1,0.6,0.1,0.9c0,0.6-0.2,1.1-0.5,1.6
L16,10.2l1.5,1c0.4,0.3,0.9,0.4,1.4,0.4c0.1,0,0.2,0,0.4,0c0.6-0.1,1.2-0.4,1.6-0.9l2.7-3.1C24.2,7,24.1,6.1,23.5,5.6z"/>
<path class="st0" d="M6.5,17c-0.3-0.2-0.5-0.5-0.7-0.8c0,0,0,0,0,0L4.6,19l-4,2.7C0,22.1-0.2,23,0.2,23.6l0,0
c0.4,0.6,1.3,0.8,1.9,0.4l4.7-3.2l1.4-3.3l-0.7-0.1C7.2,17.4,6.8,17.2,6.5,17z"/>
<path class="st0" d="M16.3,15.1l-4.4-0.5l3.5-4.7l0.3-0.4C16,9,16.2,8.6,16.2,8.2c0-0.7-0.4-1.4-1-1.7c-0.1,0-0.1-0.1-0.2-0.1
l-1.8-0.7L9.4,4.4C9.1,4.3,8.8,4.3,8.5,4.3c-0.3,0-0.5,0-0.8,0.1c-0.5,0.2-1,0.5-1.3,0.9L3.7,8.5C3.2,9.1,3.3,10,3.9,10.5l0,0
c0.3,0.2,0.6,0.3,0.9,0.3c0.4,0,0.8-0.2,1.1-0.5L8.6,7L11.4,8l-4.8,6.5l0,0c-0.1,0.1-0.1,0.3-0.2,0.4c-0.2,0.8,0.4,1.7,1.3,1.8
l0.9,0.1l6.9,0.9l-1.7,2.7c-0.4,0.7-0.2,1.5,0.5,1.9h0c0.2,0.1,0.5,0.2,0.7,0.2c0.5,0,0.9-0.2,1.2-0.7l2-3.2
c0.4-0.7,0.5-1.6,0.1-2.3C17.8,15.7,17.1,15.2,16.3,15.1z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -0,0 +1,5 @@
<!-- Generated by IcoMoon.io -->
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
<title>music</title>
<path d="M27 3.781v17.219c0 2.197-1.803 4-4 4s-4-1.803-4-4 1.803-4 4-4c0.732 0 1.407 0.214 2 0.563v-7.375l-14 2.625v11.188c0 2.197-1.803 4-4 4s-4-1.803-4-4 1.803-4 4-4c0.732 0 1.407 0.214 2 0.563v-13.406l0.813-0.125 16-3zM25 6.188l-14 2.625v2l14-2.625v-2zM23 19c-1.116 0-2 0.884-2 2s0.884 2 2 2 2-0.884 2-2-0.884-2-2-2zM7 22c-1.116 0-2 0.884-2 2s0.884 2 2 2 2-0.884 2-2-0.884-2-2-2z"></path>
</svg>

After

Width:  |  Height:  |  Size: 551 B

View File

@ -0,0 +1,5 @@
<!-- Generated by IcoMoon.io -->
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
<title>suitcase</title>
<path d="M14 3h4c1.093 0 2 0.907 2 2v1h3v-1h2v1h2c1.093 0 2 0.907 2 2v16c0 1.093-0.907 2-2 2h-22c-1.093 0-2-0.907-2-2v-16c0-1.093 0.907-2 2-2h2v-1h2v1h3v-1c0-1.093 0.907-2 2-2zM14 5v1h4v-1h-4zM5 8v16h2v-15h2v15h14v-15h2v15h2v-16h-22z"></path>
</svg>

After

Width:  |  Height:  |  Size: 405 B

View File

@ -8,6 +8,9 @@ let wrapper
describe('FilterMenu.vue', () => {
const mocks = {
$t: jest.fn((string) => string),
$env: {
CATEGORIES_ACTIVE: true,
},
}
const getters = {

View File

@ -14,6 +14,7 @@
<div class="filter-menu-options">
<h2 class="title">{{ $t('filter-menu.filter-by') }}</h2>
<following-filter />
<categories-filter v-if="categoriesActive" />
</div>
<div class="filter-menu-options">
<h2 class="title">{{ $t('filter-menu.order-by') }}</h2>
@ -28,17 +29,24 @@ import Dropdown from '~/components/Dropdown'
import { mapGetters } from 'vuex'
import FollowingFilter from './FollowingFilter'
import OrderByFilter from './OrderByFilter'
import CategoriesFilter from './CategoriesFilter'
export default {
components: {
Dropdown,
FollowingFilter,
CategoriesFilter,
OrderByFilter,
},
props: {
placement: { type: String },
offset: { type: [String, Number] },
},
data() {
return {
categoriesActive: this.$env.CATEGORIES_ACTIVE,
}
},
computed: {
...mapGetters({
filterActive: 'posts/isActive',

View File

@ -218,22 +218,25 @@
},
"category": {
"name": {
"animal-protection": "Schutz der Tiere",
"art-culture-sport": "Kunst, Kultur & Sport",
"consumption-sustainability": "Verbrauch & Nachhaltigkeit",
"cooperation-development": "Zusammenarbeit & Entwicklung",
"democracy-politics": "Demokratie & Politik",
"economy-finances": "Wirtschaft & Finanzen",
"education-sciences": "Bildung & Wissenschaft",
"energy-technology": "Energie & Technologie",
"environment-nature": "Umwelt & Natur",
"freedom-of-speech": "Redefreiheit",
"global-peace-nonviolence": "Globaler Frieden & Gewaltlosigkeit",
"happiness-values": "Glück & Werte",
"health-wellbeing": "Gesundheit & Wohlbefinden",
"human-rights-justice": "Menschenrechte & Gerechtigkeit",
"it-internet-data-privacy": "IT, Internet & Datenschutz",
"just-for-fun": "Nur zum Spaß"
"body-and-excercise": "Körper & Bewegung",
"children": "Kinder",
"culture": "Kultur",
"economy": "Wirtschaft",
"energy": "Energie",
"finance": "Finanzen",
"health": "Gesundheit",
"home": "Wohnen",
"it-and-media": "IT & Medien",
"law": "Recht",
"miscellaneous": "Sonstiges",
"mobility": "Mobilität",
"nature": "Natur",
"networking": "Vernetzung",
"peace": "Frieden",
"politics": "Politik",
"psyche": "Psyche",
"science": "Wissenschaft",
"spirituality": "Spiritualität"
}
},
"emotions-label": {

View File

@ -218,22 +218,25 @@
},
"category": {
"name": {
"animal-protection": "Animal Protection",
"art-culture-sport": "Art, Culture, & Sport",
"consumption-sustainability": "Consumption & Sustainability",
"cooperation-development": "Cooperation & Development",
"democracy-politics": "Democracy & Politics",
"economy-finances": "Economy & Finances",
"education-sciences": "Education & Sciences",
"energy-technology": "Energy & Technology",
"environment-nature": "Environment & Nature",
"freedom-of-speech": "Freedom of Speech",
"global-peace-nonviolence": "Global Peace & Nonviolence",
"happiness-values": "Happiness & Values",
"health-wellbeing": "Health & Wellbeing",
"human-rights-justice": "Human Rights & Justice",
"it-internet-data-privacy": "IT, Internet & Data Privacy",
"just-for-fun": "Just for Fun"
"body-and-excercise": "Body & Excercise",
"children": "Children",
"culture": "Culture",
"economy": "Economy",
"energy": "Energy",
"finance": "Finance",
"health": "Health",
"home": "Home",
"it-and-media": "IT & Media",
"law": "Law",
"miscellaneous": "Miscellaneous",
"mobility": "Mobility",
"nature": "Nature",
"networking": "Networking",
"peace": "Peace",
"politics": "Politics",
"psyche": "Psyche",
"science": "Science",
"spirituality": "Spirituality"
}
},
"emotions-label": {