Merge pull request #5284 from Ocelot-Social-Community/save-categories-in-frontend

feat: 🍰 Save Categories In Frontend
This commit is contained in:
Moriz Wahl 2022-09-12 15:08:09 +02:00 committed by GitHub
commit 922ca2d7ef
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 186 additions and 26 deletions

View File

@ -20,16 +20,22 @@ export default {
const result = await transaction.run(
`
MATCH (user:User {id: $id})
WITH user, [(user)<-[:OWNED_BY]-(medium:SocialMedia) | properties(medium) ] as media
RETURN user {.*, socialMedia: media } as user
OPTIONAL MATCH (category:Category) WHERE NOT ((user)-[:NOT_INTERESTED_IN]->(category))
OPTIONAL MATCH (cats:Category)
WITH user, [(user)<-[:OWNED_BY]-(medium:SocialMedia) | properties(medium) ] AS media, category, toString(COUNT(cats)) AS categoryCount
RETURN user {.*, socialMedia: media, activeCategories: collect(category.id) } AS user, categoryCount
`,
{ id: user.id },
)
log(result)
return result.records.map((record) => record.get('user'))
const [categoryCount] = result.records.map((record) => record.get('categoryCount'))
const [currentUser] = result.records.map((record) => record.get('user'))
// frontend expects empty array when all categories are selected
if (currentUser.activeCategories.length === parseInt(categoryCount))
currentUser.activeCategories = []
return currentUser
})
try {
const [currentUser] = await currentUserTransactionPromise
const currentUser = await currentUserTransactionPromise
return currentUser
} finally {
session.close()

View File

@ -6,6 +6,7 @@ import { createTestClient } from 'apollo-server-testing'
import createServer, { context } from '../../server'
import encode from '../../jwt/encode'
import { getNeode } from '../../db/neo4j'
import { categories } from '../../constants/categories'
const neode = getNeode()
let query, mutate, variables, req, user
@ -118,6 +119,7 @@ describe('currentUser', () => {
}
email
role
activeCategories
}
}
`
@ -172,6 +174,52 @@ describe('currentUser', () => {
}
await respondsWith(expected)
})
describe('with categories in DB', () => {
beforeEach(async () => {
await Promise.all(
categories.map(async ({ icon, name }, index) => {
await Factory.build('category', {
id: `cat${index + 1}`,
slug: name,
name,
icon,
})
}),
)
})
it('returns empty array for all categories', async () => {
await respondsWith({
data: {
currentUser: expect.objectContaining({ activeCategories: [] }),
},
})
})
describe('with categories saved for current user', () => {
const saveCategorySettings = gql`
mutation ($activeCategories: [String]) {
saveCategorySettings(activeCategories: $activeCategories)
}
`
beforeEach(async () => {
await mutate({
mutation: saveCategorySettings,
variables: { activeCategories: ['cat1', 'cat3', 'cat5', 'cat7'] },
})
})
it('returns only the saved active categories', async () => {
const result = await query({ query: currentUserQuery, variables })
expect(result.data.currentUser.activeCategories).toHaveLength(4)
expect(result.data.currentUser.activeCategories).toContain('cat1')
expect(result.data.currentUser.activeCategories).toContain('cat3')
expect(result.data.currentUser.activeCategories).toContain('cat5')
expect(result.data.currentUser.activeCategories).toContain('cat7')
})
})
})
})
})
})

View File

@ -286,6 +286,10 @@ export default {
{ id },
)
})
// frontend gives [] when all categories are selected (default)
if (activeCategories.length === 0) return true
const writeTxResultPromise = session.writeTransaction(async (transaction) => {
const saveCategorySettingsResponse = await transaction.run(
`

View File

@ -602,9 +602,7 @@ describe('save category settings', () => {
const userQuery = gql`
query ($id: ID) {
User(id: $id) {
activeCategories {
id
}
activeCategories
}
}
`
@ -631,11 +629,7 @@ describe('save category settings', () => {
data: {
User: [
{
activeCategories: expect.arrayContaining([
{ id: 'cat1' },
{ id: 'cat3' },
{ id: 'cat5' },
]),
activeCategories: expect.arrayContaining(['cat1', 'cat3', 'cat5']),
},
],
},
@ -678,11 +672,11 @@ describe('save category settings', () => {
User: [
{
activeCategories: expect.arrayContaining([
{ id: 'cat10' },
{ id: 'cat11' },
{ id: 'cat12' },
{ id: 'cat8' },
{ id: 'cat9' },
'cat10',
'cat11',
'cat12',
'cat8',
'cat9',
]),
},
],

View File

@ -115,11 +115,11 @@ type User {
emotions: [EMOTED]
activeCategories: [Category] @cypher(
activeCategories: [String] @cypher(
statement: """
MATCH (category:Category)
WHERE NOT ((this)-[:NOT_INTERESTED_IN]->(category))
RETURN category
RETURN collect(category.id)
"""
)
}

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>save</title>
<path d="M5 5h17.406l0.313 0.281 4 4 0.281 0.313v17.406h-22v-22zM7 7v18h2v-9h14v9h2v-14.563l-3-3v5.563h-12v-6h-3zM12 7v4h8v-4h-2v2h-2v-2h-4zM11 18v7h10v-7h-10z"></path>
</svg>

After

Width:  |  Height:  |  Size: 327 B

View File

@ -15,8 +15,19 @@ describe('CategoriesFilter.vue', () => {
'posts/filteredCategoryIds': jest.fn(() => []),
}
const apolloMutationMock = jest.fn().mockResolvedValue({
data: { saveCategorySettings: true },
})
const mocks = {
$t: jest.fn((string) => string),
$apollo: {
mutate: apolloMutationMock,
},
$toast: {
success: jest.fn(),
error: jest.fn(),
},
}
const Wrapper = () => {
@ -76,5 +87,14 @@ describe('CategoriesFilter.vue', () => {
expect(mutations['posts/RESET_CATEGORIES']).toHaveBeenCalledTimes(1)
})
})
describe('save categories', () => {
it('calls the API', async () => {
wrapper = await Wrapper()
const saveButton = wrapper.findAll('.categories-filter .sidebar .base-button').at(1)
saveButton.trigger('click')
expect(apolloMutationMock).toBeCalled()
})
})
})
})

View File

@ -7,6 +7,8 @@
icon="check"
@click="resetCategories"
/>
<hr />
<labeled-button filled :label="$t('actions.save')" icon="save" @click="saveCategories" />
</template>
<template #filter-list>
<li v-for="category in categories" :key="category.id" class="item">
@ -24,6 +26,7 @@
<script>
import { mapGetters, mapMutations } from 'vuex'
import CategoryQuery from '~/graphql/CategoryQuery.js'
import SaveCategories from '~/graphql/SaveCategories.js'
import FilterMenuSection from '~/components/FilterMenu/FilterMenuSection'
import LabeledButton from '~/components/_new/generic/LabeledButton/LabeledButton'
@ -47,6 +50,19 @@ export default {
resetCategories: 'posts/RESET_CATEGORIES',
toggleCategory: 'posts/TOGGLE_CATEGORY',
}),
saveCategories() {
this.$apollo
.mutate({
mutation: SaveCategories(),
variables: { activeCategories: this.filteredCategoryIds },
})
.then(() => {
this.$toast.success(this.$t('filter-menu.save.success'))
})
.catch(() => {
this.$toast.error(this.$t('filter-menu.save.error'))
})
},
},
apollo: {
Category: {

View File

@ -12,6 +12,8 @@ config.stubs['nuxt-link'] = '<span><slot /></span>'
config.stubs['locale-switch'] = '<span><slot /></span>'
config.stubs['client-only'] = '<span><slot /></span>'
const authUserMock = jest.fn().mockReturnValue({ activeCategories: [] })
describe('LoginForm', () => {
let mocks
let propsData
@ -26,10 +28,15 @@ describe('LoginForm', () => {
storeMocks = {
getters: {
'auth/pending': () => false,
'auth/user': authUserMock,
},
actions: {
'auth/login': jest.fn(),
},
mutations: {
'posts/TOGGLE_CATEGORY': jest.fn(),
'posts/RESET_CATEGORIES': jest.fn(),
},
}
const store = new Vuex.Store(storeMocks)
mocks = {
@ -43,20 +50,46 @@ describe('LoginForm', () => {
}
describe('fill in email and password and submit', () => {
const fillIn = (wrapper, opts = {}) => {
const fillIn = async (wrapper, opts = {}) => {
const { email = 'email@example.org', password = '1234' } = opts
wrapper.find('input[name="email"]').setValue(email)
wrapper.find('input[name="password"]').setValue(password)
wrapper.find('form').trigger('submit')
await wrapper.find('form').trigger('submit')
}
it('dispatches login with form data', () => {
fillIn(Wrapper())
it('dispatches login with form data', async () => {
await fillIn(Wrapper())
expect(storeMocks.actions['auth/login']).toHaveBeenCalledWith(expect.any(Object), {
email: 'email@example.org',
password: '1234',
})
})
describe('setting saved categories', () => {
beforeEach(() => {
jest.clearAllMocks()
})
describe('no categories saved', () => {
it('resets the categories', async () => {
await fillIn(Wrapper())
expect(storeMocks.mutations['posts/RESET_CATEGORIES']).toBeCalled()
expect(storeMocks.mutations['posts/TOGGLE_CATEGORY']).not.toBeCalled()
})
})
describe('categories saved', () => {
it('sets the categories', async () => {
authUserMock.mockReturnValue({ activeCategories: ['cat1', 'cat9', 'cat12'] })
await fillIn(Wrapper())
expect(storeMocks.mutations['posts/RESET_CATEGORIES']).toBeCalled()
expect(storeMocks.mutations['posts/TOGGLE_CATEGORY']).toBeCalledTimes(3)
expect(storeMocks.mutations['posts/TOGGLE_CATEGORY']).toBeCalledWith({}, 'cat1')
expect(storeMocks.mutations['posts/TOGGLE_CATEGORY']).toBeCalledWith({}, 'cat9')
expect(storeMocks.mutations['posts/TOGGLE_CATEGORY']).toBeCalledWith({}, 'cat12')
})
})
})
})
describe('Visibility of password', () => {

View File

@ -58,6 +58,7 @@ import PageParamsLink from '~/components/_new/features/PageParamsLink/PageParams
import LocaleSwitch from '~/components/LocaleSwitch/LocaleSwitch'
import Logo from '~/components/Logo/Logo'
import ShowPassword from '../ShowPassword/ShowPassword.vue'
import { mapGetters, mapMutations } from 'vuex'
export default {
components: {
@ -84,12 +85,27 @@ export default {
iconName() {
return this.showPassword ? 'eye-slash' : 'eye'
},
...mapGetters({
currentUser: 'auth/user',
}),
},
methods: {
...mapMutations({
toggleCategory: 'posts/TOGGLE_CATEGORY',
resetCategories: 'posts/RESET_CATEGORIES',
}),
async onSubmit() {
const { email, password } = this.form
try {
await this.$store.dispatch('auth/login', { email, password })
if (this.currentUser && this.currentUser.activeCategories) {
this.resetCategories()
if (this.currentUser.activeCategories.length > 0) {
this.currentUser.activeCategories.forEach((categoryId) => {
this.toggleCategory(categoryId)
})
}
}
this.$toast.success(this.$t('login.success'))
this.$emit('success')
} catch (err) {

View File

@ -0,0 +1,9 @@
import gql from 'graphql-tag'
export default () => {
return gql`
mutation ($activeCategories: [String]) {
saveCategorySettings(activeCategories: $activeCategories)
}
`
}

View File

@ -285,6 +285,7 @@ export const currentUserQuery = gql`
id
url
}
activeCategories
}
}
`

View File

@ -363,7 +363,11 @@
"label": "Älteste zuerst"
}
},
"order-by": "Sortieren nach ..."
"order-by": "Sortieren nach ...",
"save": {
"error": "Themen konnten nicht gespeichert werden!",
"success": "Themen gespeichert!"
}
},
"followButton": {
"follow": "Folgen",

View File

@ -363,7 +363,11 @@
"label": "Oldest first"
}
},
"order-by": "Order by ..."
"order-by": "Order by ...",
"save": {
"error": "Failed saving topic settings!",
"success": "Topics saved!"
}
},
"followButton": {
"follow": "Follow",