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_SUPPORT="devops@ocelot.social"
CATEGORIES_ACTIVE=false

View File

@ -86,6 +86,7 @@ const options = {
ORGANIZATION_URL: emails.ORGANIZATION_LINK,
PUBLIC_REGISTRATION: env.PUBLIC_REGISTRATION === 'true' || false,
INVITE_REGISTRATION: env.INVITE_REGISTRATION !== 'false', // default = true
CATEGORIES_ACTIVE: process.env.CATEGORIES_ACTIVE === 'true' || false,
}
// 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 Resolver from './helpers/Resolver'
import { filterForMutedUsers } from './helpers/filterForMutedUsers'
import CONFIG from '../../config'
const maintainPinnedPosts = (params) => {
const pinnedPostFilter = { pinned: true }
@ -76,12 +77,20 @@ export default {
},
Mutation: {
CreatePost: async (_parent, params, context, _resolveInfo) => {
const { categoryIds } = params
const { image: imageInput } = params
delete params.categoryIds
delete params.image
params.id = params.id || uuid()
const session = context.driver.session()
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(
`
CREATE (post:Post)
@ -93,9 +102,10 @@ export default {
WITH post
MATCH (author:User {id: $userId})
MERGE (post)<-[:WROTE]-(author)
${categoriesCypher}
RETURN post {.*}
`,
{ userId: context.user.id, params },
{ userId: context.user.id, params, categoryIds },
)
const [post] = createPostTransactionResponse.records.map((record) => record.get('post'))
if (imageInput) {
@ -127,7 +137,7 @@ export default {
WITH post
`
if (categoryIds && categoryIds.length) {
if (CONFIG.CATEGORIES_ACTIVE && categoryIds && categoryIds.length) {
const cypherDeletePreviousRelations = `
MATCH (post:Post { id: $params.id })-[previousRelations:CATEGORIZED]->(category:Category)
DELETE previousRelations

View File

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

View File

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

View File

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

View File

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

View File

@ -26,7 +26,19 @@
class="footer"
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
icon="bullhorn"
:count="post.shoutedCount"
@ -70,6 +82,7 @@
import UserTeaser from '~/components/UserTeaser/UserTeaser'
import ContentMenu from '~/components/ContentMenu/ContentMenu'
import HcRibbon from '~/components/Ribbon'
import HcCategory from '~/components/Category'
import CounterIcon from '~/components/_new/generic/CounterIcon/CounterIcon'
import { mapGetters } from 'vuex'
import PostMutations from '~/graphql/PostMutations'
@ -79,6 +92,7 @@ export default {
name: 'PostTeaser',
components: {
UserTeaser,
HcCategory,
HcRibbon,
ContentMenu,
CounterIcon,
@ -93,6 +107,11 @@ export default {
default: () => {},
},
},
data() {
return {
categoriesActive: this.$env.CATEGORIES_ACTIVE,
}
},
mounted() {
const { image } = this.post
if (!image) return

View File

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

View File

@ -33,6 +33,7 @@ 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,
}
const CONFIG = {

View File

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

View File

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

View File

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

View File

@ -44,6 +44,19 @@
<h2 class="title hyphenate-text">{{ post.title }}</h2>
<ds-space margin-bottom="small" />
<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 -->
<div v-if="post.tags && post.tags.length" class="tags">
<ds-space margin="xx-small" />
@ -91,6 +104,7 @@
<script>
import ContentViewer from '~/components/Editor/ContentViewer'
import HcCategory from '~/components/Category'
import HcHashtag from '~/components/Hashtag/Hashtag'
import ContentMenu from '~/components/ContentMenu/ContentMenu'
import UserTeaser from '~/components/UserTeaser/UserTeaser'
@ -118,6 +132,7 @@ export default {
CommentForm,
CommentList,
ContentViewer,
HcCategory,
HcHashtag,
HcShoutButton,
PageParamsLink,
@ -138,6 +153,7 @@ export default {
blurred: false,
blocked: null,
postAuthor: null,
categoriesActive: this.$env.CATEGORIES_ACTIVE,
}
},
mounted() {
@ -171,12 +187,12 @@ export default {
heroImageStyle() {
/* Return false when image property is not present or is not a number
so no unnecessary css variables are set.
*/
*/
if (!this.post.image || typeof this.post.image.aspectRatio !== 'number') return false
/* Return the aspect ratio as a css variable. Later to be used when calculating
the height with respect to the width.
*/
*/
return {
'--hero-image-aspect-ratio': 1.0 / this.post.image.aspectRatio,
}
@ -253,12 +269,12 @@ export default {
/* The padding top makes sure the correct height is set (according to the
hero image aspect ratio) before the hero image loads so
the autoscroll works correctly when following a comment link.
*/
*/
padding-top: calc(var(--hero-image-aspect-ratio) * (100% + 48px));
/* Letting the image fill the container, since the container
is the one determining height
*/
*/
> .image {
position: absolute;
top: 0;

View File

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

View File

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

View File

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