Merge pull request #5102 from Ocelot-Social-Community/make-categories-optional

feat: Make Categories Optional
This commit is contained in:
Wolfgang Huß 2022-08-03 14:47:01 +02:00 committed by GitHub
commit c53bd68f4d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 123 additions and 15 deletions

View File

@ -28,3 +28,5 @@ AWS_BUCKET=
EMAIL_DEFAULT_SENDER="devops@ocelot.social" EMAIL_DEFAULT_SENDER="devops@ocelot.social"
EMAIL_SUPPORT="devops@ocelot.social" EMAIL_SUPPORT="devops@ocelot.social"
CATEGORIES_ACTIVE=false

View File

@ -86,6 +86,7 @@ const options = {
ORGANIZATION_URL: emails.ORGANIZATION_LINK, ORGANIZATION_URL: emails.ORGANIZATION_LINK,
PUBLIC_REGISTRATION: env.PUBLIC_REGISTRATION === 'true' || false, PUBLIC_REGISTRATION: env.PUBLIC_REGISTRATION === 'true' || false,
INVITE_REGISTRATION: env.INVITE_REGISTRATION !== 'false', // default = true INVITE_REGISTRATION: env.INVITE_REGISTRATION !== 'false', // default = true
CATEGORIES_ACTIVE: process.env.CATEGORIES_ACTIVE === 'true' || false,
} }
// Check if all required configs are present // Check if all required configs are present

View File

@ -5,6 +5,7 @@ import { UserInputError } from 'apollo-server'
import { mergeImage, deleteImage } from './images/images' import { mergeImage, deleteImage } from './images/images'
import Resolver from './helpers/Resolver' import Resolver from './helpers/Resolver'
import { filterForMutedUsers } from './helpers/filterForMutedUsers' import { filterForMutedUsers } from './helpers/filterForMutedUsers'
import CONFIG from '../../config'
const maintainPinnedPosts = (params) => { const maintainPinnedPosts = (params) => {
const pinnedPostFilter = { pinned: true } const pinnedPostFilter = { pinned: true }
@ -76,12 +77,20 @@ export default {
}, },
Mutation: { Mutation: {
CreatePost: async (_parent, params, context, _resolveInfo) => { CreatePost: async (_parent, params, context, _resolveInfo) => {
const { categoryIds } = params
const { image: imageInput } = params const { image: imageInput } = params
delete params.categoryIds delete params.categoryIds
delete params.image delete params.image
params.id = params.id || uuid() params.id = params.id || uuid()
const session = context.driver.session() const session = context.driver.session()
const writeTxResultPromise = session.writeTransaction(async (transaction) => { const writeTxResultPromise = session.writeTransaction(async (transaction) => {
const categoriesCypher =
CONFIG.CATEGORIES_ACTIVE && categoryIds
? `WITH post
UNWIND $categoryIds AS categoryId
MATCH (category:Category {id: categoryId})
MERGE (post)-[:CATEGORIZED]->(category)`
: ''
const createPostTransactionResponse = await transaction.run( const createPostTransactionResponse = await transaction.run(
` `
CREATE (post:Post) CREATE (post:Post)
@ -93,9 +102,10 @@ export default {
WITH post WITH post
MATCH (author:User {id: $userId}) MATCH (author:User {id: $userId})
MERGE (post)<-[:WROTE]-(author) MERGE (post)<-[:WROTE]-(author)
${categoriesCypher}
RETURN post {.*} RETURN post {.*}
`, `,
{ userId: context.user.id, params }, { userId: context.user.id, params, categoryIds },
) )
const [post] = createPostTransactionResponse.records.map((record) => record.get('post')) const [post] = createPostTransactionResponse.records.map((record) => record.get('post'))
if (imageInput) { if (imageInput) {
@ -127,7 +137,7 @@ export default {
WITH post WITH post
` `
if (categoryIds && categoryIds.length) { if (CONFIG.CATEGORIES_ACTIVE && categoryIds && categoryIds.length) {
const cypherDeletePreviousRelations = ` const cypherDeletePreviousRelations = `
MATCH (post:Post { id: $params.id })-[previousRelations:CATEGORIZED]->(category:Category) MATCH (post:Post { id: $params.id })-[previousRelations:CATEGORIZED]->(category:Category)
DELETE previousRelations DELETE previousRelations

View File

@ -4,3 +4,4 @@ PUBLIC_REGISTRATION=false
INVITE_REGISTRATION=true INVITE_REGISTRATION=true
WEBSOCKETS_URI=ws://localhost:3000/api/graphql WEBSOCKETS_URI=ws://localhost:3000/api/graphql
GRAPHQL_URI=http://localhost:4000/ GRAPHQL_URI=http://localhost:4000/
CATEGORIES_ACTIVE=false

View File

@ -58,6 +58,9 @@ describe('ContributionForm.vue', () => {
back: jest.fn(), back: jest.fn(),
push: jest.fn(), push: jest.fn(),
}, },
$env: {
CATEGORIES_ACTIVE: false,
},
} }
propsData = {} propsData = {}
}) })
@ -132,6 +135,7 @@ describe('ContributionForm.vue', () => {
variables: { variables: {
title: postTitle, title: postTitle,
content: postContent, content: postContent,
categoryIds: [],
id: null, id: null,
image: null, image: null,
}, },
@ -254,6 +258,7 @@ describe('ContributionForm.vue', () => {
variables: { variables: {
title: propsData.contribution.title, title: propsData.contribution.title,
content: propsData.contribution.content, content: propsData.contribution.content,
categoryIds: [],
id: propsData.contribution.id, id: propsData.contribution.id,
image: { image: {
sensitive: false, sensitive: false,

View File

@ -51,6 +51,19 @@
{{ contentLength }} {{ contentLength }}
<base-icon v-if="errors && errors.content" name="warning" /> <base-icon v-if="errors && errors.content" name="warning" />
</ds-chip> </ds-chip>
<categories-select
v-if="categoriesActive"
model="categoryIds"
:existingCategoryIds="formData.categoryIds"
/>
<ds-chip
v-if="categoriesActive"
size="base"
:color="errors && errors.categoryIds && 'danger'"
>
{{ formData.categoryIds.length }} / 3
<base-icon v-if="errors && errors.categoryIds" name="warning" />
</ds-chip>
<div class="buttons"> <div class="buttons">
<base-button data-test="cancel-button" :disabled="loading" @click="$router.back()" danger> <base-button data-test="cancel-button" :disabled="loading" @click="$router.back()" danger>
{{ $t('actions.cancel') }} {{ $t('actions.cancel') }}
@ -69,6 +82,7 @@ import gql from 'graphql-tag'
import { mapGetters } from 'vuex' import { mapGetters } from 'vuex'
import HcEditor from '~/components/Editor/Editor' import HcEditor from '~/components/Editor/Editor'
import PostMutations from '~/graphql/PostMutations.js' import PostMutations from '~/graphql/PostMutations.js'
import CategoriesSelect from '~/components/CategoriesSelect/CategoriesSelect'
import ImageUploader from '~/components/ImageUploader/ImageUploader' import ImageUploader from '~/components/ImageUploader/ImageUploader'
import links from '~/constants/links.js' import links from '~/constants/links.js'
import PageParamsLink from '~/components/_new/features/PageParamsLink/PageParamsLink.vue' import PageParamsLink from '~/components/_new/features/PageParamsLink/PageParamsLink.vue'
@ -78,6 +92,7 @@ export default {
HcEditor, HcEditor,
ImageUploader, ImageUploader,
PageParamsLink, PageParamsLink,
CategoriesSelect,
}, },
props: { props: {
contribution: { contribution: {
@ -86,7 +101,7 @@ export default {
}, },
}, },
data() { data() {
const { title, content, image } = this.contribution const { title, content, image, categories } = this.contribution
const { const {
sensitive: imageBlurred = false, sensitive: imageBlurred = false,
aspectRatio: imageAspectRatio = null, aspectRatio: imageAspectRatio = null,
@ -94,6 +109,7 @@ export default {
} = image || {} } = image || {}
return { return {
categoriesActive: this.$env.CATEGORIES_ACTIVE,
links, links,
formData: { formData: {
title: title || '', title: title || '',
@ -102,11 +118,22 @@ export default {
imageAspectRatio, imageAspectRatio,
imageType, imageType,
imageBlurred, imageBlurred,
categoryIds: categories ? categories.map((category) => category.id) : [],
}, },
formSchema: { formSchema: {
title: { required: true, min: 3, max: 100 }, title: { required: true, min: 3, max: 100 },
content: { required: true }, content: { required: true },
imageBlurred: { required: false }, imageBlurred: { required: false },
categoryIds: {
type: 'array',
required: this.categoriesActive,
validator: (_, value = []) => {
if (this.categoriesActive && (value.length === 0 || value.length > 3)) {
return [new Error(this.$t('common.validations.categories'))]
}
return []
},
},
}, },
loading: false, loading: false,
users: [], users: [],
@ -125,7 +152,7 @@ export default {
methods: { methods: {
submit() { submit() {
let image = null let image = null
const { title, content } = this.formData const { title, content, categoryIds } = this.formData
if (this.formData.image) { if (this.formData.image) {
image = { image = {
sensitive: this.formData.imageBlurred, sensitive: this.formData.imageBlurred,
@ -143,6 +170,7 @@ export default {
variables: { variables: {
title, title,
content, content,
categoryIds,
id: this.contribution.id || null, id: this.contribution.id || null,
image, image,
}, },

View File

@ -47,6 +47,9 @@ describe('PostTeaser', () => {
data: { DeletePost: { id: 'deleted-post-id' } }, data: { DeletePost: { id: 'deleted-post-id' } },
}), }),
}, },
$env: {
CATEGORIES_ACTIVE: false,
},
} }
getters = { getters = {
'auth/isModerator': () => false, 'auth/isModerator': () => false,

View File

@ -26,7 +26,19 @@
class="footer" class="footer"
v-observe-visibility="(isVisible, entry) => visibilityChanged(isVisible, entry, post.id)" v-observe-visibility="(isVisible, entry) => visibilityChanged(isVisible, entry, post.id)"
> >
<div class="categories-placeholder"></div> <div class="categories" v-if="categoriesActive">
<hc-category
v-for="category in post.categories"
:key="category.id"
v-tooltip="{
content: $t(`contribution.category.name.${category.slug}`),
placement: 'bottom-start',
delay: { show: 500 },
}"
:icon="category.icon"
/>
</div>
<div v-else class="categories-placeholder"></div>
<counter-icon <counter-icon
icon="bullhorn" icon="bullhorn"
:count="post.shoutedCount" :count="post.shoutedCount"
@ -70,6 +82,7 @@
import UserTeaser from '~/components/UserTeaser/UserTeaser' import UserTeaser from '~/components/UserTeaser/UserTeaser'
import ContentMenu from '~/components/ContentMenu/ContentMenu' import ContentMenu from '~/components/ContentMenu/ContentMenu'
import HcRibbon from '~/components/Ribbon' import HcRibbon from '~/components/Ribbon'
import HcCategory from '~/components/Category'
import CounterIcon from '~/components/_new/generic/CounterIcon/CounterIcon' import CounterIcon from '~/components/_new/generic/CounterIcon/CounterIcon'
import { mapGetters } from 'vuex' import { mapGetters } from 'vuex'
import PostMutations from '~/graphql/PostMutations' import PostMutations from '~/graphql/PostMutations'
@ -79,6 +92,7 @@ export default {
name: 'PostTeaser', name: 'PostTeaser',
components: { components: {
UserTeaser, UserTeaser,
HcCategory,
HcRibbon, HcRibbon,
ContentMenu, ContentMenu,
CounterIcon, CounterIcon,
@ -93,6 +107,11 @@ export default {
default: () => {}, default: () => {},
}, },
}, },
data() {
return {
categoriesActive: this.$env.CATEGORIES_ACTIVE,
}
},
mounted() { mounted() {
const { image } = this.post const { image } = this.post
if (!image) return if (!image) return

View File

@ -24,6 +24,9 @@ describe('SearchResults', () => {
beforeEach(() => { beforeEach(() => {
mocks = { mocks = {
$t: jest.fn(), $t: jest.fn(),
$env: {
CATEGORIES_ACTIVE: false,
},
} }
getters = { getters = {
'auth/user': () => { 'auth/user': () => {

View File

@ -33,6 +33,7 @@ const options = {
// Cookies // Cookies
COOKIE_EXPIRE_TIME: process.env.COOKIE_EXPIRE_TIME || 730, // Two years by default 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 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,
} }
const CONFIG = { const CONFIG = {

View File

@ -78,6 +78,12 @@ export const tagsCategoriesAndPinnedFragment = gql`
tags { tags {
id id
} }
categories {
id
slug
name
icon
}
pinnedBy { pinnedBy {
id id
name name

View File

@ -18,6 +18,9 @@ describe('post/_id.vue', () => {
slug: 'my-post', slug: 'my-post',
}, },
}, },
$env: {
CATEGORIES_ACTIVE: false,
},
} }
}) })

View File

@ -72,6 +72,9 @@ describe('PostSlug', () => {
query: jest.fn().mockResolvedValue({ data: { PostEmotionsCountByEmotion: {} } }), query: jest.fn().mockResolvedValue({ data: { PostEmotionsCountByEmotion: {} } }),
}, },
$scrollTo: jest.fn(), $scrollTo: jest.fn(),
$env: {
CATEGORIES_ACTIVE: false,
},
} }
stubs = { stubs = {
HcEditor: { render: () => {}, methods: { insertReply: jest.fn(() => null) } }, HcEditor: { render: () => {}, methods: { insertReply: jest.fn(() => null) } },

View File

@ -44,6 +44,19 @@
<h2 class="title hyphenate-text">{{ post.title }}</h2> <h2 class="title hyphenate-text">{{ post.title }}</h2>
<ds-space margin-bottom="small" /> <ds-space margin-bottom="small" />
<content-viewer class="content hyphenate-text" :content="post.content" /> <content-viewer class="content hyphenate-text" :content="post.content" />
<!-- Categories -->
<div v-if="categoriesActive" class="categories">
<!-- eslint-enable vue/no-v-html -->
<ds-space margin="xx-large" />
<ds-space margin="xx-small" />
<hc-category
v-for="category in post.categories"
:key="category.id"
:icon="category.icon"
:name="$t(`contribution.category.name.${category.slug}`)"
/>
</div>
<ds-space margin-bottom="small" />
<!-- Tags --> <!-- Tags -->
<div v-if="post.tags && post.tags.length" class="tags"> <div v-if="post.tags && post.tags.length" class="tags">
<ds-space margin="xx-small" /> <ds-space margin="xx-small" />
@ -91,6 +104,7 @@
<script> <script>
import ContentViewer from '~/components/Editor/ContentViewer' import ContentViewer from '~/components/Editor/ContentViewer'
import HcCategory from '~/components/Category'
import HcHashtag from '~/components/Hashtag/Hashtag' import HcHashtag from '~/components/Hashtag/Hashtag'
import ContentMenu from '~/components/ContentMenu/ContentMenu' import ContentMenu from '~/components/ContentMenu/ContentMenu'
import UserTeaser from '~/components/UserTeaser/UserTeaser' import UserTeaser from '~/components/UserTeaser/UserTeaser'
@ -118,6 +132,7 @@ export default {
CommentForm, CommentForm,
CommentList, CommentList,
ContentViewer, ContentViewer,
HcCategory,
HcHashtag, HcHashtag,
HcShoutButton, HcShoutButton,
PageParamsLink, PageParamsLink,
@ -138,6 +153,7 @@ export default {
blurred: false, blurred: false,
blocked: null, blocked: null,
postAuthor: null, postAuthor: null,
categoriesActive: this.$env.CATEGORIES_ACTIVE,
} }
}, },
mounted() { mounted() {

View File

@ -5,13 +5,13 @@ const localVue = global.localVue
describe('create.vue', () => { describe('create.vue', () => {
let wrapper let wrapper
let mocks
beforeEach(() => { const mocks = {
mocks = {
$t: jest.fn(), $t: jest.fn(),
$env: {
CATEGORIES_ACTIVE: false,
},
} }
})
describe('mount', () => { describe('mount', () => {
const Wrapper = () => { const Wrapper = () => {

View File

@ -34,6 +34,9 @@ describe('post/_id.vue', () => {
}), }),
}, },
}, },
$env: {
CATEGORIES_ACTIVE: false,
},
} }
store = new Vuex.Store({ store = new Vuex.Store({
getters: { getters: {

View File

@ -29,6 +29,10 @@ describe('Registration', () => {
query: {}, query: {},
}, },
$env: {}, $env: {},
$toast: {
error: jest.fn(),
success: jest.fn(),
},
} }
asyncData = false asyncData = false
isLoggedIn = false isLoggedIn = false