mirror of
https://github.com/Ocelot-Social-Community/Ocelot-Social.git
synced 2025-12-13 07:46:06 +00:00
Merge pull request #5102 from Ocelot-Social-Community/make-categories-optional
feat: Make Categories Optional
This commit is contained in:
commit
c53bd68f4d
@ -28,3 +28,5 @@ AWS_BUCKET=
|
||||
|
||||
EMAIL_DEFAULT_SENDER="devops@ocelot.social"
|
||||
EMAIL_SUPPORT="devops@ocelot.social"
|
||||
|
||||
CATEGORIES_ACTIVE=false
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
},
|
||||
|
||||
@ -47,6 +47,9 @@ describe('PostTeaser', () => {
|
||||
data: { DeletePost: { id: 'deleted-post-id' } },
|
||||
}),
|
||||
},
|
||||
$env: {
|
||||
CATEGORIES_ACTIVE: false,
|
||||
},
|
||||
}
|
||||
getters = {
|
||||
'auth/isModerator': () => false,
|
||||
|
||||
@ -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
|
||||
|
||||
@ -24,6 +24,9 @@ describe('SearchResults', () => {
|
||||
beforeEach(() => {
|
||||
mocks = {
|
||||
$t: jest.fn(),
|
||||
$env: {
|
||||
CATEGORIES_ACTIVE: false,
|
||||
},
|
||||
}
|
||||
getters = {
|
||||
'auth/user': () => {
|
||||
|
||||
@ -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 = {
|
||||
|
||||
@ -78,6 +78,12 @@ export const tagsCategoriesAndPinnedFragment = gql`
|
||||
tags {
|
||||
id
|
||||
}
|
||||
categories {
|
||||
id
|
||||
slug
|
||||
name
|
||||
icon
|
||||
}
|
||||
pinnedBy {
|
||||
id
|
||||
name
|
||||
|
||||
@ -18,6 +18,9 @@ describe('post/_id.vue', () => {
|
||||
slug: 'my-post',
|
||||
},
|
||||
},
|
||||
$env: {
|
||||
CATEGORIES_ACTIVE: false,
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@ -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) } },
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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 = () => {
|
||||
|
||||
@ -34,6 +34,9 @@ describe('post/_id.vue', () => {
|
||||
}),
|
||||
},
|
||||
},
|
||||
$env: {
|
||||
CATEGORIES_ACTIVE: false,
|
||||
},
|
||||
}
|
||||
store = new Vuex.Store({
|
||||
getters: {
|
||||
|
||||
@ -29,6 +29,10 @@ describe('Registration', () => {
|
||||
query: {},
|
||||
},
|
||||
$env: {},
|
||||
$toast: {
|
||||
error: jest.fn(),
|
||||
success: jest.fn(),
|
||||
},
|
||||
}
|
||||
asyncData = false
|
||||
isLoggedIn = false
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user