diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 18b01a1d9..551f91c43 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -15,32 +15,78 @@ jobs:
scripts/translations/sort.sh
scripts/translations/missing-keys.sh
- - name: Build neo4j image
+ ##########################################################################
+ # NEO4J ##################################################################
+ ##########################################################################
+ - name: Neo4J | Build `community` image
uses: docker/build-push-action@v1.1.0
with:
repository: ocelotsocialnetwork/neo4j
tags: latest
path: neo4j/
push: false
- - name: Build backend base image
+
+ ##########################################################################
+ # BACKEND ################################################################
+ ##########################################################################
+ # TODO: We want to push this to dockerhub
+ #- name: Build backend production image
+ # uses: docker/build-push-action@v1.1.0
+ # with:
+ # repository: ocelotsocialnetwork/backend
+ # tags: production
+ # target: production
+ # path: backend/
+ # push: false
+
+ # Build Docker Image (build)
+ - name: backend | Build `build` image
uses: docker/build-push-action@v1.1.0
with:
repository: ocelotsocialnetwork/backend
- tags: build-and-test
- target: build-and-test
+ tags: build
+ target: build
path: backend/
push: false
- - name: Build webapp base image
+
+ # Lint
+ - name: backend | Lint
+ run: docker run --rm ocelotsocialnetwork/backend:build yarn run lint
+
+ # Unit Tests
+ #- name: backend | Unit tests
+ # run: |
+ # docker-compose up
+ # docker-compose exec backend yarn test
+
+ ##########################################################################
+ # WEBAPP #################################################################
+ ##########################################################################
+ # TODO: We want to push this to dockerhub
+ #- name: Build webapp production image
+ # uses: docker/build-push-action@v1.1.0
+ # with:
+ # repository: ocelotsocialnetwork/webapp
+ # tags: production
+ # target: production
+ # path: webapp/
+ # push: false
+
+ # Build Docker Image (build)
+ - name: webapp | Build `build` image
uses: docker/build-push-action@v1.1.0
with:
repository: ocelotsocialnetwork/webapp
- tags: build-and-test
- target: build-and-test
+ tags: build
+ target: build
path: webapp/
push: false
- - name: Lint backend
- run: docker run --rm ocelotsocialnetwork/backend:build-and-test yarn run lint
- - name: Lint webapp
- run: docker run --rm ocelotsocialnetwork/webapp:build-and-test yarn run lint
+ # Lint
+ - name: webapp | Lint
+ run: docker run --rm ocelotsocialnetwork/webapp:build yarn run lint
+
+ # Unit Tests
+ - name: webapp | Unit tests
+ run: docker run --rm ocelotsocialnetwork/webapp:build yarn run test
diff --git a/README.md b/README.md
index 998f722f0..8f1abf6ea 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,4 @@
-# Human-Connection
+# ocelot.social
[](https://travis-ci.com/Human-Connection/Human-Connection)
[](https://codecov.io/gh/Human-Connection/Human-Connection/)
@@ -6,22 +6,13 @@
[](https://discordapp.com/invite/DFSjPaX)
[](https://www.codetriage.com/human-connection/human-connection)
-Human Connection is a nonprofit social, action and knowledge network that connects information to action and promotes positive local and global change in all areas of life.
+ocelot.social is a nonprofit social, action and knowledge network that connects information to action and promotes positive local and global change in all areas of life.
* **Social**: Interact with other people not just by commenting their posts, but by providing **Pro & Contra** arguments, give a **Versus** or ask them by integrated **Chat** or **Let's Talk**
* **Knowledge**: Read articles about interesting topics and find related posts in the **More Info** tab or by **Filtering** based on **Categories** and **Tagging** or by using the **Fulltext Search**.
* **Action**: Don't just read about how to make the world a better place, but come into **Action** by following provided suggestions on the **Action** tab provided by other people or **Organisations**.
- [](https://human-connection.org)
-
-**Technology Stack**
-
-* [VueJS](https://vuejs.org/)
-* [NuxtJS](https://nuxtjs.org/)
-* [GraphQL](https://graphql.org/)
-* [NodeJS](https://nodejs.org/en/)
-* [Neo4J](https://neo4j.com/)
-
+ [](https://ocelot.social)
## Live demo
@@ -35,14 +26,77 @@ Logins:
| `moderator@example.org` | 1234 | moderator |
| `admin@example.org` | 1234 | admin |
-## Documentation
+## Directory Layout
-Learn how to set up a local development environment in our [Docs](https://docs.human-connection.org/human-connection/) :mag_right:
+There are four important directories:
+* [Backend](./backend) runs on the server and is a middleware between database and frontend
+* [Frontend](./webapp) is a server-side-rendered and client-side-rendered web frontend
+* [Deployment](./deployment) configuration for kubernetes
+* [Cypress](./cypress) contains end-to-end tests and executable feature specifications
-## Translations
+In order to setup the application and start to develop features you have to
+setup **frontend** and **backend**.
-You can help translating the interface by joining us on [lokalise.co](https://lokalise.co/public/556252725c18dd752dd546.13222042/).
-Thank you lokalise for providing us with a premium account :raised_hands:.
+There are two approaches:
+
+1. Local installation, which means you have to take care of dependencies yourself
+2. **Or** Install everything through Docker which takes care of dependencies for you
+
+## Installation
+
+### Clone the Repository
+Clone the repository, this will create a new folder called `Ocelot-Social`:
+
+Using HTTPS:
+```bash
+$ git clone https://github.com/Ocelot-Social-Community/Ocelot-Social.git
+```
+
+Using SSH:
+```bash
+$ git clone git@github.com:Human-Connection/Human-Connection.git
+```
+
+Change into the new folder.
+
+```bash
+$ cd Ocelot-Social
+```
+
+### Docker Installation
+
+Docker is a software development container tool that combines software and its dependencies into one standardized unit that contains everything needed to run it. This helps us to avoid problems with dependencies and makes installation easier.
+
+#### General Installation of Docker
+
+There are [sevaral ways to install Docker CE](https://docs.docker.com/install/) on your computer or server.
+
+ * [install Docker Desktop on macOS](https://docs.docker.com/docker-for-mac/install/)
+ * [install Docker Desktop on Windows](https://docs.docker.com/docker-for-windows/install/)
+ * [install Docker CE on Linux](https://docs.docker.com/install/)
+
+Check the correct Docker installation by checking the version before proceeding. E.g. we have the following versions:
+
+```bash
+$ docker --version
+Docker version 18.09.2
+$ docker-compose --version
+docker-compose version 1.23.2
+```
+
+#### Start Ocelot-Social via Docker-Compose
+
+For Development:
+```bash
+docker-compose up
+```
+
+For Production
+```bash
+docker-compose -f docker-compose.yml up
+```
+
+This will start all required Docker containers
## Developer Chat
@@ -50,12 +104,16 @@ Join our friendly open-source community on [Discord](https://discordapp.com/invi
Just introduce yourself at `#introduce-yourself` and mention `@@Mentor` to get you onboard :neckbeard:
Check out the [contribution guideline](./CONTRIBUTING.md), too!
-[](https://sourcerer.io/fame/roschaefer/Human-Connection/Human-Connection/links/0)[](https://sourcerer.io/fame/roschaefer/Human-Connection/Human-Connection/links/1)[](https://sourcerer.io/fame/roschaefer/Human-Connection/Human-Connection/links/2)[](https://sourcerer.io/fame/roschaefer/Human-Connection/Human-Connection/links/3)[](https://sourcerer.io/fame/roschaefer/Human-Connection/Human-Connection/links/4)[](https://sourcerer.io/fame/roschaefer/Human-Connection/Human-Connection/links/5)[](https://sourcerer.io/fame/roschaefer/Human-Connection/Human-Connection/links/6)[](https://sourcerer.io/fame/roschaefer/Human-Connection/Human-Connection/links/7)
+We give write permissions to every developer who asks for it. Just text us on
+[Discord](https://discord.gg/6ub73U3).
-## Open-Source Bounties
+## Technology Stack
-You can get a small financial compensation for your contribution :moneybag: See
-details in our [Contribution Guidelines](./CONTRIBUTING.md#open-source-bounties).
+* [VueJS](https://vuejs.org/)
+* [NuxtJS](https://nuxtjs.org/)
+* [GraphQL](https://graphql.org/)
+* [NodeJS](https://nodejs.org/en/)
+* [Neo4J](https://neo4j.com/)
## Attributions
diff --git a/SUMMARY.md b/SUMMARY.md
index 8891579a3..a04c96d98 100644
--- a/SUMMARY.md
+++ b/SUMMARY.md
@@ -2,7 +2,6 @@
* [Introduction](README.md)
* [Edit this Documentation](edit-this-documentation.md)
-* [Installation](installation.md)
* [Neo4J](neo4j/README.md)
* [Backend](backend/README.md)
* [GraphQL](backend/graphql.md)
diff --git a/backend/Dockerfile b/backend/Dockerfile
index 6d3def015..b1cd52b30 100644
--- a/backend/Dockerfile
+++ b/backend/Dockerfile
@@ -1,28 +1,87 @@
+##################################################################################
+# BASE ###########################################################################
+##################################################################################
FROM node:12.19.0-alpine3.10 as base
-LABEL Description="Backend of the Social Network ocelot.social" Vendor="ocelot.social Community" Version="0.0.1" Maintainer="ocelot.social Community (devops@ocelot.social)"
-EXPOSE 4000
-CMD ["yarn", "run", "start"]
-ARG BUILD_COMMIT
-ENV BUILD_COMMIT=$BUILD_COMMIT
-ARG WORKDIR=/develop-backend
-RUN mkdir -p $WORKDIR
-WORKDIR $WORKDIR
+# ENVs (available in production aswell, can be overwritten by commandline or env file)
+## DOCKER_WORKDIR would be a classical ARG, but that is not multi layer persistent - shame
+ENV DOCKER_WORKDIR="/app"
+## We Cannot do `$(date -u +'%Y-%m-%dT%H:%M:%SZ')` here so we use unix timestamp=0
+ENV BUILD_DATE="1970-01-01T00:00:00.00Z"
+## We cannot do $(yarn run version) here so we default to 0.0.0
+## TODO: Missing Build number - do that once we have a CI which actually generates it
+ENV BUILD_VERSION="0.0.0"
+## We cannot do `$(git rev-parse --short HEAD)` here so we default to 0000000
+ENV BUILD_COMMIT="0000000"
+## SET NODE_ENV
+ENV NODE_ENV="production"
+## App relevant Envs
+ENV PORT="4000"
+# Labels
+LABEL org.label-schema.build-date="${BUILD_DATE}"
+LABEL org.label-schema.name="ocelot.social:backend"
+LABEL org.label-schema.description="Backend of the Social Network Software ocelot.social"
+LABEL org.label-schema.usage="https://github.com/Ocelot-Social-Community/Ocelot-Social/blob/master/README.md"
+LABEL org.label-schema.url="https://ocelot.social"
+LABEL org.label-schema.vcs-url="https://github.com/Ocelot-Social-Community/Ocelot-Social/tree/master/backend"
+LABEL org.label-schema.vcs-ref="${BUILD_COMMIT}"
+LABEL org.label-schema.vendor="ocelot.social Community"
+LABEL org.label-schema.version="${BUILD_VERSION}"
+LABEL org.label-schema.schema-version="1.0"
+LABEL maintainer="devops@ocelot.social"
+
+# Install Additional Software
+## install: git
RUN apk --no-cache add git
-COPY package.json yarn.lock ./
-COPY .env.template .env
+# Settings
+## Expose Container Port
+EXPOSE ${PORT}
-FROM base as build-and-test
-RUN yarn install --production=false --frozen-lockfile --non-interactive
+## Workdir
+RUN mkdir -p ${DOCKER_WORKDIR}
+WORKDIR ${DOCKER_WORKDIR}
+
+##################################################################################
+# DEVELOPMENT (Connected to the local environment, to reload on demand) ##########
+##################################################################################
+FROM base as development
+
+# We don't need to copy or build anything since we gonna bind to the
+# local filesystem which will need a rebuild anyway
+
+# Run command
+# (for development we need to execute yarn install since the
+# node_modules are on another volume and need updating)
+CMD /bin/sh -c "yarn install && yarn run dev"
+
+##################################################################################
+# BUILD (Does contain all files and is therefore bloated) ########################
+##################################################################################
+FROM base as build
+
+# Copy everything
COPY . .
-RUN NODE_ENV=production yarn run build
+# yarn install
+RUN yarn install --production=false --frozen-lockfile --non-interactive
+# yarn build
+RUN yarn run build
-# reduce image size with a multistage build
+##################################################################################
+# PRODUCTION (Does contain only "binary"- and static-files to reduce image size) #
+##################################################################################
FROM base as production
-ENV NODE_ENV=production
-COPY --from=build-and-test /develop-backend/dist ./dist
-COPY ./public/img/ ./public/img/
-COPY ./public/providers.json ./public/providers.json
-RUN yarn install --production=true --frozen-lockfile --non-interactive --no-cache
+
+# Copy "binary"-files from build image
+COPY --from=build ${DOCKER_WORKDIR}/dist ./dist
+COPY --from=build ${DOCKER_WORKDIR}/node_modules ./node_modules
+# Copy static files
+# TODO - externalize the uploads so we can copy the whole folder
+COPY --from=build ${DOCKER_WORKDIR}/public/img/ ./public/img/
+COPY --from=build ${DOCKER_WORKDIR}/public/providers.json ./public/providers.json
+# Copy package.json for script definitions (lock file should not be needed)
+COPY --from=build ${DOCKER_WORKDIR}/package.json ./package.json
+
+# Run command
+CMD /bin/sh -c "yarn run start"
\ No newline at end of file
diff --git a/backend/README.md b/backend/README.md
index b472ef530..d7031106e 100644
--- a/backend/README.md
+++ b/backend/README.md
@@ -178,32 +178,20 @@ database after each test, running the tests will wipe out all your data!
{% tabs %}
{% tab title="Docker" %}
-Run the _**jest**_ tests:
+Run the unit tests:
```bash
-$ docker-compose exec backend yarn run test:jest
-```
-
-Run the _**cucumber**_ features:
-
-```bash
-$ docker-compose exec backend yarn run test:cucumber
+$ docker-compose exec backend yarn run test
```
{% endtab %}
{% tab title="Without Docker" %}
-Run the _**jest**_ tests:
+Run the unit tests:
```bash
-$ yarn run test:jest
-```
-
-Run the _**cucumber**_ features:
-
-```bash
-$ yarn run test:cucumber
+$ yarn run test
```
{% endtab %}
diff --git a/backend/package.json b/backend/package.json
index 786fe6641..e5cc976d6 100644
--- a/backend/package.json
+++ b/backend/package.json
@@ -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": "jest --forceExit --detectOpenHandles --runInBand",
+ "test": "cross-env NODE_ENV=test jest --forceExit --detectOpenHandles --runInBand",
"db:clean": "babel-node src/db/clean.js",
"db:reset": "yarn run db:clean",
"db:seed": "babel-node src/db/seed.js",
diff --git a/backend/src/activitypub/routes/webfinger.spec.js b/backend/src/activitypub/routes/webfinger.spec.js
index 213c1ab33..528490541 100644
--- a/backend/src/activitypub/routes/webfinger.spec.js
+++ b/backend/src/activitypub/routes/webfinger.spec.js
@@ -1,6 +1,7 @@
import { handler } from './webfinger'
import Factory, { cleanDatabase } from '../../db/factories'
import { getDriver } from '../../db/neo4j'
+import CONFIG from '../../config'
let resource, res, json, status, contentType
@@ -98,12 +99,12 @@ describe('webfinger', () => {
expect(json).toHaveBeenCalledWith({
links: [
{
- href: 'http://localhost:3000/activitypub/users/some-user',
+ href: `${CONFIG.CLIENT_URI}/activitypub/users/some-user`,
rel: 'self',
type: 'application/activity+json',
},
],
- subject: 'acct:some-user@localhost:3000',
+ subject: `acct:some-user@${new URL(CONFIG.CLIENT_URI).host}`,
})
})
})
diff --git a/backend/src/config/index.js b/backend/src/config/index.js
index 9ebde6cee..d03fb7e00 100644
--- a/backend/src/config/index.js
+++ b/backend/src/config/index.js
@@ -2,111 +2,106 @@ import dotenv from 'dotenv'
import links from './links.js'
import metadata from './metadata.js'
+// Load env file
if (require.resolve) {
- // are we in a nodejs environment?
try {
dotenv.config({ path: require.resolve('../../.env') })
} catch (error) {
- if (error.code !== 'MODULE_NOT_FOUND') throw error
- console.log('WARN: No `.env` file found in /backend') // eslint-disable-line no-console
+ if (error.code === 'MODULE_NOT_FOUND') {
+ console.log('WARN: No `.env` file found in `/app` (docker) or `/backend` (no docker)') // eslint-disable-line no-console
+ } else {
+ throw error
+ }
}
}
-// eslint-disable-next-line no-undef
-const env = typeof Cypress !== 'undefined' ? Cypress.env() : process.env
+// Use Cypress env or process.env
+const env = typeof Cypress !== 'undefined' ? Cypress.env() : process.env // eslint-disable-line no-undef
-const {
- MAPBOX_TOKEN,
- JWT_SECRET,
- PRIVATE_KEY_PASSPHRASE,
- SMTP_IGNORE_TLS = true,
- SMTP_HOST,
- SMTP_PORT,
- SMTP_USERNAME,
- SMTP_PASSWORD,
- SENTRY_DSN_BACKEND,
- COMMIT,
- AWS_ACCESS_KEY_ID,
- AWS_SECRET_ACCESS_KEY,
- AWS_ENDPOINT,
- AWS_REGION,
- AWS_BUCKET,
- NEO4J_URI = 'bolt://localhost:7687',
- NEO4J_USERNAME = 'neo4j',
- NEO4J_PASSWORD = 'neo4j',
- CLIENT_URI = 'http://localhost:3000',
- GRAPHQL_URI = 'http://localhost:4000',
- REDIS_DOMAIN,
- REDIS_PORT,
- REDIS_PASSWORD,
- EMAIL_DEFAULT_SENDER,
-} = env
-
-export const requiredConfigs = {
- MAPBOX_TOKEN,
- JWT_SECRET,
- PRIVATE_KEY_PASSPHRASE,
+const environment = {
+ NODE_ENV: env.NODE_ENV || process.NODE_ENV,
+ DEBUG: env.NODE_ENV !== 'production' && env.DEBUG,
+ TEST: env.NODE_ENV === 'test',
+ PRODUCTION: env.NODE_ENV === 'production',
+ DISABLED_MIDDLEWARES: (env.NODE_ENV !== 'production' && env.DISABLED_MIDDLEWARES) || false,
}
+const required = {
+ MAPBOX_TOKEN: env.MAPBOX_TOKEN,
+ JWT_SECRET: env.JWT_SECRET,
+ PRIVATE_KEY_PASSPHRASE: env.PRIVATE_KEY_PASSPHRASE,
+}
+
+const server = {
+ CLIENT_URI: env.CLIENT_URI || 'http://localhost:3000',
+ GRAPHQL_URI: env.GRAPHQL_URI || 'http://localhost:4000',
+}
+
+const smtp = {
+ SMTP_HOST: env.SMTP_HOST,
+ SMTP_PORT: env.SMTP_PORT,
+ SMTP_IGNORE_TLS: env.SMTP_IGNORE_TLS || true,
+ SMTP_USERNAME: env.SMTP_USERNAME,
+ SMTP_PASSWORD: env.SMTP_PASSWORD,
+}
+
+const neo4j = {
+ NEO4J_URI: env.NEO4J_URI || 'bolt://localhost:7687',
+ NEO4J_USERNAME: env.NEO4J_USERNAME || 'neo4j',
+ NEO4J_PASSWORD: env.NEO4J_PASSWORD || 'neo4j',
+}
+
+const sentry = {
+ SENTRY_DSN_BACKEND: env.SENTRY_DSN_BACKEND,
+ COMMIT: env.COMMIT,
+}
+
+const redis = {
+ REDIS_DOMAIN: env.REDIS_DOMAIN,
+ REDIS_PORT: env.REDIS_PORT,
+ REDIS_PASSWORD: env.REDIS_PASSWORD,
+}
+
+const s3 = {
+ AWS_ACCESS_KEY_ID: env.AWS_ACCESS_KEY_ID,
+ AWS_SECRET_ACCESS_KEY: env.AWS_SECRET_ACCESS_KEY,
+ AWS_ENDPOINT: env.AWS_ENDPOINT,
+ AWS_REGION: env.AWS_REGION,
+ AWS_BUCKET: env.AWS_BUCKET,
+ S3_CONFIGURED:
+ env.AWS_ACCESS_KEY_ID &&
+ env.AWS_SECRET_ACCESS_KEY &&
+ env.AWS_ENDPOINT &&
+ env.AWS_REGION &&
+ env.AWS_BUCKET,
+}
+
+const options = {
+ EMAIL_DEFAULT_SENDER: env.EMAIL_DEFAULT_SENDER,
+ SUPPORT_URL: links.SUPPORT,
+ APPLICATION_NAME: metadata.APPLICATION_NAME,
+ ORGANIZATION_URL: links.ORGANIZATION,
+ PUBLIC_REGISTRATION: env.PUBLIC_REGISTRATION === 'true',
+}
+
+// Check if all required configs are present
if (require.resolve) {
// are we in a nodejs environment?
- Object.entries(requiredConfigs).map((entry) => {
+ Object.entries(required).map((entry) => {
if (!entry[1]) {
throw new Error(`ERROR: "${entry[0]}" env variable is missing.`)
}
})
}
-export const smtpConfigs = {
- SMTP_HOST,
- SMTP_PORT,
- SMTP_IGNORE_TLS,
- SMTP_USERNAME,
- SMTP_PASSWORD,
-}
-export const neo4jConfigs = { NEO4J_URI, NEO4J_USERNAME, NEO4J_PASSWORD }
-export const serverConfigs = {
- CLIENT_URI,
- GRAPHQL_URI,
- PUBLIC_REGISTRATION: process.env.PUBLIC_REGISTRATION === 'true',
-}
-
-export const developmentConfigs = {
- DEBUG: process.env.NODE_ENV !== 'production' && process.env.DEBUG,
- DISABLED_MIDDLEWARES:
- (process.env.NODE_ENV !== 'production' && process.env.DISABLED_MIDDLEWARES) || '',
-}
-
-export const sentryConfigs = { SENTRY_DSN_BACKEND, COMMIT }
-export const redisConfigs = { REDIS_DOMAIN, REDIS_PORT, REDIS_PASSWORD }
-
-const S3_CONFIGURED =
- AWS_ACCESS_KEY_ID && AWS_SECRET_ACCESS_KEY && AWS_ENDPOINT && AWS_REGION && AWS_BUCKET
-
-export const s3Configs = {
- AWS_ACCESS_KEY_ID,
- AWS_SECRET_ACCESS_KEY,
- AWS_ENDPOINT,
- AWS_REGION,
- AWS_BUCKET,
- S3_CONFIGURED,
-}
-
-export const customConfigs = {
- EMAIL_DEFAULT_SENDER,
- SUPPORT_URL: links.SUPPORT,
- APPLICATION_NAME: metadata.APPLICATION_NAME,
- ORGANIZATION_URL: links.ORGANIZATION,
-}
-
export default {
- ...requiredConfigs,
- ...smtpConfigs,
- ...neo4jConfigs,
- ...serverConfigs,
- ...developmentConfigs,
- ...sentryConfigs,
- ...redisConfigs,
- ...s3Configs,
- ...customConfigs,
+ ...environment,
+ ...server,
+ ...required,
+ ...smtp,
+ ...neo4j,
+ ...sentry,
+ ...redis,
+ ...s3,
+ ...options,
}
diff --git a/backend/src/db/clean.js b/backend/src/db/clean.js
index 97a21a055..db8e8aad6 100644
--- a/backend/src/db/clean.js
+++ b/backend/src/db/clean.js
@@ -1,6 +1,7 @@
import { cleanDatabase } from '../db/factories'
+import CONFIG from '../config'
-if (process.env.NODE_ENV === 'production') {
+if (CONFIG.PRODUCTION) {
throw new Error(`You cannot clean the database in production environment!`)
}
diff --git a/backend/src/db/seed.js b/backend/src/db/seed.js
index d7bd5c73b..713a03142 100644
--- a/backend/src/db/seed.js
+++ b/backend/src/db/seed.js
@@ -941,6 +941,7 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl']
const additionalUsers = await Promise.all(
[...Array(30).keys()].map(() => Factory.build('user')),
)
+
await Promise.all(
additionalUsers.map(async (user) => {
await jennyRostock.relateTo(user, 'following')
@@ -948,6 +949,26 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl']
}),
)
+ await Promise.all(
+ [...Array(30).keys()].map((index) => Factory.build('user', { name: `Jenny${index}` })),
+ )
+
+ await Promise.all(
+ [...Array(30).keys()].map(() =>
+ Factory.build(
+ 'post',
+ { content: `Jenny ${faker.lorem.sentence()}` },
+ {
+ categoryIds: ['cat1'],
+ author: jennyRostock,
+ image: Factory.build('image', {
+ url: faker.image.unsplash.objects(),
+ }),
+ },
+ ),
+ ),
+ )
+
await Promise.all(
[...Array(30).keys()].map(() =>
Factory.build(
diff --git a/backend/src/middleware/email/emailMiddleware.js b/backend/src/middleware/email/emailMiddleware.js
index a69530582..4dbb3ad03 100644
--- a/backend/src/middleware/email/emailMiddleware.js
+++ b/backend/src/middleware/email/emailMiddleware.js
@@ -13,7 +13,7 @@ const hasAuthData = CONFIG.SMTP_USERNAME && CONFIG.SMTP_PASSWORD
let sendMail = () => {}
if (!hasEmailConfig) {
- if (process.env.NODE_ENV !== 'test') {
+ if (!CONFIG.TEST) {
// eslint-disable-next-line no-console
console.log('Warning: Email middleware will not try to send mails.')
}
diff --git a/backend/src/middleware/permissionsMiddleware.js b/backend/src/middleware/permissionsMiddleware.js
index ddf12598b..6fe82951e 100644
--- a/backend/src/middleware/permissionsMiddleware.js
+++ b/backend/src/middleware/permissionsMiddleware.js
@@ -29,15 +29,25 @@ const onlyYourself = rule({
const isMyOwn = rule({
cache: 'no_cache',
-})(async (parent, args, context, info) => {
- return context.user.id === parent.id
+})(async (parent, args, { user }, info) => {
+ return user && user.id === parent.id
})
const isMySocialMedia = rule({
cache: 'no_cache',
})(async (_, args, { user }) => {
+ // We need a User
+ if (!user) {
+ return false
+ }
let socialMedia = await neode.find('SocialMedia', args.id)
- socialMedia = await socialMedia.toJson()
+ // Did we find a social media node?
+ if (!socialMedia) {
+ return false
+ }
+ socialMedia = await socialMedia.toJson() // whats this for?
+
+ // Is it my social media entry?
return socialMedia.ownedBy.node.id === user.id
})
@@ -86,7 +96,10 @@ export default shield(
'*': deny,
findPosts: allow,
findUsers: allow,
- findResources: allow,
+ searchResults: allow,
+ searchPosts: allow,
+ searchUsers: allow,
+ searchHashtags: allow,
embed: allow,
Category: allow,
Tag: allow,
diff --git a/backend/src/middleware/sentryMiddleware.js b/backend/src/middleware/sentryMiddleware.js
index da8ef32d0..8891b8677 100644
--- a/backend/src/middleware/sentryMiddleware.js
+++ b/backend/src/middleware/sentryMiddleware.js
@@ -1,16 +1,16 @@
import { sentry } from 'graphql-middleware-sentry'
-import { sentryConfigs } from '../config'
+import CONFIG from '../config'
let sentryMiddleware = (resolve, root, args, context, resolveInfo) =>
resolve(root, args, context, resolveInfo)
-if (sentryConfigs.SENTRY_DSN_BACKEND) {
+if (CONFIG.SENTRY_DSN_BACKEND) {
sentryMiddleware = sentry({
forwardErrors: true,
config: {
- dsn: sentryConfigs.SENTRY_DSN_BACKEND,
- release: sentryConfigs.COMMIT,
- environment: process.env.NODE_ENV,
+ dsn: CONFIG.SENTRY_DSN_BACKEND,
+ release: CONFIG.COMMIT,
+ environment: CONFIG.NODE_ENV,
},
withScope: (scope, error, context) => {
scope.setUser({
@@ -23,7 +23,7 @@ if (sentryConfigs.SENTRY_DSN_BACKEND) {
})
} else {
// eslint-disable-next-line no-console
- if (process.env.NODE_ENV !== 'test') console.log('Warning: Sentry middleware inactive.')
+ if (!CONFIG.TEST) console.log('Warning: Sentry middleware inactive.')
}
export default sentryMiddleware
diff --git a/backend/src/schema/resolvers/images/images.js b/backend/src/schema/resolvers/images/images.js
index 18a3569b6..9b57579c4 100644
--- a/backend/src/schema/resolvers/images/images.js
+++ b/backend/src/schema/resolvers/images/images.js
@@ -5,10 +5,10 @@ import slug from 'slug'
import { existsSync, unlinkSync, createWriteStream } from 'fs'
import { UserInputError } from 'apollo-server'
import { getDriver } from '../../../db/neo4j'
-import { s3Configs } from '../../../config'
+import CONFIG from '../../../config'
// const widths = [34, 160, 320, 640, 1024]
-const { AWS_ENDPOINT: endpoint, AWS_REGION: region, AWS_BUCKET: Bucket, S3_CONFIGURED } = s3Configs
+const { AWS_ENDPOINT: endpoint, AWS_REGION: region, AWS_BUCKET: Bucket, S3_CONFIGURED } = CONFIG
export async function deleteImage(resource, relationshipType, opts = {}) {
sanitizeRelationshipType(relationshipType)
diff --git a/backend/src/schema/resolvers/searches.js b/backend/src/schema/resolvers/searches.js
index 58fa63f8d..7b157fc65 100644
--- a/backend/src/schema/resolvers/searches.js
+++ b/backend/src/schema/resolvers/searches.js
@@ -3,90 +3,200 @@ import { queryString } from './searches/queryString'
// see http://lucene.apache.org/core/8_3_1/queryparser/org/apache/lucene/queryparser/classic/package-summary.html#package.description
+const cypherTemplate = (setup) => `
+ CALL db.index.fulltext.queryNodes('${setup.fulltextIndex}', $query)
+ YIELD node AS resource, score
+ ${setup.match}
+ ${setup.whereClause}
+ ${setup.withClause}
+ RETURN
+ ${setup.returnClause}
+ AS result
+ SKIP $skip
+ ${setup.limit}
+`
+
+const simpleWhereClause =
+ 'WHERE score >= 0.0 AND NOT (resource.deleted = true OR resource.disabled = true)'
+
+const postWhereClause = `WHERE score >= 0.0
+ AND NOT (
+ author.deleted = true OR author.disabled = true
+ OR resource.deleted = true OR resource.disabled = true
+ OR (:User {id: $userId})-[:MUTED]->(author)
+ )`
+
+const searchPostsSetup = {
+ fulltextIndex: 'post_fulltext_search',
+ match: 'MATCH (resource:Post)<-[:WROTE]-(author:User)',
+ whereClause: postWhereClause,
+ withClause: `WITH resource, author,
+ [(resource)<-[:COMMENTS]-(comment:Comment) | comment] AS comments,
+ [(resource)<-[:SHOUTED]-(user:User) | user] AS shouter`,
+ returnClause: `resource {
+ .*,
+ __typename: labels(resource)[0],
+ author: properties(author),
+ commentsCount: toString(size(comments)),
+ shoutedCount: toString(size(shouter))
+ }`,
+ limit: 'LIMIT $limit',
+}
+
+const searchUsersSetup = {
+ fulltextIndex: 'user_fulltext_search',
+ match: 'MATCH (resource:User)',
+ whereClause: simpleWhereClause,
+ withClause: '',
+ returnClause: 'resource {.*, __typename: labels(resource)[0]}',
+ limit: 'LIMIT $limit',
+}
+
+const searchHashtagsSetup = {
+ fulltextIndex: 'tag_fulltext_search',
+ match: 'MATCH (resource:Tag)',
+ whereClause: simpleWhereClause,
+ withClause: '',
+ returnClause: 'resource {.*, __typename: labels(resource)[0]}',
+ limit: 'LIMIT $limit',
+}
+
+const countSetup = {
+ returnClause: 'toString(size(collect(resource)))',
+ limit: '',
+}
+
+const countUsersSetup = {
+ ...searchUsersSetup,
+ ...countSetup,
+}
+const countPostsSetup = {
+ ...searchPostsSetup,
+ ...countSetup,
+}
+const countHashtagsSetup = {
+ ...searchHashtagsSetup,
+ ...countSetup,
+}
+
+const searchResultPromise = async (session, setup, params) => {
+ return session.readTransaction(async (transaction) => {
+ return transaction.run(cypherTemplate(setup), params)
+ })
+}
+
+const searchResultCallback = (result) => {
+ return result.records.map((r) => r.get('result'))
+}
+
+const countResultCallback = (result) => {
+ return result.records[0].get('result')
+}
+
+const getSearchResults = async (context, setup, params, resultCallback = searchResultCallback) => {
+ const session = context.driver.session()
+ try {
+ const results = await searchResultPromise(session, setup, params)
+ log(results)
+ return resultCallback(results)
+ } finally {
+ session.close()
+ }
+}
+
+const multiSearchMap = [
+ { symbol: '!', setup: searchPostsSetup, resultName: 'posts' },
+ { symbol: '@', setup: searchUsersSetup, resultName: 'users' },
+ { symbol: '#', setup: searchHashtagsSetup, resultName: 'hashtags' },
+]
+
export default {
Query: {
- findResources: async (_parent, args, context, _resolveInfo) => {
+ searchPosts: async (_parent, args, context, _resolveInfo) => {
+ const { query, postsOffset, firstPosts } = args
+ const { id: userId } = context.user
+
+ return {
+ postCount: getSearchResults(
+ context,
+ countPostsSetup,
+ {
+ query: queryString(query),
+ skip: 0,
+ userId,
+ },
+ countResultCallback,
+ ),
+ posts: getSearchResults(context, searchPostsSetup, {
+ query: queryString(query),
+ skip: postsOffset,
+ limit: firstPosts,
+ userId,
+ }),
+ }
+ },
+ searchUsers: async (_parent, args, context, _resolveInfo) => {
+ const { query, usersOffset, firstUsers } = args
+ return {
+ userCount: getSearchResults(
+ context,
+ countUsersSetup,
+ {
+ query: queryString(query),
+ skip: 0,
+ },
+ countResultCallback,
+ ),
+ users: getSearchResults(context, searchUsersSetup, {
+ query: queryString(query),
+ skip: usersOffset,
+ limit: firstUsers,
+ }),
+ }
+ },
+ searchHashtags: async (_parent, args, context, _resolveInfo) => {
+ const { query, hashtagsOffset, firstHashtags } = args
+ return {
+ hashtagCount: getSearchResults(
+ context,
+ countHashtagsSetup,
+ {
+ query: queryString(query),
+ skip: 0,
+ },
+ countResultCallback,
+ ),
+ hashtags: getSearchResults(context, searchHashtagsSetup, {
+ query: queryString(query),
+ skip: hashtagsOffset,
+ limit: firstHashtags,
+ }),
+ }
+ },
+ searchResults: async (_parent, args, context, _resolveInfo) => {
const { query, limit } = args
- const { id: thisUserId } = context.user
+ const { id: userId } = context.user
- const postCypher = `
- CALL db.index.fulltext.queryNodes('post_fulltext_search', $query)
- YIELD node as resource, score
- MATCH (resource)<-[:WROTE]-(author:User)
- WHERE score >= 0.0
- AND NOT (
- author.deleted = true OR author.disabled = true
- OR resource.deleted = true OR resource.disabled = true
- OR (:User {id: $thisUserId})-[:MUTED]->(author)
- )
- WITH resource, author,
- [(resource)<-[:COMMENTS]-(comment:Comment) | comment] as comments,
- [(resource)<-[:SHOUTED]-(user:User) | user] as shouter
- RETURN resource {
- .*,
- __typename: labels(resource)[0],
- author: properties(author),
- commentsCount: toString(size(comments)),
- shoutedCount: toString(size(shouter))
+ const searchType = query.replace(/^([!@#]?).*$/, '$1')
+ const searchString = query.replace(/^([!@#])/, '')
+
+ const params = {
+ query: queryString(searchString),
+ skip: 0,
+ limit,
+ userId,
}
- LIMIT $limit
- `
- const userCypher = `
- CALL db.index.fulltext.queryNodes('user_fulltext_search', $query)
- YIELD node as resource, score
- MATCH (resource)
- WHERE score >= 0.0
- AND NOT (resource.deleted = true OR resource.disabled = true)
- RETURN resource {.*, __typename: labels(resource)[0]}
- LIMIT $limit
- `
- const tagCypher = `
- CALL db.index.fulltext.queryNodes('tag_fulltext_search', $query)
- YIELD node as resource, score
- MATCH (resource)
- WHERE score >= 0.0
- AND NOT (resource.deleted = true OR resource.disabled = true)
- RETURN resource {.*, __typename: labels(resource)[0]}
- LIMIT $limit
- `
+ if (searchType === '')
+ return [
+ ...(await getSearchResults(context, searchPostsSetup, params)),
+ ...(await getSearchResults(context, searchUsersSetup, params)),
+ ...(await getSearchResults(context, searchHashtagsSetup, params)),
+ ]
- const myQuery = queryString(query)
-
- const session = context.driver.session()
- const searchResultPromise = session.readTransaction(async (transaction) => {
- const postTransactionResponse = transaction.run(postCypher, {
- query: myQuery,
- limit,
- thisUserId,
- })
- const userTransactionResponse = transaction.run(userCypher, {
- query: myQuery,
- limit,
- thisUserId,
- })
- const tagTransactionResponse = transaction.run(tagCypher, {
- query: myQuery,
- limit,
- })
- return Promise.all([
- postTransactionResponse,
- userTransactionResponse,
- tagTransactionResponse,
- ])
- })
-
- try {
- const [postResults, userResults, tagResults] = await searchResultPromise
- log(postResults)
- log(userResults)
- log(tagResults)
- return [...postResults.records, ...userResults.records, ...tagResults.records].map((r) =>
- r.get('resource'),
- )
- } finally {
- session.close()
- }
+ params.limit = 15
+ const type = multiSearchMap.find((obj) => obj.symbol === searchType)
+ return getSearchResults(context, type.setup, params)
},
},
}
diff --git a/backend/src/schema/resolvers/searches.spec.js b/backend/src/schema/resolvers/searches.spec.js
index 3d7bd039d..a859bf296 100644
--- a/backend/src/schema/resolvers/searches.spec.js
+++ b/backend/src/schema/resolvers/searches.spec.js
@@ -29,7 +29,7 @@ afterAll(async () => {
const searchQuery = gql`
query($query: String!) {
- findResources(query: $query, limit: 5) {
+ searchResults(query: $query, limit: 5) {
__typename
... on Post {
id
@@ -47,6 +47,21 @@ const searchQuery = gql`
}
}
`
+
+const searchPostQuery = gql`
+ query($query: String!, $firstPosts: Int, $postsOffset: Int) {
+ searchPosts(query: $query, firstPosts: $firstPosts, postsOffset: $postsOffset) {
+ postCount
+ posts {
+ __typename
+ id
+ title
+ content
+ }
+ }
+ }
+`
+
describe('resolvers/searches', () => {
let variables
@@ -65,7 +80,7 @@ describe('resolvers/searches', () => {
variables = { query: 'John' }
await expect(query({ query: searchQuery, variables })).resolves.toMatchObject({
data: {
- findResources: [
+ searchResults: [
{
id: 'a-user',
name: 'John Doe',
@@ -95,7 +110,7 @@ describe('resolvers/searches', () => {
variables = { query: 'beitrag' }
await expect(query({ query: searchQuery, variables })).resolves.toMatchObject({
data: {
- findResources: [
+ searchResults: [
{
__typename: 'Post',
id: 'a-post',
@@ -114,7 +129,7 @@ describe('resolvers/searches', () => {
variables = { query: 'BEITRAG' }
await expect(query({ query: searchQuery, variables })).resolves.toMatchObject({
data: {
- findResources: [
+ searchResults: [
{
__typename: 'Post',
id: 'a-post',
@@ -132,7 +147,7 @@ describe('resolvers/searches', () => {
it('returns empty search results', async () => {
await expect(
query({ query: searchQuery, variables: { query: 'Unfug' } }),
- ).resolves.toMatchObject({ data: { findResources: [] } })
+ ).resolves.toMatchObject({ data: { searchResults: [] } })
})
})
@@ -189,7 +204,7 @@ und hinter tausend Stäben keine Welt.`,
variables = { query: 'beitrag' }
await expect(query({ query: searchQuery, variables })).resolves.toMatchObject({
data: {
- findResources: expect.arrayContaining([
+ searchResults: expect.arrayContaining([
{
__typename: 'Post',
id: 'a-post',
@@ -216,7 +231,7 @@ und hinter tausend Stäben keine Welt.`,
variables = { query: 'tee-ei' }
await expect(query({ query: searchQuery, variables })).resolves.toMatchObject({
data: {
- findResources: [
+ searchResults: [
{
__typename: 'Post',
id: 'g-post',
@@ -235,7 +250,7 @@ und hinter tausend Stäben keine Welt.`,
variables = { query: '„teeei“' }
await expect(query({ query: searchQuery, variables })).resolves.toMatchObject({
data: {
- findResources: [
+ searchResults: [
{
__typename: 'Post',
id: 'g-post',
@@ -256,7 +271,7 @@ und hinter tausend Stäben keine Welt.`,
variables = { query: '(a - b)²' }
await expect(query({ query: searchQuery, variables })).resolves.toMatchObject({
data: {
- findResources: [
+ searchResults: [
{
__typename: 'Post',
id: 'c-post',
@@ -277,7 +292,7 @@ und hinter tausend Stäben keine Welt.`,
variables = { query: '(a-b)²' }
await expect(query({ query: searchQuery, variables })).resolves.toMatchObject({
data: {
- findResources: [
+ searchResults: [
{
__typename: 'Post',
id: 'c-post',
@@ -298,7 +313,7 @@ und hinter tausend Stäben keine Welt.`,
variables = { query: '+ b² 2.' }
await expect(query({ query: searchQuery, variables })).resolves.toMatchObject({
data: {
- findResources: [
+ searchResults: [
{
__typename: 'Post',
id: 'c-post',
@@ -321,7 +336,7 @@ und hinter tausend Stäben keine Welt.`,
variables = { query: 'der panther' }
await expect(query({ query: searchQuery, variables })).resolves.toMatchObject({
data: {
- findResources: [
+ searchResults: [
{
__typename: 'Post',
id: 'd-post',
@@ -349,7 +364,7 @@ und hinter tausend Stäben keine Welt.`,
variables = { query: 'Vorü Subs' }
await expect(query({ query: searchQuery, variables })).resolves.toMatchObject({
data: {
- findResources: expect.arrayContaining([
+ searchResults: expect.arrayContaining([
{
__typename: 'Post',
id: 'd-post',
@@ -395,7 +410,7 @@ und hinter tausend Stäben keine Welt.`,
variables = { query: '-maria-' }
await expect(query({ query: searchQuery, variables })).resolves.toMatchObject({
data: {
- findResources: expect.arrayContaining([
+ searchResults: expect.arrayContaining([
{
__typename: 'User',
id: 'c-user',
@@ -416,6 +431,128 @@ und hinter tausend Stäben keine Welt.`,
})
})
+ describe('adding a user and a hashtag with a name that is content of a post', () => {
+ beforeAll(async () => {
+ await Promise.all([
+ Factory.build('user', {
+ id: 'f-user',
+ name: 'Peter Panther',
+ slug: 'peter-panther',
+ }),
+ await Factory.build('tag', { id: 'Panther' }),
+ ])
+ })
+
+ describe('query the word that contains the post, the hashtag and the name of the user', () => {
+ it('finds the user, the post and the hashtag', async () => {
+ variables = { query: 'panther' }
+ await expect(query({ query: searchQuery, variables })).resolves.toMatchObject({
+ data: {
+ searchResults: expect.arrayContaining([
+ {
+ __typename: 'User',
+ id: 'f-user',
+ name: 'Peter Panther',
+ slug: 'peter-panther',
+ },
+ {
+ __typename: 'Post',
+ id: 'd-post',
+ title: 'Der Panther',
+ content: `Sein Blick ist vom Vorübergehn der Stäbe
+so müd geworden, daß er nichts mehr hält.
+Ihm ist, als ob es tausend Stäbe gäbe
+und hinter tausend Stäben keine Welt.`,
+ },
+ {
+ __typename: 'Tag',
+ id: 'Panther',
+ },
+ ]),
+ },
+ errors: undefined,
+ })
+ })
+ })
+
+ describe('@query the word that contains the post, the hashtag and the name of the user', () => {
+ it('only finds the user', async () => {
+ variables = { query: '@panther' }
+ await expect(query({ query: searchQuery, variables })).resolves.toMatchObject({
+ data: {
+ searchResults: expect.not.arrayContaining([
+ {
+ __typename: 'Post',
+ id: 'd-post',
+ title: 'Der Panther',
+ content: `Sein Blick ist vom Vorübergehn der Stäbe
+so müd geworden, daß er nichts mehr hält.
+Ihm ist, als ob es tausend Stäbe gäbe
+und hinter tausend Stäben keine Welt.`,
+ },
+ {
+ __typename: 'Tag',
+ id: 'Panther',
+ },
+ ]),
+ },
+ errors: undefined,
+ })
+ })
+ })
+
+ describe('!query the word that contains the post, the hashtag and the name of the user', () => {
+ it('only finds the post', async () => {
+ variables = { query: '!panther' }
+ await expect(query({ query: searchQuery, variables })).resolves.toMatchObject({
+ data: {
+ searchResults: expect.not.arrayContaining([
+ {
+ __typename: 'User',
+ id: 'f-user',
+ name: 'Peter Panther',
+ slug: 'peter-panther',
+ },
+ {
+ __typename: 'Tag',
+ id: 'Panther',
+ },
+ ]),
+ },
+ errors: undefined,
+ })
+ })
+ })
+
+ describe('#query the word that contains the post, the hashtag and the name of the user', () => {
+ it('only finds the hashtag', async () => {
+ variables = { query: '#panther' }
+ await expect(query({ query: searchQuery, variables })).resolves.toMatchObject({
+ data: {
+ searchResults: expect.not.arrayContaining([
+ {
+ __typename: 'User',
+ id: 'f-user',
+ name: 'Peter Panther',
+ slug: 'peter-panther',
+ },
+ {
+ __typename: 'Post',
+ id: 'd-post',
+ title: 'Der Panther',
+ content: `Sein Blick ist vom Vorübergehn der Stäbe
+so müd geworden, daß er nichts mehr hält.
+Ihm ist, als ob es tausend Stäbe gäbe
+und hinter tausend Stäben keine Welt.`,
+ },
+ ]),
+ },
+ errors: undefined,
+ })
+ })
+ })
+ })
+
describe('adding a post, written by a user who is muted by the authenticated user', () => {
beforeAll(async () => {
const mutedUser = await Factory.build('user', {
@@ -440,7 +577,7 @@ und hinter tausend Stäben keine Welt.`,
variables = { query: 'beitrag' }
await expect(query({ query: searchQuery, variables })).resolves.toMatchObject({
data: {
- findResources: expect.not.arrayContaining([
+ searchResults: expect.not.arrayContaining([
{
__typename: 'Post',
id: 'muted-post',
@@ -465,7 +602,7 @@ und hinter tausend Stäben keine Welt.`,
variables = { query: 'myha' }
await expect(query({ query: searchQuery, variables })).resolves.toMatchObject({
data: {
- findResources: [
+ searchResults: [
{
__typename: 'Tag',
id: 'myHashtag',
@@ -477,6 +614,30 @@ und hinter tausend Stäben keine Welt.`,
})
})
})
+
+ describe('searchPostQuery', () => {
+ describe('query with limit 1', () => {
+ it('has a count greater than 1', async () => {
+ variables = { query: 'beitrag', firstPosts: 1, postsOffset: 0 }
+ await expect(query({ query: searchPostQuery, variables })).resolves.toMatchObject({
+ data: {
+ searchPosts: {
+ postCount: 2,
+ posts: [
+ {
+ __typename: 'Post',
+ id: 'a-post',
+ title: 'Beitrag',
+ content: 'Ein erster Beitrag',
+ },
+ ],
+ },
+ },
+ errors: undefined,
+ })
+ })
+ })
+ })
})
})
})
diff --git a/backend/src/schema/resolvers/searches/queryString.js b/backend/src/schema/resolvers/searches/queryString.js
index 064f17f48..5ef84cdce 100644
--- a/backend/src/schema/resolvers/searches/queryString.js
+++ b/backend/src/schema/resolvers/searches/queryString.js
@@ -39,7 +39,11 @@ const matchBeginningOfWords = (str) => {
}
export function normalizeWhitespace(str) {
- return str.replace(/\s+/g, ' ').trim()
+ // delete the first character if it is !, @ or #
+ return str
+ .replace(/^([!@#])/, '')
+ .replace(/\s+/g, ' ')
+ .trim()
}
export function escapeSpecialCharacters(str) {
diff --git a/backend/src/schema/resolvers/statistics.spec.js b/backend/src/schema/resolvers/statistics.spec.js
index c5bb5f88b..0aedd0cef 100644
--- a/backend/src/schema/resolvers/statistics.spec.js
+++ b/backend/src/schema/resolvers/statistics.spec.js
@@ -21,7 +21,7 @@ const statisticsQuery = gql`
}
}
`
-beforeAll(() => {
+beforeAll(async () => {
authenticatedUser = undefined
const { server } = createServer({
context: () => {
@@ -33,6 +33,7 @@ beforeAll(() => {
},
})
query = createTestClient(server).query
+ await cleanDatabase()
})
afterEach(async () => {
diff --git a/backend/src/schema/resolvers/users.spec.js b/backend/src/schema/resolvers/users.spec.js
index cce45ae6e..6509f0e68 100644
--- a/backend/src/schema/resolvers/users.spec.js
+++ b/backend/src/schema/resolvers/users.spec.js
@@ -265,9 +265,9 @@ describe('UpdateUser', () => {
})
it('supports updating location', async () => {
- variables = { ...variables, locationName: 'Hamburg, New Jersey, United States of America' }
+ variables = { ...variables, locationName: 'Hamburg, New Jersey, United States' }
await expect(mutate({ mutation: updateUserMutation, variables })).resolves.toMatchObject({
- data: { UpdateUser: { locationName: 'Hamburg, New Jersey, United States of America' } },
+ data: { UpdateUser: { locationName: 'Hamburg, New Jersey, United States' } },
errors: undefined,
})
})
diff --git a/backend/src/schema/types/type/Search.gql b/backend/src/schema/types/type/Search.gql
index 1ce38001d..9537b5a84 100644
--- a/backend/src/schema/types/type/Search.gql
+++ b/backend/src/schema/types/type/Search.gql
@@ -1,5 +1,23 @@
union SearchResult = Post | User | Tag
-type Query {
- findResources(query: String!, limit: Int = 5): [SearchResult]!
+type postSearchResults {
+ postCount: Int
+ posts: [Post]!
+}
+
+type userSearchResults {
+ userCount: Int
+ users: [User]!
+}
+
+type hashtagSearchResults {
+ hashtagCount: Int
+ hashtags: [Tag]!
+}
+
+type Query {
+ searchPosts(query: String!, firstPosts: Int, postsOffset: Int): postSearchResults!
+ searchUsers(query: String!, firstUsers: Int, usersOffset: Int): userSearchResults!
+ searchHashtags(query: String!, firstHashtags: Int, hashtagsOffset: Int): hashtagSearchResults!
+ searchResults(query: String!, limit: Int = 5): [SearchResult]!
}
diff --git a/cypress/integration/common/search.js b/cypress/integration/common/search.js
index 1feece77e..5eae20a22 100644
--- a/cypress/integration/common/search.js
+++ b/cypress/integration/common/search.js
@@ -37,7 +37,7 @@ Then("I should see the following posts in the select dropdown:", table => {
});
Then("I should see the following users in the select dropdown:", table => {
- cy.get(".ds-heading").should("contain", "Users");
+ cy.get(".search-heading").should("contain", "Users");
table.hashes().forEach(({ slug }) => {
cy.get(".ds-select-dropdown").should("contain", slug);
});
@@ -85,6 +85,26 @@ Then(
}
);
+Then("I should see the search results page", () => {
+ cy.location("pathname").should(
+ "eq",
+ "/search/search-results"
+ );
+ cy.location("search").should(
+ "eq",
+ "?search=PR"
+ );
+});
+
+Then("I should see the following posts on the search results page",
+ () => {
+ cy.get(".post-teaser").should(
+ "contain",
+ "101 Essays that will change the way you think"
+ );
+ }
+);
+
Then(
"I should not see posts without the searched-for term in the select dropdown",
() => {
diff --git a/cypress/integration/search/Search.feature b/cypress/integration/search/Search.feature
index b77b45d8e..d128838f3 100644
--- a/cypress/integration/search/Search.feature
+++ b/cypress/integration/search/Search.feature
@@ -8,7 +8,7 @@ Feature: Search
And we have the following posts in our database:
| id | title | content |
| p1 | 101 Essays that will change the way you think | 101 Essays, of course (PR)! |
- | p2 | No searched for content | will be found in this post, I guarantee |
+ | p2 | No content | will be found in this post, I guarantee |
And we have the following user accounts:
| slug | name | id |
| search-for-me | Search for me | user-for-search |
@@ -23,10 +23,10 @@ Feature: Search
| title |
| 101 Essays that will change the way you think |
- Scenario: Press enter starts search
+ Scenario: Press enter opens search page
When I type "PR" and press Enter
- Then I should have one item in the select dropdown
- Then I should see the following posts in the select dropdown:
+ Then I should see the search results page
+ Then I should see the following posts on the search results page
| title |
| 101 Essays that will change the way you think |
diff --git a/deployment/helm/ocelot.social/values.yaml b/deployment/helm/ocelot.social/values.yaml
index 42eed0c7b..4c15c99a7 100644
--- a/deployment/helm/ocelot.social/values.yaml
+++ b/deployment/helm/ocelot.social/values.yaml
@@ -7,13 +7,13 @@ dbInitializion: "yarn prod:migrate init"
# dbMigrations runs the database migrations in a post-upgrade hook.
dbMigrations: "yarn prod:migrate up"
# bakendImage is the docker image for the backend deployment
-backendImage: ocelotsocialnetwork/develop-backend
+backendImage: ocelotsocialnetwork/backend
# maintenanceImage is the docker image for the maintenance deployment
-maintenanceImage: ocelotsocialnetwork/develop-maintenance
+maintenanceImage: ocelotsocialnetwork/maintenance
# neo4jImage is the docker image for the neo4j deployment
-neo4jImage: ocelotsocialnetwork/develop-neo4j
+neo4jImage: ocelotsocialnetwork/neo4j
# webappImage is the docker image for the webapp deployment
-webappImage: ocelotsocialnetwork/develop-webapp
+webappImage: ocelotsocialnetwork/webapp
# image configures pullPolicy related to the docker images
image:
# pullPolicy indicates when, if ever, pods pull a new image from docker hub.
diff --git a/deployment/legacy-migration/README.md b/deployment/legacy-migration/README.md
index b692305db..66100a3c8 100644
--- a/deployment/legacy-migration/README.md
+++ b/deployment/legacy-migration/README.md
@@ -43,13 +43,13 @@ Then temporarily delete backend and database deployments
```bash
$ kubectl -n ocelot-social get deployments
NAME READY UP-TO-DATE AVAILABLE AGE
-develop-backend 1/1 1 1 3d11h
-develop-neo4j 1/1 1 1 3d11h
-develop-webapp 2/2 2 2 73d
-$ kubectl -n ocelot-social delete deployment develop-neo4j
-deployment.extensions "develop-neo4j" deleted
-$ kubectl -n ocelot-social delete deployment develop-backend
-deployment.extensions "develop-backend" deleted
+backend 1/1 1 1 3d11h
+neo4j 1/1 1 1 3d11h
+webapp 2/2 2 2 73d
+$ kubectl -n ocelot-social delete deployment neo4j
+deployment.extensions "neo4j" deleted
+$ kubectl -n ocelot-social delete deployment backend
+deployment.extensions "backend" deleted
```
Deploy one-time develop-maintenance-worker pod:
diff --git a/deployment/minikube/README.md b/deployment/minikube/README.md
index cfa2c4a5c..014f9510c 100644
--- a/deployment/minikube/README.md
+++ b/deployment/minikube/README.md
@@ -18,8 +18,8 @@ minikube dashboard, expose the services you want on your host system.
For example:
```text
-$ minikube service develop-webapp --namespace=ocelotsocialnetwork
+$ minikube service webapp --namespace=ocelotsocialnetwork
# optionally
-$ minikube service develop-backend --namespace=ocelotsocialnetwork
+$ minikube service backend --namespace=ocelotsocialnetwork
```
diff --git a/deployment/ocelot-social/deployment-webapp.yaml b/deployment/ocelot-social/deployment-webapp.yaml
index 4b0fec2a1..2cc742deb 100644
--- a/deployment/ocelot-social/deployment-webapp.yaml
+++ b/deployment/ocelot-social/deployment-webapp.yaml
@@ -37,7 +37,7 @@ spec:
name: configmap
- secretRef:
name: ocelot-social
- image: ocelotsocialnetwork/develop-webapp:latest
+ image: ocelotsocialnetwork/webapp:latest
imagePullPolicy: Always
name: web
ports:
diff --git a/docker-compose.build-and-test.yml b/docker-compose.build-and-test.yml
deleted file mode 100644
index dbbb16d9b..000000000
--- a/docker-compose.build-and-test.yml
+++ /dev/null
@@ -1,17 +0,0 @@
-version: "3.4"
-
-services:
- webapp:
- environment:
- - "CI=${CI}"
- image: ocelotsocialnetwork/develop-webapp:build-and-test
- build:
- context: webapp
- target: build-and-test
- backend:
- environment:
- - "CI=${CI}"
- image: ocelotsocialnetwork/develop-backend:build-and-test
- build:
- context: backend
- target: build-and-test
diff --git a/docker-compose.override.yml b/docker-compose.override.yml
index dd38cacde..5c0280667 100644
--- a/docker-compose.override.yml
+++ b/docker-compose.override.yml
@@ -1,57 +1,68 @@
version: "3.4"
services:
+ ########################################################
+ # WEBAPP ###############################################
+ ########################################################
webapp:
- image: ocelotsocialnetwork/develop-webapp:build-and-test
+ image: ocelotsocialnetwork/webapp:development
build:
- context: webapp
- target: build-and-test
+ target: development
environment:
- - NUXT_BUILD=/tmp/nuxt # avoid file permission issues when `rm -rf .nuxt/`
- - PUBLIC_REGISTRATION=true
- command: yarn run dev
+ - NODE_ENV="development"
+ # - DEBUG=true
+ # - NUXT_BUILD=/tmp/nuxt # avoid file permission issues when `rm -rf .nuxt/`
volumes:
- - ./webapp:/develop-webapp
- - webapp_node_modules:/develop-webapp/node_modules
+ # This makes sure the docker container has its own node modules.
+ # Therefore it is possible to have a different node version on the host machine
+ - webapp_node_modules:/app/node_modules
+ # bind the local folder to the docker to allow live reload
+ - ./webapp:/app
+
+ ########################################################
+ # BACKEND ##############################################
+ ########################################################
backend:
- image: ocelotsocialnetwork/develop-backend:build-and-test
+ image: ocelotsocialnetwork/backend:development
build:
- context: backend
- target: build-and-test
- command: yarn run dev
+ target: development
environment:
- - SMTP_HOST=mailserver
- - SMTP_PORT=25
- - SMTP_IGNORE_TLS=true
- - "DEBUG=${DEBUG}"
- - PUBLIC_REGISTRATION=false
+ - NODE_ENV="development"
+ - DEBUG=true
volumes:
- - ./backend:/develop-backend
- - backend_node_modules:/develop-backend/node_modules
- - uploads:/develop-backend/public/uploads
+ # This makes sure the docker container has its own node modules.
+ # Therefore it is possible to have a different node version on the host machine
+ - backend_node_modules:/app/node_modules
+ # bind the local folder to the docker to allow live reload
+ - ./backend:/app
+
+ ########################################################
+ # NEO4J ################################################
+ ########################################################
neo4j:
- volumes:
- - neo4j_data:/data
- maintenance:
- image: ocelotsocialnetwork/develop-maintenance:latest
- build:
- context: webapp
- dockerfile: Dockerfile.maintenance
- networks:
- - hc-network
+ image: ocelotsocialnetwork/neo4j:development
ports:
- - 3503:80
+ # Also expose the neo4j query browser
+ - 7474:7474
+ networks:
+ # So we can access the neo4j query browser from our host machine
+ - external-net
+
+ ########################################################
+ # MAINTENANCE ##########################################
+ ########################################################
+ maintenance:
+ image: ocelotsocialnetwork/maintenance:development
+
+ ########################################################
+ # MAILSERVER TO FAKE SMTP ##############################
+ ########################################################
mailserver:
image: djfarrelly/maildev
ports:
- 1080:80
networks:
- - hc-network
-
-networks:
- hc-network:
+ - external-net
volumes:
webapp_node_modules:
backend_node_modules:
- neo4j_data:
- uploads:
diff --git a/docker-compose.production.yml b/docker-compose.production.yml
deleted file mode 100644
index 285e9f110..000000000
--- a/docker-compose.production.yml
+++ /dev/null
@@ -1,20 +0,0 @@
-version: "3.4"
-
-services:
- webapp:
- build:
- context: webapp
- target: production
- args:
- - "BUILD_COMMIT=${TRAVIS_COMMIT}"
- backend:
- build:
- context: backend
- target: production
- args:
- - "BUILD_COMMIT=${TRAVIS_COMMIT}"
- neo4j:
- build:
- context: neo4j
- args:
- - "BUILD_COMMIT=${TRAVIS_COMMIT}"
diff --git a/docker-compose.yml b/docker-compose.yml
index 5297bc399..b3d034621 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -1,75 +1,117 @@
+# This file defines the production settings. It is overwritten by docker-compose.override.yml,
+# which defines the development settings. The override.yml is loaded by default. Therefore it
+# is required to explicitly define if you want an production build:
+# > docker-compose -f docker-compose.yml up
+
version: "3.4"
services:
+ ########################################################
+ # WEBAPP ###############################################
+ ########################################################
webapp:
- image: ocelotsocialnetwork/develop-webapp:latest
+ image: ocelotsocialnetwork/webapp:latest
build:
- context: webapp
+ context: ./webapp
target: production
- args:
- - "BUILD_COMMIT=${TRAVIS_COMMIT}"
- ports:
- - 3000:3000
- - 3002:3002
networks:
- - hc-network
+ - external-net
depends_on:
- backend
- volumes:
- - ./webapp:/develop-webapp
- - webapp_node_modules:/develop-webapp/node_modules
+ ports:
+ - 3000:3000
+ # Storybook: Todo externalize, its not working anyways
+ # - 3002:3002
environment:
- - HOST=0.0.0.0
+ # Envs used in Dockerfile
+ # - DOCKER_WORKDIR="/app"
+ # - PORT="3000"
+ - BUILD_DATE
+ - BUILD_VERSION
+ - BUILD_COMMIT
+ - NODE_ENV="development"
+ # Application only envs
+ - HOST=0.0.0.0 # This is nuxt specific, alternative value is HOST=webapp
- GRAPHQL_URI=http://backend:4000
- - MAPBOX_TOKEN="pk.eyJ1IjoiYnVzZmFrdG9yIiwiYSI6ImNraDNiM3JxcDBhaWQydG1uczhpZWtpOW4ifQ.7TNRTO-o9aK1Y6MyW_Nd4g"
+ env_file:
+ - ./webapp/.env
+
+ ########################################################
+ # BACKEND ##############################################
+ ########################################################
backend:
- image: ocelotsocialnetwork/develop-backend:latest
+ image: ocelotsocialnetwork/backend:latest
build:
- context: backend
+ context: ./backend
target: production
- args:
- - "BUILD_COMMIT=${TRAVIS_COMMIT}"
networks:
- - hc-network
+ - external-net
+ - internal-net
depends_on:
- neo4j
ports:
- 4000:4000
volumes:
- - ./backend:/develop-backend
- - backend_node_modules:/develop-backend/node_modules
- - uploads:/develop-backend/public/uploads
+ - backend_uploads:/app/public/uploads
environment:
+ # Envs used in Dockerfile
+ # - DOCKER_WORKDIR="/app"
+ # - PORT="4000"
+ - BUILD_DATE
+ - BUILD_VERSION
+ - BUILD_COMMIT
+ - NODE_ENV="development"
+ # Application only envs
+ - DEBUG=false
- NEO4J_URI=bolt://neo4j:7687
- GRAPHQL_URI=http://backend:4000
- - CLIENT_URI=http://localhost:3000
- - JWT_SECRET=b/&&7b78BF&fv/Vd
- - MAPBOX_TOKEN=pk.eyJ1IjoiYnVzZmFrdG9yIiwiYSI6ImNraDNiM3JxcDBhaWQydG1uczhpZWtpOW4ifQ.7TNRTO-o9aK1Y6MyW_Nd4g
- - PRIVATE_KEY_PASSPHRASE=a7dsf78sadg87ad87sfagsadg78
- - "DEBUG=${DEBUG}"
- - EMAIL_DEFAULT_SENDER=devops@ocelot.social
+ - CLIENT_URI=http://webapp:3000
+ env_file:
+ - ./backend/.env
+
+ ########################################################
+ # NEO4J ################################################
+ ########################################################
neo4j:
- image: ocelotsocialnetwork/develop-neo4j:latest
+ image: ocelotsocialnetwork/neo4j:latest
build:
- context: neo4j
- args:
- - "BUILD_COMMIT=${TRAVIS_COMMIT}"
+ context: ./neo4j
+ # community edition 👆🏼, because we have no enterprise licence 👇🏼 at the moment
+ target: community
networks:
- - hc-network
- environment:
- - NEO4J_AUTH=none
- - NEO4J_dbms_security_procedures_unrestricted=algo.*,apoc.*
- # decomment following line for Neo4j Enterprice version instead of Community version
- # - NEO4J_ACCEPT_LICENSE_AGREEMENT=yes
+ - internal-net
ports:
- 7687:7687
- - 7474:7474
volumes:
- neo4j_data:/data
+ environment:
+ # TODO: This sounds scary for a production environment
+ - NEO4J_AUTH=none
+ - NEO4J_dbms_security_procedures_unrestricted=algo.*,apoc.*
+ # Uncomment following line for Neo4j Enterprise version instead of Community version
+ # TODO: clarify if that is the only thing needed to unlock the Enterprise version
+ # - NEO4J_ACCEPT_LICENSE_AGREEMENT=yes
+ # TODO: Remove the playground from production
+
+ ########################################################
+ # MAINTENANCE ##########################################
+ ########################################################
+ maintenance:
+ image: ocelotsocialnetwork/maintenance:latest
+ build:
+ # TODO: Separate from webapp, this must be independent
+ context: ./webapp
+ dockerfile: Dockerfile.maintenance
+ networks:
+ - external-net
+ ports:
+ - 5000:80
+
networks:
- hc-network:
+ external-net:
+ internal-net:
+ internal: true
+
volumes:
- webapp_node_modules:
- backend_node_modules:
+ backend_uploads:
neo4j_data:
- uploads:
\ No newline at end of file
diff --git a/docu/kubernetes.drawio b/docu/kubernetes.drawio
new file mode 100644
index 000000000..42ebba06c
--- /dev/null
+++ b/docu/kubernetes.drawio
@@ -0,0 +1,121 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/docu/kubernetes.png b/docu/kubernetes.png
new file mode 100644
index 000000000..29d6d0595
Binary files /dev/null and b/docu/kubernetes.png differ
diff --git a/installation.md b/installation.md
deleted file mode 100644
index 986166197..000000000
--- a/installation.md
+++ /dev/null
@@ -1,82 +0,0 @@
-# Installation
-
-The repository can be found on GitHub. [https://github.com/Ocelot-Social-Community/Ocelot-Social](https://github.com/Ocelot-Social-Community/Ocelot-Social)
-
-We give write permissions to every developer who asks for it. Just text us on
-[Discord](https://discord.gg/6ub73U3).
-
-## Clone the Repository
-
-
-Clone the repository, this will create a new folder called `Human-Connection`:
-
-{% tabs %}
-{% tab title="HTTPS" %}
-```bash
-$ git clone https://github.com/Ocelot-Social-Community/Ocelot-Social.git
-```
-{% endtab %}
-
-{% tab title="SSH" %}
-```bash
-$ git clone git@github.com:Human-Connection/Human-Connection.git
-```
-{% endtab %}
-{% endtabs %}
-
-Change into the new folder.
-
-```bash
-$ cd Human-Connection
-```
-
-## Directory Layout
-
-There are four important directories:
-* [Backend](./backend) runs on the server and is a middleware between database and frontend
-* [Frontend](./webapp) is a server-side-rendered and client-side-rendered web frontend
-* [Deployment](./deployment) configuration for kubernetes
-* [Cypress](./cypress) contains end-to-end tests and executable feature specifications
-
-In order to setup the application and start to develop features you have to
-setup **frontend** and **backend**.
-
-There are two approaches:
-
-1. Local installation, which means you have to take care of dependencies yourself
-2. **Or** Install everything through docker which takes care of dependencies for you
-
-## Docker Installation
-
-Docker is a software development container tool that combines software and its dependencies into one standardized unit that contains everything needed to run it. This helps us to avoid problems with dependencies and makes installation easier.
-
-### General Installation of Docker
-
-There are [sevaral ways to install Docker CE](https://docs.docker.com/install/) on your computer or server.
-
-{% tabs %}
-{% tab title="Docker Desktop macOS" %}
-Follow these instructions to [install Docker Desktop on macOS](https://docs.docker.com/docker-for-mac/install/).
-{% endtab %}
-
-{% tab title="Docker Desktop Windows" %}
-Follow these instructions to [install Docker Desktop on Windows](https://docs.docker.com/docker-for-windows/install/).
-{% endtab %}
-
-{% tab title="Docker CE" %}
-Follow these instructions to [install Docker CE](https://docs.docker.com/install/).
-
-This is a great option for Linux users.
-{% endtab %}
-{% endtabs %}
-
-Check the correct Docker installation by checking the version before proceeding. E.g. we have the following versions:
-
-```bash
-$ docker --version
-Docker version 18.09.2
-$ docker-compose --version
-docker-compose version 1.23.2
-```
-
-
diff --git a/neo4j/Dockerfile b/neo4j/Dockerfile
index e08e482a0..634674656 100644
--- a/neo4j/Dockerfile
+++ b/neo4j/Dockerfile
@@ -1,10 +1,43 @@
-FROM neo4j:3.5.14
-LABEL Description="Neo4J database of the Social Network ocelot.social with preinstalled database constraints and indices" Vendor="ocelot.social Community" Version="0.0.1" Maintainer="ocelot.social Community (devops@ocelot.social)"
-# community edition 👆🏼, because we have no enterprise licence 👇🏼 at the moment
-# FROM neo4j:3.5.14-enterprise
+##################################################################################
+# COMMUNITY ######################################################################
+##################################################################################
+FROM neo4j:3.5.14 as community
-ARG BUILD_COMMIT
-ENV BUILD_COMMIT=$BUILD_COMMIT
+# ENVs (available in production aswell, can be overwritten by commandline or env file)
+## We Cannot do `$(date -u +'%Y-%m-%dT%H:%M:%SZ')` here so we use unix timestamp=0
+ENV BUILD_DATE="1970-01-01T00:00:00.00Z"
+## We cannot do $(yarn run version) here so we default to 0.0.0
+## TODO: Missing Build number - do that once we have a CI which actually generates it
+ENV BUILD_VERSION="0.0.0"
+## We cannot do `$(git rev-parse --short HEAD)` here so we default to 0000000
+ENV BUILD_COMMIT="0000000"
+# Labels
+LABEL org.label-schema.build-date="${BUILD_DATE}"
+LABEL org.label-schema.name="ocelot.social:backend"
+LABEL org.label-schema.description="Neo4J database of the Social Network Software ocelot.social with preinstalled database constraints and indices"
+LABEL org.label-schema.usage="https://github.com/Ocelot-Social-Community/Ocelot-Social/blob/master/README.md"
+LABEL org.label-schema.url="https://ocelot.social"
+LABEL org.label-schema.vcs-url="https://github.com/Ocelot-Social-Community/Ocelot-Social/tree/master/backend"
+LABEL org.label-schema.vcs-ref="${BUILD_COMMIT}"
+LABEL org.label-schema.vendor="ocelot.social Community"
+LABEL org.label-schema.version="${BUILD_VERSION}"
+LABEL org.label-schema.schema-version="1.0"
+LABEL maintainer="devops@ocelot.social"
+
+# Install Additional Software
+## install: wget, htop (TODO: why do we need htop?)
RUN apt-get update && apt-get -y install wget htop
+## install: apoc plugin for neo4j
RUN wget https://github.com/neo4j-contrib/neo4j-apoc-procedures/releases/download/3.5.0.4/apoc-3.5.0.4-all.jar -P plugins/
+
+##################################################################################
+# ENTERPRISE #####################################################################
+##################################################################################
+FROM neo4j:3.5.14-enterprise as enterprise
+
+# Install Additional Software
+## install: wget, htop (TODO: why do we need htop?)
+RUN apt-get update && apt-get -y install wget htop
+## install: apoc plugin for neo4j
+RUN wget https://github.com/neo4j-contrib/neo4j-apoc-procedures/releases/download/3.5.0.4/apoc-3.5.0.4-all.jar -P plugins/
\ No newline at end of file
diff --git a/scripts/deploy.sh b/scripts/deploy.sh
index 77b5501ca..c79223c69 100755
--- a/scripts/deploy.sh
+++ b/scripts/deploy.sh
@@ -2,5 +2,5 @@
sed -i "s//${TRAVIS_COMMIT}/g" $TRAVIS_BUILD_DIR/scripts/patches/patch-deployment.yaml
sed -i "s//${TRAVIS_COMMIT}/g" $TRAVIS_BUILD_DIR/scripts/patches/patch-configmap.yaml
kubectl -n ocelot-social patch configmap develop-configmap -p "$(cat $TRAVIS_BUILD_DIR/scripts/patches/patch-configmap.yaml)"
-kubectl -n ocelot-social patch deployment develop-backend -p "$(cat $TRAVIS_BUILD_DIR/scripts/patches/patch-deployment.yaml)"
-kubectl -n ocelot-social patch deployment develop-webapp -p "$(cat $TRAVIS_BUILD_DIR/scripts/patches/patch-deployment.yaml)"
+kubectl -n ocelot-social patch deployment backend -p "$(cat $TRAVIS_BUILD_DIR/scripts/patches/patch-deployment.yaml)"
+kubectl -n ocelot-social patch deployment webapp -p "$(cat $TRAVIS_BUILD_DIR/scripts/patches/patch-deployment.yaml)"
diff --git a/scripts/docker_push.sh b/scripts/docker_push.sh
index b1ae8fbc0..90e0fb8c1 100755
--- a/scripts/docker_push.sh
+++ b/scripts/docker_push.sh
@@ -4,14 +4,14 @@ ROOT_DIR=$(dirname "$0")/..
VERSION=$(jq -r '.version' $ROOT_DIR/package.json)
IFS='.' read -r major minor patch <<< $VERSION
-apps=(develop-webapp develop-backend develop-neo4j develop-maintenance)
+apps=(webapp backend neo4j maintenance)
tags=($major $major.$minor $major.$minor.$patch)
# These three docker images have already been built by now:
-# docker build --build-arg BUILD_COMMIT=$BUILD_COMMIT --target production -t ocelotsocialnetwork/develop-backend:latest $ROOT_DIR/backend
-# docker build --build-arg BUILD_COMMIT=$BUILD_COMMIT --target production -t ocelotsocialnetwork/develop-webapp:latest $ROOT_DIR/webapp
-# docker build --build-arg BUILD_COMMIT=$BUILD_COMMIT -t ocelotsocialnetwork/develop-neo4j:latest $ROOT_DIR/neo4j
-docker build -t ocelotsocialnetwork/develop-maintenance:latest $ROOT_DIR/webapp/ -f $ROOT_DIR/webapp/Dockerfile.maintenance
+# docker build --build-arg BUILD_COMMIT=$BUILD_COMMIT --target production -t ocelotsocialnetwork/backend:latest $ROOT_DIR/backend
+# docker build --build-arg BUILD_COMMIT=$BUILD_COMMIT --target production -t ocelotsocialnetwork/webapp:latest $ROOT_DIR/webapp
+# docker build --build-arg BUILD_COMMIT=$BUILD_COMMIT -t ocelotsocialnetwork/neo4j:latest $ROOT_DIR/neo4j
+docker build -t ocelotsocialnetwork/maintenance:latest $ROOT_DIR/webapp/ -f $ROOT_DIR/webapp/Dockerfile.maintenance
echo "$DOCKER_PASSWORD" | docker login -u "$DOCKER_USERNAME" --password-stdin
diff --git a/webapp/Dockerfile b/webapp/Dockerfile
index b752299e6..a82e51f94 100644
--- a/webapp/Dockerfile
+++ b/webapp/Dockerfile
@@ -1,32 +1,89 @@
+##################################################################################
+# BASE ###########################################################################
+##################################################################################
FROM node:12.19.0-alpine3.10 as base
-LABEL Description="Web Frontend of the Social Network ocelot.social" Vendor="ocelot.social Community" Version="0.0.1" Maintainer="ocelot.social Community (devops@ocelot.social)"
-EXPOSE 3000
-CMD ["yarn", "run", "start"]
+# ENVs (available in production aswell, can be overwritten by commandline or env file)
+## DOCKER_WORKDIR would be a classical ARG, but that is not multi layer persistent - shame
+ENV DOCKER_WORKDIR="/app"
+## We Cannot do `$(date -u +'%Y-%m-%dT%H:%M:%SZ')` here so we use unix timestamp=0
+ENV BUILD_DATE="1970-01-01T00:00:00.00Z"
+## We cannot do $(yarn run version) here so we default to 0.0.0
+## TODO: Missing Build number - do that once we have a CI which actually generates it
+ENV BUILD_VERSION="0.0.0"
+## We cannot do `$(git rev-parse --short HEAD)` here so we default to 0000000
+ENV BUILD_COMMIT="0000000"
+## SET NODE_ENV
+ENV NODE_ENV="production"
+## App relevant Envs
+ENV PORT="3000"
-# Expose the app port
-ARG BUILD_COMMIT
-ENV BUILD_COMMIT=$BUILD_COMMIT
-ARG WORKDIR=/develop-webapp
-RUN mkdir -p $WORKDIR
-WORKDIR $WORKDIR
+# Labels
+LABEL org.label-schema.build-date="${BUILD_DATE}"
+LABEL org.label-schema.name="ocelot.social:backend"
+LABEL org.label-schema.description="Web Frontend of the Social Network Software ocelot.social"
+LABEL org.label-schema.usage="https://github.com/Ocelot-Social-Community/Ocelot-Social/blob/master/README.md"
+LABEL org.label-schema.url="https://ocelot.social"
+LABEL org.label-schema.vcs-url="https://github.com/Ocelot-Social-Community/Ocelot-Social/tree/master/backend"
+LABEL org.label-schema.vcs-ref="${BUILD_COMMIT}"
+LABEL org.label-schema.vendor="ocelot.social Community"
+LABEL org.label-schema.version="${BUILD_VERSION}"
+LABEL org.label-schema.schema-version="1.0"
+LABEL maintainer="devops@ocelot.social"
-# See: https://github.com/nodejs/docker-node/pull/367#issuecomment-430807898
+# Install Additional Software
+## install: git
RUN apk --no-cache add git
-COPY package.json yarn.lock ./
-COPY .env.template .env
+# Settings
+## Expose Container Port
+EXPOSE ${PORT}
+## Workdir
+RUN mkdir -p ${DOCKER_WORKDIR}
+WORKDIR ${DOCKER_WORKDIR}
-FROM base as build-and-test
-RUN yarn install --production=false --frozen-lockfile --non-interactive
+##################################################################################
+# DEVELOPMENT (Connected to the local environment, to reload on demand) ##########
+##################################################################################
+FROM base as development
+
+# We don't need to copy or build anything since we gonna bind to the
+# local filesystem which will need a rebuild anyway
+
+# Run command
+# (for development we need to execute yarn install since the
+# node_modules are on another volume and need updating)
+CMD /bin/sh -c "yarn install && yarn run dev"
+
+##################################################################################
+# BUILD (Does contain all files and is therefore bloated) ########################
+##################################################################################
+FROM base as build
+
+# Copy everything
COPY . .
-RUN NODE_ENV=production yarn run build
+# yarn install
+RUN yarn install --production=false --frozen-lockfile --non-interactive
+# yarn build
+RUN yarn run build
+##################################################################################
+# PRODUCTION (Does contain only "binary"- and static-files to reduce image size) #
+##################################################################################
FROM base as production
-RUN yarn install --production=true --frozen-lockfile --non-interactive --no-cache
-COPY --from=build-and-test ./develop-webapp/.nuxt ./.nuxt
-COPY --from=build-and-test ./develop-webapp/constants ./constants
-COPY --from=build-and-test ./develop-webapp/static ./static
-COPY nuxt.config.js .
-COPY locales locales
+
+# Copy "binary"-files from build image
+COPY --from=build ${DOCKER_WORKDIR}/.nuxt ./.nuxt
+COPY --from=build ${DOCKER_WORKDIR}/node_modules ./node_modules
+COPY --from=build ${DOCKER_WORKDIR}/nuxt.config.js ./nuxt.config.js
+# Copy static files
+# TODO - this should be one Folder containign all stuff needed to be copied
+COPY --from=build ${DOCKER_WORKDIR}/constants ./constants
+COPY --from=build ${DOCKER_WORKDIR}/static ./static
+COPY --from=build ${DOCKER_WORKDIR}/locales ./locales
+# Copy package.json for script definitions (lock file should not be needed)
+COPY --from=build ${DOCKER_WORKDIR}/package.json ./package.json
+
+# Run command
+CMD /bin/sh -c "yarn run start"
\ No newline at end of file
diff --git a/webapp/Dockerfile.maintenance b/webapp/Dockerfile.maintenance
index a688e0f82..b02fe352b 100644
--- a/webapp/Dockerfile.maintenance
+++ b/webapp/Dockerfile.maintenance
@@ -27,6 +27,7 @@ COPY plugins/i18n.js plugins/v-tooltip.js plugins/styleguide.js plugins/
COPY static static
COPY constants constants
COPY nuxt.config.js nuxt.config.js
+COPY config/ config/
# this will also ovewrite the existing package.json
COPY maintenance/source ./
diff --git a/webapp/assets/_new/styles/resets.scss b/webapp/assets/_new/styles/resets.scss
index 72a6184b3..4545634cc 100644
--- a/webapp/assets/_new/styles/resets.scss
+++ b/webapp/assets/_new/styles/resets.scss
@@ -16,13 +16,14 @@ h3,
h4,
h5,
h6,
-p {
+p,
+li {
margin: 0;
}
-ul,
-ol {
+ol,
+ul {
list-style-type: none;
- padding: 0;
margin: 0;
+ padding: 0;
}
diff --git a/webapp/assets/_new/styles/tokens.scss b/webapp/assets/_new/styles/tokens.scss
index 74699a097..5ab1e8aef 100644
--- a/webapp/assets/_new/styles/tokens.scss
+++ b/webapp/assets/_new/styles/tokens.scss
@@ -251,7 +251,7 @@ $size-ribbon: 6px;
*/
$size-width-filter-sidebar: 85px;
-$size-width-paginate: 100px;
+$size-width-paginate: 200px;
$size-max-width-filter-menu: 1026px;
/**
diff --git a/webapp/components/_new/features/SearchResults/SearchResults.spec.js b/webapp/components/_new/features/SearchResults/SearchResults.spec.js
new file mode 100644
index 000000000..b1886a754
--- /dev/null
+++ b/webapp/components/_new/features/SearchResults/SearchResults.spec.js
@@ -0,0 +1,239 @@
+import { config, mount } from '@vue/test-utils'
+import Vuex from 'vuex'
+import SearchResults from './SearchResults'
+import helpers from '~/storybook/helpers'
+
+helpers.init()
+
+const localVue = global.localVue
+
+localVue.directive('scrollTo', jest.fn())
+
+config.stubs['client-only'] = ''
+config.stubs['nuxt-link'] = ''
+
+describe('SearchResults', () => {
+ let mocks, getters, propsData, wrapper
+ const Wrapper = () => {
+ const store = new Vuex.Store({
+ getters,
+ })
+ return mount(SearchResults, { mocks, localVue, propsData, store })
+ }
+
+ beforeEach(() => {
+ mocks = {
+ $t: jest.fn(),
+ }
+ getters = {
+ 'auth/user': () => {
+ return { id: 'u343', name: 'Matt' }
+ },
+ 'auth/isModerator': () => false,
+ }
+ propsData = {
+ pageSize: 12,
+ }
+ wrapper = Wrapper()
+ })
+
+ describe('mount', () => {
+ it('renders tab-navigation component', () => {
+ expect(wrapper.find('.tab-navigation').exists()).toBe(true)
+ })
+
+ describe('searchResults', () => {
+ describe('contains no results', () => {
+ it('renders hc-empty component', () => {
+ expect(wrapper.find('.hc-empty').exists()).toBe(true)
+ })
+ })
+
+ describe('result contains 25 posts, 8 users and 0 hashtags', () => {
+ // we couldn't get it running with "jest.runAllTimers()" and so we used "setTimeout"
+ // time is a bit more then 3000 milisec see "webapp/components/CountTo.vue"
+ const counterTimeout = 3000 + 10
+
+ beforeEach(async () => {
+ wrapper.setData({
+ posts: helpers.fakePost(12),
+ postCount: 25,
+ users: helpers.fakeUser(8),
+ userCount: 8,
+ activeTab: 'Post',
+ })
+ })
+
+ it('shows a total of 33 results', () => {
+ setTimeout(() => {
+ expect(wrapper.find('.total-search-results').text()).toContain('33')
+ }, counterTimeout)
+ })
+
+ it('shows tab with 25 posts found', () => {
+ setTimeout(() => {
+ expect(wrapper.find('[data-test="Post-tab"]').text()).toContain('25')
+ }, counterTimeout)
+ })
+
+ it('shows tab with 8 users found', () => {
+ setTimeout(() => {
+ expect(wrapper.find('[data-test="User-tab"]').text()).toContain('8')
+ }, counterTimeout)
+ })
+
+ it('shows tab with 0 hashtags found', () => {
+ setTimeout(() => {
+ expect(wrapper.find('[data-test="Hashtag-tab"]').text()).toContain('0')
+ }, counterTimeout)
+ })
+
+ it('has post tab as active tab', () => {
+ expect(wrapper.find('[data-test="Post-tab"]').classes('--active')).toBe(true)
+ })
+
+ it('has user tab inactive', () => {
+ expect(wrapper.find('[data-test="User-tab"]').classes('--active')).toBe(false)
+ })
+
+ it('has hashtag tab disabled', () => {
+ expect(wrapper.find('[data-test="Hashtag-tab"]').classes('--disabled')).toBe(true)
+ })
+
+ it('displays 12 (pageSize) posts', () => {
+ expect(wrapper.findAll('.post-teaser')).toHaveLength(12)
+ })
+
+ it('has post tab inactive after emitting switch-tab', async () => {
+ wrapper.find('.tab-navigation').vm.$emit('switch-tab', 'User') // emits direct from tab component to search results
+ await wrapper.vm.$nextTick()
+ await expect(wrapper.find('[data-test="Post-tab"]').classes('--active')).toBe(false)
+ })
+
+ it('has post tab inactive after clicking on user tab', async () => {
+ wrapper.find('[data-test="User-tab-click"]').trigger('click')
+ await wrapper.vm.$nextTick()
+ await expect(wrapper.find('[data-test="Post-tab"]').classes('--active')).toBe(false)
+ })
+
+ it('has user tab active after clicking on user tab', async () => {
+ wrapper.find('[data-test="User-tab-click"]').trigger('click')
+ await wrapper.vm.$nextTick()
+ await expect(wrapper.find('[data-test="User-tab"]').classes('--active')).toBe(true)
+ })
+
+ it('displays 8 users after clicking on user tab', async () => {
+ wrapper.find('[data-test="User-tab-click"]').trigger('click')
+ await wrapper.vm.$nextTick()
+ await expect(wrapper.findAll('.user-teaser')).toHaveLength(8)
+ })
+
+ it('shows the pagination buttons for posts', () => {
+ expect(wrapper.find('.pagination-buttons').exists()).toBe(true)
+ })
+
+ it('shows no pagination buttons for users', async () => {
+ wrapper.find('[data-test="User-tab-click"]').trigger('click')
+ await wrapper.vm.$nextTick()
+ await expect(wrapper.find('.pagination-buttons').exists()).toBe(false)
+ })
+
+ it('displays page 1 of 3 for the 25 posts', () => {
+ expect(wrapper.find('.pagination-pageCount').text().replace(/\s+/g, ' ')).toContain(
+ '1 / 3',
+ )
+ })
+
+ it('displays the next page button for the 25 posts', () => {
+ expect(wrapper.find('.next-button').exists()).toBe(true)
+ })
+
+ it('deactivates previous page button for the 25 posts', () => {
+ const previousButton = wrapper.find('[data-test="previous-button"]')
+ expect(previousButton.attributes().disabled).toEqual('disabled')
+ })
+
+ it('displays page 2 / 3 when next-button is clicked', async () => {
+ wrapper.find('.next-button').trigger('click')
+ await wrapper.vm.$nextTick()
+ await expect(wrapper.find('.pagination-pageCount').text().replace(/\s+/g, ' ')).toContain(
+ '2 / 3',
+ )
+ })
+
+ it('sets apollo searchPosts offset to 12 when next-button is clicked', async () => {
+ wrapper.find('.next-button').trigger('click')
+ await wrapper.vm.$nextTick()
+ await expect(
+ wrapper.vm.$options.apollo.searchPosts.variables.bind(wrapper.vm)(),
+ ).toMatchObject({ query: undefined, firstPosts: 12, postsOffset: 12 })
+ })
+
+ it('displays the next page button when next-button is clicked', async () => {
+ wrapper.find('.next-button').trigger('click')
+ await wrapper.vm.$nextTick()
+ await expect(wrapper.find('.next-button').exists()).toBe(true)
+ })
+
+ it('displays the previous page button when next-button is clicked', async () => {
+ wrapper.find('.next-button').trigger('click')
+ await wrapper.vm.$nextTick()
+ await expect(wrapper.find('.previous-button').exists()).toBe(true)
+ })
+
+ it('displays page 3 / 3 when next-button is clicked twice', async () => {
+ wrapper.find('.next-button').trigger('click')
+ wrapper.find('.next-button').trigger('click')
+ await wrapper.vm.$nextTick()
+ await expect(wrapper.find('.pagination-pageCount').text().replace(/\s+/g, ' ')).toContain(
+ '3 / 3',
+ )
+ })
+
+ it('sets apollo searchPosts offset to 24 when next-button is clicked twice', async () => {
+ wrapper.find('.next-button').trigger('click')
+ wrapper.find('.next-button').trigger('click')
+ await wrapper.vm.$nextTick()
+ await expect(
+ wrapper.vm.$options.apollo.searchPosts.variables.bind(wrapper.vm)(),
+ ).toMatchObject({ query: undefined, firstPosts: 12, postsOffset: 24 })
+ })
+
+ it('deactivates next page button when next-button is clicked twice', async () => {
+ const nextButton = wrapper.find('[data-test="next-button"]')
+ nextButton.trigger('click')
+ nextButton.trigger('click')
+ await wrapper.vm.$nextTick()
+ expect(nextButton.attributes().disabled).toEqual('disabled')
+ })
+
+ it('displays the previous page button when next-button is clicked twice', async () => {
+ wrapper.find('.next-button').trigger('click')
+ wrapper.find('.next-button').trigger('click')
+ await wrapper.vm.$nextTick()
+ await expect(wrapper.find('.previous-button').exists()).toBe(true)
+ })
+
+ it('displays page 1 / 3 when previous-button is clicked after next-button', async () => {
+ wrapper.find('.next-button').trigger('click')
+ await wrapper.vm.$nextTick()
+ wrapper.find('.previous-button').trigger('click')
+ await wrapper.vm.$nextTick()
+ await expect(wrapper.find('.pagination-pageCount').text().replace(/\s+/g, ' ')).toContain(
+ '1 / 3',
+ )
+ })
+
+ it('sets apollo searchPosts offset to 0 when previous-button is clicked after next-button', async () => {
+ wrapper.find('.next-button').trigger('click')
+ await wrapper.vm.$nextTick()
+ wrapper.find('.previous-button').trigger('click')
+ await wrapper.vm.$nextTick()
+ await expect(
+ wrapper.vm.$options.apollo.searchPosts.variables.bind(wrapper.vm)(),
+ ).toMatchObject({ query: undefined, firstPosts: 12, postsOffset: 0 })
+ })
+ })
+ })
+ })
+})
diff --git a/webapp/components/_new/features/SearchResults/SearchResults.vue b/webapp/components/_new/features/SearchResults/SearchResults.vue
new file mode 100644
index 000000000..f3730f61d
--- /dev/null
+++ b/webapp/components/_new/features/SearchResults/SearchResults.vue
@@ -0,0 +1,374 @@
+
+
+
+
+
+
+
+
+ {{ $t('search.for') }}
+ {{ '"' + (search || '') + '"' }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/webapp/components/_new/generic/PaginationButtons/PaginationButtons.spec.js b/webapp/components/_new/generic/PaginationButtons/PaginationButtons.spec.js
index f214ba55e..03c66e345 100644
--- a/webapp/components/_new/generic/PaginationButtons/PaginationButtons.spec.js
+++ b/webapp/components/_new/generic/PaginationButtons/PaginationButtons.spec.js
@@ -5,62 +5,81 @@ import PaginationButtons from './PaginationButtons'
const localVue = global.localVue
describe('PaginationButtons.vue', () => {
- let propsData = {}
+ const propsData = {
+ showPageCounter: true,
+ activePage: 1,
+ activeResourceCount: 57,
+ }
let wrapper
- let nextButton
- let backButton
+ const mocks = {
+ $t: jest.fn(),
+ }
const Wrapper = () => {
- return mount(PaginationButtons, { propsData, localVue })
+ return mount(PaginationButtons, { mocks, propsData, localVue })
}
describe('mount', () => {
- describe('next button', () => {
- beforeEach(() => {
- propsData.hasNext = true
- wrapper = Wrapper()
- nextButton = wrapper.find('[data-test="next-button"]')
- })
+ beforeEach(() => {
+ wrapper = Wrapper()
+ })
+ describe('next button', () => {
it('is disabled by default', () => {
- propsData = {}
- wrapper = Wrapper()
- nextButton = wrapper.find('[data-test="next-button"]')
+ const nextButton = wrapper.find('[data-test="next-button"]')
expect(nextButton.attributes().disabled).toEqual('disabled')
})
- it('is enabled if hasNext is true', () => {
+ it('is enabled if hasNext is true', async () => {
+ wrapper.setProps({ hasNext: true })
+ await wrapper.vm.$nextTick()
+ const nextButton = wrapper.find('[data-test="next-button"]')
expect(nextButton.attributes().disabled).toBeUndefined()
})
it('emits next when clicked', async () => {
- await nextButton.trigger('click')
+ wrapper.setProps({ hasNext: true })
+ await wrapper.vm.$nextTick()
+ wrapper.find('[data-test="next-button"]').trigger('click')
+ await wrapper.vm.$nextTick()
expect(wrapper.emitted().next).toHaveLength(1)
})
})
- describe('back button', () => {
- beforeEach(() => {
- propsData.hasPrevious = true
- wrapper = Wrapper()
- backButton = wrapper.find('[data-test="previous-button"]')
- })
-
+ describe('previous button', () => {
it('is disabled by default', () => {
- propsData = {}
- wrapper = Wrapper()
- backButton = wrapper.find('[data-test="previous-button"]')
- expect(backButton.attributes().disabled).toEqual('disabled')
+ const previousButton = wrapper.find('[data-test="previous-button"]')
+ expect(previousButton.attributes().disabled).toEqual('disabled')
})
- it('is enabled if hasPrevious is true', () => {
- expect(backButton.attributes().disabled).toBeUndefined()
+ it('is enabled if hasPrevious is true', async () => {
+ wrapper.setProps({ hasPrevious: true })
+ await wrapper.vm.$nextTick()
+ const previousButton = wrapper.find('[data-test="previous-button"]')
+ expect(previousButton.attributes().disabled).toBeUndefined()
})
it('emits back when clicked', async () => {
- await backButton.trigger('click')
+ wrapper.setProps({ hasPrevious: true })
+ await wrapper.vm.$nextTick()
+ wrapper.find('[data-test="previous-button"]').trigger('click')
+ await wrapper.vm.$nextTick()
expect(wrapper.emitted().back).toHaveLength(1)
})
})
+
+ describe('page counter', () => {
+ it('displays the page counter when showPageCount is true', () => {
+ const paginationPageCount = wrapper.find('[data-test="pagination-pageCount"]')
+ expect(paginationPageCount.text().replace(/\s+/g, ' ')).toEqual('2 / 3')
+ })
+
+ it('does not display the page counter when showPageCount is false', async () => {
+ wrapper.setProps({ showPageCounter: false })
+ await wrapper.vm.$nextTick()
+ const paginationPageCount = wrapper.find('[data-test="pagination-pageCount"]')
+ expect(paginationPageCount.exists()).toEqual(false)
+ })
+ })
})
})
diff --git a/webapp/components/_new/generic/PaginationButtons/PaginationButtons.vue b/webapp/components/_new/generic/PaginationButtons/PaginationButtons.vue
index 592492f7d..b2ebe9139 100644
--- a/webapp/components/_new/generic/PaginationButtons/PaginationButtons.vue
+++ b/webapp/components/_new/generic/PaginationButtons/PaginationButtons.vue
@@ -1,18 +1,26 @@
@@ -20,12 +28,31 @@
+
+
diff --git a/webapp/components/_new/generic/TabNavigation/TabNavigator.story.js b/webapp/components/_new/generic/TabNavigation/TabNavigator.story.js
new file mode 100644
index 000000000..457e78209
--- /dev/null
+++ b/webapp/components/_new/generic/TabNavigation/TabNavigator.story.js
@@ -0,0 +1,188 @@
+import { storiesOf } from '@storybook/vue'
+import { withA11y } from '@storybook/addon-a11y'
+import HcEmpty from '~/components/Empty/Empty'
+import MasonryGrid from '~/components/MasonryGrid/MasonryGrid'
+import MasonryGridItem from '~/components/MasonryGrid/MasonryGridItem'
+import PostTeaser from '~/components/PostTeaser/PostTeaser'
+import TabNavigation from '~/components/_new/generic/TabNavigation/TabNavigation'
+import UserTeaser from '~/components/UserTeaser/UserTeaser'
+import HcHashtag from '~/components/Hashtag/Hashtag'
+import helpers from '~/storybook/helpers'
+import faker from 'faker'
+import { post } from '~/components/PostTeaser/PostTeaser.story.js'
+import { user } from '~/components/UserTeaser/UserTeaser.story.js'
+
+helpers.init()
+
+const postMock = (fields) => {
+ return {
+ ...post,
+ id: faker.random.uuid(),
+ createdAt: faker.date.past(),
+ updatedAt: faker.date.recent(),
+ deleted: false,
+ disabled: false,
+ typename: 'Post',
+ ...fields,
+ }
+}
+
+const userMock = (fields) => {
+ return {
+ ...user,
+ id: faker.random.uuid(),
+ createdAt: faker.date.past(),
+ updatedAt: faker.date.recent(),
+ deleted: false,
+ disabled: false,
+ typename: 'User',
+ ...fields,
+ }
+}
+
+const posts = [
+ postMock(),
+ postMock({ author: user }),
+ postMock({ title: faker.lorem.sentence() }),
+ postMock({ contentExcerpt: faker.lorem.paragraph() }),
+ postMock({ author: user }),
+ postMock({ title: faker.lorem.sentence() }),
+ postMock({ author: user }),
+]
+
+const users = [
+ userMock(),
+ userMock({ slug: 'louie-rider', name: 'Louie Rider' }),
+ userMock({ slug: 'louicinda-johnson', name: 'Louicinda Jonhson' }),
+ userMock({ slug: 'sam-louie', name: 'Sam Louie' }),
+ userMock({ slug: 'loucette', name: 'Loucette Rider' }),
+ userMock({ slug: 'louis', name: 'Louis' }),
+ userMock({ slug: 'louanna', name: 'Louanna' }),
+]
+
+storiesOf('TabNavigator', module)
+ .addDecorator(withA11y)
+ .addDecorator(helpers.layout)
+ .add('given search results of posts, users, hashtags', () => ({
+ components: {
+ TabNavigation,
+ HcEmpty,
+ MasonryGrid,
+ MasonryGridItem,
+ PostTeaser,
+ UserTeaser,
+ HcHashtag,
+ },
+ store: helpers.store,
+ data: () => ({
+ posts: posts,
+ users: users,
+ hashtags: [],
+
+ postCount: posts.length,
+ userCount: users.length,
+ hashtagCount: 0,
+
+ activeTab: 'Post',
+ }),
+ computed: {
+ activeResources() {
+ if (this.activeTab === 'Post') return this.posts
+ if (this.activeTab === 'User') return this.users
+ if (this.activeTab === 'Hashtag') return this.hashtags
+ return []
+ },
+ activeResourceCount() {
+ if (this.activeTab === 'Post') return this.postCount
+ if (this.activeTab === 'User') return this.userCount
+ if (this.activeTab === 'Hashtag') return this.hashtagCount
+ return 0
+ },
+ tabOptions() {
+ return [
+ {
+ type: 'Post',
+ title: this.$t('search.heading.Post', {}, this.postCount),
+ count: this.postCount,
+ disabled: this.postCount === 0,
+ },
+ {
+ type: 'User',
+ title: this.$t('search.heading.User', {}, this.userCount),
+ count: this.userCount,
+ disabled: this.userCount === 0,
+ },
+ {
+ type: 'Hashtag',
+ title: this.$t('search.heading.Tag', {}, this.hashtagCount),
+ count: this.hashtagCount,
+ disabled: this.hashtagCount === 0,
+ },
+ ]
+ },
+ searchCount() {
+ return this.postCount + this.userCount + this.hashtagCount
+ },
+ },
+ methods: {
+ switchTab(tabType) {
+ if (this.activeTab !== tabType) {
+ this.activeTab = tabType
+ }
+ },
+ },
+ template: `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ `,
+ }))
diff --git a/webapp/components/features/SearchField/SearchField.vue b/webapp/components/features/SearchField/SearchField.vue
index 29ab8650d..6aa7db865 100644
--- a/webapp/components/features/SearchField/SearchField.vue
+++ b/webapp/components/features/SearchField/SearchField.vue
@@ -9,7 +9,7 @@