mirror of
https://github.com/IT4Change/Ocelot-Social.git
synced 2025-12-13 07:45:56 +00:00
Merge master.
This commit is contained in:
commit
eb396424ef
10
.github/dependabot.yml
vendored
10
.github/dependabot.yml
vendored
@ -2,16 +2,16 @@ version: 2
|
|||||||
updates:
|
updates:
|
||||||
- package-ecosystem: "github-actions"
|
- package-ecosystem: "github-actions"
|
||||||
directory: "/"
|
directory: "/"
|
||||||
|
rebase-strategy: "disabled"
|
||||||
schedule:
|
schedule:
|
||||||
interval: weekly
|
interval: weekly
|
||||||
day: "saturday"
|
day: "saturday"
|
||||||
timezone: "Europe/Berlin"
|
timezone: "Europe/Berlin"
|
||||||
time: "03:00"
|
time: "03:00"
|
||||||
# open-pull-requests-limit: 10
|
|
||||||
# ignore:
|
|
||||||
|
|
||||||
- package-ecosystem: npm
|
- package-ecosystem: npm
|
||||||
directory: "/"
|
directory: "/"
|
||||||
|
rebase-strategy: "disabled"
|
||||||
schedule:
|
schedule:
|
||||||
interval: weekly
|
interval: weekly
|
||||||
day: "saturday"
|
day: "saturday"
|
||||||
@ -19,6 +19,7 @@ updates:
|
|||||||
time: "03:00"
|
time: "03:00"
|
||||||
- package-ecosystem: npm
|
- package-ecosystem: npm
|
||||||
directory: "/backend"
|
directory: "/backend"
|
||||||
|
rebase-strategy: "disabled"
|
||||||
schedule:
|
schedule:
|
||||||
interval: weekly
|
interval: weekly
|
||||||
day: "saturday"
|
day: "saturday"
|
||||||
@ -26,6 +27,7 @@ updates:
|
|||||||
time: "03:00"
|
time: "03:00"
|
||||||
- package-ecosystem: npm
|
- package-ecosystem: npm
|
||||||
directory: "/webapp"
|
directory: "/webapp"
|
||||||
|
rebase-strategy: "disabled"
|
||||||
schedule:
|
schedule:
|
||||||
interval: weekly
|
interval: weekly
|
||||||
day: "saturday"
|
day: "saturday"
|
||||||
@ -34,6 +36,7 @@ updates:
|
|||||||
|
|
||||||
- package-ecosystem: docker
|
- package-ecosystem: docker
|
||||||
directory: "/backend"
|
directory: "/backend"
|
||||||
|
rebase-strategy: "disabled"
|
||||||
schedule:
|
schedule:
|
||||||
interval: weekly
|
interval: weekly
|
||||||
day: "saturday"
|
day: "saturday"
|
||||||
@ -41,6 +44,7 @@ updates:
|
|||||||
time: "03:00"
|
time: "03:00"
|
||||||
- package-ecosystem: docker
|
- package-ecosystem: docker
|
||||||
directory: "/webapp"
|
directory: "/webapp"
|
||||||
|
rebase-strategy: "disabled"
|
||||||
schedule:
|
schedule:
|
||||||
interval: weekly
|
interval: weekly
|
||||||
day: "saturday"
|
day: "saturday"
|
||||||
@ -48,6 +52,7 @@ updates:
|
|||||||
time: "03:00"
|
time: "03:00"
|
||||||
- package-ecosystem: docker
|
- package-ecosystem: docker
|
||||||
directory: "/neo4j"
|
directory: "/neo4j"
|
||||||
|
rebase-strategy: "disabled"
|
||||||
schedule:
|
schedule:
|
||||||
interval: weekly
|
interval: weekly
|
||||||
day: "saturday"
|
day: "saturday"
|
||||||
@ -55,6 +60,7 @@ updates:
|
|||||||
time: "03:00"
|
time: "03:00"
|
||||||
- package-ecosystem: docker
|
- package-ecosystem: docker
|
||||||
directory: "/deployment/src/docker"
|
directory: "/deployment/src/docker"
|
||||||
|
rebase-strategy: "disabled"
|
||||||
schedule:
|
schedule:
|
||||||
interval: weekly
|
interval: weekly
|
||||||
day: "saturday"
|
day: "saturday"
|
||||||
|
|||||||
2
.github/workflows/lint_pr.yml
vendored
2
.github/workflows/lint_pr.yml
vendored
@ -1,7 +1,6 @@
|
|||||||
name: "ocelot.social lint pull request CI"
|
name: "ocelot.social lint pull request CI"
|
||||||
|
|
||||||
on:
|
on:
|
||||||
pull_request:
|
|
||||||
pull_request_target:
|
pull_request_target:
|
||||||
types:
|
types:
|
||||||
- opened
|
- opened
|
||||||
@ -12,6 +11,7 @@ jobs:
|
|||||||
main:
|
main:
|
||||||
name: Validate PR title
|
name: Validate PR title
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
if: ${{ github.actor != 'dependabot[bot]' }}
|
||||||
steps:
|
steps:
|
||||||
- uses: amannn/action-semantic-pull-request@v5
|
- uses: amannn/action-semantic-pull-request@v5
|
||||||
env:
|
env:
|
||||||
|
|||||||
24
.github/workflows/publish-branded.yml
vendored
24
.github/workflows/publish-branded.yml
vendored
@ -1,9 +1,7 @@
|
|||||||
name: ocelot.social publish branded CI
|
name: publish-branded
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
repository_dispatch:
|
||||||
branches:
|
types: [trigger-build-success]
|
||||||
- master
|
|
||||||
jobs:
|
jobs:
|
||||||
build_branded:
|
build_branded:
|
||||||
name: Docker Build Branded
|
name: Docker Build Branded
|
||||||
@ -11,7 +9,10 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
|
with:
|
||||||
|
ref: ${{ github.event.client_payload.ref }}
|
||||||
|
- name: Setup env
|
||||||
|
run: echo "OCELOT_VERSION=${{ github.event.client_payload.BUILD_VERSION }}" >> $GITHUB_ENV
|
||||||
- name: Build branded images
|
- name: Build branded images
|
||||||
run: |
|
run: |
|
||||||
deployment/scripts/branded-images.build.sh
|
deployment/scripts/branded-images.build.sh
|
||||||
@ -47,6 +48,8 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
|
with:
|
||||||
|
ref: ${{ github.event.client_payload.ref }}
|
||||||
|
|
||||||
- name: Download Docker Image (Backend)
|
- name: Download Docker Image (Backend)
|
||||||
uses: actions/download-artifact@v2
|
uses: actions/download-artifact@v2
|
||||||
@ -74,12 +77,3 @@ jobs:
|
|||||||
|
|
||||||
- name: Upload to dockerhub
|
- name: Upload to dockerhub
|
||||||
run: deployment/scripts/branded-images.upload.sh
|
run: deployment/scripts/branded-images.upload.sh
|
||||||
# - name: login to dockerhub
|
|
||||||
# run: echo "${DOCKERHUB_TOKEN}" | docker login -u "${DOCKERHUB_USERNAME}" --password-stdin
|
|
||||||
#
|
|
||||||
# - name: Push Backend
|
|
||||||
# run: docker push --all-tags ocelotsocialnetwork/backend-branded
|
|
||||||
# - name: Push Webapp
|
|
||||||
# run: docker push --all-tags ocelotsocialnetwork/webapp-branded
|
|
||||||
# - name: Push Maintenance
|
|
||||||
# run: docker push --all-tags ocelotsocialnetwork/maintenance-branded
|
|
||||||
471
.github/workflows/publish.yml
vendored
471
.github/workflows/publish.yml
vendored
@ -1,64 +1,36 @@
|
|||||||
name: ocelot.social publish CI
|
name: publish
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- master
|
- master
|
||||||
# - 5059-epic-groups # for testing while developing
|
|
||||||
# template branches in repo
|
|
||||||
# - template--separate-branch-auto-deployment--5059-epic-groups
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
##############################################################################
|
|
||||||
# JOB: PREPARE ###############################################################
|
|
||||||
##############################################################################
|
|
||||||
prepare:
|
|
||||||
name: Prepare
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
# needs: [nothing]
|
|
||||||
steps:
|
|
||||||
##########################################################################
|
|
||||||
# CHECKOUT CODE ##########################################################
|
|
||||||
##########################################################################
|
|
||||||
- name: Checkout code
|
|
||||||
uses: actions/checkout@v3
|
|
||||||
##########################################################################
|
|
||||||
# TODO: DO STUFF ??? #####################################################
|
|
||||||
##########################################################################
|
|
||||||
- name: Check translation files
|
|
||||||
run: |
|
|
||||||
scripts/translations/sort.sh
|
|
||||||
scripts/translations/missing-keys.sh
|
|
||||||
|
|
||||||
##############################################################################
|
##############################################################################
|
||||||
# JOB: DOCKER BUILD COMMUNITY NEO4J ##########################################
|
# JOB: DOCKER BUILD COMMUNITY NEO4J ##########################################
|
||||||
##############################################################################
|
##############################################################################
|
||||||
build_production_neo4j:
|
build_production_neo4j:
|
||||||
name: Docker Build Production - Neo4J
|
name: Docker Build Production - Neo4J
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs: [prepare]
|
|
||||||
steps:
|
steps:
|
||||||
##########################################################################
|
|
||||||
# CHECKOUT CODE ##########################################################
|
|
||||||
##########################################################################
|
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v3
|
||||||
##########################################################################
|
- name: Setup env
|
||||||
# SET ENVS ###############################################################
|
run: |
|
||||||
##########################################################################
|
echo "VERSION=$(node -p -e "require('./package.json').version")" >> $GITHUB_ENV
|
||||||
- name: ENV - VERSION
|
echo "BUILD_DATE=$(date -u +'%Y-%m-%dT%H:%M:%SZ')" >> $GITHUB_ENV
|
||||||
run: echo "VERSION=$(node -p -e "require('./package.json').version")" >> $GITHUB_ENV
|
echo "BUILD_COMMIT=${GITHUB_SHA}" >> $GITHUB_ENV
|
||||||
- name: ENV - BUILD_DATE
|
- run: echo "BUILD_VERSION=${VERSION}-${GITHUB_RUN_NUMBER}" >> $GITHUB_ENV
|
||||||
run: echo "BUILD_DATE=$(date -u +'%Y-%m-%dT%H:%M:%SZ')" >> $GITHUB_ENV
|
|
||||||
- name: ENV - BUILD_VERSION
|
|
||||||
run: echo "BUILD_VERSION=${VERSION}-${GITHUB_RUN_NUMBER}" >> $GITHUB_ENV
|
|
||||||
- name: ENV - BUILD_COMMIT
|
|
||||||
run: echo "BUILD_COMMIT=${GITHUB_SHA}" >> $GITHUB_ENV
|
|
||||||
##########################################################################
|
|
||||||
# NEO4J ##################################################################
|
|
||||||
##########################################################################
|
|
||||||
- name: Neo4J | Build `community` image
|
- name: Neo4J | Build `community` image
|
||||||
run: docker build --target community -t "ocelotsocialnetwork/neo4j-community:latest" -t "ocelotsocialnetwork/neo4j-community:${VERSION}" -t "ocelotsocialnetwork/neo4j-community:${BUILD_VERSION}" --build-arg BBUILD_DATE=$BUILD_DATE --build-arg BBUILD_VERSION=$BUILD_VERSION --build-arg BBUILD_COMMIT=$BUILD_COMMIT neo4j/
|
run: |
|
||||||
|
docker build --target community \
|
||||||
|
--tag "ocelotsocialnetwork/neo4j-community:latest" \
|
||||||
|
--tag "ocelotsocialnetwork/neo4j-community:${VERSION}" \
|
||||||
|
--tag "ocelotsocialnetwork/neo4j-community:${BUILD_VERSION}" \
|
||||||
|
--build-arg BBUILD_DATE=$BUILD_DATE \
|
||||||
|
--build-arg BBUILD_VERSION=$BUILD_VERSION \
|
||||||
|
--build-arg BBUILD_COMMIT=$BUILD_COMMIT \
|
||||||
|
neo4j/
|
||||||
- name: Neo4J | Save docker image
|
- name: Neo4J | Save docker image
|
||||||
run: docker save "ocelotsocialnetwork/neo4j-community" > /tmp/neo4j.tar
|
run: docker save "ocelotsocialnetwork/neo4j-community" > /tmp/neo4j.tar
|
||||||
- name: Upload Artifact
|
- name: Upload Artifact
|
||||||
@ -73,32 +45,41 @@ jobs:
|
|||||||
build_production_backend:
|
build_production_backend:
|
||||||
name: Docker Build Production - Backend
|
name: Docker Build Production - Backend
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs: [prepare]
|
|
||||||
steps:
|
steps:
|
||||||
##########################################################################
|
|
||||||
# CHECKOUT CODE ##########################################################
|
|
||||||
##########################################################################
|
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v3
|
||||||
##########################################################################
|
- name: Setup env
|
||||||
# SET ENVS ###############################################################
|
run: |
|
||||||
##########################################################################
|
echo "VERSION=$(node -p -e "require('./package.json').version")" >> $GITHUB_ENV
|
||||||
- name: ENV - VERSION
|
echo "BUILD_DATE=$(date -u +'%Y-%m-%dT%H:%M:%SZ')" >> $GITHUB_ENV
|
||||||
run: echo "VERSION=$(node -p -e "require('./package.json').version")" >> $GITHUB_ENV
|
echo "BUILD_COMMIT=${GITHUB_SHA}" >> $GITHUB_ENV
|
||||||
- name: ENV - BUILD_DATE
|
- run: echo "BUILD_VERSION=${VERSION}-${GITHUB_RUN_NUMBER}" >> $GITHUB_ENV
|
||||||
run: echo "BUILD_DATE=$(date -u +'%Y-%m-%dT%H:%M:%SZ')" >> $GITHUB_ENV
|
|
||||||
- name: ENV - BUILD_VERSION
|
|
||||||
run: echo "BUILD_VERSION=${VERSION}-${GITHUB_RUN_NUMBER}" >> $GITHUB_ENV
|
|
||||||
- name: ENV - BUILD_COMMIT
|
|
||||||
run: echo "BUILD_COMMIT=${GITHUB_SHA}" >> $GITHUB_ENV
|
|
||||||
##########################################################################
|
|
||||||
# BUILD BACKEND DOCKER IMAGE (production) ################################
|
|
||||||
##########################################################################
|
|
||||||
- name: Backend | Build `production` image
|
- name: Backend | Build `production` image
|
||||||
run: |
|
run: |
|
||||||
docker build --target base -t "ocelotsocialnetwork/backend:latest-base" -t "ocelotsocialnetwork/backend:${VERSION}-base" -t "ocelotsocialnetwork/backend:${BUILD_VERSION}-base" --build-arg BBUILD_DATE=$BUILD_DATE --build-arg BBUILD_VERSION=$BUILD_VERSION --build-arg BBUILD_COMMIT=$BUILD_COMMIT backend/
|
docker build --target base \
|
||||||
docker build --target code -t "ocelotsocialnetwork/backend:latest-code" -t "ocelotsocialnetwork/backend:${VERSION}-code" -t "ocelotsocialnetwork/backend:${BUILD_VERSION}-code" --build-arg BBUILD_DATE=$BUILD_DATE --build-arg BBUILD_VERSION=$BUILD_VERSION --build-arg BBUILD_COMMIT=$BUILD_COMMIT backend/
|
--tag "ocelotsocialnetwork/backend:latest-base" \
|
||||||
docker build --target production -t "ocelotsocialnetwork/backend:latest" -t "ocelotsocialnetwork/backend:${VERSION}" -t "ocelotsocialnetwork/backend:${BUILD_VERSION}" --build-arg BBUILD_DATE=$BUILD_DATE --build-arg BBUILD_VERSION=$BUILD_VERSION --build-arg BBUILD_COMMIT=$BUILD_COMMIT backend/
|
--tag "ocelotsocialnetwork/backend:${VERSION}-base" \
|
||||||
|
--tag "ocelotsocialnetwork/backend:${BUILD_VERSION}-base" \
|
||||||
|
--build-arg BBUILD_DATE=$BUILD_DATE \
|
||||||
|
--build-arg BBUILD_VERSION=$BUILD_VERSION \
|
||||||
|
--build-arg BBUILD_COMMIT=$BUILD_COMMIT \
|
||||||
|
backend/
|
||||||
|
docker build --target code \
|
||||||
|
--tag "ocelotsocialnetwork/backend:latest-code" \
|
||||||
|
--tag "ocelotsocialnetwork/backend:${VERSION}-code" \
|
||||||
|
--tag "ocelotsocialnetwork/backend:${BUILD_VERSION}-code" \
|
||||||
|
--build-arg BBUILD_DATE=$BUILD_DATE \
|
||||||
|
--build-arg BBUILD_VERSION=$BUILD_VERSION \
|
||||||
|
--build-arg BBUILD_COMMIT=$BUILD_COMMIT \
|
||||||
|
backend/
|
||||||
|
docker build --target production \
|
||||||
|
--tag "ocelotsocialnetwork/backend:latest" \
|
||||||
|
--tag "ocelotsocialnetwork/backend:${VERSION}" \
|
||||||
|
--tag "ocelotsocialnetwork/backend:${BUILD_VERSION}" \
|
||||||
|
--build-arg BBUILD_DATE=$BUILD_DATE \
|
||||||
|
--build-arg BBUILD_VERSION=$BUILD_VERSION \
|
||||||
|
--build-arg BBUILD_COMMIT=$BUILD_COMMIT \
|
||||||
|
backend/
|
||||||
- name: Backend | Save docker image
|
- name: Backend | Save docker image
|
||||||
run: docker save "ocelotsocialnetwork/backend" > /tmp/backend.tar
|
run: docker save "ocelotsocialnetwork/backend" > /tmp/backend.tar
|
||||||
- name: Upload Artifact
|
- name: Upload Artifact
|
||||||
@ -113,32 +94,41 @@ jobs:
|
|||||||
build_production_webapp:
|
build_production_webapp:
|
||||||
name: Docker Build Production - WebApp
|
name: Docker Build Production - WebApp
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs: [prepare]
|
|
||||||
steps:
|
steps:
|
||||||
##########################################################################
|
|
||||||
# CHECKOUT CODE ##########################################################
|
|
||||||
##########################################################################
|
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v3
|
||||||
##########################################################################
|
- name: Setup env
|
||||||
# SET ENVS ###############################################################
|
run: |
|
||||||
##########################################################################
|
echo "VERSION=$(node -p -e "require('./package.json').version")" >> $GITHUB_ENV
|
||||||
- name: ENV - VERSION
|
echo "BUILD_DATE=$(date -u +'%Y-%m-%dT%H:%M:%SZ')" >> $GITHUB_ENV
|
||||||
run: echo "VERSION=$(node -p -e "require('./package.json').version")" >> $GITHUB_ENV
|
echo "BUILD_COMMIT=${GITHUB_SHA}" >> $GITHUB_ENV
|
||||||
- name: ENV - BUILD_DATE
|
- run: echo "BUILD_VERSION=${VERSION}-${GITHUB_RUN_NUMBER}" >> $GITHUB_ENV
|
||||||
run: echo "BUILD_DATE=$(date -u +'%Y-%m-%dT%H:%M:%SZ')" >> $GITHUB_ENV
|
|
||||||
- name: ENV - BUILD_VERSION
|
|
||||||
run: echo "BUILD_VERSION=${VERSION}-${GITHUB_RUN_NUMBER}" >> $GITHUB_ENV
|
|
||||||
- name: ENV - BUILD_COMMIT
|
|
||||||
run: echo "BUILD_COMMIT=${GITHUB_SHA}" >> $GITHUB_ENV
|
|
||||||
##########################################################################
|
|
||||||
# BUILD WEBAPP DOCKER IMAGE (build) ######################################
|
|
||||||
##########################################################################
|
|
||||||
- name: Webapp | Build `production` image
|
- name: Webapp | Build `production` image
|
||||||
run: |
|
run: |
|
||||||
docker build --target base -t "ocelotsocialnetwork/webapp:latest-base" -t "ocelotsocialnetwork/webapp:${VERSION}-base" -t "ocelotsocialnetwork/webapp:${BUILD_VERSION}-base" --build-arg BBUILD_DATE=$BUILD_DATE --build-arg BBUILD_VERSION=$BUILD_VERSION --build-arg BBUILD_COMMIT=$BUILD_COMMIT webapp/
|
docker build --target base \
|
||||||
docker build --target code -t "ocelotsocialnetwork/webapp:latest-code" -t "ocelotsocialnetwork/webapp:${VERSION}-code" -t "ocelotsocialnetwork/webapp:${BUILD_VERSION}-code" --build-arg BBUILD_DATE=$BUILD_DATE --build-arg BBUILD_VERSION=$BUILD_VERSION --build-arg BBUILD_COMMIT=$BUILD_COMMIT webapp/
|
--tag "ocelotsocialnetwork/webapp:latest-base" \
|
||||||
docker build --target production -t "ocelotsocialnetwork/webapp:latest" -t "ocelotsocialnetwork/webapp:${VERSION}" -t "ocelotsocialnetwork/webapp:${BUILD_VERSION}" --build-arg BBUILD_DATE=$BUILD_DATE --build-arg BBUILD_VERSION=$BUILD_VERSION --build-arg BBUILD_COMMIT=$BUILD_COMMIT webapp/
|
--tag "ocelotsocialnetwork/webapp:${VERSION}-base" \
|
||||||
|
--tag "ocelotsocialnetwork/webapp:${BUILD_VERSION}-base" \
|
||||||
|
--build-arg BBUILD_DATE=$BUILD_DATE \
|
||||||
|
--build-arg BBUILD_VERSION=$BUILD_VERSION \
|
||||||
|
--build-arg BBUILD_COMMIT=$BUILD_COMMIT \
|
||||||
|
webapp/
|
||||||
|
docker build --target code \
|
||||||
|
--tag "ocelotsocialnetwork/webapp:latest-code" \
|
||||||
|
--tag "ocelotsocialnetwork/webapp:${VERSION}-code" \
|
||||||
|
--tag "ocelotsocialnetwork/webapp:${BUILD_VERSION}-code" \
|
||||||
|
--build-arg BBUILD_DATE=$BUILD_DATE \
|
||||||
|
--build-arg BBUILD_VERSION=$BUILD_VERSION \
|
||||||
|
--build-arg BBUILD_COMMIT=$BUILD_COMMIT \
|
||||||
|
webapp/
|
||||||
|
docker build --target production \
|
||||||
|
--tag "ocelotsocialnetwork/webapp:latest" \
|
||||||
|
--tag "ocelotsocialnetwork/webapp:${VERSION}" \
|
||||||
|
--tag "ocelotsocialnetwork/webapp:${BUILD_VERSION}" \
|
||||||
|
--build-arg BBUILD_DATE=$BUILD_DATE \
|
||||||
|
--build-arg BBUILD_VERSION=$BUILD_VERSION \
|
||||||
|
--build-arg BBUILD_COMMIT=$BUILD_COMMIT \
|
||||||
|
webapp/
|
||||||
- name: Webapp | Save docker image
|
- name: Webapp | Save docker image
|
||||||
run: docker save "ocelotsocialnetwork/webapp" > /tmp/webapp.tar
|
run: docker save "ocelotsocialnetwork/webapp" > /tmp/webapp.tar
|
||||||
- name: Upload Artifact
|
- name: Upload Artifact
|
||||||
@ -153,32 +143,44 @@ jobs:
|
|||||||
build_production_maintenance:
|
build_production_maintenance:
|
||||||
name: Docker Build Production - Maintenance
|
name: Docker Build Production - Maintenance
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs: [prepare]
|
|
||||||
steps:
|
steps:
|
||||||
##########################################################################
|
|
||||||
# CHECKOUT CODE ##########################################################
|
|
||||||
##########################################################################
|
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v3
|
||||||
##########################################################################
|
- name: Setup env
|
||||||
# SET ENVS ###############################################################
|
run: |
|
||||||
##########################################################################
|
echo "VERSION=$(node -p -e "require('./package.json').version")" >> $GITHUB_ENV
|
||||||
- name: ENV - VERSION
|
echo "BUILD_DATE=$(date -u +'%Y-%m-%dT%H:%M:%SZ')" >> $GITHUB_ENV
|
||||||
run: echo "VERSION=$(node -p -e "require('./package.json').version")" >> $GITHUB_ENV
|
echo "BUILD_COMMIT=${GITHUB_SHA}" >> $GITHUB_ENV
|
||||||
- name: ENV - BUILD_DATE
|
- run: echo "BUILD_VERSION=${VERSION}-${GITHUB_RUN_NUMBER}" >> $GITHUB_ENV
|
||||||
run: echo "BUILD_DATE=$(date -u +'%Y-%m-%dT%H:%M:%SZ')" >> $GITHUB_ENV
|
|
||||||
- name: ENV - BUILD_VERSION
|
|
||||||
run: echo "BUILD_VERSION=${VERSION}-${GITHUB_RUN_NUMBER}" >> $GITHUB_ENV
|
|
||||||
- name: ENV - BUILD_COMMIT
|
|
||||||
run: echo "BUILD_COMMIT=${GITHUB_SHA}" >> $GITHUB_ENV
|
|
||||||
##########################################################################
|
|
||||||
# BUILD MAINTENANCE DOCKER IMAGE (build) #################################
|
|
||||||
##########################################################################
|
|
||||||
- name: Maintenance | Build `production` image
|
- name: Maintenance | Build `production` image
|
||||||
run: |
|
run: |
|
||||||
docker build --target base -t "ocelotsocialnetwork/maintenance:latest-base" -t "ocelotsocialnetwork/maintenance:${VERSION}-base" -t "ocelotsocialnetwork/maintenance:${BUILD_VERSION}-base" --build-arg BBUILD_DATE=$BUILD_DATE --build-arg BBUILD_VERSION=$BUILD_VERSION --build-arg BBUILD_COMMIT=$BUILD_COMMIT webapp/ -f webapp/Dockerfile.maintenance
|
docker build --target base \
|
||||||
docker build --target code -t "ocelotsocialnetwork/maintenance:latest-code" -t "ocelotsocialnetwork/maintenance:${VERSION}-code" -t "ocelotsocialnetwork/maintenance:${BUILD_VERSION}-code" --build-arg BBUILD_DATE=$BUILD_DATE --build-arg BBUILD_VERSION=$BUILD_VERSION --build-arg BBUILD_COMMIT=$BUILD_COMMIT webapp/ -f webapp/Dockerfile.maintenance
|
--tag "ocelotsocialnetwork/maintenance:latest-base" \
|
||||||
docker build --target production -t "ocelotsocialnetwork/maintenance:latest" -t "ocelotsocialnetwork/maintenance:${VERSION}" -t "ocelotsocialnetwork/maintenance:${BUILD_VERSION}" --build-arg BBUILD_DATE=$BUILD_DATE --build-arg BBUILD_VERSION=$BUILD_VERSION --build-arg BBUILD_COMMIT=$BUILD_COMMIT webapp/ -f webapp/Dockerfile.maintenance
|
--tag "ocelotsocialnetwork/maintenance:${VERSION}-base" \
|
||||||
|
--tag "ocelotsocialnetwork/maintenance:${BUILD_VERSION}-base" \
|
||||||
|
--build-arg BBUILD_DATE=$BUILD_DATE \
|
||||||
|
--build-arg BBUILD_VERSION=$BUILD_VERSION \
|
||||||
|
--build-arg BBUILD_COMMIT=$BUILD_COMMIT \
|
||||||
|
-f webapp/Dockerfile.maintenance \
|
||||||
|
webapp/
|
||||||
|
docker build --target code \
|
||||||
|
--tag "ocelotsocialnetwork/maintenance:latest-code" \
|
||||||
|
--tag "ocelotsocialnetwork/maintenance:${VERSION}-code" \
|
||||||
|
--tag "ocelotsocialnetwork/maintenance:${BUILD_VERSION}-code" \
|
||||||
|
--build-arg BBUILD_DATE=$BUILD_DATE \
|
||||||
|
--build-arg BBUILD_VERSION=$BUILD_VERSION \
|
||||||
|
--build-arg BBUILD_COMMIT=$BUILD_COMMIT \
|
||||||
|
-f webapp/Dockerfile.maintenance \
|
||||||
|
webapp/
|
||||||
|
docker build --target production \
|
||||||
|
--tag "ocelotsocialnetwork/maintenance:latest" \
|
||||||
|
--tag "ocelotsocialnetwork/maintenance:${VERSION}" \
|
||||||
|
--tag "ocelotsocialnetwork/maintenance:${BUILD_VERSION}" \
|
||||||
|
--build-arg BBUILD_DATE=$BUILD_DATE \
|
||||||
|
--build-arg BBUILD_VERSION=$BUILD_VERSION \
|
||||||
|
--build-arg BBUILD_COMMIT=$BUILD_COMMIT \
|
||||||
|
-f webapp/Dockerfile.maintenance \
|
||||||
|
webapp/
|
||||||
- name: Maintenance | Save docker image
|
- name: Maintenance | Save docker image
|
||||||
run: docker save "ocelotsocialnetwork/maintenance" > /tmp/maintenance.tar
|
run: docker save "ocelotsocialnetwork/maintenance" > /tmp/maintenance.tar
|
||||||
- name: Upload Artifact
|
- name: Upload Artifact
|
||||||
@ -198,130 +200,39 @@ jobs:
|
|||||||
DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
|
DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }}
|
DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
steps:
|
steps:
|
||||||
##########################################################################
|
|
||||||
# CHECKOUT CODE ##########################################################
|
|
||||||
##########################################################################
|
|
||||||
- name: Checkout code
|
|
||||||
uses: actions/checkout@v3
|
|
||||||
##########################################################################
|
|
||||||
# DOWNLOAD DOCKER IMAGES #################################################
|
|
||||||
##########################################################################
|
|
||||||
- name: Download Docker Image (Neo4J)
|
- name: Download Docker Image (Neo4J)
|
||||||
uses: actions/download-artifact@v3
|
uses: actions/download-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: docker-neo4j-community
|
name: docker-neo4j-community
|
||||||
path: /tmp
|
path: /tmp
|
||||||
- name: Load Docker Image
|
- run: docker load < /tmp/neo4j.tar
|
||||||
run: docker load < /tmp/neo4j.tar
|
|
||||||
- name: Download Docker Image (Backend)
|
- name: Download Docker Image (Backend)
|
||||||
uses: actions/download-artifact@v3
|
uses: actions/download-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: docker-backend-production
|
name: docker-backend-production
|
||||||
path: /tmp
|
path: /tmp
|
||||||
- name: Load Docker Image
|
- run: docker load < /tmp/backend.tar
|
||||||
run: docker load < /tmp/backend.tar
|
|
||||||
- name: Download Docker Image (WebApp)
|
- name: Download Docker Image (WebApp)
|
||||||
uses: actions/download-artifact@v3
|
uses: actions/download-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: docker-webapp-production
|
name: docker-webapp-production
|
||||||
path: /tmp
|
path: /tmp
|
||||||
- name: Load Docker Image
|
- run: docker load < /tmp/webapp.tar
|
||||||
run: docker load < /tmp/webapp.tar
|
|
||||||
- name: Download Docker Image (Maintenance)
|
- name: Download Docker Image (Maintenance)
|
||||||
uses: actions/download-artifact@v3
|
uses: actions/download-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: docker-maintenance-production
|
name: docker-maintenance-production
|
||||||
path: /tmp
|
path: /tmp
|
||||||
- name: Load Docker Image
|
- run: docker load < /tmp/maintenance.tar
|
||||||
run: docker load < /tmp/maintenance.tar
|
|
||||||
##########################################################################
|
|
||||||
# Upload #################################################################
|
|
||||||
##########################################################################
|
|
||||||
- name: login to dockerhub
|
- name: login to dockerhub
|
||||||
run: echo "${DOCKERHUB_TOKEN}" | docker login -u "${DOCKERHUB_USERNAME}" --password-stdin
|
run: echo "${DOCKERHUB_TOKEN}" | docker login -u "${DOCKERHUB_USERNAME}" --password-stdin
|
||||||
- name: Push neo4j
|
- name: Push images
|
||||||
run: docker push --all-tags ocelotsocialnetwork/neo4j-community
|
|
||||||
- name: Push backend
|
|
||||||
run: docker push --all-tags ocelotsocialnetwork/backend
|
|
||||||
- name: Push webapp
|
|
||||||
run: docker push --all-tags ocelotsocialnetwork/webapp
|
|
||||||
- name: Push maintenance
|
|
||||||
run: docker push --all-tags ocelotsocialnetwork/maintenance
|
|
||||||
|
|
||||||
##############################################################################
|
|
||||||
# JOB: KUBERNETES DEPLOY ACTUAL/LATEST VERSION ######################################
|
|
||||||
##############################################################################
|
|
||||||
kubernetes_deploy:
|
|
||||||
# see example https://github.com/do-community/example-doctl-action
|
|
||||||
# see example https://github.com/do-community/example-doctl-action/blob/main/.github/workflows/workflow.yaml
|
|
||||||
name: Kubernetes deploy of latest version to stage.ocelot.social cluster at DigitalOcean
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
needs: [upload_to_dockerhub]
|
|
||||||
steps:
|
|
||||||
##########################################################################
|
|
||||||
# CHECKOUT CODE ##########################################################
|
|
||||||
##########################################################################
|
|
||||||
- name: Checkout code
|
|
||||||
uses: actions/checkout@v3
|
|
||||||
##########################################################################
|
|
||||||
# SET ENVS ###############################################################
|
|
||||||
##########################################################################
|
|
||||||
- name: ENV - VERSION
|
|
||||||
run: echo "VERSION=$(node -p -e "require('./package.json').version")" >> $GITHUB_ENV
|
|
||||||
- name: ENV - BUILD_VERSION
|
|
||||||
run: echo "BUILD_VERSION=${VERSION}-${GITHUB_RUN_NUMBER}" >> $GITHUB_ENV
|
|
||||||
##########################################################################
|
|
||||||
# Install DigitalOceans doctl and set kubeconfig #########################
|
|
||||||
##########################################################################
|
|
||||||
- name: Install doctl
|
|
||||||
uses: digitalocean/action-doctl@v2
|
|
||||||
with:
|
|
||||||
token: ${{ secrets.DIGITALOCEAN_ACCESS_TOKEN }}
|
|
||||||
- name: Save DigitalOcean kubeconfig with short-lived credentials
|
|
||||||
run: doctl kubernetes cluster kubeconfig save --expiry-seconds 600 cluster-stage-ocelot-social
|
|
||||||
##########################################################################
|
|
||||||
# Deploy new Docker images to DigitalOcean Kubernetes cluster ############
|
|
||||||
##########################################################################
|
|
||||||
# - name: Deploy 'latest' to DigitalOcean Kubernetes
|
|
||||||
# run: |
|
|
||||||
# kubectl -n default set image deployment/ocelot-webapp container-ocelot-webapp=ocelotsocialnetwork/webapp:latest
|
|
||||||
# kubectl -n default rollout restart deployment/ocelot-webapp
|
|
||||||
# kubectl -n default set image deployment/ocelot-backend container-ocelot-backend=ocelotsocialnetwork/backend:latest
|
|
||||||
# kubectl -n default rollout restart deployment/ocelot-backend
|
|
||||||
# kubectl -n default set image deployment/ocelot-maintenance container-ocelot-maintenance=ocelotsocialnetwork/maintenance:latest
|
|
||||||
# kubectl -n default rollout restart deployment/ocelot-maintenance
|
|
||||||
# kubectl -n default set image deployment/ocelot-neo4j container-ocelot-neo4j=ocelotsocialnetwork/neo4j-community:latest
|
|
||||||
# kubectl -n default rollout restart deployment/ocelot-neo4j
|
|
||||||
- name: Deploy actual version '$BUILD_VERSION' to DigitalOcean Kubernetes
|
|
||||||
run: |
|
run: |
|
||||||
kubectl -n default set image deployment/ocelot-webapp container-ocelot-webapp=ocelotsocialnetwork/webapp:$BUILD_VERSION
|
docker push --all-tags ocelotsocialnetwork/neo4j-community
|
||||||
kubectl -n default rollout restart deployment/ocelot-webapp
|
docker push --all-tags ocelotsocialnetwork/backend
|
||||||
kubectl -n default set image deployment/ocelot-backend container-ocelot-backend=ocelotsocialnetwork/backend:$BUILD_VERSION
|
docker push --all-tags ocelotsocialnetwork/webapp
|
||||||
kubectl -n default rollout restart deployment/ocelot-backend
|
docker push --all-tags ocelotsocialnetwork/maintenance
|
||||||
kubectl -n default set image deployment/ocelot-maintenance container-ocelot-maintenance=ocelotsocialnetwork/maintenance:$BUILD_VERSION
|
|
||||||
kubectl -n default rollout restart deployment/ocelot-maintenance
|
|
||||||
kubectl -n default set image deployment/ocelot-neo4j container-ocelot-neo4j=ocelotsocialnetwork/neo4j-community:$BUILD_VERSION
|
|
||||||
kubectl -n default rollout restart deployment/ocelot-neo4j
|
|
||||||
# because this step 'kubectl -n default rollout status deployment/* --timeout=600s' does not work as expected
|
|
||||||
# and we need the pods to be up again for cleaning and seeding the Neo4j database and the backend.
|
|
||||||
# !!! this is not a perfect solution !!!
|
|
||||||
# deployments are regularly up again after 3 minutes and 10 seconds
|
|
||||||
- name: Sleep for 4 minutes, means 240 seconds
|
|
||||||
run: sleep 240s
|
|
||||||
shell: bash
|
|
||||||
- name: Verify deployment and wait for the pods of each deployment to get ready for cleaning and seeding of the database
|
|
||||||
run: |
|
|
||||||
kubectl -n default rollout status deployment/ocelot-backend --timeout=600s
|
|
||||||
kubectl -n default rollout status deployment/ocelot-neo4j --timeout=600s
|
|
||||||
kubectl -n default rollout status deployment/ocelot-maintenance --timeout=600s
|
|
||||||
kubectl -n default rollout status deployment/ocelot-webapp --timeout=600s
|
|
||||||
- name: Run migrations for Neo4j database via backend for staging
|
|
||||||
run: |
|
|
||||||
kubectl -n default exec -it $(kubectl -n default get pods | grep ocelot-backend | awk '{ print $1 }') -- /bin/sh -c "yarn prod:migrate up"
|
|
||||||
- name: Reset and seed Neo4j database via backend for staging
|
|
||||||
# db cleaning and seeding is only possible in production if env 'PRODUCTION_DB_CLEAN_ALLOW=true' is set in deployment
|
|
||||||
run: |
|
|
||||||
kubectl -n default exec -it $(kubectl -n default get pods | grep ocelot-backend | awk '{ print $1 }') -- /bin/sh -c "node --experimental-repl-await dist/db/clean.js && node --experimental-repl-await dist/db/seed.js"
|
|
||||||
|
|
||||||
##############################################################################
|
##############################################################################
|
||||||
# JOB: GITHUB TAG LATEST VERSION #############################################
|
# JOB: GITHUB TAG LATEST VERSION #############################################
|
||||||
@ -331,39 +242,16 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs: [upload_to_dockerhub]
|
needs: [upload_to_dockerhub]
|
||||||
steps:
|
steps:
|
||||||
##########################################################################
|
|
||||||
# CHECKOUT CODE ##########################################################
|
|
||||||
##########################################################################
|
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v3
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0 # Fetch full History for changelog
|
fetch-depth: 0 # Fetch full History for changelog
|
||||||
##########################################################################
|
- name: Setup env
|
||||||
# SET ENVS ###############################################################
|
run: |
|
||||||
##########################################################################
|
echo "VERSION=$(node -p -e "require('./package.json').version")" >> $GITHUB_ENV
|
||||||
- name: ENV - VERSION
|
echo "BUILD_DATE=$(date -u +'%Y-%m-%dT%H:%M:%SZ')" >> $GITHUB_ENV
|
||||||
run: echo "VERSION=$(node -p -e "require('./package.json').version")" >> $GITHUB_ENV
|
echo "BUILD_COMMIT=${GITHUB_SHA}" >> $GITHUB_ENV
|
||||||
- name: ENV - BUILD_DATE
|
- run: echo "BUILD_VERSION=${VERSION}-${GITHUB_RUN_NUMBER}" >> $GITHUB_ENV
|
||||||
run: echo "BUILD_DATE=$(date -u +'%Y-%m-%dT%H:%M:%SZ')" >> $GITHUB_ENV
|
|
||||||
- name: ENV - BUILD_VERSION
|
|
||||||
run: echo "BUILD_VERSION=${VERSION}-${GITHUB_RUN_NUMBER}" >> $GITHUB_ENV
|
|
||||||
- name: ENV - BUILD_COMMIT
|
|
||||||
run: echo "BUILD_COMMIT=${GITHUB_SHA}" >> $GITHUB_ENV
|
|
||||||
##########################################################################
|
|
||||||
# Push version tag to GitHub #############################################
|
|
||||||
##########################################################################
|
|
||||||
# TODO: this will error on duplicate
|
|
||||||
#- name: package-version-to-git-tag
|
|
||||||
# uses: pkgdeps/git-tag-action@v2
|
|
||||||
# with:
|
|
||||||
# github_token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
# github_repo: ${{ github.repository }}
|
|
||||||
# version: ${{ env.VERSION }}
|
|
||||||
# git_commit_sha: ${{ github.sha }}
|
|
||||||
# git_tag_prefix: "v"
|
|
||||||
##########################################################################
|
|
||||||
# Push build tag to GitHub ###############################################
|
|
||||||
##########################################################################
|
|
||||||
- name: package-version-to-git-tag + build number
|
- name: package-version-to-git-tag + build number
|
||||||
uses: pkgdeps/git-tag-action@v2
|
uses: pkgdeps/git-tag-action@v2
|
||||||
with:
|
with:
|
||||||
@ -372,13 +260,10 @@ jobs:
|
|||||||
version: ${{ env.BUILD_VERSION }}
|
version: ${{ env.BUILD_VERSION }}
|
||||||
git_commit_sha: ${{ github.sha }}
|
git_commit_sha: ${{ github.sha }}
|
||||||
git_tag_prefix: "b"
|
git_tag_prefix: "b"
|
||||||
##########################################################################
|
- name: Generate changelog
|
||||||
# Push release tag to GitHub #############################################
|
run: |
|
||||||
##########################################################################
|
yarn install
|
||||||
- name: yarn install
|
yarn auto-changelog --latest-version ${{ env.VERSION }} --unreleased-only
|
||||||
run: yarn install
|
|
||||||
- name: generate changelog
|
|
||||||
run: yarn auto-changelog --latest-version ${{ env.VERSION }} --unreleased-only
|
|
||||||
- name: package-version-to-git-release
|
- name: package-version-to-git-release
|
||||||
continue-on-error: true # Will fail if tag exists
|
continue-on-error: true # Will fail if tag exists
|
||||||
id: create_release
|
id: create_release
|
||||||
@ -391,3 +276,101 @@ jobs:
|
|||||||
body_path: ./CHANGELOG.md
|
body_path: ./CHANGELOG.md
|
||||||
draft: false
|
draft: false
|
||||||
prerelease: false
|
prerelease: false
|
||||||
|
|
||||||
|
build_trigger:
|
||||||
|
name: Trigger successful build
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: [github_tag]
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
fetch-depth: 0 # Fetch full History for changelog
|
||||||
|
- name: Setup env
|
||||||
|
run: |
|
||||||
|
echo "VERSION=$(node -p -e "require('./package.json').version")" >> $GITHUB_ENV
|
||||||
|
echo "BUILD_DATE=$(date -u +'%Y-%m-%dT%H:%M:%SZ')" >> $GITHUB_ENV
|
||||||
|
echo "BUILD_COMMIT=${GITHUB_SHA}" >> $GITHUB_ENV
|
||||||
|
- run: echo "BUILD_VERSION=${VERSION}-${GITHUB_RUN_NUMBER}" >> $GITHUB_ENV
|
||||||
|
- name: Repository Dispatch
|
||||||
|
uses: peter-evans/repository-dispatch@v2
|
||||||
|
with:
|
||||||
|
token: ${{ github.token }}
|
||||||
|
event-type: trigger-build-success
|
||||||
|
repository: ${{ github.repository }}
|
||||||
|
client-payload: '{"ref": "${{ github.ref }}", "sha": "${{ github.sha }}", "VERSION": "${VERSION}", "BUILD_DATE": "${BUILD_DATE}", "BUILD_COMMIT": "${BUILD_COMMIT}", "BUILD_VERSION": "${BUILD_VERSION}"}'
|
||||||
|
|
||||||
|
# ##############################################################################
|
||||||
|
# # JOB: KUBERNETES DEPLOY ACTUAL/LATEST VERSION ######################################
|
||||||
|
# ##############################################################################
|
||||||
|
# kubernetes_deploy:
|
||||||
|
# # see example https://github.com/do-community/example-doctl-action
|
||||||
|
# # see example https://github.com/do-community/example-doctl-action/blob/main/.github/workflows/workflow.yaml
|
||||||
|
# name: Kubernetes deploy of latest version to stage.ocelot.social cluster at DigitalOcean
|
||||||
|
# runs-on: ubuntu-latest
|
||||||
|
# needs: [upload_to_dockerhub]
|
||||||
|
# steps:
|
||||||
|
# ##########################################################################
|
||||||
|
# # CHECKOUT CODE ##########################################################
|
||||||
|
# ##########################################################################
|
||||||
|
# - name: Checkout code
|
||||||
|
# uses: actions/checkout@v3
|
||||||
|
# ##########################################################################
|
||||||
|
# # SET ENVS ###############################################################
|
||||||
|
# ##########################################################################
|
||||||
|
# - name: ENV - VERSION
|
||||||
|
# run: echo "VERSION=$(node -p -e "require('./package.json').version")" >> $GITHUB_ENV
|
||||||
|
# - name: ENV - BUILD_VERSION
|
||||||
|
# run: echo "BUILD_VERSION=${VERSION}-${GITHUB_RUN_NUMBER}" >> $GITHUB_ENV
|
||||||
|
# ##########################################################################
|
||||||
|
# # Install DigitalOceans doctl and set kubeconfig #########################
|
||||||
|
# ##########################################################################
|
||||||
|
# - name: Install doctl
|
||||||
|
# uses: digitalocean/action-doctl@v2
|
||||||
|
# with:
|
||||||
|
# token: ${{ secrets.DIGITALOCEAN_ACCESS_TOKEN }}
|
||||||
|
# - name: Save DigitalOcean kubeconfig with short-lived credentials
|
||||||
|
# run: doctl kubernetes cluster kubeconfig save --expiry-seconds 600 cluster-stage-ocelot-social
|
||||||
|
# ##########################################################################
|
||||||
|
# # Deploy new Docker images to DigitalOcean Kubernetes cluster ############
|
||||||
|
# ##########################################################################
|
||||||
|
# # - name: Deploy 'latest' to DigitalOcean Kubernetes
|
||||||
|
# # run: |
|
||||||
|
# # kubectl -n default set image deployment/ocelot-webapp container-ocelot-webapp=ocelotsocialnetwork/webapp:latest
|
||||||
|
# # kubectl -n default rollout restart deployment/ocelot-webapp
|
||||||
|
# # kubectl -n default set image deployment/ocelot-backend container-ocelot-backend=ocelotsocialnetwork/backend:latest
|
||||||
|
# # kubectl -n default rollout restart deployment/ocelot-backend
|
||||||
|
# # kubectl -n default set image deployment/ocelot-maintenance container-ocelot-maintenance=ocelotsocialnetwork/maintenance:latest
|
||||||
|
# # kubectl -n default rollout restart deployment/ocelot-maintenance
|
||||||
|
# # kubectl -n default set image deployment/ocelot-neo4j container-ocelot-neo4j=ocelotsocialnetwork/neo4j-community:latest
|
||||||
|
# # kubectl -n default rollout restart deployment/ocelot-neo4j
|
||||||
|
# - name: Deploy actual version '$BUILD_VERSION' to DigitalOcean Kubernetes
|
||||||
|
# run: |
|
||||||
|
# kubectl -n default set image deployment/ocelot-webapp container-ocelot-webapp=ocelotsocialnetwork/webapp:$BUILD_VERSION
|
||||||
|
# kubectl -n default rollout restart deployment/ocelot-webapp
|
||||||
|
# kubectl -n default set image deployment/ocelot-backend container-ocelot-backend=ocelotsocialnetwork/backend:$BUILD_VERSION
|
||||||
|
# kubectl -n default rollout restart deployment/ocelot-backend
|
||||||
|
# kubectl -n default set image deployment/ocelot-maintenance container-ocelot-maintenance=ocelotsocialnetwork/maintenance:$BUILD_VERSION
|
||||||
|
# kubectl -n default rollout restart deployment/ocelot-maintenance
|
||||||
|
# kubectl -n default set image deployment/ocelot-neo4j container-ocelot-neo4j=ocelotsocialnetwork/neo4j-community:$BUILD_VERSION
|
||||||
|
# kubectl -n default rollout restart deployment/ocelot-neo4j
|
||||||
|
# # because this step 'kubectl -n default rollout status deployment/* --timeout=600s' does not work as expected
|
||||||
|
# # and we need the pods to be up again for cleaning and seeding the Neo4j database and the backend.
|
||||||
|
# # !!! this is not a perfect solution !!!
|
||||||
|
# # deployments are regularly up again after 3 minutes and 10 seconds
|
||||||
|
# - name: Sleep for 4 minutes, means 240 seconds
|
||||||
|
# run: sleep 240s
|
||||||
|
# shell: bash
|
||||||
|
# - name: Verify deployment and wait for the pods of each deployment to get ready for cleaning and seeding of the database
|
||||||
|
# run: |
|
||||||
|
# kubectl -n default rollout status deployment/ocelot-backend --timeout=600s
|
||||||
|
# kubectl -n default rollout status deployment/ocelot-neo4j --timeout=600s
|
||||||
|
# kubectl -n default rollout status deployment/ocelot-maintenance --timeout=600s
|
||||||
|
# kubectl -n default rollout status deployment/ocelot-webapp --timeout=600s
|
||||||
|
# - name: Run migrations for Neo4j database via backend for staging
|
||||||
|
# run: |
|
||||||
|
# kubectl -n default exec -it $(kubectl -n default get pods | grep ocelot-backend | awk '{ print $1 }') -- /bin/sh -c "yarn prod:migrate up"
|
||||||
|
# - name: Reset and seed Neo4j database via backend for staging
|
||||||
|
# # db cleaning and seeding is only possible in production if env 'PRODUCTION_DB_CLEAN_ALLOW=true' is set in deployment
|
||||||
|
# run: |
|
||||||
|
# kubectl -n default exec -it $(kubectl -n default get pods | grep ocelot-backend | awk '{ print $1 }') -- /bin/sh -c "node --experimental-repl-await dist/db/clean.js && node --experimental-repl-await dist/db/seed.js"
|
||||||
@ -174,34 +174,14 @@ Please copy and paste the following quotes for the languages:
|
|||||||
|
|
||||||
#### Environment Variable For Apple M1 Platform
|
#### Environment Variable For Apple M1 Platform
|
||||||
|
|
||||||
To set the Docker platform environment variable in your terminal tab, run:
|
If you encounter trouble building the docker containers on an Apple M1 chip you can try to explicitly define the target platform docker builds and pulls images for:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# set env variable for your shell
|
# set env variable for your shell
|
||||||
$ export DOCKER_DEFAULT_PLATFORM=linux/amd64
|
$ export DOCKER_DEFAULT_PLATFORM=linux/amd64
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Docker Compose Override File For Apple M1 Platform
|
Or alternatively use a `YAML` docker compose overwrite file with `platform: linux/amd64`.
|
||||||
|
|
||||||
For Docker compose `up` or `build` commands, you can use our Apple M1 override file that specifies the M1 platform:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# in main folder
|
|
||||||
|
|
||||||
# for development
|
|
||||||
$ docker compose -f docker-compose.yml -f docker-compose.override.yml -f docker-compose.apple-m1.override.yml up
|
|
||||||
# only once: init admin user and create indexes and constraints in Neo4j database
|
|
||||||
$ docker compose exec backend yarn prod:migrate init
|
|
||||||
# clean db
|
|
||||||
$ docker compose exec backend yarn db:reset
|
|
||||||
# seed db
|
|
||||||
$ docker compose exec backend yarn db:seed
|
|
||||||
|
|
||||||
# for production
|
|
||||||
$ docker compose -f docker-compose.yml -f docker-compose.apple-m1.override.yml up
|
|
||||||
# only once: init admin user and create indexes and constraints in Neo4j database
|
|
||||||
$ docker compose exec backend /bin/sh -c "yarn prod:migrate init"
|
|
||||||
```
|
|
||||||
|
|
||||||
### Analyzing Docker Builds
|
### Analyzing Docker Builds
|
||||||
|
|
||||||
|
|||||||
93
README.md
93
README.md
@ -20,12 +20,71 @@ At the same time, it should be possible in the future to link these networks wit
|
|||||||
|
|
||||||
In other words, we are interested in a network of networks and in keeping the data as close as possible to the user and the operator they trusts.
|
In other words, we are interested in a network of networks and in keeping the data as close as possible to the user and the operator they trusts.
|
||||||
|
|
||||||
## Introduction
|
## Screenshots
|
||||||
|
|
||||||
|
<img src="https://user-images.githubusercontent.com/17728384/218597429-554e4082-3906-4721-8f68-0c13146fc218.png" alt="Post feed" title="Post feed" />
|
||||||
|
|
||||||
|
Check out more screenshots [here](/wiki/en:Screenshots).
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
Ocelot.social networks feature:
|
||||||
|
* <strong>user accounts</strong>
|
||||||
|
* <strong>user roles</strong>
|
||||||
|
* <strong>posts</strong> and <strong>comments</strong>
|
||||||
|
* <strong>groups</strong>
|
||||||
|
* <strong>maps</strong>
|
||||||
|
* <strong>search</strong>
|
||||||
|
* <strong>filters</strong>
|
||||||
|
* and more …
|
||||||
|
|
||||||
|
Check out the [full feature list](/wiki/en:FAQ#what-are-the-features).
|
||||||
|
|
||||||
|
## User Guide and Frequently Asked Questions
|
||||||
|
|
||||||
|
In the [wiki](/wiki) you can find more information.
|
||||||
|
|
||||||
|
* [User Guide](/wiki/en:User-Guide)
|
||||||
|
* [Frequently Asked Questions](/wiki/en:FAQ)
|
||||||
|
|
||||||
|
## Demo
|
||||||
|
|
||||||
|
Try out our live demo network, see [here](#live-demo-and-developer-logins).
|
||||||
|
|
||||||
|
## Help us
|
||||||
|
|
||||||
|
If you're wondering how you could help, there are plenty of ways, e.g.:
|
||||||
|
|
||||||
|
* Spread the good word about ocelot.social to make it more popular:
|
||||||
|
* Add the link [ocelot.social](https://ocelot.social) to your website.
|
||||||
|
* Give ocelot.social a Like at https://alternativeto.net/software/ocelot-social/.
|
||||||
|
* Star our project on GitHub at https://github.com/Ocelot-Social-Community/Ocelot-Social/.
|
||||||
|
* Promote it on your social networks.
|
||||||
|
* Tell your friends about it by word-of-mouth.
|
||||||
|
* Write a press article on ocelot.social or contact the editorial office of your local newspage or radio station.
|
||||||
|
* Take a [good first issue](https://github.com/Ocelot-Social-Community/Ocelot-Social/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22) or issues that need help. Make sure to read [CONTRIBUTING.md](/CONTRIBUTING.md) first though.
|
||||||
|
* Testing and [reporting](/issues/new/choose) bugs.
|
||||||
|
* Translating: Please [contact us](#contact).
|
||||||
|
* Reviewing and testing pull requests.
|
||||||
|
|
||||||
|
## Donate
|
||||||
|
|
||||||
|
Your donation is very welcome and helps to enhance and improve the network. This software is mostly developped and maintained by the association [busFaktor() e.V.](https://www.busfaktor.org/en). Please support us with a [donation](https://www.busfaktor.org/en/donations) to busFaktor() e.V.. Thanks a lot! ❤️
|
||||||
|
|
||||||
|
## Contact
|
||||||
|
|
||||||
|
Are you interested in operating your own ocelot.social network or becoming a user? Please contact us here:
|
||||||
|
* [hello@ocelot.social](mailto:hello@ocelot.social)
|
||||||
|
* [Discord](https://discord.com/invite/DFSjPaX)
|
||||||
|
|
||||||
|
## For Developers and Contributors
|
||||||
|
|
||||||
|
### Introduction
|
||||||
|
|
||||||
Have a look into our short video:
|
Have a look into our short video:
|
||||||
[ocelot.social - GitHub - Developer Welcome - Tutorial (english)](https://www.youtube.com/watch?v=gZSL6KvBIiY&list=PLFMD5liPP01kbuReHxYXxv_1fI5rIgS1f&index=1)
|
[ocelot.social - GitHub - Developer Welcome - Tutorial (english)](https://www.youtube.com/watch?v=gZSL6KvBIiY&list=PLFMD5liPP01kbuReHxYXxv_1fI5rIgS1f&index=1)
|
||||||
|
|
||||||
## Directory Layout
|
### Directory Layout
|
||||||
|
|
||||||
There are three important directories:
|
There are three important directories:
|
||||||
|
|
||||||
@ -41,9 +100,9 @@ There are two approaches:
|
|||||||
1. [Local](#local-installation) installation, which means you have to take care of dependencies yourself.
|
1. [Local](#local-installation) installation, which means you have to take care of dependencies yourself.
|
||||||
2. **Or** Install everything through [Docker](#docker-installation) which takes care of dependencies for you.
|
2. **Or** Install everything through [Docker](#docker-installation) which takes care of dependencies for you.
|
||||||
|
|
||||||
## Installation
|
### Installation
|
||||||
|
|
||||||
### Clone the Repository
|
#### Clone the Repository
|
||||||
|
|
||||||
Clone the repository, this will create a new folder called `Ocelot-Social`:
|
Clone the repository, this will create a new folder called `Ocelot-Social`:
|
||||||
|
|
||||||
@ -65,7 +124,7 @@ Change into the new folder.
|
|||||||
$ cd Ocelot-Social
|
$ cd Ocelot-Social
|
||||||
```
|
```
|
||||||
|
|
||||||
## Live Demo And Developer Logins
|
### Live Demo And Developer Logins
|
||||||
|
|
||||||
**Try out our deployed [development environment](https://stage.ocelot.social).**
|
**Try out our deployed [development environment](https://stage.ocelot.social).**
|
||||||
|
|
||||||
@ -74,7 +133,7 @@ Visit our staging networks:
|
|||||||
* central staging network: [stage.ocelot.social](https://stage.ocelot.social)
|
* central staging network: [stage.ocelot.social](https://stage.ocelot.social)
|
||||||
<!-- - rebranded staging network: [rebrand.ocelot.social](https://stage.ocelot.social). -->
|
<!-- - rebranded staging network: [rebrand.ocelot.social](https://stage.ocelot.social). -->
|
||||||
|
|
||||||
### Login
|
#### Login
|
||||||
|
|
||||||
Logins for the live demos and developers (local developers after the following installations) in the browser:
|
Logins for the live demos and developers (local developers after the following installations) in the browser:
|
||||||
|
|
||||||
@ -84,11 +143,11 @@ Logins for the live demos and developers (local developers after the following i
|
|||||||
| `moderator@example.org` | 1234 | moderator |
|
| `moderator@example.org` | 1234 | moderator |
|
||||||
| `admin@example.org` | 1234 | admin |
|
| `admin@example.org` | 1234 | admin |
|
||||||
|
|
||||||
### Docker Installation
|
#### 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.
|
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
|
##### General Installation of Docker
|
||||||
|
|
||||||
There are [several ways to install Docker CE](https://docs.docker.com/install/) on your computer or server.
|
There are [several ways to install Docker CE](https://docs.docker.com/install/) on your computer or server.
|
||||||
|
|
||||||
@ -105,7 +164,7 @@ $ docker-compose --version
|
|||||||
docker-compose version 1.23.2
|
docker-compose version 1.23.2
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Start Ocelot-Social via Docker-Compose
|
##### Start Ocelot-Social via Docker-Compose
|
||||||
|
|
||||||
Prepare ENVs once beforehand:
|
Prepare ENVs once beforehand:
|
||||||
|
|
||||||
@ -153,11 +212,11 @@ $ docker-compose exec backend yarn run db:seed
|
|||||||
For a closer description see [backend README.md](./backend/README.md).
|
For a closer description see [backend README.md](./backend/README.md).
|
||||||
For a full documentation see [SUMMARY](./SUMMARY.md).
|
For a full documentation see [SUMMARY](./SUMMARY.md).
|
||||||
|
|
||||||
### Local Installation
|
#### Local Installation
|
||||||
|
|
||||||
For a full documentation see [SUMMARY](./SUMMARY.md).
|
For a full documentation see [SUMMARY](./SUMMARY.md).
|
||||||
|
|
||||||
## Contributing
|
### Contributing
|
||||||
|
|
||||||
Choose an issue (consider our label [good-first-issue](https://github.com/Ocelot-Social-Community/Ocelot-Social/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22)) and leave a comment there. We will then invite you to join our volunteers team.
|
Choose an issue (consider our label [good-first-issue](https://github.com/Ocelot-Social-Community/Ocelot-Social/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22)) and leave a comment there. We will then invite you to join our volunteers team.
|
||||||
To have the necessary permission to push directly to this repository, please accept our invitation to join our volunteers team, you will receive via the email, Github will send you, once invited. If we did not invite you yet, please request an invitation via Discord.
|
To have the necessary permission to push directly to this repository, please accept our invitation to join our volunteers team, you will receive via the email, Github will send you, once invited. If we did not invite you yet, please request an invitation via Discord.
|
||||||
@ -183,7 +242,7 @@ $ yarn test
|
|||||||
|
|
||||||
Check out our [contribution guideline](./CONTRIBUTING.md), too!
|
Check out our [contribution guideline](./CONTRIBUTING.md), too!
|
||||||
|
|
||||||
### Developer Chat
|
#### Developer Chat
|
||||||
|
|
||||||
Join our friendly open-source community on [Discord](https://discord.gg/AJSX9DCSUA) :heart_eyes_cat:
|
Join our friendly open-source community on [Discord](https://discord.gg/AJSX9DCSUA) :heart_eyes_cat:
|
||||||
Just introduce yourself at `#introduce-yourself` and mention a mentor or `@@Mentors` to get you onboard :neckbeard:
|
Just introduce yourself at `#introduce-yourself` and mention a mentor or `@@Mentors` to get you onboard :neckbeard:
|
||||||
@ -191,13 +250,13 @@ Just introduce yourself at `#introduce-yourself` and mention a mentor or `@@Ment
|
|||||||
We give write permissions to every developer who asks for it. Just text us on
|
We give write permissions to every developer who asks for it. Just text us on
|
||||||
[Discord](https://discord.gg/AJSX9DCSUA).
|
[Discord](https://discord.gg/AJSX9DCSUA).
|
||||||
|
|
||||||
## Deployment
|
### Deployment
|
||||||
|
|
||||||
Deployment methods can be found in the [Ocelot-Social-Deploy-Rebranding](https://github.com/Ocelot-Social-Community/Ocelot-Social-Deploy-Rebranding) repository.
|
Deployment methods can be found in the [Ocelot-Social-Deploy-Rebranding](https://github.com/Ocelot-Social-Community/Ocelot-Social-Deploy-Rebranding) repository.
|
||||||
|
|
||||||
The only deployment method in this repository for development purposes as described above is `docker-compose`.
|
The only deployment method in this repository for development purposes as described above is `docker-compose`.
|
||||||
|
|
||||||
## Technology Stack
|
### Technology Stack
|
||||||
|
|
||||||
* [VueJS](https://vuejs.org/)
|
* [VueJS](https://vuejs.org/)
|
||||||
* [NuxtJS](https://nuxtjs.org/)
|
* [NuxtJS](https://nuxtjs.org/)
|
||||||
@ -205,7 +264,7 @@ The only deployment method in this repository for development purposes as descri
|
|||||||
* [NodeJS](https://nodejs.org/en/)
|
* [NodeJS](https://nodejs.org/en/)
|
||||||
* [Neo4J](https://neo4j.com/)
|
* [Neo4J](https://neo4j.com/)
|
||||||
|
|
||||||
### For Testing
|
#### For Testing
|
||||||
|
|
||||||
* [Cypress](https://docs.cypress.io/)
|
* [Cypress](https://docs.cypress.io/)
|
||||||
* [Storybook](https://storybook.js.org/)
|
* [Storybook](https://storybook.js.org/)
|
||||||
@ -213,7 +272,7 @@ The only deployment method in this repository for development purposes as descri
|
|||||||
* [Vue Test Utils](https://vue-test-utils.vuejs.org/)
|
* [Vue Test Utils](https://vue-test-utils.vuejs.org/)
|
||||||
* [ESLint](https://eslint.org/)
|
* [ESLint](https://eslint.org/)
|
||||||
|
|
||||||
## Attributions
|
### Attributions
|
||||||
|
|
||||||
Locale Icons made by [Freepik](http://www.freepik.com/) from [www.flaticon.com](https://www.flaticon.com/) is licensed by [CC 3.0 BY](http://creativecommons.org/licenses/by/3.0/).
|
Locale Icons made by [Freepik](http://www.freepik.com/) from [www.flaticon.com](https://www.flaticon.com/) is licensed by [CC 3.0 BY](http://creativecommons.org/licenses/by/3.0/).
|
||||||
|
|
||||||
@ -221,6 +280,6 @@ Browser compatibility testing with [BrowserStack](https://www.browserstack.com/)
|
|||||||
|
|
||||||
<img alt="BrowserStack Logo" src=".gitbook/assets/browserstack-logo.svg" width="256">
|
<img alt="BrowserStack Logo" src=".gitbook/assets/browserstack-logo.svg" width="256">
|
||||||
|
|
||||||
## License
|
### License
|
||||||
|
|
||||||
See the [LICENSE](LICENSE.md) file for license rights and limitations (MIT).
|
See the [LICENSE](LICENSE.md) file for license rights and limitations (MIT).
|
||||||
|
|||||||
@ -89,6 +89,12 @@ Database indexes and constraints need to be created and upgraded when the databa
|
|||||||
```bash
|
```bash
|
||||||
# in main folder while docker-compose is running
|
# in main folder while docker-compose is running
|
||||||
$ docker exec backend yarn run db:migrate init
|
$ docker exec backend yarn run db:migrate init
|
||||||
|
|
||||||
|
# only once: init admin user and create indexes and constraints in Neo4j database
|
||||||
|
# for development
|
||||||
|
$ docker compose exec backend yarn prod:migrate init
|
||||||
|
# in production mode use command
|
||||||
|
$ docker compose exec backend /bin/sh -c "yarn prod:migrate init"
|
||||||
```
|
```
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
@ -150,6 +150,19 @@ export const changeGroupMemberRoleMutation = () => {
|
|||||||
`
|
`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const removeUserFromGroupMutation = () => {
|
||||||
|
return gql`
|
||||||
|
mutation ($groupId: ID!, $userId: ID!) {
|
||||||
|
RemoveUserFromGroup(groupId: $groupId, userId: $userId) {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
slug
|
||||||
|
myRoleInGroup
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
||||||
// ------ queries
|
// ------ queries
|
||||||
|
|
||||||
export const groupQuery = () => {
|
export const groupQuery = () => {
|
||||||
|
|||||||
65
backend/src/graphql/notifications.js
Normal file
65
backend/src/graphql/notifications.js
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
import gql from 'graphql-tag'
|
||||||
|
|
||||||
|
// ------ mutations
|
||||||
|
|
||||||
|
export const markAsReadMutation = () => {
|
||||||
|
return gql`
|
||||||
|
mutation ($id: ID!) {
|
||||||
|
markAsRead(id: $id) {
|
||||||
|
from {
|
||||||
|
__typename
|
||||||
|
... on Post {
|
||||||
|
content
|
||||||
|
}
|
||||||
|
... on Comment {
|
||||||
|
content
|
||||||
|
}
|
||||||
|
}
|
||||||
|
read
|
||||||
|
createdAt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
||||||
|
export const markAllAsReadMutation = () => {
|
||||||
|
return gql`
|
||||||
|
mutation {
|
||||||
|
markAllAsRead {
|
||||||
|
from {
|
||||||
|
__typename
|
||||||
|
... on Post {
|
||||||
|
content
|
||||||
|
}
|
||||||
|
... on Comment {
|
||||||
|
content
|
||||||
|
}
|
||||||
|
}
|
||||||
|
read
|
||||||
|
createdAt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------ queries
|
||||||
|
|
||||||
|
export const notificationQuery = () => {
|
||||||
|
return gql`
|
||||||
|
query ($read: Boolean, $orderBy: NotificationOrdering) {
|
||||||
|
notifications(read: $read, orderBy: $orderBy) {
|
||||||
|
from {
|
||||||
|
__typename
|
||||||
|
... on Post {
|
||||||
|
content
|
||||||
|
}
|
||||||
|
... on Comment {
|
||||||
|
content
|
||||||
|
}
|
||||||
|
}
|
||||||
|
read
|
||||||
|
createdAt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
}
|
||||||
@ -1,13 +1,3 @@
|
|||||||
// TODO: can be replaced with: (which is no a fake)
|
|
||||||
// import gql from 'graphql-tag'
|
|
||||||
// See issue: https://github.com/Ocelot-Social-Community/Ocelot-Social/issues/5152
|
|
||||||
|
|
||||||
//* This is a fake ES2015 template string, just to benefit of syntax
|
|
||||||
// highlighting of `gql` template strings in certain editors.
|
|
||||||
export function gql(strings) {
|
|
||||||
return strings.join('')
|
|
||||||
}
|
|
||||||
|
|
||||||
// sometime we have to wait to check a db state by having a look into the db in a certain moment
|
// sometime we have to wait to check a db state by having a look into the db in a certain moment
|
||||||
// or we wait a bit to check if we missed to set an await somewhere
|
// or we wait a bit to check if we missed to set an await somewhere
|
||||||
// see: https://www.sitepoint.com/delay-sleep-pause-wait/
|
// see: https://www.sitepoint.com/delay-sleep-pause-wait/
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { gql } from '../../helpers/jest'
|
import gql from 'graphql-tag'
|
||||||
import { cleanDatabase } from '../../db/factories'
|
import { cleanDatabase } from '../../db/factories'
|
||||||
import { createTestClient } from 'apollo-server-testing'
|
import { createTestClient } from 'apollo-server-testing'
|
||||||
import { getNeode, getDriver } from '../../db/neo4j'
|
import { getNeode, getDriver } from '../../db/neo4j'
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import Factory, { cleanDatabase } from '../../db/factories'
|
import Factory, { cleanDatabase } from '../../db/factories'
|
||||||
import { gql } from '../../helpers/jest'
|
import gql from 'graphql-tag'
|
||||||
import { getNeode, getDriver } from '../../db/neo4j'
|
import { getNeode, getDriver } from '../../db/neo4j'
|
||||||
import createServer from '../../server'
|
import createServer from '../../server'
|
||||||
import { createTestClient } from 'apollo-server-testing'
|
import { createTestClient } from 'apollo-server-testing'
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { gql } from '../../helpers/jest'
|
import gql from 'graphql-tag'
|
||||||
import { cleanDatabase } from '../../db/factories'
|
import { cleanDatabase } from '../../db/factories'
|
||||||
import { createTestClient } from 'apollo-server-testing'
|
import { createTestClient } from 'apollo-server-testing'
|
||||||
import { getNeode, getDriver } from '../../db/neo4j'
|
import { getNeode, getDriver } from '../../db/neo4j'
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { gql } from '../helpers/jest'
|
import gql from 'graphql-tag'
|
||||||
import { cleanDatabase } from '../db/factories'
|
import { cleanDatabase } from '../db/factories'
|
||||||
import { getNeode, getDriver } from '../db/neo4j'
|
import { getNeode, getDriver } from '../db/neo4j'
|
||||||
import { createTestClient } from 'apollo-server-testing'
|
import { createTestClient } from 'apollo-server-testing'
|
||||||
|
|||||||
@ -253,6 +253,42 @@ const isMemberOfGroup = rule({
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const canRemoveUserFromGroup = rule({
|
||||||
|
cache: 'no_cache',
|
||||||
|
})(async (_parent, args, { user, driver }) => {
|
||||||
|
if (!(user && user.id)) return false
|
||||||
|
const { groupId, userId } = args
|
||||||
|
const currentUserId = user.id
|
||||||
|
if (currentUserId === userId) return false
|
||||||
|
const session = driver.session()
|
||||||
|
const readTxPromise = session.readTransaction(async (transaction) => {
|
||||||
|
const transactionResponse = await transaction.run(
|
||||||
|
`
|
||||||
|
MATCH (User {id: $currentUserId})-[currentUserMembership:MEMBER_OF]->(group:Group {id: $groupId})
|
||||||
|
OPTIONAL MATCH (group)<-[userMembership:MEMBER_OF]-(user:User { id: $userId })
|
||||||
|
RETURN currentUserMembership.role AS currentUserRole, userMembership.role AS userRole
|
||||||
|
`,
|
||||||
|
{ currentUserId, groupId, userId },
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
currentUserRole: transactionResponse.records.map((record) =>
|
||||||
|
record.get('currentUserRole'),
|
||||||
|
)[0],
|
||||||
|
userRole: transactionResponse.records.map((record) => record.get('userRole'))[0],
|
||||||
|
}
|
||||||
|
})
|
||||||
|
try {
|
||||||
|
const { currentUserRole, userRole } = await readTxPromise
|
||||||
|
return (
|
||||||
|
currentUserRole && ['owner'].includes(currentUserRole) && userRole && userRole !== 'owner'
|
||||||
|
)
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(error)
|
||||||
|
} finally {
|
||||||
|
session.close()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
const canCommentPost = rule({
|
const canCommentPost = rule({
|
||||||
cache: 'no_cache',
|
cache: 'no_cache',
|
||||||
})(async (_parent, args, { user, driver }) => {
|
})(async (_parent, args, { user, driver }) => {
|
||||||
@ -382,6 +418,7 @@ export default shield(
|
|||||||
JoinGroup: isAllowedToJoinGroup,
|
JoinGroup: isAllowedToJoinGroup,
|
||||||
LeaveGroup: isAllowedToLeaveGroup,
|
LeaveGroup: isAllowedToLeaveGroup,
|
||||||
ChangeGroupMemberRole: isAllowedToChangeGroupMemberRole,
|
ChangeGroupMemberRole: isAllowedToChangeGroupMemberRole,
|
||||||
|
RemoveUserFromGroup: canRemoveUserFromGroup,
|
||||||
CreatePost: and(isAuthenticated, isMemberOfGroup),
|
CreatePost: and(isAuthenticated, isMemberOfGroup),
|
||||||
UpdatePost: isAuthor,
|
UpdatePost: isAuthor,
|
||||||
DeletePost: isAuthor,
|
DeletePost: isAuthor,
|
||||||
@ -412,6 +449,7 @@ export default shield(
|
|||||||
blockUser: isAuthenticated,
|
blockUser: isAuthenticated,
|
||||||
unblockUser: isAuthenticated,
|
unblockUser: isAuthenticated,
|
||||||
markAsRead: isAuthenticated,
|
markAsRead: isAuthenticated,
|
||||||
|
markAllAsRead: isAuthenticated,
|
||||||
AddEmailAddress: isAuthenticated,
|
AddEmailAddress: isAuthenticated,
|
||||||
VerifyEmailAddress: isAuthenticated,
|
VerifyEmailAddress: isAuthenticated,
|
||||||
pinPost: isAdmin,
|
pinPost: isAdmin,
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { createTestClient } from 'apollo-server-testing'
|
import { createTestClient } from 'apollo-server-testing'
|
||||||
import createServer from '../server'
|
import createServer from '../server'
|
||||||
import Factory, { cleanDatabase } from '../db/factories'
|
import Factory, { cleanDatabase } from '../db/factories'
|
||||||
import { gql } from '../helpers/jest'
|
import gql from 'graphql-tag'
|
||||||
import { getDriver, getNeode } from '../db/neo4j'
|
import { getDriver, getNeode } from '../db/neo4j'
|
||||||
import CONFIG from '../config'
|
import CONFIG from '../config'
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import Factory, { cleanDatabase } from '../../db/factories'
|
import Factory, { cleanDatabase } from '../../db/factories'
|
||||||
import { gql } from '../../helpers/jest'
|
import gql from 'graphql-tag'
|
||||||
import { getNeode, getDriver } from '../../db/neo4j'
|
import { getNeode, getDriver } from '../../db/neo4j'
|
||||||
import createServer from '../../server'
|
import createServer from '../../server'
|
||||||
import { createTestClient } from 'apollo-server-testing'
|
import { createTestClient } from 'apollo-server-testing'
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import Factory, { cleanDatabase } from '../db/factories'
|
import Factory, { cleanDatabase } from '../db/factories'
|
||||||
import { gql } from '../helpers/jest'
|
import gql from 'graphql-tag'
|
||||||
import { getNeode, getDriver } from '../db/neo4j'
|
import { getNeode, getDriver } from '../db/neo4j'
|
||||||
import createServer from '../server'
|
import createServer from '../server'
|
||||||
import { createTestClient } from 'apollo-server-testing'
|
import { createTestClient } from 'apollo-server-testing'
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { gql } from '../../helpers/jest'
|
import gql from 'graphql-tag'
|
||||||
import Factory, { cleanDatabase } from '../../db/factories'
|
import Factory, { cleanDatabase } from '../../db/factories'
|
||||||
import { getNeode, getDriver } from '../../db/neo4j'
|
import { getNeode, getDriver } from '../../db/neo4j'
|
||||||
import { createTestClient } from 'apollo-server-testing'
|
import { createTestClient } from 'apollo-server-testing'
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import Factory, { cleanDatabase } from '../../db/factories'
|
import Factory, { cleanDatabase } from '../../db/factories'
|
||||||
import { gql } from '../../helpers/jest'
|
import gql from 'graphql-tag'
|
||||||
import { createTestClient } from 'apollo-server-testing'
|
import { createTestClient } from 'apollo-server-testing'
|
||||||
import createServer from '../../server'
|
import createServer from '../../server'
|
||||||
import { getNeode, getDriver } from '../../db/neo4j'
|
import { getNeode, getDriver } from '../../db/neo4j'
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { createTestClient } from 'apollo-server-testing'
|
import { createTestClient } from 'apollo-server-testing'
|
||||||
import Factory, { cleanDatabase } from '../../db/factories'
|
import Factory, { cleanDatabase } from '../../db/factories'
|
||||||
import { gql } from '../../helpers/jest'
|
import gql from 'graphql-tag'
|
||||||
import { getNeode, getDriver } from '../../db/neo4j'
|
import { getNeode, getDriver } from '../../db/neo4j'
|
||||||
import createServer from '../../server'
|
import createServer from '../../server'
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import Factory, { cleanDatabase } from '../../db/factories'
|
import Factory, { cleanDatabase } from '../../db/factories'
|
||||||
import { gql } from '../../helpers/jest'
|
import gql from 'graphql-tag'
|
||||||
import { getDriver, getNeode } from '../../db/neo4j'
|
import { getDriver, getNeode } from '../../db/neo4j'
|
||||||
import createServer from '../../server'
|
import createServer from '../../server'
|
||||||
import { createTestClient } from 'apollo-server-testing'
|
import { createTestClient } from 'apollo-server-testing'
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import fs from 'fs'
|
|||||||
import path from 'path'
|
import path from 'path'
|
||||||
import { createTestClient } from 'apollo-server-testing'
|
import { createTestClient } from 'apollo-server-testing'
|
||||||
import createServer from '../../server'
|
import createServer from '../../server'
|
||||||
import { gql } from '../../helpers/jest'
|
import gql from 'graphql-tag'
|
||||||
|
|
||||||
jest.mock('node-fetch')
|
jest.mock('node-fetch')
|
||||||
const { Response } = jest.requireActual('node-fetch')
|
const { Response } = jest.requireActual('node-fetch')
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import { createTestClient } from 'apollo-server-testing'
|
|||||||
import Factory, { cleanDatabase } from '../../db/factories'
|
import Factory, { cleanDatabase } from '../../db/factories'
|
||||||
import { getDriver, getNeode } from '../../db/neo4j'
|
import { getDriver, getNeode } from '../../db/neo4j'
|
||||||
import createServer from '../../server'
|
import createServer from '../../server'
|
||||||
import { gql } from '../../helpers/jest'
|
import gql from 'graphql-tag'
|
||||||
|
|
||||||
const driver = getDriver()
|
const driver = getDriver()
|
||||||
const neode = getNeode()
|
const neode = getNeode()
|
||||||
|
|||||||
@ -295,25 +295,8 @@ export default {
|
|||||||
LeaveGroup: async (_parent, params, context, _resolveInfo) => {
|
LeaveGroup: async (_parent, params, context, _resolveInfo) => {
|
||||||
const { groupId, userId } = params
|
const { groupId, userId } = params
|
||||||
const session = context.driver.session()
|
const session = context.driver.session()
|
||||||
const writeTxResultPromise = session.writeTransaction(async (transaction) => {
|
|
||||||
const leaveGroupCypher = `
|
|
||||||
MATCH (member:User {id: $userId})-[membership:MEMBER_OF]->(group:Group {id: $groupId})
|
|
||||||
DELETE membership
|
|
||||||
WITH member, group
|
|
||||||
OPTIONAL MATCH (p:Post)-[:IN]->(group)
|
|
||||||
WHERE NOT group.groupType = 'public'
|
|
||||||
WITH member, group, collect(p) AS posts
|
|
||||||
FOREACH (post IN posts |
|
|
||||||
MERGE (member)-[:CANNOT_SEE]->(post))
|
|
||||||
RETURN member {.*, myRoleInGroup: NULL}
|
|
||||||
`
|
|
||||||
|
|
||||||
const transactionResponse = await transaction.run(leaveGroupCypher, { groupId, userId })
|
|
||||||
const [member] = await transactionResponse.records.map((record) => record.get('member'))
|
|
||||||
return member
|
|
||||||
})
|
|
||||||
try {
|
try {
|
||||||
return await writeTxResultPromise
|
return await removeUserFromGroupWriteTxResultPromise(session, groupId, userId)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new Error(error)
|
throw new Error(error)
|
||||||
} finally {
|
} finally {
|
||||||
@ -368,6 +351,17 @@ export default {
|
|||||||
session.close()
|
session.close()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
RemoveUserFromGroup: async (_parent, params, context, _resolveInfo) => {
|
||||||
|
const { groupId, userId } = params
|
||||||
|
const session = context.driver.session()
|
||||||
|
try {
|
||||||
|
return await removeUserFromGroupWriteTxResultPromise(session, groupId, userId)
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(error)
|
||||||
|
} finally {
|
||||||
|
session.close()
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
Group: {
|
Group: {
|
||||||
...Resolver('Group', {
|
...Resolver('Group', {
|
||||||
@ -383,3 +377,27 @@ export default {
|
|||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const removeUserFromGroupWriteTxResultPromise = async (session, groupId, userId) => {
|
||||||
|
return session.writeTransaction(async (transaction) => {
|
||||||
|
const removeUserFromGroupCypher = `
|
||||||
|
MATCH (user:User {id: $userId})-[membership:MEMBER_OF]->(group:Group {id: $groupId})
|
||||||
|
DELETE membership
|
||||||
|
WITH user, group
|
||||||
|
OPTIONAL MATCH (author:User)-[:WROTE]->(p:Post)-[:IN]->(group)
|
||||||
|
WHERE NOT group.groupType = 'public'
|
||||||
|
AND NOT author.id = $userId
|
||||||
|
WITH user, collect(p) AS posts
|
||||||
|
FOREACH (post IN posts |
|
||||||
|
MERGE (user)-[:CANNOT_SEE]->(post))
|
||||||
|
RETURN user {.*, myRoleInGroup: NULL}
|
||||||
|
`
|
||||||
|
|
||||||
|
const transactionResponse = await transaction.run(removeUserFromGroupCypher, {
|
||||||
|
groupId,
|
||||||
|
userId,
|
||||||
|
})
|
||||||
|
const [user] = await transactionResponse.records.map((record) => record.get('user'))
|
||||||
|
return user
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import {
|
|||||||
joinGroupMutation,
|
joinGroupMutation,
|
||||||
leaveGroupMutation,
|
leaveGroupMutation,
|
||||||
changeGroupMemberRoleMutation,
|
changeGroupMemberRoleMutation,
|
||||||
|
removeUserFromGroupMutation,
|
||||||
groupMembersQuery,
|
groupMembersQuery,
|
||||||
groupQuery,
|
groupQuery,
|
||||||
} from '../../graphql/groups'
|
} from '../../graphql/groups'
|
||||||
@ -196,7 +197,6 @@ const seedComplexScenarioAndClearAuthentication = async () => {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
// hidden-group
|
// hidden-group
|
||||||
authenticatedUser = await adminMemberUser.toJson()
|
|
||||||
await mutate({
|
await mutate({
|
||||||
mutation: createGroupMutation(),
|
mutation: createGroupMutation(),
|
||||||
variables: {
|
variables: {
|
||||||
@ -214,32 +214,17 @@ const seedComplexScenarioAndClearAuthentication = async () => {
|
|||||||
mutation: changeGroupMemberRoleMutation(),
|
mutation: changeGroupMemberRoleMutation(),
|
||||||
variables: {
|
variables: {
|
||||||
groupId: 'hidden-group',
|
groupId: 'hidden-group',
|
||||||
userId: 'admin-member-user',
|
userId: 'usual-member-user',
|
||||||
roleInGroup: 'usual',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
await mutate({
|
|
||||||
mutation: changeGroupMemberRoleMutation(),
|
|
||||||
variables: {
|
|
||||||
groupId: 'hidden-group',
|
|
||||||
userId: 'second-owner-member-user',
|
|
||||||
roleInGroup: 'usual',
|
roleInGroup: 'usual',
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
await mutate({
|
await mutate({
|
||||||
mutation: changeGroupMemberRoleMutation(),
|
mutation: changeGroupMemberRoleMutation(),
|
||||||
variables: {
|
variables: {
|
||||||
groupId: 'hidden-group',
|
groupId: 'hidden-group',
|
||||||
userId: 'admin-member-user',
|
userId: 'admin-member-user',
|
||||||
roleInGroup: 'usual',
|
roleInGroup: 'admin',
|
||||||
},
|
|
||||||
})
|
|
||||||
await mutate({
|
|
||||||
mutation: changeGroupMemberRoleMutation(),
|
|
||||||
variables: {
|
|
||||||
groupId: 'hidden-group',
|
|
||||||
userId: 'second-owner-member-user',
|
|
||||||
roleInGroup: 'usual',
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -2982,4 +2967,192 @@ describe('in mode', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('RemoveUserFromGroup', () => {
|
||||||
|
beforeAll(async () => {
|
||||||
|
await seedComplexScenarioAndClearAuthentication()
|
||||||
|
})
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await cleanDatabase()
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('unauthenticated', () => {
|
||||||
|
it('throws an error', async () => {
|
||||||
|
await expect(
|
||||||
|
mutate({
|
||||||
|
mutation: removeUserFromGroupMutation(),
|
||||||
|
variables: {
|
||||||
|
groupId: 'hidden-group',
|
||||||
|
userId: 'usual-member-user',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
).resolves.toMatchObject({
|
||||||
|
errors: expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
message: 'Not Authorized!',
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('authenticated', () => {
|
||||||
|
describe('as usual member', () => {
|
||||||
|
it('throws an error', async () => {
|
||||||
|
authenticatedUser = await usualMemberUser.toJson()
|
||||||
|
await expect(
|
||||||
|
mutate({
|
||||||
|
mutation: removeUserFromGroupMutation(),
|
||||||
|
variables: {
|
||||||
|
groupId: 'hidden-group',
|
||||||
|
userId: 'admin-member-user',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
).resolves.toMatchObject({
|
||||||
|
errors: expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
message: 'Not Authorized!',
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('as owner', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
authenticatedUser = await ownerMemberUser.toJson()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('removes the user from the group', async () => {
|
||||||
|
await expect(
|
||||||
|
mutate({
|
||||||
|
mutation: removeUserFromGroupMutation(),
|
||||||
|
variables: {
|
||||||
|
groupId: 'hidden-group',
|
||||||
|
userId: 'usual-member-user',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
).resolves.toMatchObject({
|
||||||
|
data: {
|
||||||
|
RemoveUserFromGroup: expect.objectContaining({
|
||||||
|
id: 'usual-member-user',
|
||||||
|
myRoleInGroup: null,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
errors: undefined,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('cannot remove self', async () => {
|
||||||
|
await expect(
|
||||||
|
mutate({
|
||||||
|
mutation: removeUserFromGroupMutation(),
|
||||||
|
variables: {
|
||||||
|
groupId: 'hidden-group',
|
||||||
|
userId: 'owner-member-user',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
).resolves.toMatchObject({
|
||||||
|
errors: expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
message: 'Not Authorized!',
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('as admin', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
authenticatedUser = await adminMemberUser.toJson()
|
||||||
|
await mutate({
|
||||||
|
mutation: changeGroupMemberRoleMutation(),
|
||||||
|
variables: {
|
||||||
|
groupId: 'hidden-group',
|
||||||
|
userId: 'usual-member-user',
|
||||||
|
roleInGroup: 'usual',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('throws an error', async () => {
|
||||||
|
authenticatedUser = await usualMemberUser.toJson()
|
||||||
|
await expect(
|
||||||
|
mutate({
|
||||||
|
mutation: removeUserFromGroupMutation(),
|
||||||
|
variables: {
|
||||||
|
groupId: 'hidden-group',
|
||||||
|
userId: 'admin-member-user',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
).resolves.toMatchObject({
|
||||||
|
errors: expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
message: 'Not Authorized!',
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
/*
|
||||||
|
it('removes the user from the group', async () => {
|
||||||
|
await expect(
|
||||||
|
mutate({
|
||||||
|
mutation: removeUserFromGroupMutation(),
|
||||||
|
variables: {
|
||||||
|
groupId: 'hidden-group',
|
||||||
|
userId: 'usual-member-user',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
).resolves.toMatchObject({
|
||||||
|
data: {
|
||||||
|
RemoveUserFromGroup: expect.objectContaining({
|
||||||
|
id: 'usual-member-user',
|
||||||
|
myRoleInGroup: null,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
errors: undefined,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('cannot remove self', async () => {
|
||||||
|
await expect(
|
||||||
|
mutate({
|
||||||
|
mutation: removeUserFromGroupMutation(),
|
||||||
|
variables: {
|
||||||
|
groupId: 'hidden-group',
|
||||||
|
userId: 'admin-member-user',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
).resolves.toMatchObject({
|
||||||
|
errors: expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
message: 'Not Authorized!',
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('cannot remove owner', async () => {
|
||||||
|
await expect(
|
||||||
|
mutate({
|
||||||
|
mutation: removeUserFromGroupMutation(),
|
||||||
|
variables: {
|
||||||
|
groupId: 'hidden-group',
|
||||||
|
userId: 'owner-member-user',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
).resolves.toMatchObject({
|
||||||
|
errors: expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
message: 'Not Authorized!',
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
*/
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@ -0,0 +1,40 @@
|
|||||||
|
import { mergeWith, isArray } from 'lodash'
|
||||||
|
|
||||||
|
const getMyGroupIds = async (context) => {
|
||||||
|
const { user } = context
|
||||||
|
if (!(user && user.id)) return []
|
||||||
|
const session = context.driver.session()
|
||||||
|
|
||||||
|
const readTxResultPromise = await session.readTransaction(async (transaction) => {
|
||||||
|
const cypher = `
|
||||||
|
MATCH (group:Group)<-[membership:MEMBER_OF]-(:User { id: $userId })
|
||||||
|
WHERE membership.role IN ['usual', 'admin', 'owner']
|
||||||
|
RETURN collect(group.id) AS myGroupIds`
|
||||||
|
const getMyGroupIdsResponse = await transaction.run(cypher, { userId: user.id })
|
||||||
|
return getMyGroupIdsResponse.records.map((record) => record.get('myGroupIds'))
|
||||||
|
})
|
||||||
|
try {
|
||||||
|
const [myGroupIds] = readTxResultPromise
|
||||||
|
return myGroupIds
|
||||||
|
} finally {
|
||||||
|
session.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const filterPostsOfMyGroups = async (params, context) => {
|
||||||
|
if (!(params.filter && params.filter.postsInMyGroups)) return params
|
||||||
|
delete params.filter.postsInMyGroups
|
||||||
|
const myGroupIds = await getMyGroupIds(context)
|
||||||
|
params.filter = mergeWith(
|
||||||
|
params.filter,
|
||||||
|
{
|
||||||
|
group: { id_in: myGroupIds },
|
||||||
|
},
|
||||||
|
(objValue, srcValue) => {
|
||||||
|
if (isArray(objValue)) {
|
||||||
|
return objValue.concat(srcValue)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
return params
|
||||||
|
}
|
||||||
@ -1,6 +1,6 @@
|
|||||||
import Factory, { cleanDatabase } from '../../db/factories'
|
import Factory, { cleanDatabase } from '../../db/factories'
|
||||||
import { getDriver } from '../../db/neo4j'
|
import { getDriver } from '../../db/neo4j'
|
||||||
import { gql } from '../../helpers/jest'
|
import gql from 'graphql-tag'
|
||||||
import createServer from '../../server'
|
import createServer from '../../server'
|
||||||
import { createTestClient } from 'apollo-server-testing'
|
import { createTestClient } from 'apollo-server-testing'
|
||||||
import CONSTANTS_REGISTRATION from './../../constants/registration'
|
import CONSTANTS_REGISTRATION from './../../constants/registration'
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import Factory, { cleanDatabase } from '../../db/factories'
|
import Factory, { cleanDatabase } from '../../db/factories'
|
||||||
import { gql } from '../../helpers/jest'
|
import gql from 'graphql-tag'
|
||||||
import { getNeode, getDriver } from '../../db/neo4j'
|
import { getNeode, getDriver } from '../../db/neo4j'
|
||||||
import createServer from '../../server'
|
import createServer from '../../server'
|
||||||
import { createTestClient } from 'apollo-server-testing'
|
import { createTestClient } from 'apollo-server-testing'
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { createTestClient } from 'apollo-server-testing'
|
import { createTestClient } from 'apollo-server-testing'
|
||||||
import Factory, { cleanDatabase } from '../../db/factories'
|
import Factory, { cleanDatabase } from '../../db/factories'
|
||||||
import { gql } from '../../helpers/jest'
|
import gql from 'graphql-tag'
|
||||||
import { getNeode, getDriver } from '../../db/neo4j'
|
import { getNeode, getDriver } from '../../db/neo4j'
|
||||||
import createServer from '../../server'
|
import createServer from '../../server'
|
||||||
|
|
||||||
|
|||||||
@ -99,6 +99,35 @@ export default {
|
|||||||
session.close()
|
session.close()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
markAllAsRead: async (parent, args, context, resolveInfo) => {
|
||||||
|
const { user: currentUser } = context
|
||||||
|
const session = context.driver.session()
|
||||||
|
const writeTxResultPromise = session.writeTransaction(async (transaction) => {
|
||||||
|
const markAllNotificationAsReadTransactionResponse = await transaction.run(
|
||||||
|
`
|
||||||
|
MATCH (resource)-[notification:NOTIFIED {read: FALSE}]->(user:User {id:$id})
|
||||||
|
SET notification.read = TRUE
|
||||||
|
WITH user, notification, resource,
|
||||||
|
[(resource)<-[:WROTE]-(author:User) | author {.*}] AS authors,
|
||||||
|
[(resource)-[:COMMENTS]->(post:Post)<-[:WROTE]-(author:User) | post{.*, author: properties(author)} ] AS posts
|
||||||
|
WITH resource, user, notification, authors, posts,
|
||||||
|
resource {.*, __typename: labels(resource)[0], author: authors[0], post: posts[0]} AS finalResource
|
||||||
|
RETURN notification {.*, from: finalResource, to: properties(user)}
|
||||||
|
`,
|
||||||
|
{ id: currentUser.id },
|
||||||
|
)
|
||||||
|
log(markAllNotificationAsReadTransactionResponse)
|
||||||
|
return markAllNotificationAsReadTransactionResponse.records.map((record) =>
|
||||||
|
record.get('notification'),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
try {
|
||||||
|
const notifications = await writeTxResultPromise
|
||||||
|
return notifications
|
||||||
|
} finally {
|
||||||
|
session.close()
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
NOTIFIED: {
|
NOTIFIED: {
|
||||||
id: async (parent) => {
|
id: async (parent) => {
|
||||||
|
|||||||
@ -1,8 +1,13 @@
|
|||||||
import Factory, { cleanDatabase } from '../../db/factories'
|
import Factory, { cleanDatabase } from '../../db/factories'
|
||||||
import { gql } from '../../helpers/jest'
|
import gql from 'graphql-tag'
|
||||||
import { getDriver } from '../../db/neo4j'
|
import { getDriver } from '../../db/neo4j'
|
||||||
import { createTestClient } from 'apollo-server-testing'
|
import { createTestClient } from 'apollo-server-testing'
|
||||||
import createServer from '../.././server'
|
import createServer from '../.././server'
|
||||||
|
import {
|
||||||
|
markAsReadMutation,
|
||||||
|
markAllAsReadMutation,
|
||||||
|
notificationQuery,
|
||||||
|
} from '../../graphql/notifications'
|
||||||
|
|
||||||
const driver = getDriver()
|
const driver = getDriver()
|
||||||
let authenticatedUser
|
let authenticatedUser
|
||||||
@ -146,26 +151,9 @@ describe('given some notifications', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
describe('notifications', () => {
|
describe('notifications', () => {
|
||||||
const notificationQuery = gql`
|
|
||||||
query ($read: Boolean, $orderBy: NotificationOrdering) {
|
|
||||||
notifications(read: $read, orderBy: $orderBy) {
|
|
||||||
from {
|
|
||||||
__typename
|
|
||||||
... on Post {
|
|
||||||
content
|
|
||||||
}
|
|
||||||
... on Comment {
|
|
||||||
content
|
|
||||||
}
|
|
||||||
}
|
|
||||||
read
|
|
||||||
createdAt
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`
|
|
||||||
describe('unauthenticated', () => {
|
describe('unauthenticated', () => {
|
||||||
it('throws authorization error', async () => {
|
it('throws authorization error', async () => {
|
||||||
const { errors } = await query({ query: notificationQuery })
|
const { errors } = await query({ query: notificationQuery() })
|
||||||
expect(errors[0]).toHaveProperty('message', 'Not Authorized!')
|
expect(errors[0]).toHaveProperty('message', 'Not Authorized!')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@ -212,7 +200,7 @@ describe('given some notifications', () => {
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
await expect(query({ query: notificationQuery, variables })).resolves.toMatchObject({
|
await expect(query({ query: notificationQuery(), variables })).resolves.toMatchObject({
|
||||||
data: {
|
data: {
|
||||||
notifications: expect.arrayContaining(expected),
|
notifications: expect.arrayContaining(expected),
|
||||||
},
|
},
|
||||||
@ -246,7 +234,7 @@ describe('given some notifications', () => {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
const response = await query({
|
const response = await query({
|
||||||
query: notificationQuery,
|
query: notificationQuery(),
|
||||||
variables: { ...variables, read: false },
|
variables: { ...variables, read: false },
|
||||||
})
|
})
|
||||||
await expect(response).toMatchObject(expected)
|
await expect(response).toMatchObject(expected)
|
||||||
@ -275,14 +263,14 @@ describe('given some notifications', () => {
|
|||||||
|
|
||||||
it('reduces notifications list', async () => {
|
it('reduces notifications list', async () => {
|
||||||
await expect(
|
await expect(
|
||||||
query({ query: notificationQuery, variables: { ...variables, read: false } }),
|
query({ query: notificationQuery(), variables: { ...variables, read: false } }),
|
||||||
).resolves.toMatchObject({
|
).resolves.toMatchObject({
|
||||||
data: { notifications: [expect.any(Object), expect.any(Object)] },
|
data: { notifications: [expect.any(Object), expect.any(Object)] },
|
||||||
errors: undefined,
|
errors: undefined,
|
||||||
})
|
})
|
||||||
await deletePostAction()
|
await deletePostAction()
|
||||||
await expect(
|
await expect(
|
||||||
query({ query: notificationQuery, variables: { ...variables, read: false } }),
|
query({ query: notificationQuery(), variables: { ...variables, read: false } }),
|
||||||
).resolves.toMatchObject({ data: { notifications: [] }, errors: undefined })
|
).resolves.toMatchObject({ data: { notifications: [] }, errors: undefined })
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@ -291,27 +279,10 @@ describe('given some notifications', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
describe('markAsRead', () => {
|
describe('markAsRead', () => {
|
||||||
const markAsReadMutation = gql`
|
|
||||||
mutation ($id: ID!) {
|
|
||||||
markAsRead(id: $id) {
|
|
||||||
from {
|
|
||||||
__typename
|
|
||||||
... on Post {
|
|
||||||
content
|
|
||||||
}
|
|
||||||
... on Comment {
|
|
||||||
content
|
|
||||||
}
|
|
||||||
}
|
|
||||||
read
|
|
||||||
createdAt
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`
|
|
||||||
describe('unauthenticated', () => {
|
describe('unauthenticated', () => {
|
||||||
it('throws authorization error', async () => {
|
it('throws authorization error', async () => {
|
||||||
const result = await mutate({
|
const result = await mutate({
|
||||||
mutation: markAsReadMutation,
|
mutation: markAsReadMutation(),
|
||||||
variables: { ...variables, id: 'p1' },
|
variables: { ...variables, id: 'p1' },
|
||||||
})
|
})
|
||||||
expect(result.errors[0]).toHaveProperty('message', 'Not Authorized!')
|
expect(result.errors[0]).toHaveProperty('message', 'Not Authorized!')
|
||||||
@ -332,7 +303,7 @@ describe('given some notifications', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('returns null', async () => {
|
it('returns null', async () => {
|
||||||
const response = await mutate({ mutation: markAsReadMutation, variables })
|
const response = await mutate({ mutation: markAsReadMutation(), variables })
|
||||||
expect(response.data.markAsRead).toEqual(null)
|
expect(response.data.markAsRead).toEqual(null)
|
||||||
expect(response.errors).toBeUndefined()
|
expect(response.errors).toBeUndefined()
|
||||||
})
|
})
|
||||||
@ -348,7 +319,7 @@ describe('given some notifications', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('updates `read` attribute and returns NOTIFIED relationship', async () => {
|
it('updates `read` attribute and returns NOTIFIED relationship', async () => {
|
||||||
const { data } = await mutate({ mutation: markAsReadMutation, variables })
|
const { data } = await mutate({ mutation: markAsReadMutation(), variables })
|
||||||
expect(data).toEqual({
|
expect(data).toEqual({
|
||||||
markAsRead: {
|
markAsRead: {
|
||||||
from: {
|
from: {
|
||||||
@ -369,7 +340,7 @@ describe('given some notifications', () => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
it('returns null', async () => {
|
it('returns null', async () => {
|
||||||
const response = await mutate({ mutation: markAsReadMutation, variables })
|
const response = await mutate({ mutation: markAsReadMutation(), variables })
|
||||||
expect(response.data.markAsRead).toEqual(null)
|
expect(response.data.markAsRead).toEqual(null)
|
||||||
expect(response.errors).toBeUndefined()
|
expect(response.errors).toBeUndefined()
|
||||||
})
|
})
|
||||||
@ -385,7 +356,7 @@ describe('given some notifications', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('updates `read` attribute and returns NOTIFIED relationship', async () => {
|
it('updates `read` attribute and returns NOTIFIED relationship', async () => {
|
||||||
const { data } = await mutate({ mutation: markAsReadMutation, variables })
|
const { data } = await mutate({ mutation: markAsReadMutation(), variables })
|
||||||
expect(data).toEqual({
|
expect(data).toEqual({
|
||||||
markAsRead: {
|
markAsRead: {
|
||||||
from: {
|
from: {
|
||||||
@ -401,4 +372,46 @@ describe('given some notifications', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('markAllAsRead', () => {
|
||||||
|
describe('unauthenticated', () => {
|
||||||
|
it('throws authorization error', async () => {
|
||||||
|
const result = await mutate({
|
||||||
|
mutation: markAllAsReadMutation(),
|
||||||
|
})
|
||||||
|
expect(result.errors[0]).toHaveProperty('message', 'Not Authorized!')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('authenticated', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
authenticatedUser = await user.toJson()
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('not being notified at all', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
variables = {
|
||||||
|
...variables,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it('returns all as read', async () => {
|
||||||
|
const response = await mutate({ mutation: markAllAsReadMutation(), variables })
|
||||||
|
expect(response.data.markAllAsRead).toEqual([
|
||||||
|
{
|
||||||
|
createdAt: '2019-08-30T19:33:48.651Z',
|
||||||
|
from: { __typename: 'Comment', content: 'You have been mentioned in a comment' },
|
||||||
|
read: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
createdAt: '2019-08-31T17:33:48.651Z',
|
||||||
|
from: { __typename: 'Post', content: 'You have been mentioned in a post' },
|
||||||
|
read: true,
|
||||||
|
},
|
||||||
|
])
|
||||||
|
expect(response.errors).toBeUndefined()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import Factory, { cleanDatabase } from '../../db/factories'
|
import Factory, { cleanDatabase } from '../../db/factories'
|
||||||
import { gql } from '../../helpers/jest'
|
import gql from 'graphql-tag'
|
||||||
import { getNeode, getDriver } from '../../db/neo4j'
|
import { getNeode, getDriver } from '../../db/neo4j'
|
||||||
import CONSTANTS_REGISTRATION from './../../constants/registration'
|
import CONSTANTS_REGISTRATION from './../../constants/registration'
|
||||||
import createPasswordReset from './helpers/createPasswordReset'
|
import createPasswordReset from './helpers/createPasswordReset'
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import { mergeImage, deleteImage } from './images/images'
|
|||||||
import Resolver from './helpers/Resolver'
|
import Resolver from './helpers/Resolver'
|
||||||
import { filterForMutedUsers } from './helpers/filterForMutedUsers'
|
import { filterForMutedUsers } from './helpers/filterForMutedUsers'
|
||||||
import { filterInvisiblePosts } from './helpers/filterInvisiblePosts'
|
import { filterInvisiblePosts } from './helpers/filterInvisiblePosts'
|
||||||
|
import { filterPostsOfMyGroups } from './helpers/filterPostsOfMyGroups'
|
||||||
import CONFIG from '../../config'
|
import CONFIG from '../../config'
|
||||||
|
|
||||||
const maintainPinnedPosts = (params) => {
|
const maintainPinnedPosts = (params) => {
|
||||||
@ -21,12 +22,14 @@ const maintainPinnedPosts = (params) => {
|
|||||||
export default {
|
export default {
|
||||||
Query: {
|
Query: {
|
||||||
Post: async (object, params, context, resolveInfo) => {
|
Post: async (object, params, context, resolveInfo) => {
|
||||||
|
params = await filterPostsOfMyGroups(params, context)
|
||||||
params = await filterInvisiblePosts(params, context)
|
params = await filterInvisiblePosts(params, context)
|
||||||
params = await filterForMutedUsers(params, context)
|
params = await filterForMutedUsers(params, context)
|
||||||
params = await maintainPinnedPosts(params)
|
params = await maintainPinnedPosts(params)
|
||||||
return neo4jgraphql(object, params, context, resolveInfo)
|
return neo4jgraphql(object, params, context, resolveInfo)
|
||||||
},
|
},
|
||||||
profilePagePosts: async (object, params, context, resolveInfo) => {
|
profilePagePosts: async (object, params, context, resolveInfo) => {
|
||||||
|
params = await filterPostsOfMyGroups(params, context)
|
||||||
params = await filterInvisiblePosts(params, context)
|
params = await filterInvisiblePosts(params, context)
|
||||||
params = await filterForMutedUsers(params, context)
|
params = await filterForMutedUsers(params, context)
|
||||||
return neo4jgraphql(object, params, context, resolveInfo)
|
return neo4jgraphql(object, params, context, resolveInfo)
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { createTestClient } from 'apollo-server-testing'
|
import { createTestClient } from 'apollo-server-testing'
|
||||||
import Factory, { cleanDatabase } from '../../db/factories'
|
import Factory, { cleanDatabase } from '../../db/factories'
|
||||||
import { gql } from '../../helpers/jest'
|
import gql from 'graphql-tag'
|
||||||
import { getNeode, getDriver } from '../../db/neo4j'
|
import { getNeode, getDriver } from '../../db/neo4j'
|
||||||
import createServer from '../../server'
|
import createServer from '../../server'
|
||||||
|
|
||||||
|
|||||||
@ -1524,9 +1524,9 @@ describe('Posts in Groups', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('does not show the posts of the closed group anymore', async () => {
|
it('stil shows the posts of the closed group', async () => {
|
||||||
const result = await query({ query: filterPosts(), variables: {} })
|
const result = await query({ query: filterPosts(), variables: {} })
|
||||||
expect(result.data.Post).toHaveLength(3)
|
expect(result.data.Post).toHaveLength(4)
|
||||||
expect(result).toMatchObject({
|
expect(result).toMatchObject({
|
||||||
data: {
|
data: {
|
||||||
Post: expect.arrayContaining([
|
Post: expect.arrayContaining([
|
||||||
@ -1540,6 +1540,11 @@ describe('Posts in Groups', () => {
|
|||||||
title: 'A post without a group',
|
title: 'A post without a group',
|
||||||
content: 'I am a user who does not belong to a group yet.',
|
content: 'I am a user who does not belong to a group yet.',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 'post-to-closed-group',
|
||||||
|
title: 'A post to a closed group',
|
||||||
|
content: 'I am posting into a closed group as a member of the group',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: 'post-to-hidden-group',
|
id: 'post-to-hidden-group',
|
||||||
title: 'A post to a hidden group',
|
title: 'A post to a hidden group',
|
||||||
@ -1564,9 +1569,9 @@ describe('Posts in Groups', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('does only show the public posts', async () => {
|
it('still shows the post of the hidden group', async () => {
|
||||||
const result = await query({ query: filterPosts(), variables: {} })
|
const result = await query({ query: filterPosts(), variables: {} })
|
||||||
expect(result.data.Post).toHaveLength(2)
|
expect(result.data.Post).toHaveLength(4)
|
||||||
expect(result).toMatchObject({
|
expect(result).toMatchObject({
|
||||||
data: {
|
data: {
|
||||||
Post: expect.arrayContaining([
|
Post: expect.arrayContaining([
|
||||||
@ -1580,6 +1585,16 @@ describe('Posts in Groups', () => {
|
|||||||
title: 'A post without a group',
|
title: 'A post without a group',
|
||||||
content: 'I am a user who does not belong to a group yet.',
|
content: 'I am a user who does not belong to a group yet.',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 'post-to-closed-group',
|
||||||
|
title: 'A post to a closed group',
|
||||||
|
content: 'I am posting into a closed group as a member of the group',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'post-to-hidden-group',
|
||||||
|
title: 'A post to a hidden group',
|
||||||
|
content: 'I am posting into a hidden group as a member of the group',
|
||||||
|
},
|
||||||
]),
|
]),
|
||||||
},
|
},
|
||||||
errors: undefined,
|
errors: undefined,
|
||||||
@ -1603,9 +1618,9 @@ describe('Posts in Groups', () => {
|
|||||||
authenticatedUser = await allGroupsUser.toJson()
|
authenticatedUser = await allGroupsUser.toJson()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('does not show the posts of the closed group', async () => {
|
it('shows the posts of the closed group', async () => {
|
||||||
const result = await query({ query: filterPosts(), variables: {} })
|
const result = await query({ query: filterPosts(), variables: {} })
|
||||||
expect(result.data.Post).toHaveLength(3)
|
expect(result.data.Post).toHaveLength(4)
|
||||||
expect(result).toMatchObject({
|
expect(result).toMatchObject({
|
||||||
data: {
|
data: {
|
||||||
Post: expect.arrayContaining([
|
Post: expect.arrayContaining([
|
||||||
@ -1624,6 +1639,11 @@ describe('Posts in Groups', () => {
|
|||||||
title: 'A post to a closed group',
|
title: 'A post to a closed group',
|
||||||
content: 'I am posting into a closed group as a member of the group',
|
content: 'I am posting into a closed group as a member of the group',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 'post-to-hidden-group',
|
||||||
|
title: 'A post to a hidden group',
|
||||||
|
content: 'I am posting into a hidden group as a member of the group',
|
||||||
|
},
|
||||||
]),
|
]),
|
||||||
},
|
},
|
||||||
errors: undefined,
|
errors: undefined,
|
||||||
@ -1678,5 +1698,59 @@ describe('Posts in Groups', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('filter posts in my groups', () => {
|
||||||
|
describe('without any posts in groups', () => {
|
||||||
|
beforeAll(async () => {
|
||||||
|
authenticatedUser = await anyUser.toJson()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('finds no posts', async () => {
|
||||||
|
const result = await query({
|
||||||
|
query: filterPosts(),
|
||||||
|
variables: { filter: { postsInMyGroups: true } },
|
||||||
|
})
|
||||||
|
expect(result.data.Post).toHaveLength(0)
|
||||||
|
expect(result).toMatchObject({
|
||||||
|
data: {
|
||||||
|
Post: [],
|
||||||
|
},
|
||||||
|
errors: undefined,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('with posts in groups', () => {
|
||||||
|
beforeAll(async () => {
|
||||||
|
// member of hidden-group and closed-group
|
||||||
|
authenticatedUser = await allGroupsUser.toJson()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('finds two posts', async () => {
|
||||||
|
const result = await query({
|
||||||
|
query: filterPosts(),
|
||||||
|
variables: { filter: { postsInMyGroups: true } },
|
||||||
|
})
|
||||||
|
expect(result.data.Post).toHaveLength(2)
|
||||||
|
expect(result).toMatchObject({
|
||||||
|
data: {
|
||||||
|
Post: expect.arrayContaining([
|
||||||
|
{
|
||||||
|
id: 'post-to-closed-group',
|
||||||
|
title: 'A post to a closed group',
|
||||||
|
content: 'I am posting into a closed group as a member of the group',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'post-to-hidden-group',
|
||||||
|
title: 'A post to a hidden group',
|
||||||
|
content: 'I am posting into a hidden group as a member of the group',
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
},
|
||||||
|
errors: undefined,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import Factory, { cleanDatabase } from '../../db/factories'
|
import Factory, { cleanDatabase } from '../../db/factories'
|
||||||
import { gql } from '../../helpers/jest'
|
import gql from 'graphql-tag'
|
||||||
import { getDriver, getNeode } from '../../db/neo4j'
|
import { getDriver, getNeode } from '../../db/neo4j'
|
||||||
import createServer from '../../server'
|
import createServer from '../../server'
|
||||||
import { createTestClient } from 'apollo-server-testing'
|
import { createTestClient } from 'apollo-server-testing'
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { createTestClient } from 'apollo-server-testing'
|
import { createTestClient } from 'apollo-server-testing'
|
||||||
import createServer from '../.././server'
|
import createServer from '../.././server'
|
||||||
import Factory, { cleanDatabase } from '../../db/factories'
|
import Factory, { cleanDatabase } from '../../db/factories'
|
||||||
import { gql } from '../../helpers/jest'
|
import gql from 'graphql-tag'
|
||||||
import { getDriver, getNeode } from '../../db/neo4j'
|
import { getDriver, getNeode } from '../../db/neo4j'
|
||||||
|
|
||||||
const instance = getNeode()
|
const instance = getNeode()
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { createTestClient } from 'apollo-server-testing'
|
import { createTestClient } from 'apollo-server-testing'
|
||||||
import Factory, { cleanDatabase } from '../../db/factories'
|
import Factory, { cleanDatabase } from '../../db/factories'
|
||||||
import { gql } from '../../helpers/jest'
|
import gql from 'graphql-tag'
|
||||||
import { getNeode, getDriver } from '../../db/neo4j'
|
import { getNeode, getDriver } from '../../db/neo4j'
|
||||||
import createServer from '../../server'
|
import createServer from '../../server'
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import Factory, { cleanDatabase } from '../../db/factories'
|
import Factory, { cleanDatabase } from '../../db/factories'
|
||||||
import { gql } from '../../helpers/jest'
|
import gql from 'graphql-tag'
|
||||||
import { getNeode, getDriver } from '../../db/neo4j'
|
import { getNeode, getDriver } from '../../db/neo4j'
|
||||||
import createServer from '../../server'
|
import createServer from '../../server'
|
||||||
import { createTestClient } from 'apollo-server-testing'
|
import { createTestClient } from 'apollo-server-testing'
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { createTestClient } from 'apollo-server-testing'
|
import { createTestClient } from 'apollo-server-testing'
|
||||||
import Factory, { cleanDatabase } from '../../db/factories'
|
import Factory, { cleanDatabase } from '../../db/factories'
|
||||||
import { gql } from '../../helpers/jest'
|
import gql from 'graphql-tag'
|
||||||
import { getNeode, getDriver } from '../../db/neo4j'
|
import { getNeode, getDriver } from '../../db/neo4j'
|
||||||
import createServer from '../../server'
|
import createServer from '../../server'
|
||||||
|
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { createTestClient } from 'apollo-server-testing'
|
import { createTestClient } from 'apollo-server-testing'
|
||||||
import createServer from '../../server'
|
import createServer from '../../server'
|
||||||
import Factory, { cleanDatabase } from '../../db/factories'
|
import Factory, { cleanDatabase } from '../../db/factories'
|
||||||
import { gql } from '../../helpers/jest'
|
import gql from 'graphql-tag'
|
||||||
import { getDriver } from '../../db/neo4j'
|
import { getDriver } from '../../db/neo4j'
|
||||||
|
|
||||||
const driver = getDriver()
|
const driver = getDriver()
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { createTestClient } from 'apollo-server-testing'
|
import { createTestClient } from 'apollo-server-testing'
|
||||||
import Factory, { cleanDatabase } from '../../db/factories'
|
import Factory, { cleanDatabase } from '../../db/factories'
|
||||||
import { gql } from '../../helpers/jest'
|
import gql from 'graphql-tag'
|
||||||
import { getNeode, getDriver } from '../../db/neo4j'
|
import { getNeode, getDriver } from '../../db/neo4j'
|
||||||
import createServer from '../../server'
|
import createServer from '../../server'
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import Factory, { cleanDatabase } from '../../db/factories'
|
import Factory, { cleanDatabase } from '../../db/factories'
|
||||||
import { gql } from '../../helpers/jest'
|
import gql from 'graphql-tag'
|
||||||
import { getNeode, getDriver } from '../../db/neo4j'
|
import { getNeode, getDriver } from '../../db/neo4j'
|
||||||
import createServer from '../../server'
|
import createServer from '../../server'
|
||||||
import { createTestClient } from 'apollo-server-testing'
|
import { createTestClient } from 'apollo-server-testing'
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import jwt from 'jsonwebtoken'
|
import jwt from 'jsonwebtoken'
|
||||||
import CONFIG from './../../config'
|
import CONFIG from './../../config'
|
||||||
import Factory, { cleanDatabase } from '../../db/factories'
|
import Factory, { cleanDatabase } from '../../db/factories'
|
||||||
import { gql } from '../../helpers/jest'
|
import gql from 'graphql-tag'
|
||||||
import { loginMutation } from '../../graphql/userManagement'
|
import { loginMutation } from '../../graphql/userManagement'
|
||||||
import { createTestClient } from 'apollo-server-testing'
|
import { createTestClient } from 'apollo-server-testing'
|
||||||
import createServer, { context } from '../../server'
|
import createServer, { context } from '../../server'
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import Factory, { cleanDatabase } from '../../db/factories'
|
import Factory, { cleanDatabase } from '../../db/factories'
|
||||||
import { gql } from '../../helpers/jest'
|
import gql from 'graphql-tag'
|
||||||
import { getNeode, getDriver } from '../../db/neo4j'
|
import { getNeode, getDriver } from '../../db/neo4j'
|
||||||
import createServer from '../../server'
|
import createServer from '../../server'
|
||||||
import { createTestClient } from 'apollo-server-testing'
|
import { createTestClient } from 'apollo-server-testing'
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { gql } from '../../../helpers/jest'
|
import gql from 'graphql-tag'
|
||||||
import Factory, { cleanDatabase } from '../../../db/factories'
|
import Factory, { cleanDatabase } from '../../../db/factories'
|
||||||
import { getNeode, getDriver } from '../../../db/neo4j'
|
import { getNeode, getDriver } from '../../../db/neo4j'
|
||||||
import { createTestClient } from 'apollo-server-testing'
|
import { createTestClient } from 'apollo-server-testing'
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { createTestClient } from 'apollo-server-testing'
|
import { createTestClient } from 'apollo-server-testing'
|
||||||
import createServer from '../../../server'
|
import createServer from '../../../server'
|
||||||
import { cleanDatabase } from '../../../db/factories'
|
import { cleanDatabase } from '../../../db/factories'
|
||||||
import { gql } from '../../../helpers/jest'
|
import gql from 'graphql-tag'
|
||||||
import { getNeode, getDriver } from '../../../db/neo4j'
|
import { getNeode, getDriver } from '../../../db/neo4j'
|
||||||
|
|
||||||
const driver = getDriver()
|
const driver = getDriver()
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { createTestClient } from 'apollo-server-testing'
|
import { createTestClient } from 'apollo-server-testing'
|
||||||
import Factory, { cleanDatabase } from '../../db/factories'
|
import Factory, { cleanDatabase } from '../../db/factories'
|
||||||
import { gql } from '../../helpers/jest'
|
import gql from 'graphql-tag'
|
||||||
import { getNeode, getDriver } from '../../db/neo4j'
|
import { getNeode, getDriver } from '../../db/neo4j'
|
||||||
import createServer from '../../server'
|
import createServer from '../../server'
|
||||||
|
|
||||||
|
|||||||
@ -132,4 +132,9 @@ type Mutation {
|
|||||||
userId: ID!
|
userId: ID!
|
||||||
roleInGroup: GroupMemberRole!
|
roleInGroup: GroupMemberRole!
|
||||||
): User
|
): User
|
||||||
|
|
||||||
|
RemoveUserFromGroup(
|
||||||
|
groupId: ID!
|
||||||
|
userId: ID!
|
||||||
|
): User
|
||||||
}
|
}
|
||||||
|
|||||||
@ -29,6 +29,7 @@ type Query {
|
|||||||
|
|
||||||
type Mutation {
|
type Mutation {
|
||||||
markAsRead(id: ID!): NOTIFIED
|
markAsRead(id: ID!): NOTIFIED
|
||||||
|
markAllAsRead: [NOTIFIED]
|
||||||
}
|
}
|
||||||
|
|
||||||
type Subscription {
|
type Subscription {
|
||||||
|
|||||||
@ -82,6 +82,7 @@ input _PostFilter {
|
|||||||
emotions_single: _PostEMOTEDFilter
|
emotions_single: _PostEMOTEDFilter
|
||||||
emotions_every: _PostEMOTEDFilter
|
emotions_every: _PostEMOTEDFilter
|
||||||
group: _GroupFilter
|
group: _GroupFilter
|
||||||
|
postsInMyGroups: Boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
enum _PostOrdering {
|
enum _PostOrdering {
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
import { Given } from "cypress-cucumber-preprocessor/steps";
|
import { Given } from "cypress-cucumber-preprocessor/steps";
|
||||||
import { gql } from '../../../backend/src/helpers/jest'
|
|
||||||
|
|
||||||
Given('somebody reported the following posts:', table => {
|
Given('somebody reported the following posts:', table => {
|
||||||
table.hashes().forEach(({ submitterEmail, resourceId, reasonCategory, reasonDescription }) => {
|
table.hashes().forEach(({ submitterEmail, resourceId, reasonCategory, reasonDescription }) => {
|
||||||
@ -10,7 +9,7 @@ Given('somebody reported the following posts:', table => {
|
|||||||
cy.factory()
|
cy.factory()
|
||||||
.build('user', {}, submitter)
|
.build('user', {}, submitter)
|
||||||
.authenticateAs(submitter)
|
.authenticateAs(submitter)
|
||||||
.mutate(gql`mutation($resourceId: ID!, $reasonCategory: ReasonCategory!, $reasonDescription: String!) {
|
.mutate(`mutation($resourceId: ID!, $reasonCategory: ReasonCategory!, $reasonDescription: String!) {
|
||||||
fileReport(resourceId: $resourceId, reasonCategory: $reasonCategory, reasonDescription: $reasonDescription) {
|
fileReport(resourceId: $resourceId, reasonCategory: $reasonCategory, reasonDescription: $reasonDescription) {
|
||||||
reportId
|
reportId
|
||||||
}
|
}
|
||||||
|
|||||||
@ -15,11 +15,10 @@
|
|||||||
/* globals Cypress cy */
|
/* globals Cypress cy */
|
||||||
import "cypress-file-upload";
|
import "cypress-file-upload";
|
||||||
import { GraphQLClient, request } from 'graphql-request'
|
import { GraphQLClient, request } from 'graphql-request'
|
||||||
import { gql } from '../../backend/src/helpers/jest'
|
|
||||||
import config from '../../backend/src/config'
|
import config from '../../backend/src/config'
|
||||||
|
|
||||||
const authenticatedHeaders = (variables) => {
|
const authenticatedHeaders = (variables) => {
|
||||||
const mutation = gql`
|
const mutation = `
|
||||||
mutation($email: String!, $password: String!) {
|
mutation($email: String!, $password: String!) {
|
||||||
login(email: $email, password: $password)
|
login(email: $email, password: $password)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,36 +0,0 @@
|
|||||||
# This docker-compose file is just here for testing
|
|
||||||
|
|
||||||
version: "3.4"
|
|
||||||
|
|
||||||
services:
|
|
||||||
|
|
||||||
########################################################
|
|
||||||
# WEBAPP ###############################################
|
|
||||||
########################################################
|
|
||||||
webapp:
|
|
||||||
platform: linux/amd64
|
|
||||||
|
|
||||||
########################################################
|
|
||||||
# BACKEND ##############################################
|
|
||||||
########################################################
|
|
||||||
backend:
|
|
||||||
platform: linux/amd64
|
|
||||||
|
|
||||||
########################################################
|
|
||||||
# MAINTENANCE ##########################################
|
|
||||||
########################################################
|
|
||||||
maintenance:
|
|
||||||
platform: linux/amd64
|
|
||||||
|
|
||||||
########################################################
|
|
||||||
# NEO4J ################################################
|
|
||||||
########################################################
|
|
||||||
neo4j:
|
|
||||||
platform: linux/amd64
|
|
||||||
|
|
||||||
########################################################
|
|
||||||
# MAILSERVER TO FAKE SMTP ##############################
|
|
||||||
########################################################
|
|
||||||
# commented out, because otherwise override of production would error. and it seems unnecessary
|
|
||||||
# mailserver:
|
|
||||||
# platform: linux/amd64
|
|
||||||
47
deployment/scripts/cluster.backup.sh
Executable file
47
deployment/scripts/cluster.backup.sh
Executable file
@ -0,0 +1,47 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# base setup
|
||||||
|
SCRIPT_PATH=$(realpath $0)
|
||||||
|
SCRIPT_DIR=$(dirname $SCRIPT_PATH)
|
||||||
|
|
||||||
|
# configuration
|
||||||
|
CONFIGURATION=${CONFIGURATION:-"example"}
|
||||||
|
KUBECONFIG=${KUBECONFIG:-${SCRIPT_DIR}/../configurations/${CONFIGURATION}/kubeconfig.yaml}
|
||||||
|
BACKUP_DATE=$(date "+%F_%H-%M-%S")
|
||||||
|
BACKUP_FOLDER=${BACKUP_FOLDER:-${SCRIPT_DIR}/../configurations/${CONFIGURATION}/backup/${BACKUP_DATE}}
|
||||||
|
|
||||||
|
# create backup fodler
|
||||||
|
mkdir -p ${BACKUP_FOLDER}
|
||||||
|
|
||||||
|
# maintenance mode on
|
||||||
|
${SCRIPT_DIR}/cluster.maintenance.sh on
|
||||||
|
|
||||||
|
# shutdown database
|
||||||
|
kubectl --kubeconfig=${KUBECONFIG} get deployment ocelot-neo4j -o json \
|
||||||
|
| jq '.spec.template.spec.containers[] += {"command": ["tail", "-f", "/dev/null"]}' \
|
||||||
|
| kubectl --kubeconfig=${KUBECONFIG} apply -f -
|
||||||
|
|
||||||
|
# wait for the container to restart
|
||||||
|
sleep 60
|
||||||
|
|
||||||
|
# database backup
|
||||||
|
kubectl --kubeconfig=${KUBECONFIG} -n default exec -it \
|
||||||
|
$(kubectl --kubeconfig=${KUBECONFIG} -n default get pods | grep ocelot-neo4j | awk '{ print $1 }') \
|
||||||
|
-- neo4j-admin dump --to=/var/lib/neo4j/$BACKUP_DATE-neo4j-dump
|
||||||
|
# copy neo4j backup to local drive
|
||||||
|
kubectl --kubeconfig=${KUBECONFIG} cp \
|
||||||
|
default/$(kubectl --kubeconfig=${KUBECONFIG} -n default get pods | grep ocelot-neo4j |awk '{ print $1 }'):/var/lib/neo4j/$BACKUP_DATE-neo4j-dump $BACKUP_FOLDER/neo4j-dump
|
||||||
|
# copy image data
|
||||||
|
kubectl --kubeconfig=${KUBECONFIG} cp \
|
||||||
|
default/$(kubectl --kubeconfig=${KUBECONFIG} -n default get pods | grep ocelot-backend |awk '{ print $1 }'):/app/public/uploads $BACKUP_FOLDER/public-uploads
|
||||||
|
|
||||||
|
# restart database
|
||||||
|
kubectl --kubeconfig=${KUBECONFIG} get deployment ocelot-neo4j -o json \
|
||||||
|
| jq 'del(.spec.template.spec.containers[].command)' \
|
||||||
|
| kubectl --kubeconfig=${KUBECONFIG} apply -f -
|
||||||
|
|
||||||
|
# wait for the container to restart
|
||||||
|
sleep 60
|
||||||
|
|
||||||
|
# maintenance mode off
|
||||||
|
${SCRIPT_DIR}/cluster.maintenance.sh off
|
||||||
@ -1,34 +0,0 @@
|
|||||||
version: "3.4"
|
|
||||||
|
|
||||||
services:
|
|
||||||
|
|
||||||
########################################################
|
|
||||||
# WEBAPP ###############################################
|
|
||||||
########################################################
|
|
||||||
webapp:
|
|
||||||
platform: linux/amd64
|
|
||||||
|
|
||||||
########################################################
|
|
||||||
# BACKEND ##############################################
|
|
||||||
########################################################
|
|
||||||
backend:
|
|
||||||
platform: linux/amd64
|
|
||||||
|
|
||||||
########################################################
|
|
||||||
# MAINTENANCE ##########################################
|
|
||||||
########################################################
|
|
||||||
maintenance:
|
|
||||||
platform: linux/amd64
|
|
||||||
|
|
||||||
########################################################
|
|
||||||
# NEO4J ################################################
|
|
||||||
########################################################
|
|
||||||
neo4j:
|
|
||||||
platform: linux/amd64
|
|
||||||
|
|
||||||
########################################################
|
|
||||||
# MAILSERVER TO FAKE SMTP ##############################
|
|
||||||
########################################################
|
|
||||||
# commented out, because otherwise override of production would error. and it seems unnecessary
|
|
||||||
# mailserver:
|
|
||||||
# platform: linux/amd64
|
|
||||||
@ -10,8 +10,8 @@ between different local Node versions:
|
|||||||
```bash
|
```bash
|
||||||
# install Node
|
# install Node
|
||||||
$ cd webapp
|
$ cd webapp
|
||||||
$ nvm install v16.19.0
|
$ nvm install v19.4.0
|
||||||
$ nvm use v16.19.0
|
$ nvm use v19.4.0
|
||||||
```
|
```
|
||||||
|
|
||||||
Install node dependencies with [yarn](https://yarnpkg.com/en/):
|
Install node dependencies with [yarn](https://yarnpkg.com/en/):
|
||||||
|
|||||||
@ -376,3 +376,20 @@ $color-footer-link: $color-primary;
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
$color-locale-menu: $text-color-soft;
|
$color-locale-menu: $text-color-soft;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @tokens Donation Bar Color
|
||||||
|
*/
|
||||||
|
|
||||||
|
$color-donation-bar: $color-primary;
|
||||||
|
$color-donation-bar-light: $color-primary-light;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @tokens Toast Color
|
||||||
|
*/
|
||||||
|
|
||||||
|
$color-toast-red: $color-danger;
|
||||||
|
$color-toast-orange: $color-warning;
|
||||||
|
$color-toast-yellow: $color-yellow;
|
||||||
|
$color-toast-blue: $color-secondary;
|
||||||
|
$color-toast-green: $color-success;
|
||||||
|
|||||||
@ -11,22 +11,22 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.iziToast.iziToast-color-red {
|
.iziToast.iziToast-color-red {
|
||||||
background: $color-danger !important;
|
background: $color-toast-red !important;
|
||||||
border-color: $color-danger !important;
|
border-color: $color-toast-red !important;
|
||||||
}
|
}
|
||||||
.iziToast.iziToast-color-orange {
|
.iziToast.iziToast-color-orange {
|
||||||
background: $color-warning !important;
|
background: $color-toast-orange !important;
|
||||||
border-color: $color-warning !important;
|
border-color: $color-toast-orange !important;
|
||||||
}
|
}
|
||||||
.iziToast.iziToast-color-yellow {
|
.iziToast.iziToast-color-yellow {
|
||||||
background: $color-yellow !important;
|
background: $color-toast-yellow !important;
|
||||||
border-color: $color-yellow !important;
|
border-color: $color-toast-yellow !important;
|
||||||
}
|
}
|
||||||
.iziToast.iziToast-color-blue {
|
.iziToast.iziToast-color-blue {
|
||||||
background: $color-secondary !important;
|
background: $color-toast-blue !important;
|
||||||
border-color: $color-secondary !important;
|
border-color: $color-toast-blue !important;
|
||||||
}
|
}
|
||||||
.iziToast.iziToast-color-green {
|
.iziToast.iziToast-color-green {
|
||||||
background: $color-success !important;
|
background: $color-toast-green !important;
|
||||||
border-color: $color-success !important;
|
border-color: $color-toast-green !important;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,7 +3,7 @@
|
|||||||
<a href="#" slot="default" slot-scope="{ toggleMenu }" @click.prevent="toggleMenu()">
|
<a href="#" slot="default" slot-scope="{ toggleMenu }" @click.prevent="toggleMenu()">
|
||||||
<ds-text bold size="large">{{ $t('admin.categories.name') }}</ds-text>
|
<ds-text bold size="large">{{ $t('admin.categories.name') }}</ds-text>
|
||||||
</a>
|
</a>
|
||||||
<template slot="popover">
|
<template #popover>
|
||||||
<div class="category-menu-options">
|
<div class="category-menu-options">
|
||||||
<h2 class="title">{{ $t('filter-menu.filter-by') }}</h2>
|
<h2 class="title">{{ $t('filter-menu.filter-by') }}</h2>
|
||||||
<categories-filter v-if="categoriesActive" />
|
<categories-filter v-if="categoriesActive" />
|
||||||
|
|||||||
@ -10,7 +10,7 @@
|
|||||||
>
|
>
|
||||||
<base-icon class="dropdown-arrow" name="angle-down" />
|
<base-icon class="dropdown-arrow" name="angle-down" />
|
||||||
</base-button>
|
</base-button>
|
||||||
<template slot="popover">
|
<template #popover>
|
||||||
<filter-menu-component />
|
<filter-menu-component />
|
||||||
</template>
|
</template>
|
||||||
</dropdown>
|
</dropdown>
|
||||||
|
|||||||
@ -9,12 +9,14 @@ let wrapper
|
|||||||
describe('FollowingFilter', () => {
|
describe('FollowingFilter', () => {
|
||||||
const mutations = {
|
const mutations = {
|
||||||
'posts/TOGGLE_FILTER_BY_FOLLOWED': jest.fn(),
|
'posts/TOGGLE_FILTER_BY_FOLLOWED': jest.fn(),
|
||||||
|
'posts/TOGGLE_FILTER_BY_MY_GROUPS': jest.fn(),
|
||||||
}
|
}
|
||||||
const getters = {
|
const getters = {
|
||||||
'auth/user': () => {
|
'auth/user': () => {
|
||||||
return { id: 'u34' }
|
return { id: 'u34' }
|
||||||
},
|
},
|
||||||
'posts/filteredByUsersFollowed': jest.fn(),
|
'posts/filteredByUsersFollowed': jest.fn(),
|
||||||
|
'posts/filteredByPostsInMyGroups': jest.fn(),
|
||||||
}
|
}
|
||||||
|
|
||||||
const mocks = {
|
const mocks = {
|
||||||
@ -34,12 +36,18 @@ describe('FollowingFilter', () => {
|
|||||||
describe('mount', () => {
|
describe('mount', () => {
|
||||||
it('sets "filter-by-followed" button attribute `filled`', () => {
|
it('sets "filter-by-followed" button attribute `filled`', () => {
|
||||||
getters['posts/filteredByUsersFollowed'] = jest.fn(() => true)
|
getters['posts/filteredByUsersFollowed'] = jest.fn(() => true)
|
||||||
|
getters['posts/filteredByPostsInMyGroups'] = jest.fn(() => true)
|
||||||
const wrapper = Wrapper()
|
const wrapper = Wrapper()
|
||||||
expect(
|
expect(
|
||||||
wrapper
|
wrapper
|
||||||
.find('.following-filter .filter-list .follower-item .base-button')
|
.find('.following-filter .filter-list .follower-item .base-button')
|
||||||
.classes('--filled'),
|
.classes('--filled'),
|
||||||
).toBe(true)
|
).toBe(true)
|
||||||
|
expect(
|
||||||
|
wrapper
|
||||||
|
.find('.following-filter .filter-list .posts-in-my-groups-item .base-button')
|
||||||
|
.classes('--filled'),
|
||||||
|
).toBe(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('click "filter-by-followed" button', () => {
|
describe('click "filter-by-followed" button', () => {
|
||||||
@ -48,5 +56,14 @@ describe('FollowingFilter', () => {
|
|||||||
expect(mutations['posts/TOGGLE_FILTER_BY_FOLLOWED']).toHaveBeenCalledWith({}, 'u34')
|
expect(mutations['posts/TOGGLE_FILTER_BY_FOLLOWED']).toHaveBeenCalledWith({}, 'u34')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('click "filter-by-my-groups" button', () => {
|
||||||
|
it('calls TOGGLE_FILTER_BY_MY_GROUPS', () => {
|
||||||
|
wrapper
|
||||||
|
.find('.following-filter .filter-list .posts-in-my-groups-item .base-button')
|
||||||
|
.trigger('click')
|
||||||
|
expect(mutations['posts/TOGGLE_FILTER_BY_MY_GROUPS']).toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@ -10,6 +10,15 @@
|
|||||||
@click="toggleFilteredByFollowed(currentUser.id)"
|
@click="toggleFilteredByFollowed(currentUser.id)"
|
||||||
/>
|
/>
|
||||||
</li>
|
</li>
|
||||||
|
<li class="item posts-in-my-groups-item">
|
||||||
|
<labeled-button
|
||||||
|
icon="users"
|
||||||
|
:label="$t('filter-menu.my-groups')"
|
||||||
|
:filled="filteredByPostsInMyGroups"
|
||||||
|
:title="$t('contribution.filterMyGroups')"
|
||||||
|
@click="toggleFilteredByMyGroups()"
|
||||||
|
/>
|
||||||
|
</li>
|
||||||
</template>
|
</template>
|
||||||
</filter-menu-section>
|
</filter-menu-section>
|
||||||
</template>
|
</template>
|
||||||
@ -28,12 +37,14 @@ export default {
|
|||||||
computed: {
|
computed: {
|
||||||
...mapGetters({
|
...mapGetters({
|
||||||
filteredByUsersFollowed: 'posts/filteredByUsersFollowed',
|
filteredByUsersFollowed: 'posts/filteredByUsersFollowed',
|
||||||
|
filteredByPostsInMyGroups: 'posts/filteredByPostsInMyGroups',
|
||||||
currentUser: 'auth/user',
|
currentUser: 'auth/user',
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
...mapMutations({
|
...mapMutations({
|
||||||
toggleFilteredByFollowed: 'posts/TOGGLE_FILTER_BY_FOLLOWED',
|
toggleFilteredByFollowed: 'posts/TOGGLE_FILTER_BY_FOLLOWED',
|
||||||
|
toggleFilteredByMyGroups: 'posts/TOGGLE_FILTER_BY_MY_GROUPS',
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,26 +1,65 @@
|
|||||||
import { mount } from '@vue/test-utils'
|
import { mount } from '@vue/test-utils'
|
||||||
import GroupMember from './GroupMember.vue'
|
import GroupMember from './GroupMember.vue'
|
||||||
|
import { changeGroupMemberRoleMutation, removeUserFromGroupMutation } from '~/graphql/groups.js'
|
||||||
|
|
||||||
const localVue = global.localVue
|
const localVue = global.localVue
|
||||||
|
|
||||||
const propsData = {
|
const propsData = {
|
||||||
groupId: '',
|
groupId: 'group-id',
|
||||||
groupMembers: [],
|
groupMembers: [
|
||||||
|
{
|
||||||
|
slug: 'owner',
|
||||||
|
id: 'owner',
|
||||||
|
myRoleInGroup: 'owner',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
slug: 'user',
|
||||||
|
id: 'user',
|
||||||
|
myRoleInGroup: 'usual',
|
||||||
|
},
|
||||||
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const stubs = {
|
||||||
|
'nuxt-link': true,
|
||||||
|
}
|
||||||
|
|
||||||
|
const apolloMock = jest
|
||||||
|
.fn()
|
||||||
|
.mockRejectedValueOnce({ message: 'Oh no!' })
|
||||||
|
.mockResolvedValue({
|
||||||
|
data: {
|
||||||
|
ChangeGroupMemberRole: {
|
||||||
|
slug: 'user',
|
||||||
|
id: 'user',
|
||||||
|
myRoleInGroup: 'admin',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const toastErrorMock = jest.fn()
|
||||||
|
const toastSuccessMock = jest.fn()
|
||||||
|
|
||||||
describe('GroupMember', () => {
|
describe('GroupMember', () => {
|
||||||
let wrapper
|
let wrapper
|
||||||
let mocks
|
let mocks
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
mocks = {
|
mocks = {
|
||||||
$t: jest.fn(),
|
$t: jest.fn((t) => t),
|
||||||
|
$apollo: {
|
||||||
|
mutate: apolloMock,
|
||||||
|
},
|
||||||
|
$toast: {
|
||||||
|
error: toastErrorMock,
|
||||||
|
success: toastSuccessMock,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('mount', () => {
|
describe('mount', () => {
|
||||||
const Wrapper = () => {
|
const Wrapper = () => {
|
||||||
return mount(GroupMember, { propsData, mocks, localVue })
|
return mount(GroupMember, { propsData, mocks, localVue, stubs })
|
||||||
}
|
}
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@ -30,5 +69,120 @@ describe('GroupMember', () => {
|
|||||||
it('renders', () => {
|
it('renders', () => {
|
||||||
expect(wrapper.findAll('.group-member')).toHaveLength(1)
|
expect(wrapper.findAll('.group-member')).toHaveLength(1)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('has two users in table', () => {
|
||||||
|
expect(wrapper.find('tbody').findAll('tr')).toHaveLength(2)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('has no modal', () => {
|
||||||
|
expect(wrapper.find('div.ds-modal-wrapper').exists()).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('change user role', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks()
|
||||||
|
wrapper
|
||||||
|
.find('tbody')
|
||||||
|
.findAll('tr')
|
||||||
|
.at(1)
|
||||||
|
.find('select')
|
||||||
|
.findAll('option')
|
||||||
|
.at(2)
|
||||||
|
.setSelected()
|
||||||
|
wrapper.find('tbody').findAll('tr').at(1).find('select').trigger('change')
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('with server error', () => {
|
||||||
|
it('toasts an error message', () => {
|
||||||
|
expect(toastErrorMock).toBeCalledWith('Oh no!')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('with server success', () => {
|
||||||
|
it('calls the API', () => {
|
||||||
|
expect(apolloMock).toBeCalledWith({
|
||||||
|
mutation: changeGroupMemberRoleMutation(),
|
||||||
|
variables: { groupId: 'group-id', userId: 'user', roleInGroup: 'admin' },
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('toasts a success message', () => {
|
||||||
|
expect(toastSuccessMock).toBeCalledWith('group.changeMemberRole')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('click remove user', () => {
|
||||||
|
beforeAll(() => {
|
||||||
|
apolloMock.mockRejectedValueOnce({ message: 'Oh no!!' }).mockResolvedValue({
|
||||||
|
data: {
|
||||||
|
RemoveUserFromGroup: {
|
||||||
|
slug: 'user',
|
||||||
|
id: 'user',
|
||||||
|
myRoleInGroup: null,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
wrapper = Wrapper()
|
||||||
|
wrapper.find('tbody').findAll('tr').at(1).find('button').trigger('click')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('opens the modal', () => {
|
||||||
|
expect(wrapper.find('div.ds-modal-wrapper').isVisible()).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('click on cancel', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
wrapper.find('div.ds-modal-wrapper').find('button.ds-button-ghost').trigger('click')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('closes the modal', () => {
|
||||||
|
expect(wrapper.find('div.ds-modal-wrapper').exists()).toBe(false)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('click on confirm with server error', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
wrapper.find('div.ds-modal-wrapper').find('button.ds-button-primary').trigger('click')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('toasts an error message', () => {
|
||||||
|
expect(toastErrorMock).toBeCalledWith('Oh no!!')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('closes the modal', () => {
|
||||||
|
expect(wrapper.find('div.ds-modal-wrapper').exists()).toBe(false)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('click on confirm with success', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks()
|
||||||
|
wrapper.find('div.ds-modal-wrapper').find('button.ds-button-primary').trigger('click')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('calls the API', () => {
|
||||||
|
expect(apolloMock).toBeCalledWith({
|
||||||
|
mutation: removeUserFromGroupMutation(),
|
||||||
|
variables: { groupId: 'group-id', userId: 'user' },
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('emits load group members', () => {
|
||||||
|
expect(wrapper.emitted('loadGroupMembers')).toBeTruthy()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('toasts a success message', () => {
|
||||||
|
expect(toastSuccessMock).toBeCalledWith('group.memberRemoved')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('closes the modal', () => {
|
||||||
|
expect(wrapper.find('div.ds-modal-wrapper').exists()).toBe(false)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@ -53,30 +53,33 @@
|
|||||||
</ds-chip>
|
</ds-chip>
|
||||||
</template>
|
</template>
|
||||||
<template #edit="scope">
|
<template #edit="scope">
|
||||||
<ds-button v-if="scope.row.myRoleInGroup !== 'owner'" size="small" primary disabled>
|
<base-button
|
||||||
<!-- TODO: implement removal of group members -->
|
v-if="scope.row.myRoleInGroup !== 'owner'"
|
||||||
<!-- :disabled="scope.row.myRoleInGroup === 'owner'"
|
size="small"
|
||||||
-->
|
primary
|
||||||
|
@click="
|
||||||
|
isOpen = true
|
||||||
|
userId = scope.row.id
|
||||||
|
"
|
||||||
|
>
|
||||||
{{ $t('group.removeMemberButton') }}
|
{{ $t('group.removeMemberButton') }}
|
||||||
</ds-button>
|
</base-button>
|
||||||
</template>
|
</template>
|
||||||
</ds-table>
|
</ds-table>
|
||||||
<!-- TODO: implement removal of group members -->
|
<ds-modal
|
||||||
<!-- TODO: change to ocelot.social modal -->
|
v-if="isOpen"
|
||||||
<!-- <ds-modal
|
v-model="isOpen"
|
||||||
v-if="isOpen"
|
:title="`${$t('group.removeMember')}`"
|
||||||
v-model="isOpen"
|
force
|
||||||
:title="`${$t('group.removeMember')}`"
|
extended
|
||||||
force
|
:confirm-label="$t('group.removeMember')"
|
||||||
extended
|
:cancel-label="$t('actions.cancel')"
|
||||||
:confirm-label="$t('group.removeMember')"
|
@confirm="removeUser()"
|
||||||
:cancel-label="$t('actions.cancel')"
|
/>
|
||||||
@confirm="deleteMember(memberId)"
|
|
||||||
/> -->
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script>
|
<script>
|
||||||
import { changeGroupMemberRoleMutation } from '~/graphql/groups.js'
|
import { changeGroupMemberRoleMutation, removeUserFromGroupMutation } from '~/graphql/groups.js'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'GroupMember',
|
name: 'GroupMember',
|
||||||
@ -96,6 +99,8 @@ export default {
|
|||||||
query: '',
|
query: '',
|
||||||
searchProcess: null,
|
searchProcess: null,
|
||||||
user: {},
|
user: {},
|
||||||
|
isOpen: false,
|
||||||
|
userId: null,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@ -139,6 +144,25 @@ export default {
|
|||||||
this.$toast.error(error.message)
|
this.$toast.error(error.message)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
removeUser() {
|
||||||
|
this.$apollo
|
||||||
|
.mutate({
|
||||||
|
mutation: removeUserFromGroupMutation(),
|
||||||
|
variables: { groupId: this.groupId, userId: this.userId },
|
||||||
|
})
|
||||||
|
.then(({ data }) => {
|
||||||
|
this.$emit('loadGroupMembers')
|
||||||
|
this.$toast.success(
|
||||||
|
this.$t('group.memberRemoved', { name: data.RemoveUserFromGroup.slug }),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
this.$toast.error(error.message)
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
this.userId = null
|
||||||
|
})
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -1,7 +1,16 @@
|
|||||||
<template>
|
<template>
|
||||||
<dropdown class="invite-button" offset="8" :placement="placement">
|
<dropdown class="invite-button" offset="8" :placement="placement">
|
||||||
<template #default="{ toggleMenu }">
|
<template #default="{ toggleMenu }">
|
||||||
<base-button icon="user-plus" circle ghost @click.prevent="toggleMenu" />
|
<base-button
|
||||||
|
icon="user-plus"
|
||||||
|
circle
|
||||||
|
ghost
|
||||||
|
v-tooltip="{
|
||||||
|
content: $t('invite-codes.button.tooltip'),
|
||||||
|
placement: 'bottom-start',
|
||||||
|
}"
|
||||||
|
@click.prevent="toggleMenu"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
<template #popover>
|
<template #popover>
|
||||||
<div class="invite-button-menu-popover">
|
<div class="invite-button-menu-popover">
|
||||||
@ -15,10 +24,7 @@
|
|||||||
ghost
|
ghost
|
||||||
@click="copyInviteLink"
|
@click="copyInviteLink"
|
||||||
>
|
>
|
||||||
<ds-text bold>
|
<ds-text bold>{{ $t('invite-codes.copy-code') }}</ds-text>
|
||||||
{{ $t('invite-codes.copy-code') }}
|
|
||||||
{{ inviteCode.code }}
|
|
||||||
</ds-text>
|
|
||||||
</base-button>
|
</base-button>
|
||||||
</base-card>
|
</base-card>
|
||||||
</div>
|
</div>
|
||||||
@ -108,6 +114,6 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.invite-code {
|
.invite-code {
|
||||||
left: 50%;
|
margin-left: 25%;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -12,15 +12,24 @@
|
|||||||
<counter-icon icon="bell" :count="unreadNotificationsCount" danger />
|
<counter-icon icon="bell" :count="unreadNotificationsCount" danger />
|
||||||
</base-button>
|
</base-button>
|
||||||
</template>
|
</template>
|
||||||
<template slot="popover">
|
<template #popover>
|
||||||
<div class="notifications-menu-popover">
|
<div class="notifications-menu-popover">
|
||||||
<notification-list :notifications="notifications" @markAsRead="markAsRead" />
|
<notification-list :notifications="notifications" @markAsRead="markAsRead" />
|
||||||
</div>
|
</div>
|
||||||
<div class="notifications-link-container">
|
<ds-flex class="notifications-link-container">
|
||||||
<nuxt-link :to="{ name: 'notifications' }">
|
<ds-flex-item :width="{ base: 'auto' }" centered>
|
||||||
{{ $t('notifications.pageLink') }}
|
<nuxt-link :to="{ name: 'notifications' }">
|
||||||
</nuxt-link>
|
<ds-button ghost primary>
|
||||||
</div>
|
{{ $t('notifications.pageLink') }}
|
||||||
|
</ds-button>
|
||||||
|
</nuxt-link>
|
||||||
|
</ds-flex-item>
|
||||||
|
<ds-flex-item :width="{ base: 'auto' }" centered>
|
||||||
|
<ds-button ghost primary @click="markAllAsRead" data-test="markAllAsRead-button">
|
||||||
|
{{ $t('notifications.markAllAsRead') }}
|
||||||
|
</ds-button>
|
||||||
|
</ds-flex-item>
|
||||||
|
</ds-flex>
|
||||||
</template>
|
</template>
|
||||||
</dropdown>
|
</dropdown>
|
||||||
</template>
|
</template>
|
||||||
@ -28,7 +37,12 @@
|
|||||||
<script>
|
<script>
|
||||||
import { mapGetters } from 'vuex'
|
import { mapGetters } from 'vuex'
|
||||||
import unionBy from 'lodash/unionBy'
|
import unionBy from 'lodash/unionBy'
|
||||||
import { notificationQuery, markAsReadMutation, notificationAdded } from '~/graphql/User'
|
import {
|
||||||
|
notificationQuery,
|
||||||
|
markAsReadMutation,
|
||||||
|
notificationAdded,
|
||||||
|
markAllAsReadMutation,
|
||||||
|
} from '~/graphql/User'
|
||||||
import CounterIcon from '~/components/_new/generic/CounterIcon/CounterIcon'
|
import CounterIcon from '~/components/_new/generic/CounterIcon/CounterIcon'
|
||||||
import Dropdown from '~/components/Dropdown'
|
import Dropdown from '~/components/Dropdown'
|
||||||
import NotificationList from '../NotificationList/NotificationList'
|
import NotificationList from '../NotificationList/NotificationList'
|
||||||
@ -56,8 +70,21 @@ export default {
|
|||||||
mutation: markAsReadMutation(this.$i18n),
|
mutation: markAsReadMutation(this.$i18n),
|
||||||
variables,
|
variables,
|
||||||
})
|
})
|
||||||
} catch (err) {
|
} catch (error) {
|
||||||
this.$toast.error(err.message)
|
this.$toast.error(error.message)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async markAllAsRead() {
|
||||||
|
if (!this.hasNotifications) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.$apollo.mutate({
|
||||||
|
mutation: markAllAsReadMutation(this.$i18n),
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
this.$toast.error(error.message)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -71,6 +98,9 @@ export default {
|
|||||||
}, 0)
|
}, 0)
|
||||||
return result
|
return result
|
||||||
},
|
},
|
||||||
|
hasNotifications() {
|
||||||
|
return this.notifications.length
|
||||||
|
},
|
||||||
},
|
},
|
||||||
apollo: {
|
apollo: {
|
||||||
notifications: {
|
notifications: {
|
||||||
@ -118,7 +148,7 @@ export default {
|
|||||||
}
|
}
|
||||||
.notifications-link-container {
|
.notifications-link-container {
|
||||||
background-color: $background-color-softer-active;
|
background-color: $background-color-softer-active;
|
||||||
text-align: center;
|
justify-content: center;
|
||||||
position: fixed;
|
position: fixed;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
|
|||||||
@ -5,7 +5,7 @@
|
|||||||
<page-params-link :pageParams="pageParams">
|
<page-params-link :pageParams="pageParams">
|
||||||
{{ $t(pageParams.internalPage.footerIdent) }}
|
{{ $t(pageParams.internalPage.footerIdent) }}
|
||||||
</page-params-link>
|
</page-params-link>
|
||||||
<span class="division-line">-</span>
|
<span class="division-space"> </span>
|
||||||
</span>
|
</span>
|
||||||
<!-- version -->
|
<!-- version -->
|
||||||
<a
|
<a
|
||||||
@ -46,8 +46,8 @@ export default {
|
|||||||
.ds-footer a {
|
.ds-footer a {
|
||||||
color: $color-footer-link;
|
color: $color-footer-link;
|
||||||
}
|
}
|
||||||
.division-line {
|
.division-space {
|
||||||
margin-left: 0.2rem;
|
margin-left: 0.4rem;
|
||||||
margin-right: 0.2rem;
|
margin-right: 0.4rem;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -42,7 +42,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div v-else class="categories-placeholder"></div>
|
<div v-else class="categories-placeholder"></div>
|
||||||
<counter-icon
|
<counter-icon
|
||||||
icon="bullhorn"
|
icon="heart-o"
|
||||||
:count="post.shoutedCount"
|
:count="post.shoutedCount"
|
||||||
:title="$t('contribution.amount-shouts', { amount: post.shoutedCount })"
|
:title="$t('contribution.amount-shouts', { amount: post.shoutedCount })"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -79,17 +79,17 @@ export default {
|
|||||||
border-radius: $border-radius-base;
|
border-radius: $border-radius-base;
|
||||||
|
|
||||||
&.color-uni {
|
&.color-uni {
|
||||||
background: $color-primary-light;
|
background: $color-donation-bar;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.color-repeating-linear-gradient {
|
&.color-repeating-linear-gradient {
|
||||||
background: repeating-linear-gradient(
|
background: repeating-linear-gradient(
|
||||||
120deg,
|
120deg,
|
||||||
$color-primary 0px,
|
$color-donation-bar 0px,
|
||||||
$color-primary 30px,
|
$color-donation-bar 30px,
|
||||||
$color-primary-light 50px,
|
$color-donation-bar-light 50px,
|
||||||
$color-primary-light 75px,
|
$color-donation-bar-light 75px,
|
||||||
$color-primary 95px
|
$color-donation-bar 95px
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,7 +4,7 @@
|
|||||||
:loading="loading"
|
:loading="loading"
|
||||||
:disabled="disabled"
|
:disabled="disabled"
|
||||||
:filled="shouted"
|
:filled="shouted"
|
||||||
icon="bullhorn"
|
icon="heart-o"
|
||||||
circle
|
circle
|
||||||
@click="toggle"
|
@click="toggle"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -21,7 +21,7 @@ storiesOf('Generic/BaseButton', module)
|
|||||||
template: `
|
template: `
|
||||||
<div>
|
<div>
|
||||||
<base-button icon="edit">With Text</base-button>
|
<base-button icon="edit">With Text</base-button>
|
||||||
<base-button icon="bullhorn" />
|
<base-button icon="heart-o" />
|
||||||
<base-button icon="trash" disabled />
|
<base-button icon="trash" disabled />
|
||||||
<base-button icon="trash" loading />
|
<base-button icon="trash" loading />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -4,7 +4,7 @@
|
|||||||
<div class="metadata">
|
<div class="metadata">
|
||||||
<span class="counts">
|
<span class="counts">
|
||||||
<counter-icon icon="comments" :count="option.commentsCount" soft />
|
<counter-icon icon="comments" :count="option.commentsCount" soft />
|
||||||
<counter-icon icon="bullhorn" :count="option.shoutedCount" soft />
|
<counter-icon icon="heart-o" :count="option.shoutedCount" soft />
|
||||||
<counter-icon icon="hand-pointer" :count="option.clickedCount" soft />
|
<counter-icon icon="hand-pointer" :count="option.clickedCount" soft />
|
||||||
<counter-icon icon="eye" :count="option.viewedTeaserCount" soft />
|
<counter-icon icon="eye" :count="option.viewedTeaserCount" soft />
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@ -108,7 +108,7 @@ export const mapUserQuery = (i18n) => {
|
|||||||
`
|
`
|
||||||
}
|
}
|
||||||
|
|
||||||
export const notificationQuery = (i18n) => {
|
export const notificationQuery = (_i18n) => {
|
||||||
return gql`
|
return gql`
|
||||||
${userFragment}
|
${userFragment}
|
||||||
${commentFragment}
|
${commentFragment}
|
||||||
@ -147,7 +147,7 @@ export const notificationQuery = (i18n) => {
|
|||||||
`
|
`
|
||||||
}
|
}
|
||||||
|
|
||||||
export const markAsReadMutation = (i18n) => {
|
export const markAsReadMutation = (_i18n) => {
|
||||||
return gql`
|
return gql`
|
||||||
${userFragment}
|
${userFragment}
|
||||||
${commentFragment}
|
${commentFragment}
|
||||||
@ -183,6 +183,42 @@ export const markAsReadMutation = (i18n) => {
|
|||||||
`
|
`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const markAllAsReadMutation = (_i18n) => {
|
||||||
|
return gql`
|
||||||
|
${userFragment}
|
||||||
|
${commentFragment}
|
||||||
|
${postFragment}
|
||||||
|
|
||||||
|
mutation {
|
||||||
|
markAllAsRead {
|
||||||
|
id
|
||||||
|
read
|
||||||
|
reason
|
||||||
|
createdAt
|
||||||
|
updatedAt
|
||||||
|
from {
|
||||||
|
__typename
|
||||||
|
... on Post {
|
||||||
|
...post
|
||||||
|
author {
|
||||||
|
...user
|
||||||
|
}
|
||||||
|
}
|
||||||
|
... on Comment {
|
||||||
|
...comment
|
||||||
|
post {
|
||||||
|
...post
|
||||||
|
author {
|
||||||
|
...user
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
||||||
export const notificationAdded = () => {
|
export const notificationAdded = () => {
|
||||||
return gql`
|
return gql`
|
||||||
${userFragment}
|
${userFragment}
|
||||||
|
|||||||
@ -143,6 +143,19 @@ export const changeGroupMemberRoleMutation = () => {
|
|||||||
`
|
`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const removeUserFromGroupMutation = () => {
|
||||||
|
return gql`
|
||||||
|
mutation ($groupId: ID!, $userId: ID!) {
|
||||||
|
RemoveUserFromGroup(groupId: $groupId, userId: $userId) {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
slug
|
||||||
|
myRoleInGroup
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
||||||
// ------ queries
|
// ------ queries
|
||||||
|
|
||||||
export const groupQuery = (i18n) => {
|
export const groupQuery = (i18n) => {
|
||||||
|
|||||||
@ -24,7 +24,7 @@
|
|||||||
"posts": "Beiträge",
|
"posts": "Beiträge",
|
||||||
"projects": "Projekte",
|
"projects": "Projekte",
|
||||||
"shouts": "Empfehlungen",
|
"shouts": "Empfehlungen",
|
||||||
"users": "Benutzer"
|
"users": "Nutzer"
|
||||||
},
|
},
|
||||||
"donations": {
|
"donations": {
|
||||||
"goal": "Monatlich benötigte Spenden",
|
"goal": "Monatlich benötigte Spenden",
|
||||||
@ -38,11 +38,11 @@
|
|||||||
"nameOfHashtag": "Name",
|
"nameOfHashtag": "Name",
|
||||||
"number": "Nr.",
|
"number": "Nr.",
|
||||||
"tagCount": "Beiträge",
|
"tagCount": "Beiträge",
|
||||||
"tagCountUnique": "Benutzer"
|
"tagCountUnique": "Nutzer"
|
||||||
},
|
},
|
||||||
"invites": {
|
"invites": {
|
||||||
"description": "Einladungen sind eine wunderbare Möglichkeit, Deine Freunde in Deinem Netzwerk zu haben …",
|
"description": "Einladungen sind eine wunderbare Möglichkeit, Deine Freunde in Deinem Netzwerk zu haben …",
|
||||||
"name": "Benutzer einladen",
|
"name": "Nutzer einladen",
|
||||||
"title": "Leute einladen"
|
"title": "Leute einladen"
|
||||||
},
|
},
|
||||||
"name": "Systemverwaltung",
|
"name": "Systemverwaltung",
|
||||||
@ -59,11 +59,11 @@
|
|||||||
"name": "Einstellungen"
|
"name": "Einstellungen"
|
||||||
},
|
},
|
||||||
"users": {
|
"users": {
|
||||||
"empty": "Keine Benutzer gefunden",
|
"empty": "Keine Nutzer gefunden",
|
||||||
"form": {
|
"form": {
|
||||||
"placeholder": "E-Mail, Name oder Beschreibung"
|
"placeholder": "E-Mail, Name oder Beschreibung"
|
||||||
},
|
},
|
||||||
"name": "Benutzer",
|
"name": "Nutzer",
|
||||||
"roleChanged": "Rolle erfolgreich geändert!",
|
"roleChanged": "Rolle erfolgreich geändert!",
|
||||||
"table": {
|
"table": {
|
||||||
"columns": {
|
"columns": {
|
||||||
@ -111,7 +111,7 @@
|
|||||||
"shout": "Empfehlung ::: Empfehlungen",
|
"shout": "Empfehlung ::: Empfehlungen",
|
||||||
"tag": "Schlagwort ::: Schlagwörter",
|
"tag": "Schlagwort ::: Schlagwörter",
|
||||||
"takeAction": "Aktiv werden",
|
"takeAction": "Aktiv werden",
|
||||||
"user": "Benutzer ::: Benutzer",
|
"user": "Nutzer ::: Nutzer",
|
||||||
"validations": {
|
"validations": {
|
||||||
"categories": "es müssen eine bis drei Themen ausgewählt werden",
|
"categories": "es müssen eine bis drei Themen ausgewählt werden",
|
||||||
"email": "muss eine gültige E-Mail-Adresse sein",
|
"email": "muss eine gültige E-Mail-Adresse sein",
|
||||||
@ -138,12 +138,12 @@
|
|||||||
"registration": {
|
"registration": {
|
||||||
"create-user-account": {
|
"create-user-account": {
|
||||||
"buttonTitle": "Erstellen",
|
"buttonTitle": "Erstellen",
|
||||||
"error": "Es konnte kein Benutzerkonto erstellt werden!",
|
"error": "Es konnte kein Nutzerkonto erstellt werden!",
|
||||||
"help": "Vielleicht war der Bestätigungscode falsch oder abgelaufen? Wenn das Problem weiterhin besteht, schicke uns gerne eine E-Mail an:",
|
"help": "Vielleicht war der Bestätigungscode falsch oder abgelaufen? Wenn das Problem weiterhin besteht, schicke uns gerne eine E-Mail an:",
|
||||||
"recieveCommunicationAsEmailsEtcConfirmed": "Ich stimme auch dem Erhalt von E-Mails und anderen Formen der Kommunikation (z.B. Push-Benachrichtigungen) zu.",
|
"recieveCommunicationAsEmailsEtcConfirmed": "Ich stimme auch dem Erhalt von E-Mails und anderen Formen der Kommunikation (z.B. Push-Benachrichtigungen) zu.",
|
||||||
"success": "Dein Benutzerkonto wurde erstellt!",
|
"success": "Dein Nutzerkonto wurde erstellt!",
|
||||||
"termsAndCondsEtcConfirmed": "Ich habe folgendes gelesen, verstanden und stimme zu:",
|
"termsAndCondsEtcConfirmed": "Ich habe folgendes gelesen, verstanden und stimme zu:",
|
||||||
"title": "Benutzerkonto anlegen"
|
"title": "Nutzerkonto anlegen"
|
||||||
},
|
},
|
||||||
"email": {
|
"email": {
|
||||||
"buttonTitle": {
|
"buttonTitle": {
|
||||||
@ -197,14 +197,14 @@
|
|||||||
"data-privacy": "Ich habe die Datenschutzerklärung gelesen und verstanden.",
|
"data-privacy": "Ich habe die Datenschutzerklärung gelesen und verstanden.",
|
||||||
"description": "Um loszulegen, kannst Du Dich hier kostenfrei registrieren:",
|
"description": "Um loszulegen, kannst Du Dich hier kostenfrei registrieren:",
|
||||||
"errors": {
|
"errors": {
|
||||||
"email-exists": "Es gibt schon ein Benutzerkonto mit dieser E-Mail-Adresse!"
|
"email-exists": "Es gibt schon ein Nutzerkonto mit dieser E-Mail-Adresse!"
|
||||||
},
|
},
|
||||||
"submit": "Konto erstellen",
|
"submit": "Konto erstellen",
|
||||||
"success": "Eine E-Mail mit einem Link zum Abschließen Deiner Registrierung wurde an <b>{email}</b> geschickt",
|
"success": "Eine E-Mail mit einem Link zum Abschließen Deiner Registrierung wurde an <b>{email}</b> geschickt",
|
||||||
"terms-and-condition": "Ich stimme den <a href=\"/terms-and-conditions\" target=\"_blank\"><ds-text bold color=\"primary\">Nutzungsbedingungen</ds-text></a> zu."
|
"terms-and-condition": "Ich stimme den <a href=\"/terms-and-conditions\" target=\"_blank\"><ds-text bold color=\"primary\">Nutzungsbedingungen</ds-text></a> zu."
|
||||||
},
|
},
|
||||||
"title": "Mach mit bei {APPLICATION_NAME}!",
|
"title": "Mach mit bei {APPLICATION_NAME}!",
|
||||||
"unavailable": "Leider ist die öffentliche Registrierung von Benutzerkonten auf diesem Server derzeit nicht möglich."
|
"unavailable": "Leider ist die öffentliche Registrierung von Nutzerkonten auf diesem Server derzeit nicht möglich."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -267,12 +267,14 @@
|
|||||||
"happy": "Glücklich",
|
"happy": "Glücklich",
|
||||||
"surprised": "Erstaunt"
|
"surprised": "Erstaunt"
|
||||||
},
|
},
|
||||||
"filterFollow": "Beiträge von Benutzern filtern, denen ich folge",
|
"filterFollow": "Beiträge von Nutzern filtern, denen ich folge",
|
||||||
"filterMasonryGrid": {
|
"filterMasonryGrid": {
|
||||||
"myFriends": "Benutzer denen ich folge",
|
"myFriends": "Nutzer denen ich folge",
|
||||||
|
"myGroups": "Aus meinen Gruppen",
|
||||||
"myTopics": "Meine Themen",
|
"myTopics": "Meine Themen",
|
||||||
"noFilter": "Beiträge filtern"
|
"noFilter": "Beiträge filtern"
|
||||||
},
|
},
|
||||||
|
"filterMyGroups": "Beiträge in meinen Gruppen",
|
||||||
"inappropriatePicture": "Dieses Bild kann für einige Menschen unangemessen sein.",
|
"inappropriatePicture": "Dieses Bild kann für einige Menschen unangemessen sein.",
|
||||||
"languageSelectLabel": "Sprache Deines Beitrags",
|
"languageSelectLabel": "Sprache Deines Beitrags",
|
||||||
"languageSelectText": "Sprache wählen",
|
"languageSelectText": "Sprache wählen",
|
||||||
@ -356,7 +358,7 @@
|
|||||||
"unorderedList": "Ungeordnete Liste"
|
"unorderedList": "Ungeordnete Liste"
|
||||||
},
|
},
|
||||||
"mention": {
|
"mention": {
|
||||||
"noUsersFound": "Keine Benutzer gefunden"
|
"noUsersFound": "Keine Nutzer gefunden"
|
||||||
},
|
},
|
||||||
"placeholder": "Schreib etwas Inspirierendes …"
|
"placeholder": "Schreib etwas Inspirierendes …"
|
||||||
},
|
},
|
||||||
@ -378,8 +380,9 @@
|
|||||||
"deleteFilter": "Filter löschen",
|
"deleteFilter": "Filter löschen",
|
||||||
"emotions": "Emotionen",
|
"emotions": "Emotionen",
|
||||||
"filter-by": "Filtern nach ...",
|
"filter-by": "Filtern nach ...",
|
||||||
"following": "Benutzer denen ich folge",
|
"following": "Nutzer denen ich folge",
|
||||||
"languages": "Sprachen",
|
"languages": "Sprachen",
|
||||||
|
"my-groups": "Meinen Gruppen",
|
||||||
"order": {
|
"order": {
|
||||||
"newest": {
|
"newest": {
|
||||||
"hint": "Sortiere die Neuesten nach vorn",
|
"hint": "Sortiere die Neuesten nach vorn",
|
||||||
@ -411,9 +414,9 @@
|
|||||||
"actionRadius": "Aktionsradius der Gruppe",
|
"actionRadius": "Aktionsradius der Gruppe",
|
||||||
"addMemberToGroup": "Zur Gruppe hinzufügen",
|
"addMemberToGroup": "Zur Gruppe hinzufügen",
|
||||||
"addMemberToGroupSuccess": "„{name}“ wurde der Gruppe mit der Rolle „{role}“ hinzugefügt!",
|
"addMemberToGroupSuccess": "„{name}“ wurde der Gruppe mit der Rolle „{role}“ hinzugefügt!",
|
||||||
"addUser": "Benutzer hinzufügen",
|
"addUser": "Nutzer hinzufügen",
|
||||||
"addUserNoOptions": "Keine Nutzer gefunden!",
|
"addUserNoOptions": "Keine Nutzer gefunden!",
|
||||||
"addUserPlaceholder": "Benutzername",
|
"addUserPlaceholder": "Nutzername",
|
||||||
"allGroups": "Alle Gruppen",
|
"allGroups": "Alle Gruppen",
|
||||||
"button": {
|
"button": {
|
||||||
"tooltip": "Gruppen anzeigen"
|
"tooltip": "Gruppen anzeigen"
|
||||||
@ -434,7 +437,7 @@
|
|||||||
"title": "Meine Gruppe ändern"
|
"title": "Meine Gruppe ändern"
|
||||||
},
|
},
|
||||||
"errors": {
|
"errors": {
|
||||||
"userAlreadyMember": "Benutzer „{name}“ ist bereits Mitglied!"
|
"userAlreadyMember": "Nutzer „{name}“ ist bereits Mitglied!"
|
||||||
},
|
},
|
||||||
"follow": "Folge",
|
"follow": "Folge",
|
||||||
"foundation": "Gründung",
|
"foundation": "Gründung",
|
||||||
@ -452,6 +455,7 @@
|
|||||||
"message": "Eine Gruppe zu verlassen ist möglicherweise nicht rückgängig zu machen!<br>Gruppe <b>„{name}“</b> verlassen!",
|
"message": "Eine Gruppe zu verlassen ist möglicherweise nicht rückgängig zu machen!<br>Gruppe <b>„{name}“</b> verlassen!",
|
||||||
"title": "Möchtest du wirklich die Gruppe verlassen?"
|
"title": "Möchtest du wirklich die Gruppe verlassen?"
|
||||||
},
|
},
|
||||||
|
"memberRemoved": "Nutzer „{name}“ wurde aus der Gruppe entfernt!",
|
||||||
"members": "Mitglieder",
|
"members": "Mitglieder",
|
||||||
"membersAdministrationList": {
|
"membersAdministrationList": {
|
||||||
"avatar": "Avatar",
|
"avatar": "Avatar",
|
||||||
@ -511,10 +515,13 @@
|
|||||||
"no-results": "Keine Beiträge gefunden."
|
"no-results": "Keine Beiträge gefunden."
|
||||||
},
|
},
|
||||||
"invite-codes": {
|
"invite-codes": {
|
||||||
"copy-code": "Code:",
|
"button": {
|
||||||
|
"tooltip": "Lade deine Freunde ein"
|
||||||
|
},
|
||||||
|
"copy-code": "Einladungslink kopieren",
|
||||||
"copy-success": "Einladungscode erfolgreich in die Zwischenablage kopiert",
|
"copy-success": "Einladungscode erfolgreich in die Zwischenablage kopiert",
|
||||||
"not-available": "Du hast keinen Einladungscode zur Verfügung!",
|
"not-available": "Du hast keinen Einladungscode zur Verfügung!",
|
||||||
"your-code": "Kopiere deinen Einladungscode in die Ablage:"
|
"your-code": "Sende diesen Link per E-Mail oder in sozialen Medien, um deine Freunde einzuladen:"
|
||||||
},
|
},
|
||||||
"login": {
|
"login": {
|
||||||
"email": "Deine E-Mail",
|
"email": "Deine E-Mail",
|
||||||
@ -525,10 +532,10 @@
|
|||||||
"logout": "Abmelden",
|
"logout": "Abmelden",
|
||||||
"moreInfo": "Was ist {APPLICATION_NAME}?",
|
"moreInfo": "Was ist {APPLICATION_NAME}?",
|
||||||
"moreInfoHint": "zur Präsentationsseite",
|
"moreInfoHint": "zur Präsentationsseite",
|
||||||
"no-account": "Du hast noch kein Benutzerkonto?",
|
"no-account": "Du hast noch kein Nutzerkonto?",
|
||||||
"no-cookie": "Es kann kein Cookie angelegt werden. Du must Cookies akzeptieren.",
|
"no-cookie": "Es kann kein Cookie angelegt werden. Du must Cookies akzeptieren.",
|
||||||
"password": "Dein Passwort",
|
"password": "Dein Passwort",
|
||||||
"register": "Benutzerkonto erstellen",
|
"register": "Nutzerkonto erstellen",
|
||||||
"success": "Du bist eingeloggt!"
|
"success": "Du bist eingeloggt!"
|
||||||
},
|
},
|
||||||
"maintenance": {
|
"maintenance": {
|
||||||
@ -544,7 +551,7 @@
|
|||||||
"markerTypes": {
|
"markerTypes": {
|
||||||
"group": "Gruppe",
|
"group": "Gruppe",
|
||||||
"theUser": "deine Position",
|
"theUser": "deine Position",
|
||||||
"user": "Benutzer"
|
"user": "Nutzer"
|
||||||
},
|
},
|
||||||
"pageTitle": "Landkarte",
|
"pageTitle": "Landkarte",
|
||||||
"styles": {
|
"styles": {
|
||||||
@ -591,12 +598,12 @@
|
|||||||
"submit": "Bestätige Entscheidung",
|
"submit": "Bestätige Entscheidung",
|
||||||
"User": {
|
"User": {
|
||||||
"disable": {
|
"disable": {
|
||||||
"message": "Möchtest Du den Benutzer „<b>{name}</b>“ wirklich <b>gesperrt</b> lassen?",
|
"message": "Möchtest Du den Nutzer „<b>{name}</b>“ wirklich <b>gesperrt</b> lassen?",
|
||||||
"title": "Sperre den Benutzer abschließend"
|
"title": "Sperre den Nutzer abschließend"
|
||||||
},
|
},
|
||||||
"enable": {
|
"enable": {
|
||||||
"message": "Möchtest Du den Benutzer „<b>{name}</b>“ wirklich <b>entsperrt</b> lassen?",
|
"message": "Möchtest Du den Nutzer „<b>{name}</b>“ wirklich <b>entsperrt</b> lassen?",
|
||||||
"title": "Entsperre den Benutzer abschließend"
|
"title": "Entsperre den Nutzer abschließend"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -636,6 +643,7 @@
|
|||||||
"read": "Gelesen",
|
"read": "Gelesen",
|
||||||
"unread": "Ungelesen"
|
"unread": "Ungelesen"
|
||||||
},
|
},
|
||||||
|
"markAllAsRead": "Markiere alle als gelesen",
|
||||||
"pageLink": "Alle Benachrichtigungen",
|
"pageLink": "Alle Benachrichtigungen",
|
||||||
"post": "Beitrag",
|
"post": "Beitrag",
|
||||||
"reason": {
|
"reason": {
|
||||||
@ -644,12 +652,12 @@
|
|||||||
"mentioned_in_post": "Hat Dich in einem Beitrag erwähnt …"
|
"mentioned_in_post": "Hat Dich in einem Beitrag erwähnt …"
|
||||||
},
|
},
|
||||||
"title": "Benachrichtigungen",
|
"title": "Benachrichtigungen",
|
||||||
"user": "Benutzer"
|
"user": "Nutzer"
|
||||||
},
|
},
|
||||||
"position": {
|
"position": {
|
||||||
"group": "Gruppe",
|
"group": "Gruppe",
|
||||||
"my": "Meine Position",
|
"my": "Meine Position",
|
||||||
"user": "Benutzer"
|
"user": "Nutzer"
|
||||||
},
|
},
|
||||||
"post": {
|
"post": {
|
||||||
"comment": {
|
"comment": {
|
||||||
@ -740,7 +748,7 @@
|
|||||||
"submit": "freigeben",
|
"submit": "freigeben",
|
||||||
"success": "Erfolgreich freigegeben!",
|
"success": "Erfolgreich freigegeben!",
|
||||||
"user": {
|
"user": {
|
||||||
"error": "Den Benutzer hast Du schon gemeldet!",
|
"error": "Den Nutzer hast Du schon gemeldet!",
|
||||||
"message": "Bist Du sicher, dass Du den Nutzer „<b>{name}</b>“ freigeben möchtest?",
|
"message": "Bist Du sicher, dass Du den Nutzer „<b>{name}</b>“ freigeben möchtest?",
|
||||||
"title": "Nutzer freigeben",
|
"title": "Nutzer freigeben",
|
||||||
"type": "Nutzer"
|
"type": "Nutzer"
|
||||||
@ -784,7 +792,7 @@
|
|||||||
"submit": "Meldung senden",
|
"submit": "Meldung senden",
|
||||||
"success": "Vielen Dank für diese Meldung!",
|
"success": "Vielen Dank für diese Meldung!",
|
||||||
"user": {
|
"user": {
|
||||||
"error": "Du hast den Benutzer bereits gemeldet!",
|
"error": "Du hast den Nutzer bereits gemeldet!",
|
||||||
"message": "Bist Du sicher, dass Du den Nutzer „<b>{name}</b>“ melden möchtest?",
|
"message": "Bist Du sicher, dass Du den Nutzer „<b>{name}</b>“ melden möchtest?",
|
||||||
"title": "Nutzer melden",
|
"title": "Nutzer melden",
|
||||||
"type": "Nutzer"
|
"type": "Nutzer"
|
||||||
@ -797,7 +805,7 @@
|
|||||||
"Group": "Gruppe ::: Gruppen",
|
"Group": "Gruppe ::: Gruppen",
|
||||||
"Post": "Beitrag ::: Beiträge",
|
"Post": "Beitrag ::: Beiträge",
|
||||||
"Tag": "Hashtag ::: Hashtags",
|
"Tag": "Hashtag ::: Hashtags",
|
||||||
"User": "Benutzer ::: Benutzer"
|
"User": "Nutzer ::: Nutzer"
|
||||||
},
|
},
|
||||||
"hint": "Wonach suchst Du? Nutze !… für Beiträge, @… für Mitglieder, &… für Gruppen, #… für Hashtags",
|
"hint": "Wonach suchst Du? Nutze !… für Beiträge, @… für Mitglieder, &… für Gruppen, #… für Hashtags",
|
||||||
"no-results": "Keine Ergebnisse für \"{search}\" gefunden. Versuch' es mit einem anderen Begriff!",
|
"no-results": "Keine Ergebnisse für \"{search}\" gefunden. Versuch' es mit einem anderen Begriff!",
|
||||||
@ -816,16 +824,16 @@
|
|||||||
},
|
},
|
||||||
"empty": "Bislang hast Du niemanden blockiert.",
|
"empty": "Bislang hast Du niemanden blockiert.",
|
||||||
"explanation": {
|
"explanation": {
|
||||||
"closing": "Das sollte fürs Erste genügen, damit blockierte Benutzer Dich nicht mehr länger belästigen können.",
|
"closing": "Das sollte fürs Erste genügen, damit blockierte Nutzer Dich nicht mehr länger belästigen können.",
|
||||||
"commenting-disabled": "Du kannst den Beitrag derzeit nicht kommentieren.",
|
"commenting-disabled": "Du kannst den Beitrag derzeit nicht kommentieren.",
|
||||||
"commenting-explanation": "Dafür kann es mehrere Gründe geben, bitte schau in unsere ",
|
"commenting-explanation": "Dafür kann es mehrere Gründe geben, bitte schau in unsere ",
|
||||||
"intro": "Wenn ein anderer Benutzer durch Dich blockiert wurde, dann passiert Folgendes:",
|
"intro": "Wenn ein anderer Nutzer durch Dich blockiert wurde, dann passiert Folgendes:",
|
||||||
"notifications": "Von Dir blockierte Benutzer werden keine Benachrichtigungen mehr erhalten, falls sie in Deinen Beiträgen erwähnt werden.",
|
"notifications": "Von Dir blockierte Nutzer werden keine Benachrichtigungen mehr erhalten, falls sie in Deinen Beiträgen erwähnt werden.",
|
||||||
"their-perspective": "Umgekehrt das gleiche: Die blockierte Person bekommt auch in ihren Benachrichtigungen Deine Beiträge nicht mehr zu sehen.",
|
"their-perspective": "Umgekehrt das gleiche: Die blockierte Person bekommt auch in ihren Benachrichtigungen Deine Beiträge nicht mehr zu sehen.",
|
||||||
"your-perspective": "In Deinen Benachrichtigungen tauchen keine Beiträge der blockierten Person mehr auf."
|
"your-perspective": "In Deinen Benachrichtigungen tauchen keine Beiträge der blockierten Person mehr auf."
|
||||||
},
|
},
|
||||||
"how-to": "Du kannst andere Benutzer auf deren Profilseite über das Inhaltsmenü blockieren.",
|
"how-to": "Du kannst andere Nutzer auf deren Profilseite über das Inhaltsmenü blockieren.",
|
||||||
"name": "Blockierte Benutzer",
|
"name": "Blockierte Nutzer",
|
||||||
"unblock": "Blockierten Nutzer freigeben",
|
"unblock": "Blockierten Nutzer freigeben",
|
||||||
"unblocked": "{name} ist wieder entsperrt"
|
"unblocked": "{name} ist wieder entsperrt"
|
||||||
},
|
},
|
||||||
@ -834,7 +842,7 @@
|
|||||||
"labelCity": "Deine Stadt oder Region",
|
"labelCity": "Deine Stadt oder Region",
|
||||||
"labelCityHint": "(zeigt ungefähre Position auf der Landkarte)",
|
"labelCityHint": "(zeigt ungefähre Position auf der Landkarte)",
|
||||||
"labelName": "Dein Name",
|
"labelName": "Dein Name",
|
||||||
"labelSlug": "Dein eindeutiger Benutzername",
|
"labelSlug": "Dein eindeutiger Nutzername",
|
||||||
"name": "Deine Daten",
|
"name": "Deine Daten",
|
||||||
"namePlaceholder": "Petra Lustig",
|
"namePlaceholder": "Petra Lustig",
|
||||||
"success": "Deine Daten wurden erfolgreich aktualisiert!"
|
"success": "Deine Daten wurden erfolgreich aktualisiert!"
|
||||||
@ -843,12 +851,12 @@
|
|||||||
"accountDescription": "Sei dir bewusst, dass deine Beiträge und Kommentare für unsere Community wichtig sind. Wenn du sie trotzdem löschen möchtest, musst du sie unten markieren.",
|
"accountDescription": "Sei dir bewusst, dass deine Beiträge und Kommentare für unsere Community wichtig sind. Wenn du sie trotzdem löschen möchtest, musst du sie unten markieren.",
|
||||||
"accountWarning": "Dein Konto, deine Beiträge oder Kommentare kannst du nach dem Löschen WEDER VERWALTEN NOCH WIEDERHERSTELLEN!",
|
"accountWarning": "Dein Konto, deine Beiträge oder Kommentare kannst du nach dem Löschen WEDER VERWALTEN NOCH WIEDERHERSTELLEN!",
|
||||||
"accountWarningAdmin": "Das Konto, die Beiträge oder Kommentare können nach dem Löschen WEDER VERWALTET NOCH WIEDERHERGESTELLT WERDEN!",
|
"accountWarningAdmin": "Das Konto, die Beiträge oder Kommentare können nach dem Löschen WEDER VERWALTET NOCH WIEDERHERGESTELLT WERDEN!",
|
||||||
"accountWarningIsAdmin": "Achtung! Du löschst jetzt ein Benutzerkonto!",
|
"accountWarningIsAdmin": "Achtung! Du löschst jetzt ein Nutzerkonto!",
|
||||||
"commentedCount": "Meinen {count} Kommentar löschen ::: Meine {count} Kommentare löschen",
|
"commentedCount": "Meinen {count} Kommentar löschen ::: Meine {count} Kommentare löschen",
|
||||||
"confirmDeleting": "Benutzerkonto jetzt löschen",
|
"confirmDeleting": "Nutzerkonto jetzt löschen",
|
||||||
"contributionsCount": "Meinen {count} Beitrag löschen ::: Meine {count} Beiträge löschen",
|
"contributionsCount": "Meinen {count} Beitrag löschen ::: Meine {count} Beiträge löschen",
|
||||||
"infoAdmin": "Alle Beiträge und Kommentare des Users werden zusätzlich gelöscht!",
|
"infoAdmin": "Alle Beiträge und Kommentare des Users werden zusätzlich gelöscht!",
|
||||||
"name": "Benutzerkonto löschen",
|
"name": "Nutzerkonto löschen",
|
||||||
"pleaseConfirm": "Zerstörerische Aktion! Gib „{confirm}“ ein, um zu bestätigen.",
|
"pleaseConfirm": "Zerstörerische Aktion! Gib „{confirm}“ ein, um zu bestätigen.",
|
||||||
"success": "Konto erfolgreich gelöscht!"
|
"success": "Konto erfolgreich gelöscht!"
|
||||||
},
|
},
|
||||||
@ -908,13 +916,13 @@
|
|||||||
},
|
},
|
||||||
"empty": "Bislang hast du niemanden stummgeschaltet.",
|
"empty": "Bislang hast du niemanden stummgeschaltet.",
|
||||||
"explanation": {
|
"explanation": {
|
||||||
"intro": "Wenn ein anderer Benutzer von dir stummgeschaltet wurde, dann passiert folgendes:",
|
"intro": "Wenn ein anderer Nutzer von dir stummgeschaltet wurde, dann passiert folgendes:",
|
||||||
"search": "Die Beiträge von stummgeschalteten Personen verschwinden aus deinen Suchergebnissen.",
|
"search": "Die Beiträge von stummgeschalteten Personen verschwinden aus deinen Suchergebnissen.",
|
||||||
"your-perspective": "In deiner Beitragsübersicht tauchen keine Beiträge der stummgeschalteten Person mehr auf."
|
"your-perspective": "In deiner Beitragsübersicht tauchen keine Beiträge der stummgeschalteten Person mehr auf."
|
||||||
},
|
},
|
||||||
"how-to": "Du kannst andere Benutzer auf deren Profilseite über das Inhaltsmenü stummschalten.",
|
"how-to": "Du kannst andere Nutzer auf deren Profilseite über das Inhaltsmenü stummschalten.",
|
||||||
"mute": "Stumm schalten",
|
"mute": "Stumm schalten",
|
||||||
"name": "Stummgeschaltete Benutzer",
|
"name": "Stummgeschaltete Nutzer",
|
||||||
"unmute": "Stummschaltung aufheben",
|
"unmute": "Stummschaltung aufheben",
|
||||||
"unmuted": "{name} ist nicht mehr stummgeschaltet"
|
"unmuted": "{name} ist nicht mehr stummgeschaltet"
|
||||||
},
|
},
|
||||||
@ -969,7 +977,7 @@
|
|||||||
},
|
},
|
||||||
"validation": {
|
"validation": {
|
||||||
"slug": {
|
"slug": {
|
||||||
"alreadyTaken": "Dieser Benutzername ist schon vergeben.",
|
"alreadyTaken": "Dieser Nutzername ist schon vergeben.",
|
||||||
"regex": "Es sind nur Kleinbuchstaben, Zahlen, Unterstriche oder Bindestriche erlaubt."
|
"regex": "Es sind nur Kleinbuchstaben, Zahlen, Unterstriche oder Bindestriche erlaubt."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -270,9 +270,11 @@
|
|||||||
"filterFollow": "Filter contributions from users I follow",
|
"filterFollow": "Filter contributions from users I follow",
|
||||||
"filterMasonryGrid": {
|
"filterMasonryGrid": {
|
||||||
"myFriends": "Users I follow",
|
"myFriends": "Users I follow",
|
||||||
|
"myGroups": "By my groups",
|
||||||
"myTopics": "My topics",
|
"myTopics": "My topics",
|
||||||
"noFilter": "Filter posts"
|
"noFilter": "Filter posts"
|
||||||
},
|
},
|
||||||
|
"filterMyGroups": "Contributions in my groups",
|
||||||
"inappropriatePicture": "This image may be inappropriate for some people.",
|
"inappropriatePicture": "This image may be inappropriate for some people.",
|
||||||
"languageSelectLabel": "Language of your contribution",
|
"languageSelectLabel": "Language of your contribution",
|
||||||
"languageSelectText": "Select Language",
|
"languageSelectText": "Select Language",
|
||||||
@ -380,6 +382,7 @@
|
|||||||
"filter-by": "Filter by ...",
|
"filter-by": "Filter by ...",
|
||||||
"following": "Users I follow",
|
"following": "Users I follow",
|
||||||
"languages": "Languages",
|
"languages": "Languages",
|
||||||
|
"my-groups": "My groups",
|
||||||
"order": {
|
"order": {
|
||||||
"newest": {
|
"newest": {
|
||||||
"hint": "Sort posts by the newest first",
|
"hint": "Sort posts by the newest first",
|
||||||
@ -452,6 +455,7 @@
|
|||||||
"message": "Leaving a group may be irreversible!<br>Leave group <b>“{name}”</b>!",
|
"message": "Leaving a group may be irreversible!<br>Leave group <b>“{name}”</b>!",
|
||||||
"title": "Do you really want to leave the group?"
|
"title": "Do you really want to leave the group?"
|
||||||
},
|
},
|
||||||
|
"memberRemoved": "User “{name}” was removed from group!",
|
||||||
"members": "Members",
|
"members": "Members",
|
||||||
"membersAdministrationList": {
|
"membersAdministrationList": {
|
||||||
"avatar": "Avatar",
|
"avatar": "Avatar",
|
||||||
@ -511,10 +515,13 @@
|
|||||||
"no-results": "No contributions found."
|
"no-results": "No contributions found."
|
||||||
},
|
},
|
||||||
"invite-codes": {
|
"invite-codes": {
|
||||||
"copy-code": "Code:",
|
"button": {
|
||||||
|
"tooltip": "Invite your friends"
|
||||||
|
},
|
||||||
|
"copy-code": "Copy Invite Link",
|
||||||
"copy-success": "Invite code copied to clipboard",
|
"copy-success": "Invite code copied to clipboard",
|
||||||
"not-available": "You have no valid invite code available!",
|
"not-available": "You have no valid invite code available!",
|
||||||
"your-code": "Copy your invite code to the clipboard:"
|
"your-code": "Send this link per e-mail or in social media to invite your friends:"
|
||||||
},
|
},
|
||||||
"login": {
|
"login": {
|
||||||
"email": "Your E-mail",
|
"email": "Your E-mail",
|
||||||
@ -636,6 +643,7 @@
|
|||||||
"read": "Read",
|
"read": "Read",
|
||||||
"unread": "Unread"
|
"unread": "Unread"
|
||||||
},
|
},
|
||||||
|
"markAllAsRead": "Mark all as read",
|
||||||
"pageLink": "All notifications",
|
"pageLink": "All notifications",
|
||||||
"post": "Post",
|
"post": "Post",
|
||||||
"reason": {
|
"reason": {
|
||||||
|
|||||||
@ -412,6 +412,7 @@
|
|||||||
"read": "Leído",
|
"read": "Leído",
|
||||||
"unread": "No leído"
|
"unread": "No leído"
|
||||||
},
|
},
|
||||||
|
"markAllAsRead": "Marcar todas como leido",
|
||||||
"pageLink": "Todas las notificaciones",
|
"pageLink": "Todas las notificaciones",
|
||||||
"post": "Contribución",
|
"post": "Contribución",
|
||||||
"reason": {
|
"reason": {
|
||||||
|
|||||||
@ -401,6 +401,7 @@
|
|||||||
"read": "Lire",
|
"read": "Lire",
|
||||||
"unread": "Non lu"
|
"unread": "Non lu"
|
||||||
},
|
},
|
||||||
|
"markAllAsRead": "Tout marquer comme lu",
|
||||||
"pageLink": "Toutes les notifications",
|
"pageLink": "Toutes les notifications",
|
||||||
"post": "Post",
|
"post": "Post",
|
||||||
"reason": {
|
"reason": {
|
||||||
|
|||||||
@ -354,6 +354,7 @@
|
|||||||
"read": null,
|
"read": null,
|
||||||
"unread": null
|
"unread": null
|
||||||
},
|
},
|
||||||
|
"markAllAsRead": "Segna tutti come letti",
|
||||||
"pageLink": null,
|
"pageLink": null,
|
||||||
"post": null,
|
"post": null,
|
||||||
"reason": {
|
"reason": {
|
||||||
|
|||||||
@ -100,6 +100,26 @@
|
|||||||
"moreInfo": "Wat is {APPLICATION_NAME}?",
|
"moreInfo": "Wat is {APPLICATION_NAME}?",
|
||||||
"password": "Uw Wachtwoord"
|
"password": "Uw Wachtwoord"
|
||||||
},
|
},
|
||||||
|
"notifications": {
|
||||||
|
"comment": null,
|
||||||
|
"content": null,
|
||||||
|
"empty": null,
|
||||||
|
"filterLabel": {
|
||||||
|
"all": null,
|
||||||
|
"read": null,
|
||||||
|
"unread": null
|
||||||
|
},
|
||||||
|
"markAllAsRead": "Markeer alles als gelezen",
|
||||||
|
"pageLink": null,
|
||||||
|
"post": null,
|
||||||
|
"reason": {
|
||||||
|
"commented_on_post": null,
|
||||||
|
"mentioned_in_comment": null,
|
||||||
|
"mentioned_in_post": null
|
||||||
|
},
|
||||||
|
"title": null,
|
||||||
|
"user": null
|
||||||
|
},
|
||||||
"post": {
|
"post": {
|
||||||
"moreInfo": {
|
"moreInfo": {
|
||||||
"name": "Meer info"
|
"name": "Meer info"
|
||||||
|
|||||||
@ -199,6 +199,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"notifications": {
|
"notifications": {
|
||||||
|
"markAllAsRead": "Oznacz wszystkie jako przeczytane",
|
||||||
"menu": {
|
"menu": {
|
||||||
"mentioned": "wspomiał o Tobie we wpisie"
|
"mentioned": "wspomiał o Tobie we wpisie"
|
||||||
}
|
}
|
||||||
|
|||||||
@ -390,6 +390,7 @@
|
|||||||
"read": "Lido",
|
"read": "Lido",
|
||||||
"unread": "Não lido"
|
"unread": "Não lido"
|
||||||
},
|
},
|
||||||
|
"markAllAsRead": "Marcar todas como lidas",
|
||||||
"pageLink": "Todas as notificações",
|
"pageLink": "Todas as notificações",
|
||||||
"post": "Post",
|
"post": "Post",
|
||||||
"reason": {
|
"reason": {
|
||||||
|
|||||||
@ -426,6 +426,7 @@
|
|||||||
"read": "Прочитанные",
|
"read": "Прочитанные",
|
||||||
"unread": "Непрочитанные"
|
"unread": "Непрочитанные"
|
||||||
},
|
},
|
||||||
|
"markAllAsRead": "Отметить все как прочитанное",
|
||||||
"pageLink": "Все уведомления",
|
"pageLink": "Все уведомления",
|
||||||
"post": "Пост",
|
"post": "Пост",
|
||||||
"reason": {
|
"reason": {
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div v-if="isGroupVisible">
|
<div class="group-profile" v-if="isGroupVisible">
|
||||||
<ds-space />
|
<ds-space />
|
||||||
<ds-flex v-if="group" :width="{ base: '100%' }" gutter="base">
|
<ds-flex v-if="group" :width="{ base: '100%' }" gutter="base">
|
||||||
<ds-flex-item :width="{ base: '100%', sm: 2, md: 2, lg: 1 }">
|
<ds-flex-item :width="{ base: '100%', sm: 2, md: 2, lg: 1 }">
|
||||||
|
|||||||
@ -30,7 +30,11 @@
|
|||||||
<div class="filterButtonMenu" :class="{ 'hide-filter': hideByScroll }">
|
<div class="filterButtonMenu" :class="{ 'hide-filter': hideByScroll }">
|
||||||
<base-button
|
<base-button
|
||||||
class="my-filter-button"
|
class="my-filter-button"
|
||||||
v-if="!postsFilter['categories_some'] && !postsFilter['author']"
|
v-if="
|
||||||
|
!postsFilter['categories_some'] &&
|
||||||
|
!postsFilter['author'] &&
|
||||||
|
!postsFilter['postsInMyGroups']
|
||||||
|
"
|
||||||
right
|
right
|
||||||
@click="showFilter = !showFilter"
|
@click="showFilter = !showFilter"
|
||||||
filled
|
filled
|
||||||
@ -66,6 +70,20 @@
|
|||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
|
<span v-if="postsFilter['postsInMyGroups']">
|
||||||
|
<base-button class="my-filter-button" right @click="showFilter = !showFilter" filled>
|
||||||
|
{{ $t('contribution.filterMasonryGrid.myGroups') }}
|
||||||
|
</base-button>
|
||||||
|
<base-button
|
||||||
|
class="filter-remove"
|
||||||
|
@click="resetByGroups"
|
||||||
|
icon="close"
|
||||||
|
:title="$t('filter-menu.deleteFilter')"
|
||||||
|
style="margin-left: -8px"
|
||||||
|
filled
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
|
||||||
<div id="my-filter" v-if="showFilter">
|
<div id="my-filter" v-if="showFilter">
|
||||||
<div @mouseleave="showFilter = false">
|
<div @mouseleave="showFilter = false">
|
||||||
<filter-menu-component @showFilterMenu="showFilterMenu" />
|
<filter-menu-component @showFilterMenu="showFilterMenu" />
|
||||||
@ -74,16 +92,16 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</ds-grid-item>
|
</ds-grid-item>
|
||||||
<ds-space :margin-bottom="{ base: 'small', md: 'base', lg: 'large' }" />
|
<!-- Placeholder/Space Row -->
|
||||||
|
<ds-grid-item :row-span="1" column-span="fullWidth" />
|
||||||
|
<!-- hashtag filter -->
|
||||||
<ds-grid-item v-if="hashtag" :row-span="2" column-span="fullWidth">
|
<ds-grid-item v-if="hashtag" :row-span="2" column-span="fullWidth">
|
||||||
<hashtags-filter :hashtag="hashtag" @clearSearch="clearSearch" />
|
<hashtags-filter :hashtag="hashtag" @clearSearch="clearSearch" />
|
||||||
</ds-grid-item>
|
</ds-grid-item>
|
||||||
<ds-space :margin-bottom="{ base: 'small', md: 'base', lg: 'large' }" />
|
|
||||||
<!-- donation info -->
|
<!-- donation info -->
|
||||||
<ds-grid-item v-if="showDonations" class="top-info-bar" :row-span="1" column-span="fullWidth">
|
<ds-grid-item v-if="showDonations" class="top-info-bar" :row-span="1" column-span="fullWidth">
|
||||||
<donation-info :goal="goal" :progress="progress" />
|
<donation-info :goal="goal" :progress="progress" />
|
||||||
</ds-grid-item>
|
</ds-grid-item>
|
||||||
<ds-space :margin-bottom="{ base: 'small', md: 'base', lg: 'large' }" />
|
|
||||||
<!-- news feed -->
|
<!-- news feed -->
|
||||||
<template v-if="hasResults">
|
<template v-if="hasResults">
|
||||||
<masonry-grid-item
|
<masonry-grid-item
|
||||||
@ -203,6 +221,7 @@ export default {
|
|||||||
methods: {
|
methods: {
|
||||||
...mapMutations({
|
...mapMutations({
|
||||||
resetByFollowed: 'posts/TOGGLE_FILTER_BY_FOLLOWED',
|
resetByFollowed: 'posts/TOGGLE_FILTER_BY_FOLLOWED',
|
||||||
|
resetByGroups: 'posts/TOGGLE_FILTER_BY_MY_GROUPS',
|
||||||
resetCategories: 'posts/RESET_CATEGORIES',
|
resetCategories: 'posts/RESET_CATEGORIES',
|
||||||
toggleCategory: 'posts/TOGGLE_CATEGORY',
|
toggleCategory: 'posts/TOGGLE_CATEGORY',
|
||||||
}),
|
}),
|
||||||
@ -399,5 +418,8 @@ export default {
|
|||||||
font-size: 23px;
|
font-size: 23px;
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
}
|
}
|
||||||
|
.ds-grid {
|
||||||
|
padding-top: 1em;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -1,21 +1,22 @@
|
|||||||
import { shallowMount, mount } from '@vue/test-utils'
|
import { mount } from '@vue/test-utils'
|
||||||
import NotificationsPage from './index.vue'
|
import NotificationsPage from './index.vue'
|
||||||
|
|
||||||
import DropdownFilter from '~/components/DropdownFilter/DropdownFilter'
|
import DropdownFilter from '~/components/DropdownFilter/DropdownFilter'
|
||||||
import NotificationsTable from '~/components/NotificationsTable/NotificationsTable'
|
import NotificationsTable from '~/components/NotificationsTable/NotificationsTable'
|
||||||
import PaginationButtons from '~/components/_new/generic/PaginationButtons/PaginationButtons'
|
import PaginationButtons from '~/components/_new/generic/PaginationButtons/PaginationButtons'
|
||||||
|
|
||||||
|
import { markAsReadMutation, markAllAsReadMutation } from '~/graphql/User'
|
||||||
const localVue = global.localVue
|
const localVue = global.localVue
|
||||||
|
|
||||||
const stubs = {
|
const stubs = {
|
||||||
'client-only': true,
|
'client-only': true,
|
||||||
|
'notifications-table': true,
|
||||||
}
|
}
|
||||||
|
|
||||||
describe('PostIndex', () => {
|
describe('PostIndex', () => {
|
||||||
let wrapper, Wrapper, mocks, propsData
|
let wrapper, Wrapper, mocks
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
propsData = {}
|
|
||||||
mocks = {
|
mocks = {
|
||||||
$t: (string) => string,
|
$t: (string) => string,
|
||||||
$toast: {
|
$toast: {
|
||||||
@ -37,86 +38,94 @@ describe('PostIndex', () => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('shallowMount', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
Wrapper = () => {
|
|
||||||
return shallowMount(NotificationsPage, {
|
|
||||||
mocks,
|
|
||||||
localVue,
|
|
||||||
propsData,
|
|
||||||
stubs,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
wrapper = Wrapper()
|
|
||||||
})
|
|
||||||
|
|
||||||
it('renders a Notications header', () => {
|
|
||||||
expect(wrapper.find('ds-heading-stub').exists()).toBe(true)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('renders a `dropdown-filter` component', () => {
|
|
||||||
expect(wrapper.find('dropdown-filter-stub').exists()).toBe(true)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('renders a `notifications-table` component', () => {
|
|
||||||
expect(wrapper.find('notifications-table-stub').exists()).toBe(true)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('mount', () => {
|
describe('mount', () => {
|
||||||
|
jest.clearAllMocks()
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
Wrapper = () => {
|
Wrapper = () => {
|
||||||
return mount(NotificationsPage, {
|
return mount(NotificationsPage, {
|
||||||
mocks,
|
mocks,
|
||||||
localVue,
|
localVue,
|
||||||
propsData,
|
|
||||||
stubs,
|
stubs,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
wrapper = Wrapper()
|
||||||
|
wrapper.setData({
|
||||||
|
notifications: [
|
||||||
|
{
|
||||||
|
id: 'mentioned_in_comment/c4-1/u1',
|
||||||
|
read: false,
|
||||||
|
reason: 'mentioned_in_comment',
|
||||||
|
createdAt: '2023-03-06T14:32:47.924Z',
|
||||||
|
updatedAt: '2023-03-06T14:32:47.924Z',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'mentioned_in_post/p8/u1',
|
||||||
|
read: false,
|
||||||
|
reason: 'mentioned_in_post',
|
||||||
|
createdAt: '2023-03-06T14:32:47.667Z',
|
||||||
|
updatedAt: '2023-03-06T14:32:47.667Z',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders a Notications header', () => {
|
||||||
|
expect(wrapper.find('.ds-heading').exists()).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders a `dropdown-filter` component', () => {
|
||||||
|
expect(wrapper.find('.dropdown-filter').exists()).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders a `notifications-table` component', () => {
|
||||||
|
expect(wrapper.findComponent(NotificationsTable).exists()).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders a `mark-all-as-read` button', () => {
|
||||||
|
expect(wrapper.find('[data-test="markAllAsRead-button"]').exists()).toBe(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('filter', () => {
|
describe('filter', () => {
|
||||||
beforeEach(() => {
|
it('has "All" as default', () => {
|
||||||
propsData.filterOptions = [
|
expect(wrapper.find('a.dropdown-filter').text()).toBe('notifications.filterLabel.all')
|
||||||
{ label: 'All', value: null },
|
|
||||||
{ label: 'Read', value: true },
|
|
||||||
{ label: 'Unread', value: false },
|
|
||||||
]
|
|
||||||
wrapper = Wrapper()
|
|
||||||
wrapper.findComponent(DropdownFilter).vm.$emit('filter', propsData.filterOptions[1])
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('sets `notificationRead` to value of received option', () => {
|
describe('select Read', () => {
|
||||||
expect(wrapper.vm.notificationRead).toEqual(propsData.filterOptions[1].value)
|
beforeEach(() => {
|
||||||
})
|
wrapper.findComponent(DropdownFilter).vm.$emit('filter', wrapper.vm.filterOptions[1])
|
||||||
|
})
|
||||||
|
|
||||||
it('set label to the label of the received option', () => {
|
it('sets `notificationRead` to value of received option', () => {
|
||||||
expect(wrapper.vm.selected).toEqual(propsData.filterOptions[1].label)
|
expect(wrapper.vm.notificationRead).toEqual(wrapper.vm.filterOptions[1].value)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('refreshes the notifications', () => {
|
it('sets label to the label of the received option', () => {
|
||||||
expect(mocks.$apollo.queries.notifications.refresh).toHaveBeenCalledTimes(1)
|
expect(wrapper.vm.selected).toEqual(wrapper.vm.filterOptions[1].label)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('refreshes the notifications', () => {
|
||||||
|
expect(mocks.$apollo.queries.notifications.refresh).toHaveBeenCalledTimes(1)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('markNotificationAsRead', () => {
|
describe('markNotificationAsRead', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
wrapper = Wrapper()
|
|
||||||
wrapper
|
wrapper
|
||||||
.findComponent(NotificationsTable)
|
.findComponent(NotificationsTable)
|
||||||
.vm.$emit('markNotificationAsRead', 'notificationSourceId')
|
.vm.$emit('markNotificationAsRead', 'notificationSourceId')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('calls markNotificationAsRead mutation', () => {
|
it('calls markAllAsRead mutation', () => {
|
||||||
expect(mocks.$apollo.mutate).toHaveBeenCalledWith(
|
expect(mocks.$apollo.mutate).toHaveBeenCalledWith({
|
||||||
expect.objectContaining({ variables: { id: 'notificationSourceId' } }),
|
mutation: markAsReadMutation(),
|
||||||
)
|
variables: { id: 'notificationSourceId' },
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('error handling', () => {
|
describe('error handling', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
mocks.$apollo.mutate = jest.fn().mockRejectedValueOnce({ message: 'Some error message' })
|
mocks.$apollo.mutate = jest.fn().mockRejectedValueOnce({ message: 'Some error message' })
|
||||||
wrapper = Wrapper()
|
|
||||||
wrapper
|
wrapper
|
||||||
.findComponent(NotificationsTable)
|
.findComponent(NotificationsTable)
|
||||||
.vm.$emit('markNotificationAsRead', 'notificationSourceId')
|
.vm.$emit('markNotificationAsRead', 'notificationSourceId')
|
||||||
@ -128,6 +137,26 @@ describe('PostIndex', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('markAllNotificationAsRead', () => {
|
||||||
|
it('calls markAllNotificationAsRead mutation and refreshes notification', async () => {
|
||||||
|
wrapper.find('button[data-test="markAllAsRead-button"]').trigger('click')
|
||||||
|
await expect(mocks.$apollo.mutate).toHaveBeenCalledWith({
|
||||||
|
mutation: markAllAsReadMutation(),
|
||||||
|
})
|
||||||
|
expect(mocks.$apollo.queries.notifications.refresh).toHaveBeenCalledTimes(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('error handling', () => {
|
||||||
|
it('shows an error message if there is an error', async () => {
|
||||||
|
mocks.$apollo.mutate = jest
|
||||||
|
.fn()
|
||||||
|
.mockRejectedValueOnce({ message: 'Another error message' })
|
||||||
|
await wrapper.find('button[data-test="markAllAsRead-button"]').trigger('click')
|
||||||
|
expect(mocks.$toast.error).toHaveBeenCalledWith('Another error message')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
describe('PaginationButtons', () => {
|
describe('PaginationButtons', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
wrapper = Wrapper()
|
wrapper = Wrapper()
|
||||||
|
|||||||
@ -15,7 +15,27 @@
|
|||||||
@markNotificationAsRead="markNotificationAsRead"
|
@markNotificationAsRead="markNotificationAsRead"
|
||||||
:notifications="notifications"
|
:notifications="notifications"
|
||||||
/>
|
/>
|
||||||
<pagination-buttons :hasNext="hasNext" :hasPrevious="hasPrevious" @back="back" @next="next" />
|
|
||||||
|
<ds-flex class="notifications-footer">
|
||||||
|
<ds-flex-item :width="{ base: 'auto' }" centered>
|
||||||
|
<pagination-buttons
|
||||||
|
:hasNext="hasNext"
|
||||||
|
:hasPrevious="hasPrevious"
|
||||||
|
@back="back"
|
||||||
|
@next="next"
|
||||||
|
/>
|
||||||
|
</ds-flex-item>
|
||||||
|
<ds-flex-item class="notifications-footer-button" :width="{ base: 'auto' }" centered>
|
||||||
|
<ds-button
|
||||||
|
primary
|
||||||
|
:disabled="unreadNotificationsCount === 0"
|
||||||
|
@click="markAllAsRead"
|
||||||
|
data-test="markAllAsRead-button"
|
||||||
|
>
|
||||||
|
{{ $t('notifications.markAllAsRead') }}
|
||||||
|
</ds-button>
|
||||||
|
</ds-flex-item>
|
||||||
|
</ds-flex>
|
||||||
</base-card>
|
</base-card>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -23,7 +43,7 @@
|
|||||||
import NotificationsTable from '~/components/NotificationsTable/NotificationsTable'
|
import NotificationsTable from '~/components/NotificationsTable/NotificationsTable'
|
||||||
import DropdownFilter from '~/components/DropdownFilter/DropdownFilter'
|
import DropdownFilter from '~/components/DropdownFilter/DropdownFilter'
|
||||||
import PaginationButtons from '~/components/_new/generic/PaginationButtons/PaginationButtons'
|
import PaginationButtons from '~/components/_new/generic/PaginationButtons/PaginationButtons'
|
||||||
import { notificationQuery, markAsReadMutation } from '~/graphql/User'
|
import { notificationQuery, markAsReadMutation, markAllAsReadMutation } from '~/graphql/User'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
@ -54,6 +74,15 @@ export default {
|
|||||||
{ label: this.$t('notifications.filterLabel.unread'), value: false },
|
{ label: this.$t('notifications.filterLabel.unread'), value: false },
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
hasNotifications() {
|
||||||
|
return this.notifications.length
|
||||||
|
},
|
||||||
|
unreadNotificationsCount() {
|
||||||
|
const result = this.notifications.reduce((count, notification) => {
|
||||||
|
return notification.read ? count : count + 1
|
||||||
|
}, 0)
|
||||||
|
return result
|
||||||
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
filter(option) {
|
filter(option) {
|
||||||
@ -77,6 +106,20 @@ export default {
|
|||||||
next() {
|
next() {
|
||||||
this.offset += this.pageSize
|
this.offset += this.pageSize
|
||||||
},
|
},
|
||||||
|
async markAllAsRead() {
|
||||||
|
if (!this.hasNotifications) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.$apollo.mutate({
|
||||||
|
mutation: markAllAsReadMutation(this.$i18n),
|
||||||
|
})
|
||||||
|
this.$apollo.queries.notifications.refresh()
|
||||||
|
} catch (error) {
|
||||||
|
this.$toast.error(error.message)
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
apollo: {
|
apollo: {
|
||||||
notifications: {
|
notifications: {
|
||||||
@ -112,4 +155,8 @@ export default {
|
|||||||
.notifications-page-flex {
|
.notifications-page-flex {
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.notifications-footer {
|
||||||
|
justify-content: space-evenly;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -30,6 +30,18 @@ export const mutations = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
TOGGLE_FILTER_BY_MY_GROUPS(state) {
|
||||||
|
const filter = clone(state.filter)
|
||||||
|
if (get(filter, 'postsInMyGroups')) {
|
||||||
|
delete filter.postsInMyGroups
|
||||||
|
state.filter = filter
|
||||||
|
} else {
|
||||||
|
state.filter = {
|
||||||
|
...filter,
|
||||||
|
postsInMyGroups: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
RESET_CATEGORIES(state) {
|
RESET_CATEGORIES(state) {
|
||||||
const filter = clone(state.filter)
|
const filter = clone(state.filter)
|
||||||
delete filter.categories_some
|
delete filter.categories_some
|
||||||
@ -84,6 +96,9 @@ export const getters = {
|
|||||||
filteredByUsersFollowed(state) {
|
filteredByUsersFollowed(state) {
|
||||||
return !!get(state.filter, 'author.followedBy_some.id')
|
return !!get(state.filter, 'author.followedBy_some.id')
|
||||||
},
|
},
|
||||||
|
filteredByPostsInMyGroups(state) {
|
||||||
|
return !!get(state.filter, 'postsInMyGroups')
|
||||||
|
},
|
||||||
filteredByEmotions(state) {
|
filteredByEmotions(state) {
|
||||||
return get(state.filter, 'emotions_some.emotion_in') || []
|
return get(state.filter, 'emotions_some.emotion_in') || []
|
||||||
},
|
},
|
||||||
|
|||||||
@ -56,6 +56,18 @@ describe('getters', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('filteredByPostsInMyGroups', () => {
|
||||||
|
it('returns true if filter is set', () => {
|
||||||
|
state = { filter: { postsInMyGroups: true } }
|
||||||
|
expect(getters.filteredByPostsInMyGroups(state)).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('returns false if filter is not set', () => {
|
||||||
|
state = { filter: { categories_some: { id_in: [23] } } }
|
||||||
|
expect(getters.filteredByUsersFollowed(state)).toBe(false)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
describe('filteredByEmotions', () => {
|
describe('filteredByEmotions', () => {
|
||||||
it('returns an emotions array if filter is set', () => {
|
it('returns an emotions array if filter is set', () => {
|
||||||
state = { filter: { emotions_some: { emotion_in: ['sad'] } } }
|
state = { filter: { emotions_some: { emotion_in: ['sad'] } } }
|
||||||
@ -230,6 +242,35 @@ describe('mutations', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('TOGGLE_FILTER_BY_MY_GROUPS', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
testMutation = () => {
|
||||||
|
mutations.TOGGLE_FILTER_BY_MY_GROUPS(state)
|
||||||
|
return getters.filter(state)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('given empty filter', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
state = { filter: {} }
|
||||||
|
})
|
||||||
|
|
||||||
|
it('sets postsInMyGroups filter to true', () => {
|
||||||
|
expect(testMutation()).toEqual({ postsInMyGroups: true })
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('already filtered', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
state = { filter: { postsInMyGroups: true } }
|
||||||
|
})
|
||||||
|
|
||||||
|
it('removes postsInMyGroups filter', () => {
|
||||||
|
expect(testMutation()).toEqual({})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
describe('TOGGLE_ORDER', () => {
|
describe('TOGGLE_ORDER', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
testMutation = (key) => {
|
testMutation = (key) => {
|
||||||
|
|||||||
@ -1969,9 +1969,9 @@ cachedir@^2.3.0:
|
|||||||
integrity sha512-A+Fezp4zxnit6FanDmv9EqXNAi3vt9DWp51/71UEhXukb7QUuvtv9344h91dyAxuTLoSYJFU299qzR3tzwPAhw==
|
integrity sha512-A+Fezp4zxnit6FanDmv9EqXNAi3vt9DWp51/71UEhXukb7QUuvtv9344h91dyAxuTLoSYJFU299qzR3tzwPAhw==
|
||||||
|
|
||||||
caniuse-lite@^1.0.30001157:
|
caniuse-lite@^1.0.30001157:
|
||||||
version "1.0.30001159"
|
version "1.0.30001458"
|
||||||
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001159.tgz#bebde28f893fa9594dadcaa7d6b8e2aa0299df20"
|
resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001458.tgz"
|
||||||
integrity sha512-w9Ph56jOsS8RL20K9cLND3u/+5WASWdhC/PPrf+V3/HsM3uHOavWOR1Xzakbv4Puo/srmPHudkmCRWM7Aq+/UA==
|
integrity sha512-lQ1VlUUq5q9ro9X+5gOEyH7i3vm+AYVT1WDCVB69XOZ17KZRhnZ9J0Sqz7wTHQaLBJccNCHq8/Ww5LlOIZbB0w==
|
||||||
|
|
||||||
caseless@~0.12.0:
|
caseless@~0.12.0:
|
||||||
version "0.12.0"
|
version "0.12.0"
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user