refactor(webapp): store for categories (#8551)

* after authentification, query the categories if active and store them

* get categories from store

* use category store to get categories

* get categories from store

* mock store to have access to categories

* to get rid of the active categories config variable in the frontend, the Category query returns an empty array when categories are not active

* remove CATEGORIES_ACTIVE from .env

* should return string to avoid warnings in console

* replace all env calls for categories active by getter from store

* use categoriesActive getter

* ignore order of returned categories

* mixin to get the category infos from the store, to ensure, that the quey has been called

* fix misspelling

---------

Co-authored-by: Wolfgang Huß <wolle.huss@pjannto.com>
This commit is contained in:
Moriz Wahl 2025-05-27 15:03:26 +02:00 committed by GitHub
parent 5bec51ad5d
commit a3178a91b4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
39 changed files with 456 additions and 218 deletions

View File

@ -0,0 +1,97 @@
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/no-unsafe-call */
import { ApolloServer } from 'apollo-server-express'
import { createTestClient } from 'apollo-server-testing'
import gql from 'graphql-tag'
import databaseContext from '@context/database'
import Factory, { cleanDatabase } from '@db/factories'
import CONFIG from '@src/config'
import { categories } from '@src/constants/categories'
import createServer, { getContext } from '@src/server'
const database = databaseContext()
let server: ApolloServer
let query
beforeAll(async () => {
await cleanDatabase()
const authenticatedUser = null
// eslint-disable-next-line @typescript-eslint/require-await
const contextUser = async (_req) => authenticatedUser
const context = getContext({ user: contextUser, database })
server = createServer({ context }).server
const createTestClientResult = createTestClient(server)
query = createTestClientResult.query
for (const category of categories) {
await Factory.build('category', {
id: category.id,
slug: category.slug,
name: category.name,
icon: category.icon,
})
}
})
afterAll(() => {
void server.stop()
void database.driver.close()
database.neode.close()
})
const categoriesQuery = gql`
query {
Category {
id
slug
name
icon
}
}
`
describe('categroeis middleware', () => {
describe('categories are active', () => {
beforeEach(() => {
CONFIG.CATEGORIES_ACTIVE = true
})
it('returns the categories', async () => {
await expect(
query({
query: categoriesQuery,
}),
).resolves.toMatchObject({
data: {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
Category: expect.arrayContaining(categories),
},
errors: undefined,
})
})
})
describe('categories are not active', () => {
beforeEach(() => {
CONFIG.CATEGORIES_ACTIVE = false
})
it('returns an empty array though there are categories in the db', async () => {
await expect(
query({
query: categoriesQuery,
}),
).resolves.toMatchObject({
data: {
Category: [],
},
errors: undefined,
})
})
})
})

View File

@ -0,0 +1,16 @@
/* eslint-disable @typescript-eslint/no-unsafe-return */
/* eslint-disable @typescript-eslint/no-unsafe-call */
import CONFIG from '@src/config'
const checkCategoriesActive = (resolve, root, args, context, resolveInfo) => {
if (CONFIG.CATEGORIES_ACTIVE) {
return resolve(root, args, context, resolveInfo)
}
return []
}
export default {
Query: {
Category: checkCategoriesActive,
},
}

View File

@ -7,6 +7,7 @@ import CONFIG from '@config/index'
// eslint-disable-next-line import/no-cycle
import brandingMiddlewares from './branding/brandingMiddlewares'
import categories from './categories'
import chatMiddleware from './chatMiddleware'
import excerpt from './excerptMiddleware'
import hashtags from './hashtags/hashtagsMiddleware'
@ -46,6 +47,7 @@ const ocelotMiddlewares: MiddlewareOrder[] = [
{ order: -80, name: 'includedFields', middleware: includedFields },
{ order: -70, name: 'orderBy', middleware: orderBy },
{ order: -60, name: 'chatMiddleware', middleware: chatMiddleware },
{ order: -50, name: 'categories', middleware: categories },
]
export default (schema) => {

View File

@ -5,7 +5,6 @@ GRAPHQL_URI=http://localhost:4000/
MAPBOX_TOKEN="pk.eyJ1IjoiYnVzZmFrdG9yIiwiYSI6ImNraDNiM3JxcDBhaWQydG1uczhpZWtpOW4ifQ.7TNRTO-o9aK1Y6MyW_Nd4g"
PUBLIC_REGISTRATION=false
INVITE_REGISTRATION=true
CATEGORIES_ACTIVE=false
BADGES_ENABLED=true
INVITE_LINK_LIMIT=7
NETWORK_NAME="Ocelot.social"

View File

@ -1,6 +1,6 @@
import { mount } from '@vue/test-utils'
import CategoriesSelect from './CategoriesSelect'
import Vue from 'vue'
import Vuex from 'vuex'
const localVue = global.localVue
@ -12,7 +12,6 @@ describe('CategoriesSelect.vue', () => {
let environmentAndNature
let consumptionAndSustainablity
const propsData = { model: 'categoryIds' }
const categories = [
{
id: 'cat9',
@ -35,6 +34,20 @@ describe('CategoriesSelect.vue', () => {
id: 'cat8',
},
]
const propsData = { model: 'categoryIds' }
const categoriesMock = jest.fn().mockReturnValue(categories)
const storeMocks = {
getters: {
'categories/categories': categoriesMock,
'categories/isInitialized': jest.fn(() => true),
},
actions: {
'categories/init': jest.fn(),
},
}
beforeEach(() => {
provide = {
$parentForm: {
@ -48,7 +61,8 @@ describe('CategoriesSelect.vue', () => {
describe('shallowMount', () => {
const Wrapper = () => {
return mount(CategoriesSelect, { propsData, mocks, localVue, provide })
const store = new Vuex.Store(storeMocks)
return mount(CategoriesSelect, { propsData, mocks, localVue, provide, store })
}
beforeEach(() => {
@ -56,9 +70,7 @@ describe('CategoriesSelect.vue', () => {
})
describe('toggleCategory', () => {
beforeEach(async () => {
wrapper.vm.categories = categories
await Vue.nextTick()
beforeEach(() => {
democracyAndPolitics = wrapper.findAll('button').at(0)
democracyAndPolitics.trigger('click')
})

View File

@ -1,7 +1,7 @@
<template>
<section class="categories-select">
<base-button
v-for="category in categories"
v-for="category in sortCategories(categories)"
:key="category.id"
:data-test="categoryButtonsId(category.id)"
@click="toggleCategory(category.id)"
@ -20,10 +20,10 @@
</template>
<script>
import CategoryQuery from '~/graphql/CategoryQuery'
import { CATEGORIES_MAX } from '~/constants/categories.js'
import xor from 'lodash/xor'
import SortCategories from '~/mixins/sortCategoriesMixin.js'
import GetCategories from '~/mixins/getCategoriesMixin.js'
export default {
inject: {
@ -31,14 +31,13 @@ export default {
default: null,
},
},
mixins: [SortCategories],
mixins: [SortCategories, GetCategories],
props: {
existingCategoryIds: { type: Array, default: () => [] },
model: { type: String, required: true },
},
data() {
return {
categories: null,
selectedMax: CATEGORIES_MAX,
selectedCategoryIds: this.existingCategoryIds,
}
@ -76,16 +75,6 @@ export default {
return `category-buttons-${categoryId}`
},
},
apollo: {
Category: {
query() {
return CategoryQuery()
},
result({ data: { Category } }) {
this.categories = this.sortCategories(Category)
},
},
},
}
</script>

View File

@ -37,7 +37,7 @@ describe('ContributionForm.vue', () => {
const image = { sensitive: false, url: '/uploads/1562010976466-avataaars', aspectRatio: 1 }
beforeEach(() => {
mocks = {
$t: jest.fn(),
$t: jest.fn((t) => t),
$apollo: {
mutate: jest.fn().mockResolvedValueOnce({
data: {
@ -62,9 +62,6 @@ describe('ContributionForm.vue', () => {
back: jest.fn(),
push: jest.fn(),
},
$env: {
CATEGORIES_ACTIVE: false,
},
}
propsData = {}
})
@ -82,9 +79,13 @@ describe('ContributionForm.vue', () => {
slug: 'you-yourself',
}
},
'categories/categoriesActive': jest.fn(() => false),
}
const store = new Vuex.Store({
getters,
actions: {
'categories/init': jest.fn(),
},
})
const Wrapper = () => {
return mount(ContributionForm, {

View File

@ -197,8 +197,10 @@ import links from '~/constants/links.js'
import PageParamsLink from '~/components/_new/features/PageParamsLink/PageParamsLink.vue'
import DatePicker from 'vue2-datepicker'
import 'vue2-datepicker/scss/index.scss'
import GetCategories from '~/mixins/getCategoriesMixin.js'
export default {
mixins: [GetCategories],
components: {
Editor,
ImageUploader,
@ -240,7 +242,6 @@ export default {
type: imageType = null,
} = image || {}
return {
categoriesActive: this.$env.CATEGORIES_ACTIVE,
links,
formData: {
title: title || '',

View File

@ -13,6 +13,24 @@ describe('CategoriesFilter.vue', () => {
}
const getters = {
'posts/filteredCategoryIds': jest.fn(() => []),
'categories/categories': jest.fn().mockReturnValue([
{ id: 'cat4', name: 'Environment & Nature', icon: 'tree', slug: 'environment-nature' },
{
id: 'cat15',
name: 'Consumption & Sustainability',
icon: 'shopping-cart',
slug: 'consumption-sustainability',
},
{
id: 'cat9',
name: 'Democracy & Politics',
icon: 'university',
slug: 'democracy-politics',
},
]),
}
const actions = {
'categories/init': jest.fn(),
}
const apolloMutationMock = jest.fn().mockResolvedValue({
@ -31,25 +49,8 @@ describe('CategoriesFilter.vue', () => {
}
const Wrapper = () => {
const store = new Vuex.Store({ mutations, getters })
const store = new Vuex.Store({ mutations, getters, actions })
const wrapper = mount(CategoriesFilter, { mocks, localVue, store })
wrapper.setData({
categories: [
{ id: 'cat4', name: 'Environment & Nature', icon: 'tree', slug: 'environment-nature' },
{
id: 'cat15',
name: 'Consumption & Sustainability',
icon: 'shopping-cart',
slug: 'consumption-sustainability',
},
{
id: 'cat9',
name: 'Democracy & Politics',
icon: 'university',
slug: 'democracy-politics',
},
],
})
return wrapper
}
@ -75,7 +76,7 @@ describe('CategoriesFilter.vue', () => {
it('calls TOGGLE_CATEGORY when clicked', () => {
environmentAndNatureButton = wrapper.findAll('.category-filter-list .base-button').at(0)
environmentAndNatureButton.trigger('click')
expect(mutations['posts/TOGGLE_CATEGORY']).toHaveBeenCalledWith({}, 'cat4')
expect(mutations['posts/TOGGLE_CATEGORY']).toHaveBeenCalledWith({}, 'cat15')
})
})

View File

@ -15,7 +15,7 @@
<div class="category-filter-list">
<!-- <ds-space margin="small" /> -->
<base-button
v-for="category in categories"
v-for="category in sortCategories(categories)"
:key="category.id"
@click="saveCategories(category.id)"
:filled="filteredCategoryIds.includes(category.id)"
@ -35,20 +35,15 @@
<script>
import { mapGetters, mapMutations } from 'vuex'
import CategoryQuery from '~/graphql/CategoryQuery.js'
import FilterMenuSection from '~/components/FilterMenu/FilterMenuSection'
import SortCategories from '~/mixins/sortCategoriesMixin.js'
import GetCategories from '~/mixins/getCategoriesMixin.js'
export default {
components: {
FilterMenuSection,
},
mixins: [SortCategories],
data() {
return {
categories: [],
}
},
mixins: [SortCategories, GetCategories],
computed: {
...mapGetters({
filteredCategoryIds: 'posts/filteredCategoryIds',
@ -68,18 +63,6 @@ export default {
this.$emit('updateCategories', categoryId)
},
},
apollo: {
Category: {
query() {
return CategoryQuery()
},
update({ Category }) {
if (!Category) return []
this.categories = this.sortCategories(Category)
},
fetchPolicy: 'cache-and-network',
},
},
}
</script>
<style lang="scss">

View File

@ -16,9 +16,11 @@
import Dropdown from '~/components/Dropdown'
import { mapGetters } from 'vuex'
import CategoriesFilter from './CategoriesFilter'
import GetCategories from '~/mixins/getCategoriesMixin.js'
export default {
name: 'CategoriesMenu',
mixins: [GetCategories],
components: {
Dropdown,
CategoriesFilter,
@ -27,11 +29,6 @@ export default {
placement: { type: String },
offset: { type: [String, Number] },
},
data() {
return {
categoriesActive: this.$env.CATEGORIES_ACTIVE,
}
},
computed: {
...mapGetters({
// TODO: implement visibility of active filter later on

View File

@ -8,15 +8,16 @@ let wrapper
describe('FilterMenu.vue', () => {
const mocks = {
$t: jest.fn((string) => string),
$env: {
CATEGORIES_ACTIVE: true,
},
}
const getters = {
'posts/isActive': () => false,
'posts/filteredPostTypes': () => [],
'posts/orderBy': () => 'createdAt_desc',
'categories/categoriesActive': () => false,
}
const actions = {
'categories/init': jest.fn(),
}
const stubs = {
@ -28,7 +29,7 @@ describe('FilterMenu.vue', () => {
}
const Wrapper = () => {
const store = new Vuex.Store({ getters })
const store = new Vuex.Store({ getters, actions })
return mount(FilterMenu, { mocks, localVue, store, stubs })
}

View File

@ -36,8 +36,10 @@ import OrderByFilter from './OrderByFilter'
import CategoriesFilter from './CategoriesFilter'
import LabeledButton from '~/components/_new/generic/LabeledButton/LabeledButton'
import SaveCategories from '~/graphql/SaveCategories.js'
import GetCategories from '~/mixins/getCategoriesMixin.js'
export default {
mixins: [GetCategories],
components: {
EventsByFilter,
FollowingFilter,
@ -46,11 +48,6 @@ export default {
PostTypeFilter,
LabeledButton,
},
data() {
return {
categoriesActive: this.$env ? this.$env.CATEGORIES_ACTIVE : false,
}
},
computed: {
...mapGetters({
filteredPostTypes: 'posts/filteredPostTypes',

View File

@ -19,13 +19,16 @@ describe('FollowingFilter', () => {
'posts/filteredByUsersFollowed': jest.fn(),
'posts/filteredByPostsInMyGroups': jest.fn(),
}
const actions = {
'categories/init': jest.fn(),
}
const mocks = {
$t: jest.fn((string) => string),
}
const Wrapper = () => {
const store = new Vuex.Store({ mutations, getters })
const store = new Vuex.Store({ mutations, getters, actions })
const wrapper = mount(FollowingFilter, { mocks, localVue, store })
return wrapper
}

View File

@ -15,13 +15,16 @@ describe('OrderByFilter', () => {
'posts/orderedByCreationDate': () => true,
'posts/orderBy': () => 'createdAt_desc',
}
const actions = {
'categories/init': jest.fn(),
}
const mocks = {
$t: jest.fn((string) => string),
}
const Wrapper = () => {
const store = new Vuex.Store({ mutations, getters })
const store = new Vuex.Store({ mutations, getters, actions })
const wrapper = mount(OrderByFilter, { mocks, localVue, store })
return wrapper
}

View File

@ -1,5 +1,6 @@
import { mount } from '@vue/test-utils'
import GroupForm from './GroupForm.vue'
import Vuex from 'vuex'
const localVue = global.localVue
@ -15,19 +16,27 @@ const propsData = {
describe('GroupForm', () => {
let wrapper
let mocks
let storeMocks
let store
beforeEach(() => {
mocks = {
$t: jest.fn(),
$env: {
CATEGORIES_ACTIVE: true,
}
storeMocks = {
getters: {
'categories/categoriesActive': () => false,
},
actions: {
'categories/init': jest.fn(),
},
}
store = new Vuex.Store(storeMocks)
})
describe('mount', () => {
const Wrapper = () => {
return mount(GroupForm, { propsData, mocks, localVue, stubs })
return mount(GroupForm, { propsData, mocks, localVue, stubs, store })
}
beforeEach(() => {

View File

@ -177,11 +177,13 @@ import {
import Editor from '~/components/Editor/Editor'
import ActionRadiusSelect from '~/components/Select/ActionRadiusSelect'
import { queryLocations } from '~/graphql/location'
import GetCategories from '~/mixins/getCategoriesMixin.js'
let timeout
export default {
name: 'GroupForm',
mixins: [GetCategories],
components: {
CategoriesSelect,
Editor,
@ -203,7 +205,6 @@ export default {
const { name, slug, groupType, about, description, actionRadius, locationName, categories } =
this.group
return {
categoriesActive: this.$env.CATEGORIES_ACTIVE,
disabled: false,
groupTypeOptions: ['public', 'closed', 'hidden'],
loadingGeo: false,

View File

@ -79,9 +79,11 @@
<script>
import Category from '~/components/Category'
import GroupContentMenu from '~/components/ContentMenu/GroupContentMenu'
import GetCategories from '~/mixins/getCategoriesMixin.js'
export default {
name: 'GroupTeaser',
mixins: [GetCategories],
components: {
Category,
GroupContentMenu,
@ -96,11 +98,6 @@ export default {
default: () => {},
},
},
data() {
return {
categoriesActive: this.$env.CATEGORIES_ACTIVE,
}
},
computed: {
descriptionExcerpt() {
return this.$filters.removeLinks(this.group.descriptionExcerpt)

View File

@ -293,8 +293,10 @@ import SearchField from '~/components/features/SearchField/SearchField.vue'
import NotificationMenu from '~/components/NotificationMenu/NotificationMenu'
import links from '~/constants/links.js'
import PageParamsLink from '~/components/_new/features/PageParamsLink/PageParamsLink.vue'
import GetCategories from '~/mixins/getCategoriesMixin.js'
export default {
mixins: [GetCategories],
components: {
AvatarMenu,
ChatNotificationMenu,
@ -327,7 +329,6 @@ export default {
mobileSearchVisible: 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,
categoriesActive: this.$env.CATEGORIES_ACTIVE,
}
},
computed: {

View File

@ -15,11 +15,9 @@ const stubs = {
}
const authUserMock = jest.fn().mockReturnValue({ activeCategories: [] })
const apolloQueryMock = jest.fn().mockResolvedValue({
data: {
Category: [{ id: 'cat0' }, { id: 'cat1' }, { id: 'cat2' }, { id: 'cat3' }, { id: 'cat4' }],
},
})
const categoriesMock = jest
.fn()
.mockReturnValue([{ id: 'cat0' }, { id: 'cat1' }, { id: 'cat2' }, { id: 'cat3' }, { id: 'cat4' }])
describe('LoginForm', () => {
let mocks
@ -36,6 +34,7 @@ describe('LoginForm', () => {
getters: {
'auth/pending': () => false,
'auth/user': authUserMock,
'categories/categories': categoriesMock,
},
actions: {
'auth/login': jest.fn(),
@ -52,9 +51,6 @@ describe('LoginForm', () => {
success: jest.fn(),
error: jest.fn(),
},
$apollo: {
query: apolloQueryMock,
},
}
return mount(LoginForm, { mocks, localVue, propsData, store, stubs })
}

View File

@ -59,7 +59,6 @@ import LocaleSwitch from '~/components/LocaleSwitch/LocaleSwitch'
import Logo from '~/components/Logo/Logo'
import ShowPassword from '../ShowPassword/ShowPassword.vue'
import { mapGetters, mapMutations } from 'vuex'
import CategoryQuery from '~/graphql/CategoryQuery'
export default {
components: {
@ -88,6 +87,7 @@ export default {
},
...mapGetters({
currentUser: 'auth/user',
categories: 'categories/categories',
}),
},
methods: {
@ -99,13 +99,9 @@ export default {
const { email, password } = this.form
try {
await this.$store.dispatch('auth/login', { email, password })
const result = await this.$apollo.query({
query: CategoryQuery(),
})
const categories = result.data.Category
if (this.currentUser && this.currentUser.activeCategories) {
this.resetCategories()
if (this.currentUser.activeCategories.length < categories.length) {
if (this.currentUser.activeCategories.length < this.categories.length) {
this.currentUser.activeCategories.forEach((categoryId) => {
this.toggleCategory(categoryId)
})

View File

@ -12,6 +12,7 @@ describe('PostTeaser', () => {
let mocks
let propsData
let getters
let actions
let Wrapper
let wrapper
@ -47,21 +48,22 @@ describe('PostTeaser', () => {
data: { DeletePost: { id: 'deleted-post-id' } },
}),
},
$env: {
CATEGORIES_ACTIVE: false,
},
}
getters = {
'auth/isModerator': () => false,
'auth/user': () => {
return {}
},
'categories/categoriesActive': () => false,
}
actions = {
'categories/init': jest.fn(),
}
})
describe('shallowMount', () => {
Wrapper = () => {
store = new Vuex.Store({ getters })
store = new Vuex.Store({ getters, actions })
return shallowMount(PostTeaser, {
store,
propsData,
@ -114,6 +116,7 @@ describe('PostTeaser', () => {
Wrapper = () => {
const store = new Vuex.Store({
getters,
actions,
})
return mount(PostTeaser, {
stubs,

View File

@ -137,9 +137,11 @@ import UserTeaser from '~/components/UserTeaser/UserTeaser'
import { mapGetters } from 'vuex'
import PostMutations from '~/graphql/PostMutations'
import { postMenuModalsData, deletePostMutation } from '~/components/utils/PostHelpers'
import GetCategories from '~/mixins/getCategoriesMixin.js'
export default {
name: 'PostTeaser',
mixins: [GetCategories],
components: {
Category,
ContentMenu,
@ -164,11 +166,6 @@ export default {
default: () => {},
},
},
data() {
return {
categoriesActive: this.$env.CATEGORIES_ACTIVE,
}
},
mounted() {
const { image } = this.post
if (!image) return

View File

@ -26,15 +26,13 @@ describe('SearchResults', () => {
beforeEach(() => {
mocks = {
$t: jest.fn(),
$env: {
CATEGORIES_ACTIVE: false,
},
}
getters = {
'auth/user': () => {
return { id: 'u343', name: 'Matt' }
},
'auth/isModerator': () => false,
'categories/categoriesActive': () => false,
}
propsData = {
pageSize: 12,

View File

@ -34,7 +34,6 @@ const options = {
// Cookies
COOKIE_EXPIRE_TIME: process.env.COOKIE_EXPIRE_TIME || 730, // Two years by default
COOKIE_HTTPS_ONLY: process.env.COOKIE_HTTPS_ONLY || process.env.NODE_ENV === 'production', // ensure true in production if not set explicitly
CATEGORIES_ACTIVE: process.env.CATEGORIES_ACTIVE === 'true' || false,
BADGES_ENABLED: process.env.BADGES_ENABLED === 'true' || false,
INVITE_LINK_LIMIT: process.env.INVITE_LINK_LIMIT || 7,
NETWORK_NAME: process.env.NETWORK_NAME || 'Ocelot.social',

View File

@ -17,7 +17,7 @@ module.exports = {
],
coverageThreshold: {
global: {
lines: 83,
lines: 82,
},
},
coverageProvider: 'v8',

View File

@ -0,0 +1,19 @@
import { mapGetters, mapActions } from 'vuex'
export default {
computed: {
...mapGetters({
categories: 'categories/categories',
isInitialized: 'categories/isInitialized',
categoriesActive: 'categories/categoriesActive',
}),
},
methods: {
...mapActions({
storeInit: 'categories/init',
}),
},
async created() {
if (!this.storeIsInizialized) await this.storeInit()
},
}

View File

@ -1,5 +1,6 @@
import GroupProfileSlug from './_slug.vue'
import { render, screen, fireEvent } from '@testing-library/vue'
import Vuex from 'vuex'
const localVue = global.localVue
@ -32,11 +33,26 @@ describe('GroupProfileSlug', () => {
let bobDerBaumeister
let huey
const currentUserMock = jest.fn()
const getters = {
'auth/user': currentUserMock,
'auth/isModerator': () => false,
'categories/categoriesActive': () => true,
'categories/categories': () => [{ id: 'cat1' }],
}
const actions = {
'categories/init': jest.fn(),
}
const store = new Vuex.Store({
getters,
actions,
})
beforeEach(() => {
mocks = {
$env: {
CATEGORIES_ACTIVE: true,
},
// post: {
// id: 'p23',
// name: 'It is a post',
@ -213,6 +229,7 @@ describe('GroupProfileSlug', () => {
localVue,
data,
stubs,
store,
})
}
@ -220,12 +237,7 @@ describe('GroupProfileSlug', () => {
describe('given a current user', () => {
describe('as group owner "peter-lustig"', () => {
beforeEach(() => {
mocks.$store = {
getters: {
'auth/user': peterLustig,
'auth/isModerator': () => false,
},
}
currentUserMock.mockReturnValue(peterLustig)
wrapper = Wrapper(() => {
return {
Group: [
@ -265,12 +277,7 @@ describe('GroupProfileSlug', () => {
describe('as usual member "jenny-rostock"', () => {
beforeEach(() => {
mocks.$store = {
getters: {
'auth/user': jennyRostock,
'auth/isModerator': () => false,
},
}
currentUserMock.mockReturnValue(jennyRostock)
wrapper = Wrapper(() => {
return {
Group: [
@ -291,12 +298,7 @@ describe('GroupProfileSlug', () => {
describe('as pending member "bob-der-baumeister"', () => {
beforeEach(() => {
mocks.$store = {
getters: {
'auth/user': bobDerBaumeister,
'auth/isModerator': () => false,
},
}
currentUserMock.mockReturnValue(bobDerBaumeister)
wrapper = Wrapper(() => {
return {
Group: [
@ -317,12 +319,7 @@ describe('GroupProfileSlug', () => {
describe('as none(!) member "huey"', () => {
beforeEach(() => {
mocks.$store = {
getters: {
'auth/user': huey,
'auth/isModerator': () => false,
},
}
currentUserMock.mockReturnValue(huey)
wrapper = Wrapper(() => {
return {
Group: [
@ -346,12 +343,7 @@ describe('GroupProfileSlug', () => {
describe('given a current user', () => {
describe('as group owner "peter-lustig"', () => {
beforeEach(() => {
mocks.$store = {
getters: {
'auth/user': peterLustig,
'auth/isModerator': () => false,
},
}
currentUserMock.mockReturnValue(peterLustig)
wrapper = Wrapper(() => {
return {
Group: [
@ -372,12 +364,7 @@ describe('GroupProfileSlug', () => {
describe('as usual member "jenny-rostock"', () => {
beforeEach(() => {
mocks.$store = {
getters: {
'auth/user': jennyRostock,
'auth/isModerator': () => false,
},
}
currentUserMock.mockReturnValue(jennyRostock)
wrapper = Wrapper(() => {
return {
Group: [
@ -421,12 +408,7 @@ describe('GroupProfileSlug', () => {
describe('as pending member "bob-der-baumeister"', () => {
beforeEach(() => {
mocks.$store = {
getters: {
'auth/user': bobDerBaumeister,
'auth/isModerator': () => false,
},
}
currentUserMock.mockReturnValue(bobDerBaumeister)
wrapper = Wrapper(() => {
return {
Group: [
@ -447,12 +429,7 @@ describe('GroupProfileSlug', () => {
describe('as none(!) member "huey"', () => {
beforeEach(() => {
mocks.$store = {
getters: {
'auth/user': huey,
'auth/isModerator': () => false,
},
}
currentUserMock.mockReturnValue(huey)
wrapper = Wrapper(() => {
return {
Group: [
@ -477,12 +454,7 @@ describe('GroupProfileSlug', () => {
describe('given a current user', () => {
describe('as group owner "peter-lustig"', () => {
beforeEach(() => {
mocks.$store = {
getters: {
'auth/user': peterLustig,
'auth/isModerator': () => false,
},
}
currentUserMock.mockReturnValue(peterLustig)
wrapper = Wrapper(() => {
return {
Group: [
@ -503,12 +475,7 @@ describe('GroupProfileSlug', () => {
describe('as usual member "jenny-rostock"', () => {
beforeEach(() => {
mocks.$store = {
getters: {
'auth/user': jennyRostock,
'auth/isModerator': () => false,
},
}
currentUserMock.mockReturnValue(jennyRostock)
wrapper = Wrapper(() => {
return {
Group: [
@ -529,12 +496,7 @@ describe('GroupProfileSlug', () => {
describe('as pending member "bob-der-baumeister"', () => {
beforeEach(() => {
mocks.$store = {
getters: {
'auth/user': bobDerBaumeister,
'auth/isModerator': () => false,
},
}
currentUserMock.mockReturnValue(bobDerBaumeister)
wrapper = Wrapper(() => {
return {
Group: [
@ -555,12 +517,7 @@ describe('GroupProfileSlug', () => {
describe('as none(!) member "huey"', () => {
beforeEach(() => {
mocks.$store = {
getters: {
'auth/user': huey,
'auth/isModerator': () => false,
},
}
currentUserMock.mockReturnValue(huey)
wrapper = Wrapper(() => {
return {
Group: [

View File

@ -316,6 +316,8 @@ import PostTeaser from '~/components/PostTeaser/PostTeaser.vue'
import ProfileAvatar from '~/components/_new/generic/ProfileAvatar/ProfileAvatar'
import ProfileList from '~/components/features/ProfileList/ProfileList'
import SortCategories from '~/mixins/sortCategoriesMixin.js'
import { mapGetters } from 'vuex'
import GetCategories from '~/mixins/getCategoriesMixin.js'
// import SocialMedia from '~/components/SocialMedia/SocialMedia'
// import TabNavigation from '~/components/_new/generic/TabNavigation/TabNavigation'
@ -346,7 +348,7 @@ export default {
// SocialMedia,
// TabNavigation,
},
mixins: [postListActions, SortCategories],
mixins: [postListActions, SortCategories, GetCategories],
transition: {
name: 'slide-up',
mode: 'out-in',
@ -360,7 +362,6 @@ export default {
// const filter = tabToFilterMapping({ tab: 'post', id: this.$route.params.id })
const filter = { group: { id: this.$route.params.id } }
return {
categoriesActive: this.$env.CATEGORIES_ACTIVE,
loadGroupMembers: false,
posts: [],
hasMore: true,
@ -378,9 +379,9 @@ export default {
}
},
computed: {
currentUser() {
return this.$store.getters['auth/user']
},
...mapGetters({
currentUser: 'auth/user',
}),
group() {
return this.Group && this.Group[0] ? this.Group[0] : {}
},

View File

@ -36,8 +36,13 @@ describe('PostIndex', () => {
'auth/user': () => {
return { id: 'u23' }
},
'categories/categoriesActive': () => true,
'categories/categories': () => ['cat1', 'cat2', 'cat3'],
},
mutations,
actions: {
'categories/init': jest.fn(),
},
})
mocks = {
$t: (key) => key,
@ -79,9 +84,6 @@ describe('PostIndex', () => {
$route: {
query: {},
},
$env: {
CATEGORIES_ACTIVE: true,
},
}
})

View File

@ -155,6 +155,7 @@ import UpdateQuery from '~/components/utils/UpdateQuery'
import FilterMenuComponent from '~/components/FilterMenu/FilterMenuComponent'
import { SHOW_CONTENT_FILTER_MASONRY_GRID } from '~/constants/filter.js'
import { POST_ADD_BUTTON_POSITION_TOP } from '~/constants/posts.js'
import GetCategories from '~/mixins/getCategoriesMixin.js'
export default {
components: {
@ -167,7 +168,7 @@ export default {
FilterMenuComponent,
HeaderButton,
},
mixins: [postListActions, mobile()],
mixins: [postListActions, mobile(), GetCategories],
data() {
const { hashtag = null } = this.$route.query
return {
@ -184,7 +185,6 @@ export default {
offset: 0,
pageSize: 12,
hashtag,
categoriesActive: this.$env.CATEGORIES_ACTIVE,
SHOW_CONTENT_FILTER_MASONRY_GRID,
POST_ADD_BUTTON_POSITION_TOP,
}

View File

@ -42,6 +42,10 @@ describe('PostSlug', () => {
return { id: '1stUser' }
},
'auth/isModerator': () => false,
'categories/categoriesActive': () => false,
},
actions: {
'categories/init': jest.fn(),
},
})
const propsData = {}
@ -73,9 +77,6 @@ describe('PostSlug', () => {
query: jest.fn().mockResolvedValue({ data: { PostEmotionsCountByEmotion: {} } }),
},
$scrollTo: jest.fn(),
$env: {
CATEGORIES_ACTIVE: false,
},
}
stubs = {
'client-only': true,

View File

@ -182,6 +182,7 @@ import { groupQuery } from '~/graphql/groups'
import PostMutations from '~/graphql/PostMutations'
import links from '~/constants/links.js'
import SortCategories from '~/mixins/sortCategoriesMixin.js'
import GetCategories from '~/mixins/getCategoriesMixin.js'
export default {
name: 'PostSlug',
@ -203,7 +204,7 @@ export default {
PageParamsLink,
UserTeaser,
},
mixins: [SortCategories],
mixins: [SortCategories, GetCategories],
head() {
return {
title: this.title,
@ -219,7 +220,6 @@ export default {
blurred: false,
blocked: null,
postAuthor: null,
categoriesActive: this.$env.CATEGORIES_ACTIVE,
group: null,
}
},

View File

@ -1,5 +1,6 @@
import { mount } from '@vue/test-utils'
import create from './create.vue'
import Vuex from 'vuex'
const localVue = global.localVue
@ -8,9 +9,6 @@ describe('create.vue', () => {
const mocks = {
$t: jest.fn(),
$env: {
CATEGORIES_ACTIVE: false,
},
$route: {
query: {
groupId: null,
@ -23,8 +21,14 @@ describe('create.vue', () => {
}
describe('mount', () => {
const store = new Vuex.Store({
getters: {
'categories/categoriesActive': () => false,
},
})
const Wrapper = () => {
return mount(create, { mocks, localVue, stubs })
return mount(create, { mocks, localVue, stubs, store })
}
beforeEach(() => {

View File

@ -39,15 +39,17 @@ describe('post/_id.vue', () => {
}),
},
},
$env: {
CATEGORIES_ACTIVE: false,
},
}
store = new Vuex.Store({
getters: {
'auth/user': () => {
return { id: userId }
},
'categories/categories': jest.fn(() => []),
'categories/categoriesActive': () => false,
},
actions: {
'categories/init': jest.fn(),
},
})
if (asyncData) {

View File

@ -106,6 +106,7 @@ export const actions = {
await this.app.$apolloHelpers.onLogin(login)
commit('SET_TOKEN', login)
await dispatch('fetchCurrentUser')
await dispatch('categories/init', null, { root: true })
if (cookies.get(metadata.COOKIE_NAME) === undefined) {
throw new Error('no-cookie')
}

View File

@ -175,8 +175,11 @@ describe('actions', () => {
expect(commit.mock.calls).toEqual(expect.arrayContaining([['SET_TOKEN', token]]))
})
it('fetches the user', () => {
expect(dispatch.mock.calls).toEqual([['fetchCurrentUser']])
it('fetches the user and initializes categories', () => {
expect(dispatch.mock.calls).toEqual([
['fetchCurrentUser'],
['categories/init', null, { root: true }],
])
})
it('saves pending flags in order', () => {

View File

@ -0,0 +1,44 @@
import CategoryQuery from '~/graphql/CategoryQuery'
export const state = () => {
return {
categories: [],
isInitialized: false,
}
}
export const mutations = {
SET_CATEGORIES(state, categories) {
state.categories = categories || []
},
SET_INIZIALIZED(state) {
state.isInitialized = true
},
}
export const getters = {
categories(state) {
return state.categories
},
categoriesActive(state) {
return !!state.categories.length
},
isInitialized(state) {
return state.isInitialized
},
}
export const actions = {
async init({ commit }) {
try {
const client = this.app.apolloProvider.defaultClient
const {
data: { Category: categories },
} = await client.query({ query: CategoryQuery() })
commit('SET_CATEGORIES', categories)
commit('SET_INIZIALIZED')
} catch (err) {
throw new Error('Could not query categories')
}
},
}

View File

@ -0,0 +1,105 @@
import { state, mutations, getters, actions } from './categories'
import CategoryQuery from '~/graphql/CategoryQuery'
describe('categories store', () => {
describe('initial state', () => {
it('sets no categories and is not inizialized', () => {
expect(state()).toEqual({
categories: [],
isInitialized: false,
})
})
})
describe('getters', () => {
describe('categoriesActive', () => {
it('returns true if there are categories', () => {
const state = { categories: ['cat1', 'cat2'] }
expect(getters.categoriesActive(state)).toBe(true)
})
it('returns false if there are no categories', () => {
const state = { categories: [] }
expect(getters.categoriesActive(state)).toBe(false)
})
})
})
describe('mutations', () => {
let testMutation
describe('SET_CATEGORIES', () => {
beforeEach(() => {
testMutation = (categories) => {
mutations.SET_CATEGORIES(state, categories)
return getters.categories(state)
}
})
it('sets categories to [] if value is undefined', () => {
expect(testMutation(undefined)).toEqual([])
})
it('sets categories correctly', () => {
expect(testMutation(['cat1', 'cat2', 'cat3'])).toEqual(['cat1', 'cat2', 'cat3'])
})
})
describe('SET_INIZIALIZED', () => {
beforeEach(() => {
testMutation = () => {
mutations.SET_INIZIALIZED(state)
return getters.isInitialized(state)
}
})
it('sets isInitialized to true', () => {
expect(testMutation()).toBe(true)
})
})
})
describe('actions', () => {
const queryMock = jest.fn().mockResolvedValue({
data: {
Category: ['cat1', 'cat2', 'cat3'],
},
})
const commit = jest.fn()
let action
beforeEach(() => {
const module = {
app: {
apolloProvider: {
defaultClient: {
query: queryMock,
},
},
},
}
action = actions.init.bind(module)
})
describe('init', () => {
beforeEach(async () => {
await action({ commit })
})
it('calls apollo', () => {
expect(queryMock).toBeCalledWith({
query: CategoryQuery(),
})
})
it('commits SET_CATEGORIES', () => {
expect(commit).toBeCalledWith('SET_CATEGORIES', ['cat1', 'cat2', 'cat3'])
})
it('commits SET_INIZIALIZED', () => {
expect(commit).toBeCalledWith('SET_INIZIALIZED')
})
})
})
})