diff --git a/backend/public/img/badges/default_trophy.svg b/backend/public/img/badges/default_trophy.svg
new file mode 100644
index 000000000..b203cdfc6
--- /dev/null
+++ b/backend/public/img/badges/default_trophy.svg
@@ -0,0 +1,12 @@
+
+
+
diff --git a/backend/public/img/badges/default_verification.svg b/backend/public/img/badges/default_verification.svg
new file mode 100644
index 000000000..7bde29f35
--- /dev/null
+++ b/backend/public/img/badges/default_verification.svg
@@ -0,0 +1,28 @@
+
+
diff --git a/backend/src/graphql/types/type/Badge.gql b/backend/src/graphql/types/type/Badge.gql
index cbfe0193d..8cdad2ee7 100644
--- a/backend/src/graphql/types/type/Badge.gql
+++ b/backend/src/graphql/types/type/Badge.gql
@@ -4,6 +4,7 @@ type Badge {
icon: String!
createdAt: String
description: String!
+ isDefault: Boolean!
rewarded: [User]! @relation(name: "REWARDED", direction: "OUT")
verifies: [User]! @relation(name: "VERIFIES", direction: "OUT")
diff --git a/backend/src/graphql/types/type/User.gql b/backend/src/graphql/types/type/User.gql
index 751931d5b..81dd9cf5b 100644
--- a/backend/src/graphql/types/type/User.gql
+++ b/backend/src/graphql/types/type/User.gql
@@ -125,10 +125,10 @@ type User {
categories: [Category] @relation(name: "CATEGORIZED", direction: "OUT")
- badgeVerification: Badge @relation(name: "VERIFIES", direction: "IN")
+ badgeVerification: Badge! @neo4j_ignore
badgeTrophies: [Badge]! @relation(name: "REWARDED", direction: "IN")
badgeTrophiesCount: Int! @cypher(statement: "MATCH (this)<-[:REWARDED]-(r:Badge) RETURN COUNT(r)")
- badgeTrophiesSelected: [Badge]! @neo4j_ignore
+ badgeTrophiesSelected: [Badge!]! @neo4j_ignore
badgeTrophiesUnused: [Badge]! @neo4j_ignore
badgeTrophiesUnusedCount: Int! @neo4j_ignore
diff --git a/backend/src/schema/resolvers/badges.spec.ts b/backend/src/schema/resolvers/badges.spec.ts
index 17fe46c99..2588c9b5c 100644
--- a/backend/src/schema/resolvers/badges.spec.ts
+++ b/backend/src/schema/resolvers/badges.spec.ts
@@ -100,6 +100,7 @@ describe('Badges', () => {
id
badgeVerification {
id
+ isDefault
}
badgeTrophies {
id
@@ -204,7 +205,7 @@ describe('Badges', () => {
data: {
setVerificationBadge: {
id: 'regular-user-id',
- badgeVerification: { id: 'verification_moderator' },
+ badgeVerification: { id: 'verification_moderator', isDefault: false },
badgeTrophies: [],
},
},
@@ -226,7 +227,7 @@ describe('Badges', () => {
data: {
setVerificationBadge: {
id: 'regular-user-id',
- badgeVerification: { id: 'verification_admin' },
+ badgeVerification: { id: 'verification_admin', isDefault: false },
badgeTrophies: [],
},
},
@@ -255,7 +256,7 @@ describe('Badges', () => {
data: {
setVerificationBadge: {
id: 'regular-user-2-id',
- badgeVerification: { id: 'verification_moderator' },
+ badgeVerification: { id: 'verification_moderator', isDefault: false },
badgeTrophies: [],
},
},
@@ -299,6 +300,7 @@ describe('Badges', () => {
id
badgeVerification {
id
+ isDefault
}
badgeTrophies {
id
@@ -403,7 +405,7 @@ describe('Badges', () => {
data: {
rewardTrophyBadge: {
id: 'regular-user-id',
- badgeVerification: null,
+ badgeVerification: { id: 'default_verification', isDefault: true },
badgeTrophies: [{ id: 'trophy_rhino' }],
},
},
@@ -530,6 +532,7 @@ describe('Badges', () => {
id
badgeVerification {
id
+ isDefault
}
badgeTrophies {
id
@@ -596,7 +599,7 @@ describe('Badges', () => {
data: {
revokeBadge: {
id: 'regular-user-id',
- badgeVerification: { id: 'verification_moderator' },
+ badgeVerification: { id: 'verification_moderator', isDefault: false },
badgeTrophies: [],
},
},
@@ -610,7 +613,7 @@ describe('Badges', () => {
data: {
revokeBadge: {
id: 'regular-user-id',
- badgeVerification: { id: 'verification_moderator' },
+ badgeVerification: { id: 'verification_moderator', isDefault: false },
badgeTrophies: [],
},
},
@@ -631,7 +634,7 @@ describe('Badges', () => {
data: {
revokeBadge: {
id: 'regular-user-id',
- badgeVerification: null,
+ badgeVerification: { id: 'default_verification', isDefault: true },
badgeTrophies: [{ id: 'trophy_rhino' }],
},
},
@@ -659,7 +662,7 @@ describe('Badges', () => {
data: {
revokeBadge: {
id: 'regular-user-id',
- badgeVerification: null,
+ badgeVerification: { id: 'default_verification', isDefault: true },
badgeTrophies: [{ id: 'trophy_rhino' }],
},
},
diff --git a/backend/src/schema/resolvers/badges.ts b/backend/src/schema/resolvers/badges.ts
index 430e3bf75..587204b54 100644
--- a/backend/src/schema/resolvers/badges.ts
+++ b/backend/src/schema/resolvers/badges.ts
@@ -6,6 +6,22 @@
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
import { neo4jgraphql } from 'neo4j-graphql-js'
+export const defaultTrophyBadge = {
+ id: 'default_trophy',
+ type: 'trophy',
+ icon: '/img/badges/default_trophy.svg',
+ description: '',
+ createdAt: '',
+}
+
+export const defaultVerificationBadge = {
+ id: 'default_verification',
+ type: 'verification',
+ icon: '/img/badges/default_verification.svg',
+ description: '',
+ createdAt: '',
+}
+
export default {
Query: {
Badge: async (object, args, context, resolveInfo) =>
@@ -123,4 +139,8 @@ export default {
}
},
},
+ Badge: {
+ isDefault: async (parent, _params, _context, _resolveInfo) =>
+ [defaultTrophyBadge.id, defaultVerificationBadge.id].includes(parent.id),
+ },
}
diff --git a/backend/src/schema/resolvers/users.spec.ts b/backend/src/schema/resolvers/users.spec.ts
index 8d082b682..d4f5e00eb 100644
--- a/backend/src/schema/resolvers/users.spec.ts
+++ b/backend/src/schema/resolvers/users.spec.ts
@@ -81,6 +81,7 @@ const setTrophyBadgeSelected = gql`
badgeTrophiesCount
badgeTrophiesSelected {
id
+ isDefault
}
badgeTrophiesUnused {
id
@@ -96,6 +97,7 @@ const resetTrophyBadgesSelected = gql`
badgeTrophiesCount
badgeTrophiesSelected {
id
+ isDefault
}
badgeTrophiesUnused {
id
@@ -1242,15 +1244,40 @@ describe('setTrophyBadgeSelected', () => {
badgeTrophiesSelected: [
{
id: 'trophy_bear',
+ isDefault: false,
+ },
+ {
+ id: 'default_trophy',
+ isDefault: true,
+ },
+ {
+ id: 'default_trophy',
+ isDefault: true,
+ },
+ {
+ id: 'default_trophy',
+ isDefault: true,
+ },
+ {
+ id: 'default_trophy',
+ isDefault: true,
+ },
+ {
+ id: 'default_trophy',
+ isDefault: true,
+ },
+ {
+ id: 'default_trophy',
+ isDefault: true,
+ },
+ {
+ id: 'default_trophy',
+ isDefault: true,
+ },
+ {
+ id: 'default_trophy',
+ isDefault: true,
},
- null,
- null,
- null,
- null,
- null,
- null,
- null,
- null,
],
badgeTrophiesUnused: [
{
@@ -1275,17 +1302,40 @@ describe('setTrophyBadgeSelected', () => {
badgeTrophiesSelected: [
{
id: 'trophy_bear',
+ isDefault: false,
+ },
+ {
+ id: 'default_trophy',
+ isDefault: true,
+ },
+ {
+ id: 'default_trophy',
+ isDefault: true,
+ },
+ {
+ id: 'default_trophy',
+ isDefault: true,
+ },
+ {
+ id: 'default_trophy',
+ isDefault: true,
},
- null,
- null,
- null,
- null,
{
id: 'trophy_panda',
+ isDefault: false,
+ },
+ {
+ id: 'default_trophy',
+ isDefault: true,
+ },
+ {
+ id: 'default_trophy',
+ isDefault: true,
+ },
+ {
+ id: 'default_trophy',
+ isDefault: true,
},
- null,
- null,
- null,
],
badgeTrophiesUnused: [],
badgeTrophiesUnusedCount: 0,
@@ -1295,8 +1345,8 @@ describe('setTrophyBadgeSelected', () => {
)
})
- describe('set badge to null', () => {
- it('returns the user with no badge set on the selected slot', async () => {
+ describe('set badge to null or default', () => {
+ beforeEach(async () => {
await mutate({
mutation: setTrophyBadgeSelected,
variables: { slot: 0, badgeId: 'trophy_bear' },
@@ -1305,7 +1355,9 @@ describe('setTrophyBadgeSelected', () => {
mutation: setTrophyBadgeSelected,
variables: { slot: 5, badgeId: 'trophy_panda' },
})
+ })
+ it('returns the user with no badge set on the selected slot when sending null', async () => {
await expect(
mutate({
mutation: setTrophyBadgeSelected,
@@ -1319,15 +1371,101 @@ describe('setTrophyBadgeSelected', () => {
badgeTrophiesSelected: [
{
id: 'trophy_bear',
+ isDefault: false,
+ },
+ {
+ id: 'default_trophy',
+ isDefault: true,
+ },
+ {
+ id: 'default_trophy',
+ isDefault: true,
+ },
+ {
+ id: 'default_trophy',
+ isDefault: true,
+ },
+ {
+ id: 'default_trophy',
+ isDefault: true,
+ },
+ {
+ id: 'default_trophy',
+ isDefault: true,
+ },
+ {
+ id: 'default_trophy',
+ isDefault: true,
+ },
+ {
+ id: 'default_trophy',
+ isDefault: true,
+ },
+ {
+ id: 'default_trophy',
+ isDefault: true,
+ },
+ ],
+ badgeTrophiesUnused: [
+ {
+ id: 'trophy_panda',
+ },
+ ],
+ badgeTrophiesUnusedCount: 1,
+ },
+ },
+ }),
+ )
+ })
+
+ it('returns the user with no badge set on the selected slot when sending default_trophy', async () => {
+ await expect(
+ mutate({
+ mutation: setTrophyBadgeSelected,
+ variables: { slot: 5, badgeId: 'default_trophy' },
+ }),
+ ).resolves.toEqual(
+ expect.objectContaining({
+ data: {
+ setTrophyBadgeSelected: {
+ badgeTrophiesCount: 2,
+ badgeTrophiesSelected: [
+ {
+ id: 'trophy_bear',
+ isDefault: false,
+ },
+ {
+ id: 'default_trophy',
+ isDefault: true,
+ },
+ {
+ id: 'default_trophy',
+ isDefault: true,
+ },
+ {
+ id: 'default_trophy',
+ isDefault: true,
+ },
+ {
+ id: 'default_trophy',
+ isDefault: true,
+ },
+ {
+ id: 'default_trophy',
+ isDefault: true,
+ },
+ {
+ id: 'default_trophy',
+ isDefault: true,
+ },
+ {
+ id: 'default_trophy',
+ isDefault: true,
+ },
+ {
+ id: 'default_trophy',
+ isDefault: true,
},
- null,
- null,
- null,
- null,
- null,
- null,
- null,
- null,
],
badgeTrophiesUnused: [
{
@@ -1411,7 +1549,44 @@ describe('resetTrophyBadgesSelected', () => {
data: {
resetTrophyBadgesSelected: {
badgeTrophiesCount: 2,
- badgeTrophiesSelected: [null, null, null, null, null, null, null, null, null],
+ badgeTrophiesSelected: [
+ {
+ id: 'default_trophy',
+ isDefault: true,
+ },
+ {
+ id: 'default_trophy',
+ isDefault: true,
+ },
+ {
+ id: 'default_trophy',
+ isDefault: true,
+ },
+ {
+ id: 'default_trophy',
+ isDefault: true,
+ },
+ {
+ id: 'default_trophy',
+ isDefault: true,
+ },
+ {
+ id: 'default_trophy',
+ isDefault: true,
+ },
+ {
+ id: 'default_trophy',
+ isDefault: true,
+ },
+ {
+ id: 'default_trophy',
+ isDefault: true,
+ },
+ {
+ id: 'default_trophy',
+ isDefault: true,
+ },
+ ],
badgeTrophiesUnused: [
{
id: 'trophy_panda',
diff --git a/backend/src/schema/resolvers/users.ts b/backend/src/schema/resolvers/users.ts
index 913427085..c165e8e44 100644
--- a/backend/src/schema/resolvers/users.ts
+++ b/backend/src/schema/resolvers/users.ts
@@ -11,6 +11,7 @@ import { neo4jgraphql } from 'neo4j-graphql-js'
import { TROPHY_BADGES_SELECTED_MAX } from '@constants/badges'
import { getNeode } from '@db/neo4j'
+import { defaultTrophyBadge, defaultVerificationBadge } from './badges'
import log from './helpers/databaseLogger'
import Resolver from './helpers/Resolver'
import { mergeImage, deleteImage } from './images/images'
@@ -412,13 +413,15 @@ export default {
MERGE (user)-[:SELECTED{slot: toInteger($slot)}]->(badge)
RETURN user {.*}
`
- const queryNull = `
+ const queryEmpty = `
MATCH (user:User {id: $userId})
OPTIONAL MATCH (user)-[slotRelation:SELECTED {slot: $slot}]->(:Badge)
DELETE slotRelation
RETURN user {.*}
`
- const result = await transaction.run(badgeId ? queryBadge : queryNull, {
+ const isDefault = !badgeId || badgeId === defaultTrophyBadge.id
+
+ const result = await transaction.run(isDefault ? queryEmpty : queryBadge, {
userId,
badgeId,
slot,
@@ -538,7 +541,7 @@ export default {
})
try {
const badgesSelected = await query
- const result = Array(TROPHY_BADGES_SELECTED_MAX).fill(null)
+ const result = Array(TROPHY_BADGES_SELECTED_MAX).fill(defaultTrophyBadge)
badgesSelected.map((record) => {
result[record.get('slot')] = record.get('badge')
return true
@@ -550,21 +553,17 @@ export default {
session.close()
}
},
- badgeTrophiesUnused: async (_parent, _params, context, _resolveInfo) => {
- const {
- user: { id: userId },
- } = context
-
+ badgeTrophiesUnused: async (parent, _params, context, _resolveInfo) => {
const session = context.driver.session()
- const query = session.writeTransaction(async (transaction) => {
+ const query = session.readTransaction(async (transaction) => {
const result = await transaction.run(
`
- MATCH (user:User {id: $userId})<-[:REWARDED]-(badge:Badge)
+ MATCH (user:User {id: $parent.id})<-[:REWARDED]-(badge:Badge)
WHERE NOT (user)-[:SELECTED]-(badge)
RETURN badge {.*}
`,
- { userId },
+ { parent },
)
return result.records.map((record) => record.get('badge'))
})
@@ -576,21 +575,17 @@ export default {
session.close()
}
},
- badgeTrophiesUnusedCount: async (_parent, _params, context, _resolveInfo) => {
- const {
- user: { id: userId },
- } = context
-
+ badgeTrophiesUnusedCount: async (parent, _params, context, _resolveInfo) => {
const session = context.driver.session()
- const query = session.writeTransaction(async (transaction) => {
+ const query = session.readTransaction(async (transaction) => {
const result = await transaction.run(
`
- MATCH (user:User {id: $userId})<-[:REWARDED]-(badge:Badge)
+ MATCH (user:User {id: $parent.id})<-[:REWARDED]-(badge:Badge)
WHERE NOT (user)-[:SELECTED]-(badge)
RETURN toString(COUNT(badge)) as count
`,
- { userId },
+ { parent },
)
return result.records.map((record) => record.get('count'))[0]
})
@@ -602,6 +597,28 @@ export default {
session.close()
}
},
+ badgeVerification: async (parent, _params, context, _resolveInfo) => {
+ const session = context.driver.session()
+
+ const query = session.writeTransaction(async (transaction) => {
+ const result = await transaction.run(
+ `
+ MATCH (user:User {id: $parent.id})<-[:VERIFIES]-(verification:Badge)
+ RETURN verification {.*}
+ `,
+ { parent },
+ )
+ return result.records.map((record) => record.get('verification'))[0]
+ })
+ try {
+ const result = await query
+ return result ?? defaultVerificationBadge
+ } catch (error) {
+ throw new Error(error)
+ } finally {
+ session.close()
+ }
+ },
...Resolver('User', {
undefinedToNull: [
'actorId',
@@ -642,7 +659,6 @@ export default {
invitedBy: '<-[:INVITED]-(related:User)',
location: '-[:IS_IN]->(related:Location)',
redeemedInviteCode: '-[:REDEEMED]->(related:InviteCode)',
- badgeVerification: '<-[:VERIFIES]-(related:Badge)',
},
hasMany: {
followedBy: '<-[:FOLLOWS]-(related:User)',