diff --git a/backend/.env.template b/backend/.env.template index 5858a5d1e..239046dd3 100644 --- a/backend/.env.template +++ b/backend/.env.template @@ -28,3 +28,5 @@ AWS_BUCKET= EMAIL_DEFAULT_SENDER="devops@ocelot.social" EMAIL_SUPPORT="devops@ocelot.social" + +CATEGORIES_ACTIVE=false \ No newline at end of file diff --git a/backend/src/config/index.js b/backend/src/config/index.js index 6ad8c578b..7df780cfc 100644 --- a/backend/src/config/index.js +++ b/backend/src/config/index.js @@ -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 diff --git a/backend/src/schema/resolvers/posts.js b/backend/src/schema/resolvers/posts.js index d199b6f09..b09bb3edd 100644 --- a/backend/src/schema/resolvers/posts.js +++ b/backend/src/schema/resolvers/posts.js @@ -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 diff --git a/webapp/.env.template b/webapp/.env.template index 7373255a9..0a4c3405f 100644 --- a/webapp/.env.template +++ b/webapp/.env.template @@ -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 \ No newline at end of file diff --git a/webapp/components/ContributionForm/ContributionForm.spec.js b/webapp/components/ContributionForm/ContributionForm.spec.js index cce187a63..ce432fc42 100644 --- a/webapp/components/ContributionForm/ContributionForm.spec.js +++ b/webapp/components/ContributionForm/ContributionForm.spec.js @@ -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, diff --git a/webapp/components/ContributionForm/ContributionForm.vue b/webapp/components/ContributionForm/ContributionForm.vue index a06679149..3d4bb8e7c 100644 --- a/webapp/components/ContributionForm/ContributionForm.vue +++ b/webapp/components/ContributionForm/ContributionForm.vue @@ -51,6 +51,19 @@ {{ contentLength }} + + + {{ formData.categoryIds.length }} / 3 + +
{{ $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, }, diff --git a/webapp/components/PostTeaser/PostTeaser.spec.js b/webapp/components/PostTeaser/PostTeaser.spec.js index 7a37d710f..546c4581d 100644 --- a/webapp/components/PostTeaser/PostTeaser.spec.js +++ b/webapp/components/PostTeaser/PostTeaser.spec.js @@ -47,6 +47,9 @@ describe('PostTeaser', () => { data: { DeletePost: { id: 'deleted-post-id' } }, }), }, + $env: { + CATEGORIES_ACTIVE: false, + }, } getters = { 'auth/isModerator': () => false, diff --git a/webapp/components/PostTeaser/PostTeaser.vue b/webapp/components/PostTeaser/PostTeaser.vue index 949c7032e..a973ca31f 100644 --- a/webapp/components/PostTeaser/PostTeaser.vue +++ b/webapp/components/PostTeaser/PostTeaser.vue @@ -26,7 +26,19 @@ class="footer" v-observe-visibility="(isVisible, entry) => visibilityChanged(isVisible, entry, post.id)" > -
+
+ +
+
{}, }, }, + data() { + return { + categoriesActive: this.$env.CATEGORIES_ACTIVE, + } + }, mounted() { const { image } = this.post if (!image) return diff --git a/webapp/components/_new/features/SearchResults/SearchResults.spec.js b/webapp/components/_new/features/SearchResults/SearchResults.spec.js index b1886a754..b76226547 100644 --- a/webapp/components/_new/features/SearchResults/SearchResults.spec.js +++ b/webapp/components/_new/features/SearchResults/SearchResults.spec.js @@ -24,6 +24,9 @@ describe('SearchResults', () => { beforeEach(() => { mocks = { $t: jest.fn(), + $env: { + CATEGORIES_ACTIVE: false, + }, } getters = { 'auth/user': () => { diff --git a/webapp/config/index.js b/webapp/config/index.js index 00df85bac..db030e929 100644 --- a/webapp/config/index.js +++ b/webapp/config/index.js @@ -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 = { diff --git a/webapp/graphql/Fragments.js b/webapp/graphql/Fragments.js index 7b05e2369..b67851873 100644 --- a/webapp/graphql/Fragments.js +++ b/webapp/graphql/Fragments.js @@ -78,6 +78,12 @@ export const tagsCategoriesAndPinnedFragment = gql` tags { id } + categories { + id + slug + name + icon + } pinnedBy { id name diff --git a/webapp/pages/post/_id.spec.js b/webapp/pages/post/_id.spec.js index 7e6812002..cb97a1776 100644 --- a/webapp/pages/post/_id.spec.js +++ b/webapp/pages/post/_id.spec.js @@ -18,6 +18,9 @@ describe('post/_id.vue', () => { slug: 'my-post', }, }, + $env: { + CATEGORIES_ACTIVE: false, + }, } }) diff --git a/webapp/pages/post/_id/_slug/index.spec.js b/webapp/pages/post/_id/_slug/index.spec.js index 4289bb53d..4737386ef 100644 --- a/webapp/pages/post/_id/_slug/index.spec.js +++ b/webapp/pages/post/_id/_slug/index.spec.js @@ -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) } }, diff --git a/webapp/pages/post/_id/_slug/index.vue b/webapp/pages/post/_id/_slug/index.vue index b1ca870d7..d02a448da 100644 --- a/webapp/pages/post/_id/_slug/index.vue +++ b/webapp/pages/post/_id/_slug/index.vue @@ -44,6 +44,19 @@

{{ post.title }}

+ +
+ + + + +
+
@@ -91,6 +104,7 @@