diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 595c9d584..1fe27f6c6 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -1,8 +1,10 @@ --- -name: 🐛 Bug Report +name: "\U0001F41B Bug Report" about: Create a report to help us to improve. +title: "\U0001F41B [Bug] XXX" labels: bug -title: 🐛 [Bug] +assignees: '' + --- ## :bug: Bug Report diff --git a/.github/ISSUE_TEMPLATE/devops_ticket.md b/.github/ISSUE_TEMPLATE/devops_ticket.md index 115664911..17533cd54 100644 --- a/.github/ISSUE_TEMPLATE/devops_ticket.md +++ b/.github/ISSUE_TEMPLATE/devops_ticket.md @@ -1,8 +1,10 @@ --- -name: 💥 DevOps Ticket +name: "\U0001F4A5 DevOps Ticket" about: Help us manage our deployed app. +title: "\U0001F4A5 [DevOps] XXX" labels: devops -title: 💥 [DevOps] +assignees: '' + --- ## 💥 DevOps Ticket diff --git a/.github/ISSUE_TEMPLATE/epic.md b/.github/ISSUE_TEMPLATE/epic.md index cf72cd673..57eca6dfe 100644 --- a/.github/ISSUE_TEMPLATE/epic.md +++ b/.github/ISSUE_TEMPLATE/epic.md @@ -1,9 +1,12 @@ --- -name: 🌟 Epic +name: "\U0001F31F Epic" about: Define a big development step. +title: "\U0001F31F [EPIC] XXX" labels: epic -title: 🌟 [EPIC] +assignees: '' + --- + diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index beae80901..22cd5045e 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -1,8 +1,10 @@ --- -name: 🚀 Feature Request +name: "\U0001F680 Feature Request" about: Suggest an idea for this project. +title: "\U0001F680 [Feature] XXX" labels: feature -title: 🚀 [Feature] +assignees: '' + --- ## :rocket: Feature Request diff --git a/.github/ISSUE_TEMPLATE/question.md b/.github/ISSUE_TEMPLATE/question.md index 40e6e381b..f2328dcc7 100644 --- a/.github/ISSUE_TEMPLATE/question.md +++ b/.github/ISSUE_TEMPLATE/question.md @@ -1,9 +1,12 @@ --- -name: 💬 Question +name: "\U0001F4AC Question" about: If you need help understanding ocelot.social. +title: "\U0001F4AC [Question] XXX" labels: question -title: 💬 [Question] +assignees: '' + --- + diff --git a/.github/ISSUE_TEMPLATE/refactor_tickets.md b/.github/ISSUE_TEMPLATE/refactor_tickets.md index d1841e35e..867c809ae 100644 --- a/.github/ISSUE_TEMPLATE/refactor_tickets.md +++ b/.github/ISSUE_TEMPLATE/refactor_tickets.md @@ -1,10 +1,11 @@ --- -name: 🔧 Refactor +name: "\U0001F527 Refactor" about: Help us improve our code by refactoring it. +title: "\U0001F527 [Refactor] XXX" labels: refactor -title: 🔧 [Refactor] +assignees: '' + --- ## 🔧 Refactor - diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 6cfe4c30c..655c5cc93 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,13 +1,15 @@ -## 🍰 Pullrequest +## 🍰 Pull Request +XXX + ### Issues - + - fixes #XXX - relates #XXX ---> -- None ### Todo -- [X] None + +- [ ] XXX list here … diff --git a/CHANGELOG.md b/CHANGELOG.md index 8f8aa04ee..940cc77a8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,8 +4,51 @@ All notable changes to this project will be documented in this file. Dates are d Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). +#### [1.1.1](https://github.com/Ocelot-Social-Community/Ocelot-Social/compare/1.1.0...1.1.1) + +- chore: 🍰 Refactor Rebranding [`#5390`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/5390) +- feat: 🍰 Tooltips For Topics [`#5350`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/5350) +- feat: 🍰 Save Categories In Frontend [`#5284`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/5284) +- feat: 🍰 Add New Yunite Icons [`#5319`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/5319) +- chore: 🍰 Update Neode From v^0.4.7 To v^0.4.8 In Backend [`#5334`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/5334) +- fix: Category Filter Menu Client Only [`#5301`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/5301) +- feat: Save Category Settings [`#5261`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/5261) +- feat: Topics Menu [`#5248`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/5248) +- docs: 🍰 Document GraqhQL Playground [`#5253`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/5253) +- feat: Categories Filter Menu [`#5198`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/5198) +- fix: 🍰 Fix Test Description From `enter-nonce.vue` To `change-password` [`#5217`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/5217) +- Bump cookie-universal-nuxt from 2.1.5 to 2.2.2 in /webapp [`#5218`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/5218) +- Bump prettier from 2.2.1 to 2.7.1 in /webapp [`#5170`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/5170) +- Bump eslint-plugin-prettier from 3.1.2 to 3.4.1 in /backend [`#5211`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/5211) +- Bump slug from 4.0.2 to 6.0.0 in /backend [`#5193`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/5193) +- chore: 🍰 Fix typo in PULL_REQUEST_TEMPLATE.md file [`#5208`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/5208) +- Bump slug from 5.1.0 to 6.0.0 [`#5191`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/5191) +- Bump vue-sweetalert-icons from 4.3.0 to 4.3.1 in /webapp [`#5174`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/5174) +- feat: 🍰 Change Error Message With `Authorised` To `Authorized` All Over The Place To Have American English [`#5206`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/5206) +- Bump cross-env from 7.0.2 to 7.0.3 in /webapp [`#5168`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/5168) +- chore: 🍰 Add `--logHeapUsage` To Jest Test Call [`#5182`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/5182) +- refactor: 🍰 Rename `UserGroup` To `UserRole` [`#5143`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/5143) +- add new yunite icons [`bb0d632`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/bb0d6329e7e36ea03671318ea8dd128a6d5a5a7a) +- cleanup refactor rebranding [`5f5c0fa`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/5f5c0faa1f28cd4df7681eba335ae5998b2d9cca) +- change color and scss in branding [`52070b8`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/52070b8c570970bf48df561134bf67cb4111b640) + +#### [1.1.0](https://github.com/Ocelot-Social-Community/Ocelot-Social/compare/1.0.9...1.1.0) + +> 4 August 2022 + +- chore: 🍰 Release v1.1.0 - Implement Categories Again [`#5145`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/5145) +- feat: Make Categories Optional [`#5102`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/5102) +- Update issue templates [`#5101`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/5101) +- chore: 🍰 Betters Automatic Deployment To `stage.ocelot.social` On Push To `master` Branch [`#5097`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/5097) +- add optional categories to teaser and post [`bc95500`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/bc955003f7c33aabe592bee782aca973b4f00cba) +- env vatiable for CATEGORIES_ACTIVE and switch for categories in contribution form [`e31f250`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/e31f250ea5e1949f4f08e72fe82622d41ecd85f1) +- fix some tests [`5393c2a`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/5393c2aeaaf070a637390c430d5f03057030ff52) + #### [1.0.9](https://github.com/Ocelot-Social-Community/Ocelot-Social/compare/1.0.8...1.0.9) +> 20 July 2022 + +- chore: 🍰 Release v1.0.9 [`#5095`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/5095) - chore: 🍰 Automatic Deployment To `stage.ocelot.social` On Push To `master` Branch [`#5080`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/5080) - change footer version-link [`#5091`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/5091) - docs: 🍰 Add Neo4j Docu For Important Commands [`#5090`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/5090) 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/README.md b/backend/README.md index 6d837856c..083606b09 100644 --- a/backend/README.md +++ b/backend/README.md @@ -60,6 +60,7 @@ your `.env` configuration file. Your backend is up and running at [http://localhost:4000/](http://localhost:4000/) This will start the GraphQL service \(by default on localhost:4000\) where you can issue GraphQL requests or access GraphQL Playground in the browser. +More details about our GraphQL playground and how to use it with ocelot.social can be found [here](./src/graphql/GraphQL-Playground.md).  diff --git a/backend/package.json b/backend/package.json index 028b9295e..bcc91a25a 100644 --- a/backend/package.json +++ b/backend/package.json @@ -1,6 +1,6 @@ { "name": "ocelot-social-backend", - "version": "1.0.9", + "version": "1.1.1", "description": "GraphQL Backend for ocelot.social", "repository": "https://github.com/Ocelot-Social-Community/Ocelot-Social", "author": "ocelot.social Community", @@ -15,7 +15,7 @@ "dev": "nodemon --exec babel-node src/ -e js,gql", "dev:debug": "nodemon --exec babel-node --inspect=0.0.0.0:9229 src/ -e js,gql", "lint": "eslint src --config .eslintrc.js", - "test": "cross-env NODE_ENV=test jest --forceExit --detectOpenHandles --runInBand --coverage", + "test": "cross-env NODE_ENV=test NODE_OPTIONS=--max-old-space-size=8192 jest --forceExit --detectOpenHandles --runInBand --coverage --logHeapUsage", "db:clean": "babel-node src/db/clean.js", "db:reset": "yarn run db:clean", "db:seed": "babel-node src/db/seed.js", @@ -103,14 +103,14 @@ "mustache": "^4.2.0", "neo4j-driver": "^4.0.2", "neo4j-graphql-js": "^2.11.5", - "neode": "^0.4.7", + "neode": "^0.4.8", "node-fetch": "~2.6.1", "nodemailer": "^6.4.4", "nodemailer-html-to-text": "^3.2.0", "npm-run-all": "~4.1.5", "request": "~2.88.2", "sanitize-html": "~1.22.0", - "slug": "~4.0.2", + "slug": "~6.0.0", "subscriptions-transport-ws": "^0.9.19", "trunc-html": "~1.1.2", "uuid": "~8.3.2", @@ -129,7 +129,7 @@ "eslint-plugin-import": "~2.20.2", "eslint-plugin-jest": "~23.8.2", "eslint-plugin-node": "~11.1.0", - "eslint-plugin-prettier": "~3.1.2", + "eslint-plugin-prettier": "~3.4.1", "eslint-plugin-promise": "~4.3.1", "eslint-plugin-standard": "~4.0.1", "jest": "~25.3.0", 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/constants/categories.js b/backend/src/constants/categories.js new file mode 100644 index 000000000..3676fed0b --- /dev/null +++ b/backend/src/constants/categories.js @@ -0,0 +1,98 @@ +export const categories = [ + { + icon: 'networking', + name: 'networking', + description: 'Kooperation, Aktionsbündnisse, Solidarität, Hilfe', + }, + { + icon: 'home', + name: 'home', + description: 'Bauen, Lebensgemeinschaften, Tiny Houses, Gemüsegarten', + }, + { + icon: 'energy', + name: 'energy', + description: 'Öl, Gas, Kohle, Wind, Wasserkraft, Biogas, Atomenergie, ...', + }, + { + icon: 'psyche', + name: 'psyche', + description: 'Seele, Gefühle, Glück', + }, + { + icon: 'movement', + name: 'body-and-excercise', + description: 'Sport, Yoga, Massage, Tanzen, Entspannung', + }, + { + icon: 'balance-scale', + name: 'law', + description: 'Menschenrechte, Gesetze, Verordnungen', + }, + { + icon: 'finance', + name: 'finance', + description: 'Geld, Finanzsystem, Alternativwährungen, ...', + }, + { + icon: 'child', + name: 'children', + description: 'Familie, Pädagogik, Schule, Prägung', + }, + { + icon: 'mobility', + name: 'mobility', + description: 'Reise, Verkehr, Elektromobilität', + }, + { + icon: 'shopping-cart', + name: 'economy', + description: 'Handel, Konsum, Marketing, Lebensmittel, Lieferketten, ...', + }, + { + icon: 'peace', + name: 'peace', + description: 'Krieg, Militär, soziale Verteidigung, Waffen, Cyberattacken', + }, + { + icon: 'politics', + name: 'politics', + description: 'Demokratie, Mitbestimmung, Wahlen, Korruption, Parteien', + }, + { + icon: 'nature', + name: 'nature', + description: 'Tiere, Pflanzen, Landwirtschaft, Ökologie, Artenvielfalt', + }, + { + icon: 'science', + name: 'science', + description: 'Bildung, Hochschule, Publikationen, ...', + }, + { + icon: 'health', + name: 'health', + description: 'Medizin, Ernährung, WHO, Impfungen, Schadstoffe, ...', + }, + { + icon: 'media', + name: 'it-and-media', + description: + 'Nachrichten, Manipulation, Datenschutz, Überwachung, Datenkraken, AI, Software, Apps', + }, + { + icon: 'spirituality', + name: 'spirituality', + description: 'Religion, Werte, Ethik', + }, + { + icon: 'culture', + name: 'culture', + description: 'Kunst, Theater, Musik, Fotografie, Film', + }, + { + icon: 'miscellaneous', + name: 'miscellaneous', + description: '', + }, +] diff --git a/backend/src/db/migrate/store.js b/backend/src/db/migrate/store.js index 377caf0b0..d6fd25bd8 100644 --- a/backend/src/db/migrate/store.js +++ b/backend/src/db/migrate/store.js @@ -1,6 +1,8 @@ import { getDriver, getNeode } from '../../db/neo4j' import { hashSync } from 'bcryptjs' import { v4 as uuid } from 'uuid' +import { categories } from '../../constants/categories' +import CONFIG from '../../config' const defaultAdmin = { email: 'admin@example.org', @@ -10,6 +12,29 @@ const defaultAdmin = { slug: 'admin', } +const createCategories = async (session) => { + const createCategoriesTxResultPromise = session.writeTransaction(async (txc) => { + categories.forEach(({ icon, name }, index) => { + const id = `cat${index + 1}` + txc.run( + `MERGE (c:Category { + icon: "${icon}", + slug: "${name}", + name: "${name}", + id: "${id}", + createdAt: toString(datetime()) + })`, + ) + }) + }) + try { + await createCategoriesTxResultPromise + console.log('Successfully created categories!') // eslint-disable-line no-console + } catch (error) { + console.log(`Error creating categories: ${error}`) // eslint-disable-line no-console + } +} + const createDefaultAdminUser = async (session) => { const readTxResultPromise = session.readTransaction(async (txc) => { const result = await txc.run('MATCH (user:User) RETURN count(user) AS userCount') @@ -45,7 +70,7 @@ const createDefaultAdminUser = async (session) => { }) try { await createAdminTxResultPromise - console.log('Successfully created default admin user') // eslint-disable-line no-console + console.log('Successfully created default admin user!') // eslint-disable-line no-console } catch (error) { console.log(error) // eslint-disable-line no-console } @@ -58,6 +83,7 @@ class Store { const { driver } = neode const session = driver.session() await createDefaultAdminUser(session) + if (CONFIG.CATEGORIES_ACTIVE) await createCategories(session) const writeTxResultPromise = session.writeTransaction(async (txc) => { await txc.run('CALL apoc.schema.assert({},{},true)') // drop all indices return Promise.all( diff --git a/backend/src/db/seed.js b/backend/src/db/seed.js index 46c5870e0..482517e69 100644 --- a/backend/src/db/seed.js +++ b/backend/src/db/seed.js @@ -6,6 +6,7 @@ import faker from '@faker-js/faker' import Factory from '../db/factories' import { getNeode, getDriver } from '../db/neo4j' import { gql } from '../helpers/jest' +import { categories } from '../constants/categories' if (CONFIG.PRODUCTION && !CONFIG.PRODUCTION_DB_CLEAN_ALLOW) { throw new Error(`You cannot seed the database in a non-staging and real production environment!`) @@ -267,104 +268,16 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl'] dagobert.relateTo(louie, 'blocked'), ]) - await Promise.all([ - Factory.build('category', { - id: 'cat1', - name: 'Just For Fun', - slug: 'just-for-fun', - icon: 'smile', + await Promise.all( + categories.map(({ icon, name }, index) => { + Factory.build('category', { + id: `cat${index + 1}`, + slug: name, + name, + icon, + }) }), - Factory.build('category', { - id: 'cat2', - name: 'Happiness & Values', - slug: 'happiness-values', - icon: 'heart-o', - }), - Factory.build('category', { - id: 'cat3', - name: 'Health & Wellbeing', - slug: 'health-wellbeing', - icon: 'medkit', - }), - Factory.build('category', { - id: 'cat4', - name: 'Environment & Nature', - slug: 'environment-nature', - icon: 'tree', - }), - Factory.build('category', { - id: 'cat5', - name: 'Animal Protection', - slug: 'animal-protection', - icon: 'paw', - }), - Factory.build('category', { - id: 'cat6', - name: 'Human Rights & Justice', - slug: 'human-rights-justice', - icon: 'balance-scale', - }), - Factory.build('category', { - id: 'cat7', - name: 'Education & Sciences', - slug: 'education-sciences', - icon: 'graduation-cap', - }), - Factory.build('category', { - id: 'cat8', - name: 'Cooperation & Development', - slug: 'cooperation-development', - icon: 'users', - }), - Factory.build('category', { - id: 'cat9', - name: 'Democracy & Politics', - slug: 'democracy-politics', - icon: 'university', - }), - Factory.build('category', { - id: 'cat10', - name: 'Economy & Finances', - slug: 'economy-finances', - icon: 'money', - }), - Factory.build('category', { - id: 'cat11', - name: 'Energy & Technology', - slug: 'energy-technology', - icon: 'flash', - }), - Factory.build('category', { - id: 'cat12', - name: 'IT, Internet & Data Privacy', - slug: 'it-internet-data-privacy', - icon: 'mouse-pointer', - }), - Factory.build('category', { - id: 'cat13', - name: 'Art, Culture & Sport', - slug: 'art-culture-sport', - icon: 'paint-brush', - }), - Factory.build('category', { - id: 'cat14', - name: 'Freedom of Speech', - slug: 'freedom-of-speech', - icon: 'bullhorn', - }), - Factory.build('category', { - id: 'cat15', - name: 'Consumption & Sustainability', - slug: 'consumption-sustainability', - icon: 'shopping-cart', - }), - Factory.build('category', { - id: 'cat16', - name: 'Global Peace & Nonviolence', - slug: 'global-peace-nonviolence', - icon: 'angellist', - }), - ]) + ) const [environment, nature, democracy, freedom] = await Promise.all([ Factory.build('tag', { diff --git a/backend/src/graphql/GraphQL-Playground.md b/backend/src/graphql/GraphQL-Playground.md new file mode 100644 index 000000000..af248f112 --- /dev/null +++ b/backend/src/graphql/GraphQL-Playground.md @@ -0,0 +1,108 @@ +# GraphQL Playground + +To use GraphQL Playground, we need to know some basics: + +## How To Login? + +First, we need to have a user from ocelot.social to log in as. +The user can be created by seeding the Neo4j database from the backend or by multiple GraphQL mutations. + +### Seed The Neo4j Database + +In your browser you can reach the GraphQL Playground under `http://localhost:4000/`, if the database and the backend are running, see [backend](../../README.md). +There you will also find instructions on how to seed the database. + +### Use GraphQL Mutations To Create A User + +TODO: Describe how to create a user using GraphQL mutations! + +### Login Via GraphQL + +You can register a user by sending the query: + +```gql +mutation { + login(email: "user@example.org", password: "1234") +} +``` + +Or use `"moderator@example.org"` or `"admin@example.org"` for the roll you need. + +If all goes well, you will receive a QGL response like: + +```json +{ + "data": { + "login": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InUzIiwibmFtZSI6Ikplbm55IFJvc3RvY2siLCJzbHVnIjoiamVubnktcm9zdG9jayIsImlhdCI6MTY2MjAyMzMwNSwiZXhwIjoxNzI1MTM4NTA1LCJhdWQiOiJodHRwOi8vbG9jYWxob3N0OjMwMDAiLCJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjQwMDAiLCJzdWIiOiJ1MyJ9.atBS-SOeS784HPeFl_5s8sRWehEAU1BkgcOZFD8d4bU" + } +} +``` + +You can use this response to set an HTTP header when you click `HTTP HEADERS` in the footer. +Just set it with the login token you received in response: + +```json +{ + "Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InUzIiwibmFtZSI6Ikplbm55IFJvc3RvY2siLCJzbHVnIjoiamVubnktcm9zdG9jayIsImlhdCI6MTY2MjAyMzMwNSwiZXhwIjoxNzI1MTM4NTA1LCJhdWQiOiJodHRwOi8vbG9jYWxob3N0OjMwMDAiLCJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjQwMDAiLCJzdWIiOiJ1MyJ9.atBS-SOeS784HPeFl_5s8sRWehEAU1BkgcOZFD8d4bU" +} +``` + +This token is used for all other queries and mutations you send to the backend. + +## Query And Mutate + +When you are logged in and open a new playground tab by clicking "+", you can create a new group by sending the following mutation: + +```gql +mutation { + CreateGroup( + # id: "" + name: "My Group" + # slug: "" + about: "We will save the world" + description: "
English:
This group is hidden.
This group was created to allow investigative journalists to share and collaborate.
Here you can internally share posts and comments about them.
Deutsch:
Diese Gruppe ist verborgen.
Diese Gruppe wurde geschaffen, um investigativen Journalisten den Austausch und die Zusammenarbeit zu ermöglichen.
Hier könnt ihr euch intern über Beiträge und Kommentare zu ihnen austauschen.
" + groupType: hidden + actionRadius: interplanetary + categoryIds: ["cat12"] + ) { + id + name + slug + createdAt + updatedAt + disabled + deleted + about + description + groupType + actionRadius + myRole + } +} +``` + +You will receive the answer: + +```json +{ + "data": { + "CreateGroup": { + "id": "2e3bbadb-804b-4ebc-a673-2d7c7f05e827", + "name": "My Group", + "slug": "my-group", + "createdAt": "2022-09-01T09:44:47.969Z", + "updatedAt": "2022-09-01T09:44:47.969Z", + "disabled": false, + "deleted": false, + "about": "We will save the world", + "description": "English:
This group is hidden.
This group was created to allow investigative journalists to share and collaborate.
Here you can internally share posts and comments about them.
Deutsch:
Diese Gruppe ist verborgen.
Diese Gruppe wurde geschaffen, um investigativen Journalisten den Austausch und die Zusammenarbeit zu ermöglichen.
Hier könnt ihr euch intern über Beiträge und Kommentare zu ihnen austauschen.
", + "groupType": "hidden", + "actionRadius": "interplanetary", + "myRole": "owner" + } + } +} +``` + +If you look into the Neo4j database with your browser and search the groups, you will now also find your new group. +For more details about our Neo4j database read [here](../../../neo4j/README.md). diff --git a/backend/src/middleware/permissionsMiddleware.js b/backend/src/middleware/permissionsMiddleware.js index b10389f50..71a44f225 100644 --- a/backend/src/middleware/permissionsMiddleware.js +++ b/backend/src/middleware/permissionsMiddleware.js @@ -178,6 +178,7 @@ export default shield( GenerateInviteCode: isAuthenticated, switchUserRole: isAdmin, markTeaserAsViewed: allow, + saveCategorySettings: isAuthenticated, }, User: { email: or(isMyOwn, isAdmin), @@ -188,5 +189,6 @@ export default shield( debug, allowExternalErrors, fallbackRule: allow, + fallbackError: Error('Not Authorized!'), }, ) diff --git a/backend/src/middleware/permissionsMiddleware.spec.js b/backend/src/middleware/permissionsMiddleware.spec.js index e29e0a4af..9d71314bb 100644 --- a/backend/src/middleware/permissionsMiddleware.spec.js +++ b/backend/src/middleware/permissionsMiddleware.spec.js @@ -102,7 +102,7 @@ describe('authorization', () => { await expect( query({ query: userQuery, variables: { name: 'Owner' } }), ).resolves.toMatchObject({ - errors: [{ message: 'Not Authorised!' }], + errors: [{ message: 'Not Authorized!' }], data: { User: [null] }, }) }) @@ -132,7 +132,7 @@ describe('authorization', () => { await expect( query({ query: userQuery, variables: { name: 'Owner' } }), ).resolves.toMatchObject({ - errors: [{ message: 'Not Authorised!' }], + errors: [{ message: 'Not Authorized!' }], data: { User: [null] }, }) }) @@ -147,7 +147,7 @@ describe('authorization', () => { await expect( query({ query: userQuery, variables: { name: 'Owner' } }), ).resolves.toMatchObject({ - errors: [{ message: 'Not Authorised!' }], + errors: [{ message: 'Not Authorized!' }], data: { User: [null] }, }) }) @@ -198,7 +198,7 @@ describe('authorization', () => { it('denies permission', async () => { await expect(mutate({ mutation: signupMutation, variables })).resolves.toMatchObject({ - errors: [{ message: 'Not Authorised!' }], + errors: [{ message: 'Not Authorized!' }], data: { Signup: null }, }) }) @@ -288,7 +288,7 @@ describe('authorization', () => { it('denies permission', async () => { await expect(mutate({ mutation: signupMutation, variables })).resolves.toMatchObject({ - errors: [{ message: 'Not Authorised!' }], + errors: [{ message: 'Not Authorized!' }], data: { Signup: null }, }) }) diff --git a/backend/src/models/Category.js b/backend/src/models/Category.js index ea617adc8..9a3f47fd0 100644 --- a/backend/src/models/Category.js +++ b/backend/src/models/Category.js @@ -9,8 +9,7 @@ export default { updatedAt: { type: 'string', isoDate: true, - required: true, - default: () => new Date().toISOString(), + required: false, }, post: { type: 'relationship', diff --git a/backend/src/schema/resolvers/comments.spec.js b/backend/src/schema/resolvers/comments.spec.js index cbd2b98fc..33b33ed3e 100644 --- a/backend/src/schema/resolvers/comments.spec.js +++ b/backend/src/schema/resolvers/comments.spec.js @@ -88,10 +88,10 @@ describe('CreateComment', () => { variables = { ...variables, postId: 'p1', - content: "I'm not authorised to comment", + content: "I'm not authorized to comment", } const { errors } = await mutate({ mutation: createCommentMutation, variables }) - expect(errors[0]).toHaveProperty('message', 'Not Authorised!') + expect(errors[0]).toHaveProperty('message', 'Not Authorized!') }) }) @@ -107,14 +107,14 @@ describe('CreateComment', () => { variables = { ...variables, postId: 'p1', - content: "I'm authorised to comment", + content: "I'm authorized to comment", } }) it('creates a comment', async () => { await expect(mutate({ mutation: createCommentMutation, variables })).resolves.toMatchObject( { - data: { CreateComment: { content: "I'm authorised to comment" } }, + data: { CreateComment: { content: "I'm authorized to comment" } }, errors: undefined, }, ) @@ -150,7 +150,7 @@ describe('UpdateComment', () => { describe('unauthenticated', () => { it('throws authorization error', async () => { const { errors } = await mutate({ mutation: updateCommentMutation, variables }) - expect(errors[0]).toHaveProperty('message', 'Not Authorised!') + expect(errors[0]).toHaveProperty('message', 'Not Authorized!') }) }) @@ -162,7 +162,7 @@ describe('UpdateComment', () => { it('throws authorization error', async () => { const { errors } = await mutate({ mutation: updateCommentMutation, variables }) - expect(errors[0]).toHaveProperty('message', 'Not Authorised!') + expect(errors[0]).toHaveProperty('message', 'Not Authorized!') }) }) @@ -217,7 +217,7 @@ describe('UpdateComment', () => { it('returns null', async () => { const { data, errors } = await mutate({ mutation: updateCommentMutation, variables }) expect(data).toMatchObject({ UpdateComment: null }) - expect(errors[0]).toHaveProperty('message', 'Not Authorised!') + expect(errors[0]).toHaveProperty('message', 'Not Authorized!') }) }) }) @@ -242,7 +242,7 @@ describe('DeleteComment', () => { describe('unauthenticated', () => { it('throws authorization error', async () => { const result = await mutate({ mutation: deleteCommentMutation, variables }) - expect(result.errors[0]).toHaveProperty('message', 'Not Authorised!') + expect(result.errors[0]).toHaveProperty('message', 'Not Authorized!') }) }) @@ -254,7 +254,7 @@ describe('DeleteComment', () => { it('throws authorization error', async () => { const { errors } = await mutate({ mutation: deleteCommentMutation, variables }) - expect(errors[0]).toHaveProperty('message', 'Not Authorised!') + expect(errors[0]).toHaveProperty('message', 'Not Authorized!') }) }) diff --git a/backend/src/schema/resolvers/donations.spec.js b/backend/src/schema/resolvers/donations.spec.js index a9210f6a5..11df0c67b 100644 --- a/backend/src/schema/resolvers/donations.spec.js +++ b/backend/src/schema/resolvers/donations.spec.js @@ -72,7 +72,7 @@ describe('donations', () => { it('throws authorization error', async () => { authenticatedUser = undefined await expect(query({ query: donationsQuery, variables })).resolves.toMatchObject({ - errors: [{ message: 'Not Authorised!' }], + errors: [{ message: 'Not Authorized!' }], }) }) }) @@ -106,7 +106,7 @@ describe('donations', () => { await expect( mutate({ mutation: updateDonationsMutation, variables }), ).resolves.toMatchObject({ - errors: [{ message: 'Not Authorised!' }], + errors: [{ message: 'Not Authorized!' }], }) }) }) @@ -126,7 +126,7 @@ describe('donations', () => { mutate({ mutation: updateDonationsMutation, variables }), ).resolves.toMatchObject({ data: { UpdateDonations: null }, - errors: [{ message: 'Not Authorised!' }], + errors: [{ message: 'Not Authorized!' }], }) }) }) @@ -145,7 +145,7 @@ describe('donations', () => { mutate({ mutation: updateDonationsMutation, variables }), ).resolves.toMatchObject({ data: { UpdateDonations: null }, - errors: [{ message: 'Not Authorised!' }], + errors: [{ message: 'Not Authorized!' }], }) }) }) diff --git a/backend/src/schema/resolvers/emails.spec.js b/backend/src/schema/resolvers/emails.spec.js index 39b70ac0b..b527ca3b0 100644 --- a/backend/src/schema/resolvers/emails.spec.js +++ b/backend/src/schema/resolvers/emails.spec.js @@ -63,7 +63,7 @@ describe('AddEmailAddress', () => { it('throws AuthorizationError', async () => { await expect(mutate({ mutation, variables })).resolves.toMatchObject({ data: { AddEmailAddress: null }, - errors: [{ message: 'Not Authorised!' }], + errors: [{ message: 'Not Authorized!' }], }) }) }) @@ -169,7 +169,7 @@ describe('VerifyEmailAddress', () => { it('throws AuthorizationError', async () => { await expect(mutate({ mutation, variables })).resolves.toMatchObject({ data: { VerifyEmailAddress: null }, - errors: [{ message: 'Not Authorised!' }], + errors: [{ message: 'Not Authorized!' }], }) }) }) diff --git a/backend/src/schema/resolvers/follow.spec.js b/backend/src/schema/resolvers/follow.spec.js index 9cc8403e5..e0b2a277a 100644 --- a/backend/src/schema/resolvers/follow.spec.js +++ b/backend/src/schema/resolvers/follow.spec.js @@ -117,7 +117,7 @@ describe('follow', () => { variables, }), ).resolves.toMatchObject({ - errors: [{ message: 'Not Authorised!' }], + errors: [{ message: 'Not Authorized!' }], data: { followUser: null }, }) }) @@ -191,7 +191,7 @@ describe('follow', () => { authenticatedUser = null await expect(mutate({ mutation: mutationUnfollowUser, variables })).resolves.toMatchObject({ data: { unfollowUser: null }, - errors: [{ message: 'Not Authorised!' }], + errors: [{ message: 'Not Authorized!' }], }) }) }) diff --git a/backend/src/schema/resolvers/moderation.spec.js b/backend/src/schema/resolvers/moderation.spec.js index 662be41bd..3c2926c14 100644 --- a/backend/src/schema/resolvers/moderation.spec.js +++ b/backend/src/schema/resolvers/moderation.spec.js @@ -120,7 +120,7 @@ describe('moderate resources', () => { await expect( mutate({ mutation: reviewMutation, variables: disableVariables }), ).resolves.toMatchObject({ - errors: [{ message: 'Not Authorised!' }], + errors: [{ message: 'Not Authorized!' }], }) }) }) @@ -134,7 +134,7 @@ describe('moderate resources', () => { await expect( mutate({ mutation: reviewMutation, variables: disableVariables }), ).resolves.toMatchObject({ - errors: [{ message: 'Not Authorised!' }], + errors: [{ message: 'Not Authorized!' }], }) }) }) @@ -218,7 +218,7 @@ describe('moderate resources', () => { await expect( mutate({ mutation: reviewMutation, variables: disableVariables }), ).resolves.toMatchObject({ - errors: [{ message: 'Not Authorised!' }], + errors: [{ message: 'Not Authorized!' }], }) }) }) @@ -232,7 +232,7 @@ describe('moderate resources', () => { await expect( mutate({ mutation: reviewMutation, variables: disableVariables }), ).resolves.toMatchObject({ - errors: [{ message: 'Not Authorised!' }], + errors: [{ message: 'Not Authorized!' }], }) }) }) @@ -488,7 +488,7 @@ describe('moderate resources', () => { await expect( mutate({ mutation: reviewMutation, variables: enableVariables }), ).resolves.toMatchObject({ - errors: [{ message: 'Not Authorised!' }], + errors: [{ message: 'Not Authorized!' }], }) }) }) @@ -507,7 +507,7 @@ describe('moderate resources', () => { await expect( mutate({ mutation: reviewMutation, variables: enableVariables }), ).resolves.toMatchObject({ - errors: [{ message: 'Not Authorised!' }], + errors: [{ message: 'Not Authorized!' }], }) }) }) diff --git a/backend/src/schema/resolvers/notifications.spec.js b/backend/src/schema/resolvers/notifications.spec.js index 2ea468914..924154488 100644 --- a/backend/src/schema/resolvers/notifications.spec.js +++ b/backend/src/schema/resolvers/notifications.spec.js @@ -165,7 +165,7 @@ describe('given some notifications', () => { describe('unauthenticated', () => { it('throws authorization error', async () => { const { errors } = await query({ query: notificationQuery }) - expect(errors[0]).toHaveProperty('message', 'Not Authorised!') + expect(errors[0]).toHaveProperty('message', 'Not Authorized!') }) }) @@ -313,7 +313,7 @@ describe('given some notifications', () => { mutation: markAsReadMutation, variables: { ...variables, id: 'p1' }, }) - expect(result.errors[0]).toHaveProperty('message', 'Not Authorised!') + expect(result.errors[0]).toHaveProperty('message', 'Not Authorized!') }) }) diff --git a/backend/src/schema/resolvers/posts.js b/backend/src/schema/resolvers/posts.js index d199b6f09..d9a04732c 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 @@ -348,7 +358,7 @@ export default { undefinedToNull: ['activityId', 'objectId', 'language', 'pinnedAt', 'pinned'], hasMany: { tags: '-[:TAGGED]->(related:Tag)', - // categories: '-[:CATEGORIZED]->(related:Category)', + categories: '-[:CATEGORIZED]->(related:Category)', comments: '<-[:COMMENTS]-(related:Comment)', shoutedBy: '<-[:SHOUTED]-(related:User)', emotions: '<-[related:EMOTED]', diff --git a/backend/src/schema/resolvers/posts.spec.js b/backend/src/schema/resolvers/posts.spec.js index 2c8b7e90b..52bd8fcd0 100644 --- a/backend/src/schema/resolvers/posts.spec.js +++ b/backend/src/schema/resolvers/posts.spec.js @@ -281,7 +281,7 @@ describe('CreatePost', () => { describe('unauthenticated', () => { it('throws authorization error', async () => { const { errors } = await mutate({ mutation: createPostMutation, variables }) - expect(errors[0]).toHaveProperty('message', 'Not Authorised!') + expect(errors[0]).toHaveProperty('message', 'Not Authorized!') }) }) @@ -369,7 +369,7 @@ describe('UpdatePost', () => { it('throws authorization error', async () => { authenticatedUser = null expect(mutate({ mutation: updatePostMutation, variables })).resolves.toMatchObject({ - errors: [{ message: 'Not Authorised!' }], + errors: [{ message: 'Not Authorized!' }], data: { UpdatePost: null }, }) }) @@ -382,7 +382,7 @@ describe('UpdatePost', () => { it('throws authorization error', async () => { const { errors } = await mutate({ mutation: updatePostMutation, variables }) - expect(errors[0]).toHaveProperty('message', 'Not Authorised!') + expect(errors[0]).toHaveProperty('message', 'Not Authorized!') }) }) @@ -547,7 +547,7 @@ describe('pin posts', () => { it('throws authorization error', async () => { authenticatedUser = null await expect(mutate({ mutation: pinPostMutation, variables })).resolves.toMatchObject({ - errors: [{ message: 'Not Authorised!' }], + errors: [{ message: 'Not Authorized!' }], data: { pinPost: null }, }) }) @@ -556,7 +556,7 @@ describe('pin posts', () => { describe('ordinary users', () => { it('throws authorization error', async () => { await expect(mutate({ mutation: pinPostMutation, variables })).resolves.toMatchObject({ - errors: [{ message: 'Not Authorised!' }], + errors: [{ message: 'Not Authorized!' }], data: { pinPost: null }, }) }) @@ -571,7 +571,7 @@ describe('pin posts', () => { it('throws authorization error', async () => { await expect(mutate({ mutation: pinPostMutation, variables })).resolves.toMatchObject({ - errors: [{ message: 'Not Authorised!' }], + errors: [{ message: 'Not Authorized!' }], data: { pinPost: null }, }) }) @@ -854,7 +854,7 @@ describe('unpin posts', () => { it('throws authorization error', async () => { authenticatedUser = null await expect(mutate({ mutation: unpinPostMutation, variables })).resolves.toMatchObject({ - errors: [{ message: 'Not Authorised!' }], + errors: [{ message: 'Not Authorized!' }], data: { unpinPost: null }, }) }) @@ -863,7 +863,7 @@ describe('unpin posts', () => { describe('users cannot unpin posts', () => { it('throws authorization error', async () => { await expect(mutate({ mutation: unpinPostMutation, variables })).resolves.toMatchObject({ - errors: [{ message: 'Not Authorised!' }], + errors: [{ message: 'Not Authorized!' }], data: { unpinPost: null }, }) }) @@ -878,7 +878,7 @@ describe('unpin posts', () => { it('throws authorization error', async () => { await expect(mutate({ mutation: unpinPostMutation, variables })).resolves.toMatchObject({ - errors: [{ message: 'Not Authorised!' }], + errors: [{ message: 'Not Authorized!' }], data: { unpinPost: null }, }) }) @@ -975,7 +975,7 @@ describe('DeletePost', () => { describe('unauthenticated', () => { it('throws authorization error', async () => { const { errors } = await mutate({ mutation: deletePostMutation, variables }) - expect(errors[0]).toHaveProperty('message', 'Not Authorised!') + expect(errors[0]).toHaveProperty('message', 'Not Authorized!') }) }) @@ -986,7 +986,7 @@ describe('DeletePost', () => { it('throws authorization error', async () => { const { errors } = await mutate({ mutation: deletePostMutation, variables }) - expect(errors[0]).toHaveProperty('message', 'Not Authorised!') + expect(errors[0]).toHaveProperty('message', 'Not Authorized!') }) }) @@ -1128,7 +1128,7 @@ describe('emotions', () => { variables, }) - expect(addPostEmotions.errors[0]).toHaveProperty('message', 'Not Authorised!') + expect(addPostEmotions.errors[0]).toHaveProperty('message', 'Not Authorized!') }) }) @@ -1249,7 +1249,7 @@ describe('emotions', () => { mutation: removePostEmotionsMutation, variables: removePostEmotionsVariables, }) - expect(removePostEmotions.errors[0]).toHaveProperty('message', 'Not Authorised!') + expect(removePostEmotions.errors[0]).toHaveProperty('message', 'Not Authorized!') }) }) diff --git a/backend/src/schema/resolvers/registration.spec.js b/backend/src/schema/resolvers/registration.spec.js index 573af1d35..ebe36456b 100644 --- a/backend/src/schema/resolvers/registration.spec.js +++ b/backend/src/schema/resolvers/registration.spec.js @@ -61,7 +61,7 @@ describe('Signup', () => { CONFIG.INVITE_REGISTRATION = false CONFIG.PUBLIC_REGISTRATION = false await expect(mutate({ mutation, variables })).resolves.toMatchObject({ - errors: [{ message: 'Not Authorised!' }], + errors: [{ message: 'Not Authorized!' }], }) }) diff --git a/backend/src/schema/resolvers/reports.spec.js b/backend/src/schema/resolvers/reports.spec.js index 09d0869fc..7cf4d9a34 100644 --- a/backend/src/schema/resolvers/reports.spec.js +++ b/backend/src/schema/resolvers/reports.spec.js @@ -130,7 +130,7 @@ describe('file a report on a resource', () => { authenticatedUser = null await expect(mutate({ mutation: fileReportMutation, variables })).resolves.toMatchObject({ data: { fileReport: null }, - errors: [{ message: 'Not Authorised!' }], + errors: [{ message: 'Not Authorized!' }], }) }) }) @@ -729,7 +729,7 @@ describe('file a report on a resource', () => { authenticatedUser = null expect(query({ query: reportsQuery })).resolves.toMatchObject({ data: { reports: null }, - errors: [{ message: 'Not Authorised!' }], + errors: [{ message: 'Not Authorized!' }], }) }) }) @@ -739,7 +739,7 @@ describe('file a report on a resource', () => { authenticatedUser = await currentUser.toJson() expect(query({ query: reportsQuery })).resolves.toMatchObject({ data: { reports: null }, - errors: [{ message: 'Not Authorised!' }], + errors: [{ message: 'Not Authorized!' }], }) }) diff --git a/backend/src/schema/resolvers/rewards.spec.js b/backend/src/schema/resolvers/rewards.spec.js index 718378699..f44cf51f6 100644 --- a/backend/src/schema/resolvers/rewards.spec.js +++ b/backend/src/schema/resolvers/rewards.spec.js @@ -97,7 +97,7 @@ describe('rewards', () => { authenticatedUser = null await expect(mutate({ mutation: rewardMutation, variables })).resolves.toMatchObject({ data: { reward: null }, - errors: [{ message: 'Not Authorised!' }], + errors: [{ message: 'Not Authorized!' }], }) }) }) @@ -255,7 +255,7 @@ describe('rewards', () => { it('throws authorization error', async () => { await expect(mutate({ mutation: rewardMutation, variables })).resolves.toMatchObject({ data: { reward: null }, - errors: [{ message: 'Not Authorised!' }], + errors: [{ message: 'Not Authorized!' }], }) }) }) @@ -308,7 +308,7 @@ describe('rewards', () => { authenticatedUser = null await expect(mutate({ mutation: unrewardMutation, variables })).resolves.toMatchObject({ data: { unreward: null }, - errors: [{ message: 'Not Authorised!' }], + errors: [{ message: 'Not Authorized!' }], }) }) }) @@ -341,7 +341,7 @@ describe('rewards', () => { it('throws authorization error', async () => { await expect(mutate({ mutation: unrewardMutation, variables })).resolves.toMatchObject({ data: { unreward: null }, - errors: [{ message: 'Not Authorised!' }], + errors: [{ message: 'Not Authorized!' }], }) }) }) diff --git a/backend/src/schema/resolvers/shout.spec.js b/backend/src/schema/resolvers/shout.spec.js index a4e9890d5..087c452da 100644 --- a/backend/src/schema/resolvers/shout.spec.js +++ b/backend/src/schema/resolvers/shout.spec.js @@ -90,7 +90,7 @@ describe('shout and unshout posts', () => { variables = { id: 'post-to-shout-id' } authenticatedUser = undefined await expect(mutate({ mutation: mutationShoutPost, variables })).resolves.toMatchObject({ - errors: [{ message: 'Not Authorised!' }], + errors: [{ message: 'Not Authorized!' }], }) }) }) @@ -165,7 +165,7 @@ describe('shout and unshout posts', () => { authenticatedUser = undefined variables = { id: 'post-to-shout-id' } await expect(mutate({ mutation: mutationUnshoutPost, variables })).resolves.toMatchObject({ - errors: [{ message: 'Not Authorised!' }], + errors: [{ message: 'Not Authorized!' }], }) }) }) diff --git a/backend/src/schema/resolvers/socialMedia.spec.js b/backend/src/schema/resolvers/socialMedia.spec.js index bb7886c3f..fc9ee8f70 100644 --- a/backend/src/schema/resolvers/socialMedia.spec.js +++ b/backend/src/schema/resolvers/socialMedia.spec.js @@ -94,7 +94,7 @@ describe('SocialMedia', () => { const user = null const result = await socialMediaAction(user, mutation, variables) - expect(result.errors[0]).toHaveProperty('message', 'Not Authorised!') + expect(result.errors[0]).toHaveProperty('message', 'Not Authorized!') }) }) @@ -186,7 +186,7 @@ describe('SocialMedia', () => { const user = null const result = await socialMediaAction(user, mutation, variables) - expect(result.errors[0]).toHaveProperty('message', 'Not Authorised!') + expect(result.errors[0]).toHaveProperty('message', 'Not Authorized!') }) }) @@ -195,7 +195,7 @@ describe('SocialMedia', () => { const user = someUser const result = await socialMediaAction(user, mutation, variables) - expect(result.errors[0]).toHaveProperty('message', 'Not Authorised!') + expect(result.errors[0]).toHaveProperty('message', 'Not Authorized!') }) }) @@ -222,7 +222,7 @@ describe('SocialMedia', () => { variables.id = 'some-id' const result = await socialMediaAction(user, mutation, variables) - expect(result.errors[0]).toHaveProperty('message', 'Not Authorised!') + expect(result.errors[0]).toHaveProperty('message', 'Not Authorized!') }) }) }) @@ -249,7 +249,7 @@ describe('SocialMedia', () => { const user = null const result = await socialMediaAction(user, mutation, variables) - expect(result.errors[0]).toHaveProperty('message', 'Not Authorised!') + expect(result.errors[0]).toHaveProperty('message', 'Not Authorized!') }) }) @@ -258,7 +258,7 @@ describe('SocialMedia', () => { const user = someUser const result = await socialMediaAction(user, mutation, variables) - expect(result.errors[0]).toHaveProperty('message', 'Not Authorised!') + expect(result.errors[0]).toHaveProperty('message', 'Not Authorized!') }) }) diff --git a/backend/src/schema/resolvers/user_management.js b/backend/src/schema/resolvers/user_management.js index beb2cddb3..d88eafdae 100644 --- a/backend/src/schema/resolvers/user_management.js +++ b/backend/src/schema/resolvers/user_management.js @@ -20,16 +20,22 @@ export default { const result = await transaction.run( ` MATCH (user:User {id: $id}) - WITH user, [(user)<-[:OWNED_BY]-(medium:SocialMedia) | properties(medium) ] as media - RETURN user {.*, socialMedia: media } as user + OPTIONAL MATCH (category:Category) WHERE NOT ((user)-[:NOT_INTERESTED_IN]->(category)) + OPTIONAL MATCH (cats:Category) + WITH user, [(user)<-[:OWNED_BY]-(medium:SocialMedia) | properties(medium) ] AS media, category, toString(COUNT(cats)) AS categoryCount + RETURN user {.*, socialMedia: media, activeCategories: collect(category.id) } AS user, categoryCount `, { id: user.id }, ) - log(result) - return result.records.map((record) => record.get('user')) + const [categoryCount] = result.records.map((record) => record.get('categoryCount')) + const [currentUser] = result.records.map((record) => record.get('user')) + // frontend expects empty array when all categories are selected + if (currentUser.activeCategories.length === parseInt(categoryCount)) + currentUser.activeCategories = [] + return currentUser }) try { - const [currentUser] = await currentUserTransactionPromise + const currentUser = await currentUserTransactionPromise return currentUser } finally { session.close() diff --git a/backend/src/schema/resolvers/user_management.spec.js b/backend/src/schema/resolvers/user_management.spec.js index 2dcb14855..e7f2f3ed1 100644 --- a/backend/src/schema/resolvers/user_management.spec.js +++ b/backend/src/schema/resolvers/user_management.spec.js @@ -6,6 +6,7 @@ import { createTestClient } from 'apollo-server-testing' import createServer, { context } from '../../server' import encode from '../../jwt/encode' import { getNeode } from '../../db/neo4j' +import { categories } from '../../constants/categories' const neode = getNeode() let query, mutate, variables, req, user @@ -118,6 +119,7 @@ describe('currentUser', () => { } email role + activeCategories } } ` @@ -172,6 +174,52 @@ describe('currentUser', () => { } await respondsWith(expected) }) + + describe('with categories in DB', () => { + beforeEach(async () => { + await Promise.all( + categories.map(async ({ icon, name }, index) => { + await Factory.build('category', { + id: `cat${index + 1}`, + slug: name, + name, + icon, + }) + }), + ) + }) + + it('returns empty array for all categories', async () => { + await respondsWith({ + data: { + currentUser: expect.objectContaining({ activeCategories: [] }), + }, + }) + }) + + describe('with categories saved for current user', () => { + const saveCategorySettings = gql` + mutation ($activeCategories: [String]) { + saveCategorySettings(activeCategories: $activeCategories) + } + ` + beforeEach(async () => { + await mutate({ + mutation: saveCategorySettings, + variables: { activeCategories: ['cat1', 'cat3', 'cat5', 'cat7'] }, + }) + }) + + it('returns only the saved active categories', async () => { + const result = await query({ query: currentUserQuery, variables }) + expect(result.data.currentUser.activeCategories).toHaveLength(4) + expect(result.data.currentUser.activeCategories).toContain('cat1') + expect(result.data.currentUser.activeCategories).toContain('cat3') + expect(result.data.currentUser.activeCategories).toContain('cat5') + expect(result.data.currentUser.activeCategories).toContain('cat7') + }) + }) + }) }) }) }) @@ -310,8 +358,8 @@ describe('change password', () => { }) describe('unauthenticated', () => { - it('throws "Not Authorised!"', async () => { - await respondsWith({ errors: [{ message: 'Not Authorised!' }] }) + it('throws "Not Authorized!"', async () => { + await respondsWith({ errors: [{ message: 'Not Authorized!' }] }) }) }) diff --git a/backend/src/schema/resolvers/users.js b/backend/src/schema/resolvers/users.js index 5dc78c5e1..12f00ffb6 100644 --- a/backend/src/schema/resolvers/users.js +++ b/backend/src/schema/resolvers/users.js @@ -269,6 +269,49 @@ export default { session.close() } }, + saveCategorySettings: async (object, args, context, resolveInfo) => { + const { activeCategories } = args + const { + user: { id }, + } = context + + const session = context.driver.session() + await session.writeTransaction((transaction) => { + return transaction.run( + ` + MATCH (user:User { id: $id })-[previousCategories:NOT_INTERESTED_IN]->(category:Category) + DELETE previousCategories + RETURN user, category + `, + { id }, + ) + }) + + // frontend gives [] when all categories are selected (default) + if (activeCategories.length === 0) return true + + const writeTxResultPromise = session.writeTransaction(async (transaction) => { + const saveCategorySettingsResponse = await transaction.run( + ` + MATCH (category:Category) WHERE NOT category.id IN $activeCategories + MATCH (user:User { id: $id }) + MERGE (user)-[r:NOT_INTERESTED_IN]->(category) + RETURN user, r, category + `, + { id, activeCategories }, + ) + const [user] = await saveCategorySettingsResponse.records.map((record) => + record.get('user'), + ) + return user + }) + try { + await writeTxResultPromise + return true + } finally { + session.close() + } + }, }, User: { email: async (parent, params, context, resolveInfo) => { diff --git a/backend/src/schema/resolvers/users.spec.js b/backend/src/schema/resolvers/users.spec.js index 102d2f6fe..d8fce3b29 100644 --- a/backend/src/schema/resolvers/users.spec.js +++ b/backend/src/schema/resolvers/users.spec.js @@ -3,6 +3,7 @@ import { gql } from '../../helpers/jest' import { getNeode, getDriver } from '../../db/neo4j' import createServer from '../../server' import { createTestClient } from 'apollo-server-testing' +import { categories } from '../../constants/categories' const categoryIds = ['cat9'] let user @@ -45,7 +46,7 @@ const deleteUserMutation = gql` } ` const switchUserRoleMutation = gql` - mutation ($role: UserGroup!, $id: ID!) { + mutation ($role: UserRole!, $id: ID!) { switchUserRole(role: $role, id: $id) { name role @@ -56,6 +57,12 @@ const switchUserRoleMutation = gql` } ` +const saveCategorySettings = gql` + mutation ($activeCategories: [String]) { + saveCategorySettings(activeCategories: $activeCategories) + } +` + beforeAll(async () => { await cleanDatabase() @@ -101,7 +108,7 @@ describe('User', () => { it('is forbidden', async () => { await expect(query({ query: userQuery, variables })).resolves.toMatchObject({ - errors: [{ message: 'Not Authorised!' }], + errors: [{ message: 'Not Authorized!' }], }) }) @@ -207,7 +214,7 @@ describe('UpdateUser', () => { it('is not allowed to change other user accounts', async () => { const { errors } = await mutate({ mutation: updateUserMutation, variables }) - expect(errors[0]).toHaveProperty('message', 'Not Authorised!') + expect(errors[0]).toHaveProperty('message', 'Not Authorized!') }) }) @@ -500,7 +507,7 @@ describe('switch user role', () => { expect.objectContaining({ errors: [ expect.objectContaining({ - message: 'Not Authorised!', + message: 'Not Authorized!', }), ], }), @@ -544,3 +551,140 @@ describe('switch user role', () => { }) }) }) + +describe('save category settings', () => { + beforeEach(async () => { + await Promise.all( + categories.map(({ icon, name }, index) => { + Factory.build('category', { + id: `cat${index + 1}`, + slug: name, + name, + icon, + }) + }), + ) + }) + + beforeEach(async () => { + user = await Factory.build('user', { + id: 'user', + role: 'user', + }) + variables = { + activeCategories: ['cat1', 'cat3', 'cat5'], + } + }) + + describe('not authenticated', () => { + beforeEach(async () => { + authenticatedUser = undefined + }) + + it('throws an error', async () => { + await expect(mutate({ mutation: saveCategorySettings, variables })).resolves.toEqual( + expect.objectContaining({ + errors: [ + expect.objectContaining({ + message: 'Not Authorized!', + }), + ], + }), + ) + }) + }) + + describe('authenticated', () => { + beforeEach(async () => { + authenticatedUser = await user.toJson() + }) + + const userQuery = gql` + query ($id: ID) { + User(id: $id) { + activeCategories + } + } + ` + + describe('no categories saved', () => { + it('returns true for active categories mutation', async () => { + await expect(mutate({ mutation: saveCategorySettings, variables })).resolves.toEqual( + expect.objectContaining({ + data: { saveCategorySettings: true }, + }), + ) + }) + + describe('query for user', () => { + beforeEach(async () => { + await mutate({ mutation: saveCategorySettings, variables }) + }) + + it('returns the active categories when user is queried', async () => { + await expect( + query({ query: userQuery, variables: { id: authenticatedUser.id } }), + ).resolves.toEqual( + expect.objectContaining({ + data: { + User: [ + { + activeCategories: expect.arrayContaining(['cat1', 'cat3', 'cat5']), + }, + ], + }, + }), + ) + }) + }) + }) + + describe('categories already saved', () => { + beforeEach(async () => { + variables = { + activeCategories: ['cat1', 'cat3', 'cat5'], + } + await mutate({ mutation: saveCategorySettings, variables }) + variables = { + activeCategories: ['cat10', 'cat11', 'cat12', 'cat8', 'cat9'], + } + }) + + it('returns true', async () => { + await expect(mutate({ mutation: saveCategorySettings, variables })).resolves.toEqual( + expect.objectContaining({ + data: { saveCategorySettings: true }, + }), + ) + }) + + describe('query for user', () => { + beforeEach(async () => { + await mutate({ mutation: saveCategorySettings, variables }) + }) + + it('returns the new active categories when user is queried', async () => { + await expect( + query({ query: userQuery, variables: { id: authenticatedUser.id } }), + ).resolves.toEqual( + expect.objectContaining({ + data: { + User: [ + { + activeCategories: expect.arrayContaining([ + 'cat10', + 'cat11', + 'cat12', + 'cat8', + 'cat9', + ]), + }, + ], + }, + }), + ) + }) + }) + }) + }) +}) diff --git a/backend/src/schema/resolvers/users/location.spec.js b/backend/src/schema/resolvers/users/location.spec.js index 92f08893e..c0525ca1c 100644 --- a/backend/src/schema/resolvers/users/location.spec.js +++ b/backend/src/schema/resolvers/users/location.spec.js @@ -121,7 +121,7 @@ describe('Location Service', () => { const result = await query({ query: queryLocations, variables }) expect(result.data.queryLocations).toEqual( expect.arrayContaining([ - { id: 'place.14094307404564380', place_name: 'Berlin, Germany' }, + { id: expect.stringMatching(/^place\.[0-9]+$/), place_name: 'Berlin, Germany' }, { id: expect.stringMatching(/^place\.[0-9]+$/), place_name: 'Berlin, Maryland, United States', diff --git a/backend/src/schema/resolvers/users/mutedUsers.spec.js b/backend/src/schema/resolvers/users/mutedUsers.spec.js index 36b526268..d8efacfae 100644 --- a/backend/src/schema/resolvers/users/mutedUsers.spec.js +++ b/backend/src/schema/resolvers/users/mutedUsers.spec.js @@ -58,7 +58,7 @@ describe('mutedUsers', () => { it('throws permission error', async () => { const { query } = createTestClient(server) const result = await query({ query: mutedUserQuery }) - expect(result.errors[0]).toHaveProperty('message', 'Not Authorised!') + expect(result.errors[0]).toHaveProperty('message', 'Not Authorized!') }) describe('authenticated and given a muted user', () => { @@ -116,7 +116,7 @@ describe('muteUser', () => { it('throws permission error', async () => { const result = await muteAction({ id: 'u2' }) - expect(result.errors[0]).toHaveProperty('message', 'Not Authorised!') + expect(result.errors[0]).toHaveProperty('message', 'Not Authorized!') }) describe('authenticated', () => { @@ -333,7 +333,7 @@ describe('unmuteUser', () => { it('throws permission error', async () => { const result = await unmuteAction({ id: 'u2' }) - expect(result.errors[0]).toHaveProperty('message', 'Not Authorised!') + expect(result.errors[0]).toHaveProperty('message', 'Not Authorized!') }) describe('authenticated', () => { diff --git a/backend/src/schema/types/enum/UserGroup.gql b/backend/src/schema/types/enum/UserRole.gql similarity index 63% rename from backend/src/schema/types/enum/UserGroup.gql rename to backend/src/schema/types/enum/UserRole.gql index 587e90c71..846afde13 100644 --- a/backend/src/schema/types/enum/UserGroup.gql +++ b/backend/src/schema/types/enum/UserRole.gql @@ -1,4 +1,4 @@ -enum UserGroup { +enum UserRole { admin moderator user diff --git a/backend/src/schema/types/type/User.gql b/backend/src/schema/types/type/User.gql index 772dedf6b..c3fcf932b 100644 --- a/backend/src/schema/types/type/User.gql +++ b/backend/src/schema/types/type/User.gql @@ -28,7 +28,7 @@ type User { avatar: Image @relation(name: "AVATAR_IMAGE", direction: "OUT") deleted: Boolean disabled: Boolean - role: UserGroup! + role: UserRole! publicKey: String invitedBy: User @relation(name: "INVITED", direction: "IN") invited: [User] @relation(name: "INVITED", direction: "OUT") @@ -114,6 +114,14 @@ type User { badgesCount: Int! @cypher(statement: "MATCH (this)<-[:REWARDED]-(r:Badge) RETURN COUNT(r)") emotions: [EMOTED] + + activeCategories: [String] @cypher( + statement: """ + MATCH (category:Category) + WHERE NOT ((this)-[:NOT_INTERESTED_IN]->(category)) + RETURN collect(category.id) + """ + ) } @@ -151,7 +159,7 @@ input _UserFilter { followedBy_none: _UserFilter followedBy_single: _UserFilter followedBy_every: _UserFilter - role_in: [UserGroup!] + role_in: [UserRole!] } type Query { @@ -160,7 +168,7 @@ type Query { email: String # admins need to search for a user sometimes name: String slug: String - role: UserGroup + role: UserRole locationName: String about: String createdAt: String @@ -171,7 +179,7 @@ type Query { filter: _UserFilter ): [User] - availableRoles: [UserGroup]! + availableRoles: [UserRole]! mutedUsers: [User] blockedUsers: [User] isLoggedIn: Boolean! @@ -219,5 +227,7 @@ type Mutation { blockUser(id: ID!): User unblockUser(id: ID!): User - switchUserRole(role: UserGroup!, id: ID!): User + switchUserRole(role: UserRole!, id: ID!): User + + saveCategorySettings(activeCategories: [String]): Boolean } diff --git a/backend/yarn.lock b/backend/yarn.lock index 24bd00b3a..48c05947e 100644 --- a/backend/yarn.lock +++ b/backend/yarn.lock @@ -4253,10 +4253,10 @@ eslint-plugin-node@~11.1.0: resolve "^1.10.1" semver "^6.1.0" -eslint-plugin-prettier@~3.1.2: - version "3.1.2" - resolved "https://registry.yarnpkg.com/eslint-plugin-prettier/-/eslint-plugin-prettier-3.1.2.tgz#432e5a667666ab84ce72f945c72f77d996a5c9ba" - integrity sha512-GlolCC9y3XZfv3RQfwGew7NnuFDKsfI4lbvRK+PIIo23SFH+LemGs4cKwzAaRa+Mdb+lQO/STaIayno8T5sJJA== +eslint-plugin-prettier@~3.4.1: + version "3.4.1" + resolved "https://registry.yarnpkg.com/eslint-plugin-prettier/-/eslint-plugin-prettier-3.4.1.tgz#e9ddb200efb6f3d05ffe83b1665a716af4a387e5" + integrity sha512-htg25EUYUeIhKHXjOinK4BgCcDwtLHjqaxCDsMy5nbnUMkKFvIhMVCp+5GFUXQ4Nr8lBsPqtGAqBenbpFqAA2g== dependencies: prettier-linter-helpers "^1.0.0" @@ -7574,10 +7574,10 @@ neo4j-graphql-js@^2.11.5: lodash "^4.17.15" neo4j-driver "^4.0.1" -neode@^0.4.7: - version "0.4.7" - resolved "https://registry.yarnpkg.com/neode/-/neode-0.4.7.tgz#033007b57a2ee167e9ee5537493086db08d005eb" - integrity sha512-YXlc187JRpeKCBcUIkY6nimXXG+Tvlopfe71/FPno2THrwmYt5mm0RPHZ+mXF2O1Xg6zvjKvOpCpDz2vHBfroQ== +neode@^0.4.8: + version "0.4.8" + resolved "https://registry.yarnpkg.com/neode/-/neode-0.4.8.tgz#0889b4fc7f1bf0b470b01fa5b8870373b5d47ad6" + integrity sha512-pb91NfCOg4Fj5o+98H+S2XYC+ByQfbdhwcc1UVuzuUQ0Ezzj+jWz8NmKWU8ZfCH6l4plk71yDAPd2eTwpt+Xvg== dependencies: "@hapi/joi" "^15.1.1" dotenv "^4.0.0" @@ -9262,10 +9262,10 @@ slug@^0.9.2: dependencies: unicode ">= 0.3.1" -slug@~4.0.2: - version "4.0.2" - resolved "https://registry.yarnpkg.com/slug/-/slug-4.0.2.tgz#35a62b4e71582778ac08bb30a1bf439fd0a43ea7" - integrity sha512-c5XbWkwxHU13gAdSvBHQgnGy2sxv/REMz0ugcM0SOSBCO/N4wfU0TDBC3pgdOwVGjZwGnLBTRljXzdVYE+KYNw== +slug@~6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/slug/-/slug-6.0.0.tgz#39637b32e5a873bc692812a630842880499ed6c9" + integrity sha512-0MpNLyCSUSf0G1nAZmp9gY1cvesPP35a1Live25vZ23gWQ5SAopF0N+0hk9KI4ytNuTebJrHGNrgTnxboofcSg== smart-buffer@^4.1.0: version "4.1.0" diff --git a/package.json b/package.json index e23a5f5c7..54560aebd 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ocelot-social", - "version": "1.0.9", + "version": "1.1.1", "description": "Free and open source software program code available to run social networks.", "author": "ocelot.social Community", "license": "MIT", @@ -42,10 +42,10 @@ "jsonwebtoken": "^8.5.1", "mock-socket": "^9.0.3", "neo4j-driver": "^4.3.4", - "neode": "^0.4.7", + "neode": "^0.4.8", "npm-run-all": "^4.1.5", "rosie": "^2.1.0", - "slug": "^5.1.0" + "slug": "^6.0.0" }, "resolutions": { "set-value": "^2.0.1" 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/assets/_new/icons/svgs/child.svg b/webapp/assets/_new/icons/svgs/child.svg new file mode 100644 index 000000000..fcb5651f0 --- /dev/null +++ b/webapp/assets/_new/icons/svgs/child.svg @@ -0,0 +1,5 @@ + + diff --git a/webapp/assets/_new/icons/svgs/culture.svg b/webapp/assets/_new/icons/svgs/culture.svg new file mode 100644 index 000000000..d63e38cb4 --- /dev/null +++ b/webapp/assets/_new/icons/svgs/culture.svg @@ -0,0 +1,20 @@ + diff --git a/webapp/assets/_new/icons/svgs/desktop.svg b/webapp/assets/_new/icons/svgs/desktop.svg new file mode 100644 index 000000000..ba1ef8431 --- /dev/null +++ b/webapp/assets/_new/icons/svgs/desktop.svg @@ -0,0 +1,5 @@ + + diff --git a/webapp/assets/_new/icons/svgs/ellipsis-h.svg b/webapp/assets/_new/icons/svgs/ellipsis-h.svg new file mode 100644 index 000000000..eb7deeab0 --- /dev/null +++ b/webapp/assets/_new/icons/svgs/ellipsis-h.svg @@ -0,0 +1,5 @@ + + diff --git a/webapp/assets/_new/icons/svgs/energy.svg b/webapp/assets/_new/icons/svgs/energy.svg new file mode 100644 index 000000000..5035a5586 --- /dev/null +++ b/webapp/assets/_new/icons/svgs/energy.svg @@ -0,0 +1,14 @@ + diff --git a/webapp/assets/_new/icons/svgs/finance.svg b/webapp/assets/_new/icons/svgs/finance.svg new file mode 100644 index 000000000..74081bc6a --- /dev/null +++ b/webapp/assets/_new/icons/svgs/finance.svg @@ -0,0 +1,13 @@ + diff --git a/webapp/assets/_new/icons/svgs/health.svg b/webapp/assets/_new/icons/svgs/health.svg new file mode 100644 index 000000000..acf50d7c1 --- /dev/null +++ b/webapp/assets/_new/icons/svgs/health.svg @@ -0,0 +1,7 @@ + diff --git a/webapp/assets/_new/icons/svgs/home.svg b/webapp/assets/_new/icons/svgs/home.svg new file mode 100644 index 000000000..b1a13b06f --- /dev/null +++ b/webapp/assets/_new/icons/svgs/home.svg @@ -0,0 +1,5 @@ + + diff --git a/webapp/assets/_new/icons/svgs/lightbulb.svg b/webapp/assets/_new/icons/svgs/lightbulb.svg new file mode 100644 index 000000000..1c19c81b1 --- /dev/null +++ b/webapp/assets/_new/icons/svgs/lightbulb.svg @@ -0,0 +1,5 @@ + + diff --git a/webapp/assets/_new/icons/svgs/media.svg b/webapp/assets/_new/icons/svgs/media.svg new file mode 100644 index 000000000..d63c98610 --- /dev/null +++ b/webapp/assets/_new/icons/svgs/media.svg @@ -0,0 +1,7 @@ + diff --git a/webapp/assets/_new/icons/svgs/miscellaneous.svg b/webapp/assets/_new/icons/svgs/miscellaneous.svg new file mode 100644 index 000000000..07f8dbe3f --- /dev/null +++ b/webapp/assets/_new/icons/svgs/miscellaneous.svg @@ -0,0 +1,13 @@ + diff --git a/webapp/assets/_new/icons/svgs/mobility.svg b/webapp/assets/_new/icons/svgs/mobility.svg new file mode 100644 index 000000000..9e36ec21e --- /dev/null +++ b/webapp/assets/_new/icons/svgs/mobility.svg @@ -0,0 +1,11 @@ + diff --git a/webapp/assets/_new/icons/svgs/movement.svg b/webapp/assets/_new/icons/svgs/movement.svg new file mode 100644 index 000000000..81052875d --- /dev/null +++ b/webapp/assets/_new/icons/svgs/movement.svg @@ -0,0 +1,19 @@ + diff --git a/webapp/assets/_new/icons/svgs/music.svg b/webapp/assets/_new/icons/svgs/music.svg new file mode 100644 index 000000000..b84b87800 --- /dev/null +++ b/webapp/assets/_new/icons/svgs/music.svg @@ -0,0 +1,5 @@ + + diff --git a/webapp/assets/_new/icons/svgs/nature.svg b/webapp/assets/_new/icons/svgs/nature.svg new file mode 100644 index 000000000..d40250af4 --- /dev/null +++ b/webapp/assets/_new/icons/svgs/nature.svg @@ -0,0 +1,18 @@ + diff --git a/webapp/assets/_new/icons/svgs/networking.svg b/webapp/assets/_new/icons/svgs/networking.svg new file mode 100644 index 000000000..b8d35da69 --- /dev/null +++ b/webapp/assets/_new/icons/svgs/networking.svg @@ -0,0 +1,22 @@ + diff --git a/webapp/assets/_new/icons/svgs/peace.svg b/webapp/assets/_new/icons/svgs/peace.svg new file mode 100644 index 000000000..408601cae --- /dev/null +++ b/webapp/assets/_new/icons/svgs/peace.svg @@ -0,0 +1,8 @@ + diff --git a/webapp/assets/_new/icons/svgs/politics.svg b/webapp/assets/_new/icons/svgs/politics.svg new file mode 100644 index 000000000..35322097d --- /dev/null +++ b/webapp/assets/_new/icons/svgs/politics.svg @@ -0,0 +1,12 @@ + diff --git a/webapp/assets/_new/icons/svgs/psyche.svg b/webapp/assets/_new/icons/svgs/psyche.svg new file mode 100644 index 000000000..8c285d5ca --- /dev/null +++ b/webapp/assets/_new/icons/svgs/psyche.svg @@ -0,0 +1,8 @@ + diff --git a/webapp/assets/_new/icons/svgs/save.svg b/webapp/assets/_new/icons/svgs/save.svg new file mode 100644 index 000000000..31c1d8459 --- /dev/null +++ b/webapp/assets/_new/icons/svgs/save.svg @@ -0,0 +1,5 @@ + + diff --git a/webapp/assets/_new/icons/svgs/science.svg b/webapp/assets/_new/icons/svgs/science.svg new file mode 100644 index 000000000..9d3211223 --- /dev/null +++ b/webapp/assets/_new/icons/svgs/science.svg @@ -0,0 +1,25 @@ + diff --git a/webapp/assets/_new/icons/svgs/spirituality.svg b/webapp/assets/_new/icons/svgs/spirituality.svg new file mode 100644 index 000000000..0c2757071 --- /dev/null +++ b/webapp/assets/_new/icons/svgs/spirituality.svg @@ -0,0 +1,13 @@ + diff --git a/webapp/assets/_new/icons/svgs/suitcase.svg b/webapp/assets/_new/icons/svgs/suitcase.svg new file mode 100644 index 000000000..ceca5cbad --- /dev/null +++ b/webapp/assets/_new/icons/svgs/suitcase.svg @@ -0,0 +1,5 @@ + + diff --git a/webapp/assets/_new/styles/tokens.scss b/webapp/assets/_new/styles/tokens.scss index 53abd01f8..6aa6410df 100644 --- a/webapp/assets/_new/styles/tokens.scss +++ b/webapp/assets/_new/styles/tokens.scss @@ -356,3 +356,22 @@ $media-query-x-large: (min-width: 1200px); /** * @tokens Background Images */ + +/** + * @tokens Header Color + */ + +$color-header-background: $color-neutral-100; + +/** + * @tokens Footer Color + */ + +$color-footer-background: $color-neutral-100; +$color-footer-link: $color-primary; + +/** + * @tokens Locale Menu Color + */ + +$color-locale-menu: $text-color-soft; \ No newline at end of file diff --git a/webapp/assets/styles/imports/_branding.scss b/webapp/assets/styles/imports/_branding.scss new file mode 100644 index 000000000..75058595d --- /dev/null +++ b/webapp/assets/styles/imports/_branding.scss @@ -0,0 +1,5 @@ +/* + * + * Here, all SCSS variables and classes can be adapted to your custom design. + * +*/ \ No newline at end of file diff --git a/webapp/assets/styles/main.scss b/webapp/assets/styles/main.scss index d6821e013..144701f83 100644 --- a/webapp/assets/styles/main.scss +++ b/webapp/assets/styles/main.scss @@ -179,4 +179,4 @@ hr { .dropdown-arrow { font-size: $font-size-xx-small; -} +} \ No newline at end of file diff --git a/webapp/components/CategoriesSelect/CategoriesSelect.vue b/webapp/components/CategoriesSelect/CategoriesSelect.vue index b7d71de2d..92779444f 100644 --- a/webapp/components/CategoriesSelect/CategoriesSelect.vue +++ b/webapp/components/CategoriesSelect/CategoriesSelect.vue @@ -9,6 +9,11 @@ :disabled="isDisabled(category.id)" :icon="category.icon" size="small" + v-tooltip="{ + content: $t(`contribution.category.description.${category.slug}`), + placement: 'bottom-start', + delay: { show: 1500 }, + }" > {{ $t(`contribution.category.name.${category.slug}`) }} diff --git a/webapp/components/CommentCard/CommentCard.story.js b/webapp/components/CommentCard/CommentCard.story.js index 467a125d5..75078657e 100644 --- a/webapp/components/CommentCard/CommentCard.story.js +++ b/webapp/components/CommentCard/CommentCard.story.js @@ -18,8 +18,7 @@ const comment = { author: { id: '1', avatar: { - url: - 'https://steamcdn-a.akamaihd.net/steamcommunity/public/images/avatars/db/dbc9e03ebcc384b920c31542af2d27dd8eea9dc2_full.jpg', + url: 'https://steamcdn-a.akamaihd.net/steamcommunity/public/images/avatars/db/dbc9e03ebcc384b920c31542af2d27dd8eea9dc2_full.jpg', }, slug: 'jenny-rostock', name: 'Rainer Unsinn', diff --git a/webapp/components/ContributionForm/ContributionForm.spec.js b/webapp/components/ContributionForm/ContributionForm.spec.js index cce187a63..f5083a8a0 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, }, @@ -198,7 +202,7 @@ describe('ContributionForm.vue', () => { beforeEach(async () => { jest.useFakeTimers() mocks.$apollo.mutate = jest.fn().mockRejectedValueOnce({ - message: 'Not Authorised!', + message: 'Not Authorized!', }) wrapper = Wrapper() postTitleInput = wrapper.find('.ds-input') @@ -209,7 +213,7 @@ describe('ContributionForm.vue', () => { it('shows an error toaster when apollo mutation rejects', async () => { await wrapper.find('form').trigger('submit') await mocks.$apollo.mutate - await expect(mocks.$toast.error).toHaveBeenCalledWith('Not Authorised!') + await expect(mocks.$toast.error).toHaveBeenCalledWith('Not Authorized!') }) }) }) @@ -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 }}