Merge branch 'master' into dependabot/npm_and_yarn/dotenv-16.3.1
2
.github/file-filters.yml
vendored
@ -1,4 +1,5 @@
|
||||
backend: &backend
|
||||
- '.github/workflows/test-backend.yml'
|
||||
- 'backend/**/*'
|
||||
- 'neo4j/**/*'
|
||||
|
||||
@ -6,4 +7,5 @@ docker: &docker
|
||||
- 'docker-compose.*'
|
||||
|
||||
webapp: &webapp
|
||||
- '.github/workflows/test-webapp.yml'
|
||||
- 'webapp/**/*'
|
||||
|
||||
42
.github/workflows/cleanup-cache-at-pr-closing.yml
vendored
Normal file
@ -0,0 +1,42 @@
|
||||
###############################################################################
|
||||
# A Github repo has max 10 GB of cache.
|
||||
# https://github.blog/changelog/2021-11-23-github-actions-cache-size-is-now-increased-to-10gb-per-repository/
|
||||
#
|
||||
# To avoid "cache thrashing" by their cache eviction policy it is recommended
|
||||
# to apply a cache cleanup workflow at PR closing to dele cache leftovers of
|
||||
# the current branch:
|
||||
# https://docs.github.com/en/actions/using-workflows/caching-dependencies-to-speed-up-workflows#force-deleting-cache-entries
|
||||
###############################################################################
|
||||
|
||||
name: ocelot.social cache cleanup on pr closing
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types:
|
||||
- closed
|
||||
|
||||
jobs:
|
||||
clean-branch-cache:
|
||||
name: Cleanup branch cache
|
||||
runs-on: ubuntu-latest
|
||||
continue-on-error: true
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Cleanup
|
||||
run: |
|
||||
gh extension install actions/gh-actions-cache
|
||||
REPO=${{ github.repository }}
|
||||
BRANCH="refs/pull/${{ github.event.pull_request.number }}/merge"
|
||||
echo "Fetching list of cache key"
|
||||
cacheKeysForPR=$(gh actions-cache list -R $REPO -B $BRANCH | cut -f 1 )
|
||||
set +e
|
||||
echo "Deleting caches..."
|
||||
for cacheKey in $cacheKeysForPR
|
||||
do
|
||||
gh actions-cache delete $cacheKey -R $REPO -B $BRANCH --confirm
|
||||
done
|
||||
echo "Done"
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
74
.github/workflows/test-backend.yml
vendored
@ -1,7 +1,7 @@
|
||||
name: ocelot.social backend test CI
|
||||
|
||||
|
||||
on: [push]
|
||||
on: push
|
||||
|
||||
jobs:
|
||||
files-changed:
|
||||
@ -13,7 +13,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v3.3.0
|
||||
|
||||
- name: Check for frontend file changes
|
||||
- name: Check for backend file changes
|
||||
uses: dorny/paths-filter@v2.11.1
|
||||
id: changes
|
||||
with:
|
||||
@ -34,12 +34,13 @@ jobs:
|
||||
run: |
|
||||
docker build --target community -t "ocelotsocialnetwork/neo4j-community:test" neo4j/
|
||||
docker save "ocelotsocialnetwork/neo4j-community:test" > /tmp/neo4j.tar
|
||||
|
||||
- name: Upload Artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
|
||||
- name: Cache docker images
|
||||
id: cache-neo4j
|
||||
uses: actions/cache/save@v3.3.1
|
||||
with:
|
||||
name: docker-neo4j-image
|
||||
path: /tmp/neo4j.tar
|
||||
key: ${{ github.run_id }}-backend-neo4j-cache
|
||||
|
||||
build_test_backend:
|
||||
name: Docker Build Test - Backend
|
||||
@ -54,12 +55,13 @@ jobs:
|
||||
run: |
|
||||
docker build --target test -t "ocelotsocialnetwork/backend:test" backend/
|
||||
docker save "ocelotsocialnetwork/backend:test" > /tmp/backend.tar
|
||||
|
||||
- name: Upload Artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
|
||||
- name: Cache docker images
|
||||
id: cache-backend
|
||||
uses: actions/cache/save@v3.3.1
|
||||
with:
|
||||
name: docker-backend-test
|
||||
path: /tmp/backend.tar
|
||||
key: ${{ github.run_id }}-backend-cache
|
||||
|
||||
lint_backend:
|
||||
name: Lint Backend
|
||||
@ -84,28 +86,29 @@ jobs:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Download Docker Image (Neo4J)
|
||||
uses: actions/download-artifact@v3
|
||||
- name: Restore Neo4J cache
|
||||
uses: actions/cache/restore@v3.3.1
|
||||
with:
|
||||
name: docker-neo4j-image
|
||||
path: /tmp
|
||||
path: /tmp/neo4j.tar
|
||||
key: ${{ github.run_id }}-backend-neo4j-cache
|
||||
fail-on-cache-miss: true
|
||||
|
||||
- name: Load Docker Image
|
||||
run: docker load < /tmp/neo4j.tar
|
||||
|
||||
- name: Download Docker Image (Backend)
|
||||
uses: actions/download-artifact@v3
|
||||
- name: Restore Backend cache
|
||||
uses: actions/cache/restore@v3.3.1
|
||||
with:
|
||||
name: docker-backend-test
|
||||
path: /tmp
|
||||
path: /tmp/backend.tar
|
||||
key: ${{ github.run_id }}-backend-cache
|
||||
fail-on-cache-miss: true
|
||||
|
||||
- name: Load Docker Image
|
||||
run: docker load < /tmp/backend.tar
|
||||
- name: Load Docker Images
|
||||
run: |
|
||||
docker load < /tmp/neo4j.tar
|
||||
docker load < /tmp/backend.tar
|
||||
|
||||
- name: backend | copy env files webapp
|
||||
run: cp webapp/.env.template webapp/.env
|
||||
- name: backend | copy env files backend
|
||||
run: cp backend/.env.template backend/.env
|
||||
- name: backend | copy env files
|
||||
run: |
|
||||
cp webapp/.env.template webapp/.env
|
||||
cp backend/.env.template backend/.env
|
||||
|
||||
- name: backend | docker-compose
|
||||
run: docker-compose -f docker-compose.yml -f docker-compose.test.yml up --detach --no-deps neo4j backend
|
||||
@ -118,3 +121,20 @@ jobs:
|
||||
|
||||
- name: backend | Unit test incl. coverage check
|
||||
run: docker-compose exec -T backend yarn test
|
||||
|
||||
cleanup:
|
||||
name: Cleanup
|
||||
if: ${{ needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.docker == 'true' }}
|
||||
needs: [files-changed, unit_test_backend]
|
||||
runs-on: ubuntu-latest
|
||||
continue-on-error: true
|
||||
steps:
|
||||
- name: Delete cache
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
gh extension install actions/gh-actions-cache
|
||||
KEY="${{ github.run_id }}-backend-neo4j-cache"
|
||||
gh actions-cache delete $KEY -R Ocelot-Social-Community/Ocelot-Social --confirm
|
||||
KEY="${{ github.run_id }}-backend-cache"
|
||||
gh actions-cache delete $KEY -R Ocelot-Social-Community/Ocelot-Social --confirm
|
||||
|
||||
117
.github/workflows/test-e2e.yml
vendored
@ -1,9 +1,54 @@
|
||||
name: ocelot.social end-to-end test CI
|
||||
|
||||
on: push
|
||||
|
||||
jobs:
|
||||
docker_preparation:
|
||||
name: Fullstack test preparation
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Copy env files
|
||||
run: |
|
||||
cp webapp/.env.template webapp/.env
|
||||
cp backend/.env.template backend/.env
|
||||
|
||||
- name: Build docker images
|
||||
run: |
|
||||
mkdir /tmp/images
|
||||
docker build --target community -t "ocelotsocialnetwork/neo4j-community:test" neo4j/
|
||||
docker save "ocelotsocialnetwork/neo4j-community:test" > /tmp/images/neo4j.tar
|
||||
docker build --target test -t "ocelotsocialnetwork/backend:test" backend/
|
||||
docker save "ocelotsocialnetwork/backend:test" > /tmp/images/backend.tar
|
||||
docker build --target test -t "ocelotsocialnetwork/webapp:test" webapp/
|
||||
docker save "ocelotsocialnetwork/webapp:test" > /tmp/images/webapp.tar
|
||||
|
||||
- name: Install cypress requirements
|
||||
run: |
|
||||
wget --no-verbose -O /opt/cucumber-json-formatter "https://github.com/cucumber/json-formatter/releases/download/v19.0.0/cucumber-json-formatter-linux-386"
|
||||
cd backend
|
||||
yarn install
|
||||
yarn build
|
||||
cd ..
|
||||
yarn install
|
||||
|
||||
- name: Cache docker images
|
||||
id: cache
|
||||
uses: actions/cache/save@v3.3.1
|
||||
with:
|
||||
path: |
|
||||
/opt/cucumber-json-formatter
|
||||
/home/runner/.cache/Cypress
|
||||
/home/runner/work/Ocelot-Social/Ocelot-Social
|
||||
/tmp/images/
|
||||
key: ${{ github.run_id }}-e2e-preparation-cache
|
||||
|
||||
fullstack_tests:
|
||||
name: Fullstack tests
|
||||
if: success()
|
||||
needs: docker_preparation
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
jobs: 8
|
||||
@ -12,34 +57,56 @@ jobs:
|
||||
# run copies of the current job in parallel
|
||||
job: [1, 2, 3, 4, 5, 6, 7, 8]
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
- name: Restore cache
|
||||
uses: actions/cache/restore@v3.3.1
|
||||
id: cache
|
||||
with:
|
||||
path: |
|
||||
/opt/cucumber-json-formatter
|
||||
/home/runner/.cache/Cypress
|
||||
/home/runner/work/Ocelot-Social/Ocelot-Social
|
||||
/tmp/images/
|
||||
key: ${{ github.run_id }}-e2e-preparation-cache
|
||||
fail-on-cache-miss: true
|
||||
|
||||
- name: webapp | copy env file
|
||||
run: cp webapp/.env.template webapp/.env
|
||||
|
||||
- name: backend | copy env file
|
||||
run: cp backend/.env.template backend/.env
|
||||
|
||||
- name: boot up test system | docker-compose
|
||||
run: docker-compose -f docker-compose.yml -f docker-compose.test.yml up --detach --no-deps webapp neo4j backend
|
||||
|
||||
- name: cypress | Fullstack tests
|
||||
id: e2e-tests
|
||||
- name: Boot up test system | docker-compose
|
||||
run: |
|
||||
cd backend
|
||||
yarn install
|
||||
yarn build
|
||||
cd ..
|
||||
yarn install
|
||||
yarn run cypress:run --spec $(cypress/parallel-features.sh ${{ matrix.job }} ${{ env.jobs }} )
|
||||
chmod +x /opt/cucumber-json-formatter
|
||||
sudo ln -fs /opt/cucumber-json-formatter /usr/bin/cucumber-json-formatter
|
||||
docker load < /tmp/images/neo4j.tar
|
||||
docker load < /tmp/images/backend.tar
|
||||
docker load < /tmp/images/webapp.tar
|
||||
docker-compose -f docker-compose.yml -f docker-compose.test.yml up --detach --no-deps webapp neo4j backend
|
||||
sleep 90s
|
||||
|
||||
##########################################################################
|
||||
# UPLOAD SCREENSHOTS - IF TESTS FAIL #####################################
|
||||
##########################################################################
|
||||
- name: Full stack tests | if any test failed, upload screenshots
|
||||
- name: Full stack tests | run tests
|
||||
id: e2e-tests
|
||||
run: yarn run cypress:run --spec $(cypress/parallel-features.sh ${{ matrix.job }} ${{ env.jobs }} )
|
||||
|
||||
- name: Full stack tests | if tests failed, compile html report
|
||||
if: ${{ failure() && steps.e2e-tests.conclusion == 'failure' }}
|
||||
run: |
|
||||
cd cypress/
|
||||
node create-cucumber-html-report.js
|
||||
|
||||
- name: Full stack tests | if tests failed, upload report
|
||||
id: e2e-report
|
||||
if: ${{ failure() && steps.e2e-tests.conclusion == 'failure' }}
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: cypress-screenshots
|
||||
path: cypress/screenshots/
|
||||
name: ocelot-e2e-test-report-pr${{ needs.docker_preparation.outputs.pr-number }}
|
||||
path: /home/runner/work/Ocelot-Social/Ocelot-Social/cypress/reports/cucumber_html_report
|
||||
|
||||
cleanup:
|
||||
name: Cleanup
|
||||
needs: [docker_preparation, fullstack_tests]
|
||||
runs-on: ubuntu-latest
|
||||
continue-on-error: true
|
||||
steps:
|
||||
- name: Delete cache
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
gh extension install actions/gh-actions-cache
|
||||
KEY="${{ github.run_id }}-e2e-preparation-cache"
|
||||
gh actions-cache delete $KEY -R Ocelot-Social-Community/Ocelot-Social --confirm
|
||||
52
.github/workflows/test-webapp.yml
vendored
@ -1,7 +1,7 @@
|
||||
name: ocelot.social webapp test CI
|
||||
|
||||
|
||||
on: [push]
|
||||
on: push
|
||||
|
||||
jobs:
|
||||
files-changed:
|
||||
@ -23,7 +23,7 @@ jobs:
|
||||
|
||||
prepare:
|
||||
name: Prepare
|
||||
if: needs.files-changed.outputs.webapp
|
||||
if: needs.files-changed.outputs.webapp == 'true'
|
||||
needs: files-changed
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
@ -34,30 +34,30 @@ jobs:
|
||||
run: |
|
||||
scripts/translations/sort.sh
|
||||
scripts/translations/missing-keys.sh
|
||||
|
||||
|
||||
build_test_webapp:
|
||||
name: Docker Build Test - Webapp
|
||||
if: needs.files-changed.outputs.docker == 'true' || needs.files-changed.outputs.webapp
|
||||
if: needs.files-changed.outputs.docker == 'true' || needs.files-changed.outputs.webapp == 'true'
|
||||
needs: [files-changed, prepare]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: webapp | Build 'test' image
|
||||
- name: Webapp | Build 'test' image
|
||||
run: |
|
||||
docker build --target test -t "ocelotsocialnetwork/webapp:test" webapp/
|
||||
docker save "ocelotsocialnetwork/webapp:test" > /tmp/webapp.tar
|
||||
|
||||
- name: Upload Artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
- name: Cache docker image
|
||||
uses: actions/cache/save@v3.3.1
|
||||
with:
|
||||
name: docker-webapp-test
|
||||
path: /tmp/webapp.tar
|
||||
key: ${{ github.run_id }}-webapp-cache
|
||||
|
||||
lint_webapp:
|
||||
name: Lint Webapp
|
||||
if: needs.files-changed.outputs.webapp
|
||||
if: needs.files-changed.outputs.webapp == 'true'
|
||||
needs: files-changed
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
@ -69,7 +69,7 @@ jobs:
|
||||
|
||||
unit_test_webapp:
|
||||
name: Unit Tests - Webapp
|
||||
if: needs.files-changed.outputs.docker == 'true' || needs.files-changed.outputs.webapp
|
||||
if: needs.files-changed.outputs.docker == 'true' || needs.files-changed.outputs.webapp == 'true'
|
||||
needs: [files-changed, build_test_webapp]
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
@ -78,20 +78,19 @@ jobs:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Download Docker Image (Webapp)
|
||||
uses: actions/download-artifact@v3
|
||||
- name: Restore webapp cache
|
||||
uses: actions/cache/restore@v3.3.1
|
||||
with:
|
||||
name: docker-webapp-test
|
||||
path: /tmp
|
||||
path: /tmp/webapp.tar
|
||||
key: ${{ github.run_id }}-webapp-cache
|
||||
|
||||
- name: Load Docker Image
|
||||
run: docker load < /tmp/webapp.tar
|
||||
|
||||
- name: backend | copy env files webapp
|
||||
run: cp webapp/.env.template webapp/.env
|
||||
|
||||
- name: backend | copy env files backend
|
||||
run: cp backend/.env.template backend/.env
|
||||
- name: Copy env files
|
||||
run: |
|
||||
cp webapp/.env.template webapp/.env
|
||||
cp backend/.env.template backend/.env
|
||||
|
||||
- name: backend | docker-compose
|
||||
run: docker-compose -f docker-compose.yml -f docker-compose.test.yml up --detach --no-deps webapp
|
||||
@ -99,3 +98,18 @@ jobs:
|
||||
- name: webapp | Unit tests incl. coverage check
|
||||
run: docker-compose exec -T webapp yarn test
|
||||
|
||||
cleanup:
|
||||
name: Cleanup
|
||||
if: ${{ needs.files-changed.outputs.docker == 'true' || needs.files-changed.outputs.webapp == 'true' }}
|
||||
needs: [files-changed, unit_test_webapp]
|
||||
runs-on: ubuntu-latest
|
||||
continue-on-error: true
|
||||
steps:
|
||||
- name: Delete cache
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
gh extension install actions/gh-actions-cache
|
||||
KEY="${{ github.run_id }}-webapp-cache"
|
||||
gh actions-cache delete $KEY -R Ocelot-Social-Community/Ocelot-Social --confirm
|
||||
|
||||
|
||||
@ -1,25 +1,219 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
env: {
|
||||
es6: true,
|
||||
// es6: true,
|
||||
node: true,
|
||||
jest: true
|
||||
},
|
||||
parserOptions: {
|
||||
/* parserOptions: {
|
||||
parser: 'babel-eslint'
|
||||
},
|
||||
},*/
|
||||
parser: '@typescript-eslint/parser',
|
||||
plugins: ['prettier', '@typescript-eslint' /*, 'import', 'n', 'promise'*/],
|
||||
extends: [
|
||||
'standard',
|
||||
'plugin:prettier/recommended'
|
||||
// 'eslint:recommended',
|
||||
'plugin:prettier/recommended',
|
||||
// 'plugin:import/recommended',
|
||||
// 'plugin:import/typescript',
|
||||
// 'plugin:security/recommended',
|
||||
// 'plugin:@eslint-community/eslint-comments/recommended',
|
||||
],
|
||||
plugins: [
|
||||
'jest'
|
||||
],
|
||||
rules: {
|
||||
settings: {
|
||||
'import/parsers': {
|
||||
'@typescript-eslint/parser': ['.ts', '.tsx'],
|
||||
},
|
||||
'import/resolver': {
|
||||
typescript: {
|
||||
project: ['./tsconfig.json'],
|
||||
},
|
||||
node: true,
|
||||
},
|
||||
},
|
||||
/* rules: {
|
||||
//'indent': [ 'error', 2 ],
|
||||
//'quotes': [ "error", "single"],
|
||||
// 'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'off',
|
||||
'no-console': ['error'],
|
||||
'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off',
|
||||
'prettier/prettier': ['error'],
|
||||
> 'no-console': ['error'],
|
||||
> 'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off',
|
||||
> 'prettier/prettier': ['error'],
|
||||
}, */
|
||||
rules: {
|
||||
'no-console': 'error',
|
||||
camelcase: 'error',
|
||||
'no-debugger': 'error',
|
||||
'prettier/prettier': [
|
||||
'error',
|
||||
{
|
||||
htmlWhitespaceSensitivity: 'ignore',
|
||||
},
|
||||
],
|
||||
// import
|
||||
// 'import/export': 'error',
|
||||
// 'import/no-deprecated': 'error',
|
||||
// 'import/no-empty-named-blocks': 'error',
|
||||
// 'import/no-extraneous-dependencies': 'error',
|
||||
// 'import/no-mutable-exports': 'error',
|
||||
// 'import/no-unused-modules': 'error',
|
||||
// 'import/no-named-as-default': 'error',
|
||||
// 'import/no-named-as-default-member': 'error',
|
||||
// 'import/no-amd': 'error',
|
||||
// 'import/no-commonjs': 'error',
|
||||
// 'import/no-import-module-exports': 'error',
|
||||
// 'import/no-nodejs-modules': 'off',
|
||||
// 'import/unambiguous': 'error',
|
||||
// 'import/default': 'error',
|
||||
// 'import/named': 'error',
|
||||
// 'import/namespace': 'error',
|
||||
// 'import/no-absolute-path': 'error',
|
||||
// 'import/no-cycle': 'error',
|
||||
// 'import/no-dynamic-require': 'error',
|
||||
// 'import/no-internal-modules': 'off',
|
||||
// 'import/no-relative-packages': 'error',
|
||||
// 'import/no-relative-parent-imports': ['error', { ignore: ['@/*'] }],
|
||||
// 'import/no-self-import': 'error',
|
||||
// 'import/no-unresolved': 'error',
|
||||
// 'import/no-useless-path-segments': 'error',
|
||||
// 'import/no-webpack-loader-syntax': 'error',
|
||||
// 'import/consistent-type-specifier-style': 'error',
|
||||
// 'import/exports-last': 'off',
|
||||
// 'import/extensions': 'error',
|
||||
// 'import/first': 'error',
|
||||
// 'import/group-exports': 'off',
|
||||
// 'import/newline-after-import': 'error',
|
||||
// 'import/no-anonymous-default-export': 'error',
|
||||
// 'import/no-default-export': 'error',
|
||||
// 'import/no-duplicates': 'error',
|
||||
// 'import/no-named-default': 'error',
|
||||
// 'import/no-namespace': 'error',
|
||||
// 'import/no-unassigned-import': 'error',
|
||||
// 'import/order': [
|
||||
// 'error',
|
||||
// {
|
||||
// groups: ['builtin', 'external', 'internal', 'parent', 'sibling', 'index', 'object', 'type'],
|
||||
// 'newlines-between': 'always',
|
||||
// pathGroups: [
|
||||
// {
|
||||
// pattern: '@?*/**',
|
||||
// group: 'external',
|
||||
// position: 'after',
|
||||
// },
|
||||
// {
|
||||
// pattern: '@/**',
|
||||
// group: 'external',
|
||||
// position: 'after',
|
||||
// },
|
||||
// ],
|
||||
// alphabetize: {
|
||||
// order: 'asc' /* sort in ascending order. Options: ['ignore', 'asc', 'desc'] */,
|
||||
// caseInsensitive: true /* ignore case. Options: [true, false] */,
|
||||
// },
|
||||
// distinctGroup: true,
|
||||
// },
|
||||
// ],
|
||||
// 'import/prefer-default-export': 'off',
|
||||
// n
|
||||
// 'n/handle-callback-err': 'error',
|
||||
// 'n/no-callback-literal': 'error',
|
||||
// 'n/no-exports-assign': 'error',
|
||||
// 'n/no-extraneous-import': 'error',
|
||||
// 'n/no-extraneous-require': 'error',
|
||||
// 'n/no-hide-core-modules': 'error',
|
||||
// 'n/no-missing-import': 'off', // not compatible with typescript
|
||||
// 'n/no-missing-require': 'error',
|
||||
// 'n/no-new-require': 'error',
|
||||
// 'n/no-path-concat': 'error',
|
||||
// 'n/no-process-exit': 'error',
|
||||
// 'n/no-unpublished-bin': 'error',
|
||||
// 'n/no-unpublished-import': 'off', // TODO need to exclude seeds
|
||||
// 'n/no-unpublished-require': 'error',
|
||||
// 'n/no-unsupported-features': ['error', { ignores: ['modules'] }],
|
||||
// 'n/no-unsupported-features/es-builtins': 'error',
|
||||
// 'n/no-unsupported-features/es-syntax': 'error',
|
||||
// 'n/no-unsupported-features/node-builtins': 'error',
|
||||
// 'n/process-exit-as-throw': 'error',
|
||||
// 'n/shebang': 'error',
|
||||
// 'n/callback-return': 'error',
|
||||
// 'n/exports-style': 'error',
|
||||
// 'n/file-extension-in-import': 'off',
|
||||
// 'n/global-require': 'error',
|
||||
// 'n/no-mixed-requires': 'error',
|
||||
// 'n/no-process-env': 'error',
|
||||
// 'n/no-restricted-import': 'error',
|
||||
// 'n/no-restricted-require': 'error',
|
||||
// 'n/no-sync': 'error',
|
||||
// 'n/prefer-global/buffer': 'error',
|
||||
// 'n/prefer-global/console': 'error',
|
||||
// 'n/prefer-global/process': 'error',
|
||||
// 'n/prefer-global/text-decoder': 'error',
|
||||
// 'n/prefer-global/text-encoder': 'error',
|
||||
// 'n/prefer-global/url': 'error',
|
||||
// 'n/prefer-global/url-search-params': 'error',
|
||||
// 'n/prefer-promises/dns': 'error',
|
||||
// 'n/prefer-promises/fs': 'error',
|
||||
// promise
|
||||
// 'promise/catch-or-return': 'error',
|
||||
// 'promise/no-return-wrap': 'error',
|
||||
// 'promise/param-names': 'error',
|
||||
// 'promise/always-return': 'error',
|
||||
// 'promise/no-native': 'off',
|
||||
// 'promise/no-nesting': 'warn',
|
||||
// 'promise/no-promise-in-callback': 'warn',
|
||||
// 'promise/no-callback-in-promise': 'warn',
|
||||
// 'promise/avoid-new': 'warn',
|
||||
// 'promise/no-new-statics': 'error',
|
||||
// 'promise/no-return-in-finally': 'warn',
|
||||
// 'promise/valid-params': 'warn',
|
||||
// 'promise/prefer-await-to-callbacks': 'error',
|
||||
// 'promise/no-multiple-resolved': 'error',
|
||||
// eslint comments
|
||||
// '@eslint-community/eslint-comments/disable-enable-pair': ['error', { allowWholeFile: true }],
|
||||
// '@eslint-community/eslint-comments/no-restricted-disable': 'error',
|
||||
// '@eslint-community/eslint-comments/no-use': 'off',
|
||||
// '@eslint-community/eslint-comments/require-description': 'off',
|
||||
},
|
||||
overrides: [
|
||||
// only for ts files
|
||||
{
|
||||
files: ['*.ts', '*.tsx'],
|
||||
extends: [
|
||||
// 'plugin:@typescript-eslint/recommended',
|
||||
// 'plugin:@typescript-eslint/recommended-requiring-type-checking',
|
||||
// 'plugin:@typescript-eslint/strict',
|
||||
],
|
||||
rules: {
|
||||
// allow explicitly defined dangling promises
|
||||
// '@typescript-eslint/no-floating-promises': ['error', { ignoreVoid: true }],
|
||||
'no-void': ['error', { allowAsStatement: true }],
|
||||
// ignore prefer-regexp-exec rule to allow string.match(regex)
|
||||
'@typescript-eslint/prefer-regexp-exec': 'off',
|
||||
// this should not run on ts files: https://github.com/import-js/eslint-plugin-import/issues/2215#issuecomment-911245486
|
||||
'import/unambiguous': 'off',
|
||||
// this is not compatible with typeorm, due to joined tables can be null, but are not defined as nullable
|
||||
'@typescript-eslint/no-unnecessary-condition': 'off',
|
||||
},
|
||||
parserOptions: {
|
||||
tsconfigRootDir: __dirname,
|
||||
project: ['./tsconfig.json'],
|
||||
// this is to properly reference the referenced project database without requirement of compiling it
|
||||
// eslint-disable-next-line camelcase
|
||||
EXPERIMENTAL_useSourceOfProjectReferenceRedirect: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['*.spec.ts'],
|
||||
plugins: ['jest'],
|
||||
env: {
|
||||
jest: true,
|
||||
},
|
||||
rules: {
|
||||
'jest/no-disabled-tests': 'error',
|
||||
'jest/no-focused-tests': 'error',
|
||||
'jest/no-identical-title': 'error',
|
||||
'jest/prefer-to-have-length': 'error',
|
||||
'jest/valid-expect': 'error',
|
||||
'@typescript-eslint/unbound-method': 'off',
|
||||
// 'jest/unbound-method': 'error',
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
@ -11,7 +11,7 @@ module.exports = {
|
||||
],
|
||||
coverageThreshold: {
|
||||
global: {
|
||||
lines: 70,
|
||||
lines: 67,
|
||||
},
|
||||
},
|
||||
testMatch: ['**/src/**/?(*.)+(spec|test).ts?(x)'],
|
||||
|
||||
@ -9,12 +9,12 @@
|
||||
"main": "src/index.ts",
|
||||
"scripts": {
|
||||
"__migrate": "migrate --compiler 'ts:./src/db/compiler.ts' --migrations-dir ./src/db/migrations",
|
||||
"prod:migrate": "migrate --migrations-dir ./build/db/migrations --store ./build/db/migrate/store.js",
|
||||
"start": "node build/",
|
||||
"prod:migrate": "migrate --migrations-dir ./build/src/db/migrations --store ./build/src/db/migrate/store.js",
|
||||
"start": "node build/src/",
|
||||
"build": "tsc && ./scripts/build.copy.files.sh",
|
||||
"dev": "nodemon --exec ts-node src/ -e js,ts,gql",
|
||||
"dev:debug": "nodemon --exec babel-node --inspect=0.0.0.0:9229 src/ -e js,ts,gql",
|
||||
"lint": "eslint src --config .eslintrc.js",
|
||||
"lint": "eslint --max-warnings=0 --ext .js,.ts ./src",
|
||||
"test": "cross-env NODE_ENV=test NODE_OPTIONS=--max-old-space-size=8192 jest --runInBand --coverage --forceExit --detectOpenHandles",
|
||||
"db:clean": "ts-node src/db/clean.ts",
|
||||
"db:reset": "yarn run db:clean",
|
||||
@ -45,7 +45,6 @@
|
||||
"cheerio": "~1.0.0-rc.3",
|
||||
"cors": "~2.8.5",
|
||||
"cross-env": "~7.0.3",
|
||||
"debug": "~4.1.1",
|
||||
"dotenv": "~8.2.0",
|
||||
"express": "^4.17.1",
|
||||
"graphql": "^14.6.0",
|
||||
@ -97,27 +96,30 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@faker-js/faker": "7.6.0",
|
||||
"@types/jest": "^29.5.2",
|
||||
"@types/jest": "^27.0.2",
|
||||
"@types/node": "^20.2.5",
|
||||
"@typescript-eslint/eslint-plugin": "^5.57.1",
|
||||
"@typescript-eslint/parser": "^5.57.1",
|
||||
"apollo-server-testing": "~2.11.0",
|
||||
"chai": "~4.2.0",
|
||||
"cucumber": "~6.0.5",
|
||||
"eslint": "~6.8.0",
|
||||
"eslint-config-prettier": "~6.15.0",
|
||||
"eslint-config-standard": "~14.1.1",
|
||||
"eslint-plugin-import": "~2.20.2",
|
||||
"eslint-plugin-jest": "~23.8.2",
|
||||
"eslint-plugin-node": "~11.1.0",
|
||||
"eslint-plugin-prettier": "~3.4.1",
|
||||
"eslint-plugin-promise": "~4.3.1",
|
||||
"eslint-plugin-standard": "~4.0.1",
|
||||
"jest": "29.4",
|
||||
"eslint": "^8.37.0",
|
||||
"eslint-config-prettier": "^8.8.0",
|
||||
"eslint-config-standard": "^17.0.0",
|
||||
"eslint-import-resolver-typescript": "^3.5.4",
|
||||
"eslint-plugin-import": "^2.27.5",
|
||||
"eslint-plugin-jest": "^27.2.1",
|
||||
"eslint-plugin-n": "^15.7.0",
|
||||
"eslint-plugin-prettier": "^4.2.1",
|
||||
"eslint-plugin-promise": "^6.1.1",
|
||||
"eslint-plugin-security": "^1.7.1",
|
||||
"prettier": "^2.8.7",
|
||||
"jest": "^27.2.4",
|
||||
"nodemon": "~2.0.2",
|
||||
"prettier": "~2.3.2",
|
||||
"rosie": "^2.0.1",
|
||||
"ts-jest": "^29.1.0",
|
||||
"ts-jest": "^27.0.5",
|
||||
"ts-node": "^10.9.1",
|
||||
"typescript": "^5.0.4"
|
||||
"typescript": "^4.9.4"
|
||||
},
|
||||
"resolutions": {
|
||||
"**/**/fs-capacitor": "^6.2.0",
|
||||
|
||||
@ -1,24 +1,24 @@
|
||||
#!/bin/sh
|
||||
|
||||
# html files
|
||||
mkdir -p build/middleware/helpers/email/templates/
|
||||
cp -r src/middleware/helpers/email/templates/*.html build/middleware/helpers/email/templates/
|
||||
mkdir -p build/src/middleware/helpers/email/templates/
|
||||
cp -r src/middleware/helpers/email/templates/*.html build/src/middleware/helpers/email/templates/
|
||||
|
||||
mkdir -p build/middleware/helpers/email/templates/en/
|
||||
cp -r src/middleware/helpers/email/templates/en/*.html build/middleware/helpers/email/templates/en/
|
||||
mkdir -p build/src/middleware/helpers/email/templates/en/
|
||||
cp -r src/middleware/helpers/email/templates/en/*.html build/src/middleware/helpers/email/templates/en/
|
||||
|
||||
mkdir -p build/middleware/helpers/email/templates/de/
|
||||
cp -r src/middleware/helpers/email/templates/de/*.html build/middleware/helpers/email/templates/de/
|
||||
mkdir -p build/src/middleware/helpers/email/templates/de/
|
||||
cp -r src/middleware/helpers/email/templates/de/*.html build/src/middleware/helpers/email/templates/de/
|
||||
|
||||
# gql files
|
||||
mkdir -p build/schema/types/
|
||||
cp -r src/schema/types/*.gql build/schema/types/
|
||||
mkdir -p build/src/schema/types/
|
||||
cp -r src/schema/types/*.gql build/src/schema/types/
|
||||
|
||||
mkdir -p build/schema/types/enum/
|
||||
cp -r src/schema/types/enum/*.gql build/schema/types/enum/
|
||||
mkdir -p build/src/schema/types/enum/
|
||||
cp -r src/schema/types/enum/*.gql build/src/schema/types/enum/
|
||||
|
||||
mkdir -p build/schema/types/scalar/
|
||||
cp -r src/schema/types/scalar/*.gql build/schema/types/scalar/
|
||||
mkdir -p build/src/schema/types/scalar/
|
||||
cp -r src/schema/types/scalar/*.gql build/src/schema/types/scalar/
|
||||
|
||||
mkdir -p build/schema/types/type/
|
||||
cp -r src/schema/types/type/*.gql build/schema/types/type/
|
||||
mkdir -p build/src/schema/types/type/
|
||||
cp -r src/schema/types/type/*.gql build/src/schema/types/type/
|
||||
@ -15,7 +15,7 @@ if (require.resolve) {
|
||||
}
|
||||
|
||||
// Use Cypress env or process.env
|
||||
declare var Cypress: any | undefined
|
||||
declare let Cypress: any | undefined
|
||||
const env = typeof Cypress !== 'undefined' ? Cypress.env() : process.env // eslint-disable-line no-undef
|
||||
|
||||
const environment = {
|
||||
@ -95,6 +95,7 @@ Object.entries(required).map((entry) => {
|
||||
if (!entry[1]) {
|
||||
throw new Error(`ERROR: "${entry[0]}" env variable is missing.`)
|
||||
}
|
||||
return entry
|
||||
})
|
||||
|
||||
export default {
|
||||
|
||||
@ -1,2 +1,2 @@
|
||||
const tsNode = require('ts-node');
|
||||
module.exports = tsNode.register;
|
||||
const tsNode = require('ts-node')
|
||||
module.exports = tsNode.register
|
||||
|
||||
@ -2,16 +2,17 @@ import gql from 'graphql-tag'
|
||||
|
||||
export const createMessageMutation = () => {
|
||||
return gql`
|
||||
mutation (
|
||||
$roomId: ID!
|
||||
$content: String!
|
||||
) {
|
||||
CreateMessage(
|
||||
roomId: $roomId
|
||||
content: $content
|
||||
) {
|
||||
mutation ($roomId: ID!, $content: String!) {
|
||||
CreateMessage(roomId: $roomId, content: $content) {
|
||||
id
|
||||
content
|
||||
senderId
|
||||
username
|
||||
avatar
|
||||
date
|
||||
saved
|
||||
distributed
|
||||
seen
|
||||
}
|
||||
}
|
||||
`
|
||||
@ -19,16 +20,31 @@ export const createMessageMutation = () => {
|
||||
|
||||
export const messageQuery = () => {
|
||||
return gql`
|
||||
query($roomId: ID!) {
|
||||
Message(roomId: $roomId) {
|
||||
query ($roomId: ID!, $first: Int, $offset: Int) {
|
||||
Message(roomId: $roomId, first: $first, offset: $offset, orderBy: indexId_desc) {
|
||||
_id
|
||||
id
|
||||
indexId
|
||||
content
|
||||
senderId
|
||||
author {
|
||||
id
|
||||
}
|
||||
username
|
||||
avatar
|
||||
date
|
||||
saved
|
||||
distributed
|
||||
seen
|
||||
}
|
||||
}
|
||||
`
|
||||
}
|
||||
|
||||
export const markMessagesAsSeen = () => {
|
||||
return gql`
|
||||
mutation ($messageIds: [String!]) {
|
||||
MarkMessagesAsSeen(messageIds: $messageIds)
|
||||
}
|
||||
`
|
||||
}
|
||||
|
||||
@ -2,26 +2,14 @@ import gql from 'graphql-tag'
|
||||
|
||||
export const createRoomMutation = () => {
|
||||
return gql`
|
||||
mutation (
|
||||
$userId: ID!
|
||||
) {
|
||||
CreateRoom(
|
||||
userId: $userId
|
||||
) {
|
||||
id
|
||||
roomId
|
||||
}
|
||||
}
|
||||
`
|
||||
}
|
||||
|
||||
export const roomQuery = () => {
|
||||
return gql`
|
||||
query {
|
||||
Room {
|
||||
mutation ($userId: ID!) {
|
||||
CreateRoom(userId: $userId) {
|
||||
id
|
||||
roomId
|
||||
roomName
|
||||
lastMessageAt
|
||||
unreadCount
|
||||
#avatar
|
||||
users {
|
||||
_id
|
||||
id
|
||||
@ -34,3 +22,46 @@ export const roomQuery = () => {
|
||||
}
|
||||
`
|
||||
}
|
||||
|
||||
export const roomQuery = () => {
|
||||
return gql`
|
||||
query Room($first: Int, $offset: Int, $id: ID) {
|
||||
Room(first: $first, offset: $offset, id: $id, orderBy: lastMessageAt_desc) {
|
||||
id
|
||||
roomId
|
||||
roomName
|
||||
avatar
|
||||
lastMessageAt
|
||||
unreadCount
|
||||
lastMessage {
|
||||
_id
|
||||
id
|
||||
content
|
||||
senderId
|
||||
username
|
||||
avatar
|
||||
date
|
||||
saved
|
||||
distributed
|
||||
seen
|
||||
}
|
||||
users {
|
||||
_id
|
||||
id
|
||||
name
|
||||
avatar {
|
||||
url
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
}
|
||||
|
||||
export const unreadRoomsQuery = () => {
|
||||
return gql`
|
||||
query {
|
||||
UnreadRooms
|
||||
}
|
||||
`
|
||||
}
|
||||
|
||||
@ -9,10 +9,9 @@ function walkRecursive(data, fields, fieldName, callback, _key?) {
|
||||
if (!Array.isArray(fields)) {
|
||||
throw new Error('please provide an fields array for the walkRecursive helper')
|
||||
}
|
||||
if (data && typeof data === 'string' && fields.includes(_key)) {
|
||||
// well we found what we searched for, lets replace the value with our callback result
|
||||
const key = _key.split('!')
|
||||
if (key.length === 1 || key[1] !== fieldName) data = callback(data, key[0])
|
||||
const fieldDef = fields.find((f) => f.field === _key)
|
||||
if (data && typeof data === 'string' && fieldDef) {
|
||||
if (!fieldDef.excludes?.includes(fieldName)) data = callback(data, _key)
|
||||
} else if (data && Array.isArray(data)) {
|
||||
// go into the rabbit hole and dig through that array
|
||||
data.forEach((res, index) => {
|
||||
|
||||
60
backend/src/middleware/chatMiddleware.ts
Normal file
@ -0,0 +1,60 @@
|
||||
import { isArray } from 'lodash'
|
||||
|
||||
const setRoomProps = (room) => {
|
||||
if (room.users) {
|
||||
room.users.forEach((user) => {
|
||||
user._id = user.id
|
||||
})
|
||||
}
|
||||
if (room.lastMessage) {
|
||||
room.lastMessage._id = room.lastMessage.id
|
||||
}
|
||||
}
|
||||
|
||||
const setMessageProps = (message, context) => {
|
||||
message._id = message.id
|
||||
if (message.senderId !== context.user.id) {
|
||||
message.distributed = true
|
||||
}
|
||||
}
|
||||
|
||||
const roomProperties = async (resolve, root, args, context, info) => {
|
||||
const resolved = await resolve(root, args, context, info)
|
||||
if (resolved) {
|
||||
if (isArray(resolved)) {
|
||||
resolved.forEach((room) => {
|
||||
setRoomProps(room)
|
||||
})
|
||||
} else {
|
||||
setRoomProps(resolved)
|
||||
}
|
||||
}
|
||||
return resolved
|
||||
}
|
||||
|
||||
const messageProperties = async (resolve, root, args, context, info) => {
|
||||
const resolved = await resolve(root, args, context, info)
|
||||
if (resolved) {
|
||||
if (isArray(resolved)) {
|
||||
resolved.forEach((message) => {
|
||||
setMessageProps(message, context)
|
||||
})
|
||||
} else {
|
||||
setMessageProps(resolved, context)
|
||||
}
|
||||
}
|
||||
return resolved
|
||||
}
|
||||
|
||||
export default {
|
||||
Query: {
|
||||
Room: roomProperties,
|
||||
Message: messageProperties,
|
||||
},
|
||||
Mutation: {
|
||||
CreateRoom: roomProperties,
|
||||
},
|
||||
Subscription: {
|
||||
chatMessageAdded: messageProperties,
|
||||
},
|
||||
}
|
||||
@ -30,6 +30,7 @@ const standardSanitizeHtmlOptions = {
|
||||
'strike',
|
||||
'span',
|
||||
'blockquote',
|
||||
'usertag',
|
||||
],
|
||||
allowedAttributes: {
|
||||
a: ['href', 'class', 'target', 'data-*', 'contenteditable'],
|
||||
|
||||
@ -14,6 +14,7 @@ import login from './login/loginMiddleware'
|
||||
import sentry from './sentryMiddleware'
|
||||
import languages from './languages/languages'
|
||||
import userInteractions from './userInteractions'
|
||||
import chatMiddleware from './chatMiddleware'
|
||||
|
||||
export default (schema) => {
|
||||
const middlewares = {
|
||||
@ -31,6 +32,7 @@ export default (schema) => {
|
||||
orderBy,
|
||||
languages,
|
||||
userInteractions,
|
||||
chatMiddleware,
|
||||
}
|
||||
|
||||
let order = [
|
||||
@ -49,6 +51,7 @@ export default (schema) => {
|
||||
'softDelete',
|
||||
'includedFields',
|
||||
'orderBy',
|
||||
'chatMiddleware',
|
||||
]
|
||||
|
||||
// add permisions middleware at the first position (unless we're seeding)
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import cheerio from 'cheerio'
|
||||
|
||||
export default (content) => {
|
||||
export default (content?) => {
|
||||
if (!content) return []
|
||||
const $ = cheerio.load(content)
|
||||
const userIds = $('a.mention[data-mention-id]')
|
||||
|
||||
@ -50,7 +50,7 @@ beforeAll(async () => {
|
||||
context: () => {
|
||||
return {
|
||||
user: authenticatedUser,
|
||||
neode: neode,
|
||||
neode,
|
||||
driver,
|
||||
}
|
||||
},
|
||||
|
||||
@ -140,16 +140,18 @@ const postAuthorOfComment = async (commentId, { context }) => {
|
||||
|
||||
const notifyOwnersOfGroup = async (groupId, userId, reason, context) => {
|
||||
const cypher = `
|
||||
MATCH (user:User { id: $userId })
|
||||
MATCH (group:Group { id: $groupId })<-[membership:MEMBER_OF]-(owner:User)
|
||||
WHERE membership.role = 'owner'
|
||||
WITH owner, group
|
||||
WITH owner, group, user, membership
|
||||
MERGE (group)-[notification:NOTIFIED {reason: $reason}]->(owner)
|
||||
WITH group, owner, notification
|
||||
WITH group, owner, notification, user, membership
|
||||
SET notification.read = FALSE
|
||||
SET notification.createdAt = COALESCE(notification.createdAt, toString(datetime()))
|
||||
SET notification.updatedAt = toString(datetime())
|
||||
SET notification.relatedUserId = $userId
|
||||
RETURN notification {.*, from: group, to: properties(owner)}
|
||||
WITH owner, group { __typename: 'Group', .*, myRole: membership.roleInGroup } AS finalGroup, user, notification
|
||||
RETURN notification {.*, from: finalGroup, to: properties(owner), relatedUser: properties(user) }
|
||||
`
|
||||
const session = context.driver.session()
|
||||
const writeTxResultPromise = session.writeTransaction(async (transaction) => {
|
||||
@ -173,16 +175,20 @@ const notifyOwnersOfGroup = async (groupId, userId, reason, context) => {
|
||||
const notifyMemberOfGroup = async (groupId, userId, reason, context) => {
|
||||
const { user: owner } = context
|
||||
const cypher = `
|
||||
MATCH (owner:User { id: $ownerId })
|
||||
MATCH (user:User { id: $userId })
|
||||
MATCH (group:Group { id: $groupId })
|
||||
WITH user, group
|
||||
OPTIONAL MATCH (user)-[membership:MEMBER_OF]->(group)
|
||||
WITH user, group, owner, membership
|
||||
MERGE (group)-[notification:NOTIFIED {reason: $reason}]->(user)
|
||||
WITH group, user, notification
|
||||
WITH group, user, notification, owner, membership
|
||||
SET notification.read = FALSE
|
||||
SET notification.createdAt = COALESCE(notification.createdAt, toString(datetime()))
|
||||
SET notification.updatedAt = toString(datetime())
|
||||
SET notification.relatedUserId = $ownerId
|
||||
RETURN notification {.*, from: group, to: properties(user)}
|
||||
WITH group { __typename: 'Group', .*, myRole: membership.roleInGroup } AS finalGroup,
|
||||
notification, user, owner
|
||||
RETURN notification {.*, from: finalGroup, to: properties(user), relatedUser: properties(owner) }
|
||||
`
|
||||
const session = context.driver.session()
|
||||
const writeTxResultPromise = session.writeTransaction(async (transaction) => {
|
||||
@ -242,7 +248,7 @@ const notifyUsersOfMention = async (label, id, idsOfUsers, reason, context) => {
|
||||
SET notification.read = FALSE
|
||||
SET notification.createdAt = COALESCE(notification.createdAt, toString(datetime()))
|
||||
SET notification.updatedAt = toString(datetime())
|
||||
RETURN notification {.*, from: finalResource, to: properties(user)}
|
||||
RETURN notification {.*, from: finalResource, to: properties(user), relatedUser: properties(user) }
|
||||
`
|
||||
const session = context.driver.session()
|
||||
const writeTxResultPromise = session.writeTransaction(async (transaction) => {
|
||||
@ -276,9 +282,14 @@ const notifyUsersOfComment = async (label, commentId, postAuthorId, reason, cont
|
||||
SET notification.read = FALSE
|
||||
SET notification.createdAt = COALESCE(notification.createdAt, toString(datetime()))
|
||||
SET notification.updatedAt = toString(datetime())
|
||||
WITH notification, postAuthor, post,
|
||||
WITH notification, postAuthor, post, commenter,
|
||||
comment {.*, __typename: labels(comment)[0], author: properties(commenter), post: post {.*, author: properties(postAuthor) } } AS finalResource
|
||||
RETURN notification {.*, from: finalResource, to: properties(postAuthor)}
|
||||
RETURN notification {
|
||||
.*,
|
||||
from: finalResource,
|
||||
to: properties(postAuthor),
|
||||
relatedUser: properties(commenter)
|
||||
}
|
||||
`,
|
||||
{ commentId, postAuthorId, reason },
|
||||
)
|
||||
|
||||
@ -408,6 +408,7 @@ export default shield(
|
||||
getInviteCode: isAuthenticated, // and inviteRegistration
|
||||
Room: isAuthenticated,
|
||||
Message: isAuthenticated,
|
||||
UnreadRooms: isAuthenticated,
|
||||
},
|
||||
Mutation: {
|
||||
'*': deny,
|
||||
@ -463,6 +464,7 @@ export default shield(
|
||||
saveCategorySettings: isAuthenticated,
|
||||
CreateRoom: isAuthenticated,
|
||||
CreateMessage: isAuthenticated,
|
||||
MarkMessagesAsSeen: isAuthenticated,
|
||||
},
|
||||
User: {
|
||||
email: or(isMyOwn, isAdmin),
|
||||
|
||||
@ -1,22 +1,22 @@
|
||||
import uniqueSlug from './uniqueSlug'
|
||||
|
||||
describe('uniqueSlug', () => {
|
||||
it('slugifies given string', () => {
|
||||
it('slugifies given string', async () => {
|
||||
const string = 'Hello World'
|
||||
const isUnique = jest.fn().mockResolvedValue(true)
|
||||
expect(uniqueSlug(string, isUnique)).resolves.toEqual('hello-world')
|
||||
await expect(uniqueSlug(string, isUnique)).resolves.toEqual('hello-world')
|
||||
})
|
||||
|
||||
it('increments slugified string until unique', () => {
|
||||
it('increments slugified string until unique', async () => {
|
||||
const string = 'Hello World'
|
||||
const isUnique = jest.fn().mockResolvedValueOnce(false).mockResolvedValueOnce(true)
|
||||
expect(uniqueSlug(string, isUnique)).resolves.toEqual('hello-world-1')
|
||||
await expect(uniqueSlug(string, isUnique)).resolves.toEqual('hello-world-1')
|
||||
})
|
||||
|
||||
it('slugify null string', () => {
|
||||
it('slugify null string', async () => {
|
||||
const string = null
|
||||
const isUnique = jest.fn().mockResolvedValue(true)
|
||||
expect(uniqueSlug(string, isUnique)).resolves.toEqual('anonymous')
|
||||
await expect(uniqueSlug(string, isUnique)).resolves.toEqual('anonymous')
|
||||
})
|
||||
|
||||
it('Converts umlaut to a two letter equivalent', async () => {
|
||||
|
||||
@ -3,11 +3,11 @@ import { cleanHtml } from '../middleware/helpers/cleanHtml'
|
||||
|
||||
// exclamation mark separetes field names, that should not be sanitized
|
||||
const fields = [
|
||||
'content',
|
||||
'contentExcerpt',
|
||||
'reasonDescription',
|
||||
'description!embed',
|
||||
'descriptionExcerpt',
|
||||
{ field: 'content', excludes: ['CreateMessage', 'Message'] },
|
||||
{ field: 'contentExcerpt' },
|
||||
{ field: 'reasonDescription' },
|
||||
{ field: 'description', excludes: ['embed'] },
|
||||
{ field: 'descriptionExcerpt' },
|
||||
]
|
||||
|
||||
export default {
|
||||
|
||||
@ -1,38 +1,29 @@
|
||||
// NOTE: We cannot use `fs` here to clean up the code. Cypress breaks on any npm
|
||||
// module that is not browser-compatible. Node's `fs` module is server-side only
|
||||
declare var Cypress: any | undefined
|
||||
declare let Cypress: any | undefined
|
||||
export default {
|
||||
Image: typeof Cypress !== 'undefined' ? require('./Image') : require('./Image').default,
|
||||
Badge: typeof Cypress !== 'undefined' ? require('./Badge') : require('./Badge').default,
|
||||
User: typeof Cypress !== 'undefined' ? require('./User') : require('./User').default,
|
||||
Group: typeof Cypress !== 'undefined' ? require('./Group') : require('./Group').default,
|
||||
EmailAddress:
|
||||
typeof Cypress !== 'undefined'
|
||||
? require('./EmailAddress')
|
||||
: require('./EmailAddress').default,
|
||||
typeof Cypress !== 'undefined' ? require('./EmailAddress') : require('./EmailAddress').default,
|
||||
UnverifiedEmailAddress:
|
||||
typeof Cypress !== 'undefined'
|
||||
? require('./UnverifiedEmailAddress')
|
||||
: require('./UnverifiedEmailAddress').default,
|
||||
SocialMedia:
|
||||
typeof Cypress !== 'undefined'
|
||||
? require('./SocialMedia')
|
||||
: require('./SocialMedia').default,
|
||||
typeof Cypress !== 'undefined' ? require('./SocialMedia') : require('./SocialMedia').default,
|
||||
Post: typeof Cypress !== 'undefined' ? require('./Post') : require('./Post').default,
|
||||
Comment:
|
||||
typeof Cypress !== 'undefined' ? require('./Comment') : require('./Comment').default,
|
||||
Category:
|
||||
typeof Cypress !== 'undefined' ? require('./Category') : require('./Category').default,
|
||||
Comment: typeof Cypress !== 'undefined' ? require('./Comment') : require('./Comment').default,
|
||||
Category: typeof Cypress !== 'undefined' ? require('./Category') : require('./Category').default,
|
||||
Tag: typeof Cypress !== 'undefined' ? require('./Tag') : require('./Tag').default,
|
||||
Location:
|
||||
typeof Cypress !== 'undefined' ? require('./Location') : require('./Location').default,
|
||||
Location: typeof Cypress !== 'undefined' ? require('./Location') : require('./Location').default,
|
||||
Donations:
|
||||
typeof Cypress !== 'undefined' ? require('./Donations') : require('./Donations').default,
|
||||
Report: typeof Cypress !== 'undefined' ? require('./Report') : require('./Report').default,
|
||||
Migration:
|
||||
typeof Cypress !== 'undefined' ? require('./Migration') : require('./Migration').default,
|
||||
InviteCode:
|
||||
typeof Cypress !== 'undefined'
|
||||
? require('./InviteCode')
|
||||
: require('./InviteCode').default,
|
||||
typeof Cypress !== 'undefined' ? require('./InviteCode') : require('./InviteCode').default,
|
||||
}
|
||||
|
||||
@ -170,6 +170,7 @@ describe('mergeImage', () => {
|
||||
})
|
||||
})
|
||||
|
||||
// eslint-disable-next-line jest/no-disabled-tests
|
||||
it.skip('automatically creates different image sizes', async () => {
|
||||
await expect(
|
||||
mergeImage(post, 'HERO_IMAGE', imageInput, { uploadCallback, deleteCallback }),
|
||||
|
||||
@ -1,13 +1,15 @@
|
||||
import { createTestClient } from 'apollo-server-testing'
|
||||
import Factory, { cleanDatabase } from '../../db/factories'
|
||||
import { getNeode, getDriver } from '../../db/neo4j'
|
||||
import { createRoomMutation } from '../../graphql/rooms'
|
||||
import { createMessageMutation, messageQuery } from '../../graphql/messages'
|
||||
import createServer from '../../server'
|
||||
import { createRoomMutation, roomQuery } from '../../graphql/rooms'
|
||||
import { createMessageMutation, messageQuery, markMessagesAsSeen } from '../../graphql/messages'
|
||||
import createServer, { pubsub } from '../../server'
|
||||
|
||||
const driver = getDriver()
|
||||
const neode = getNeode()
|
||||
|
||||
const pubsubSpy = jest.spyOn(pubsub, 'publish')
|
||||
|
||||
let query
|
||||
let mutate
|
||||
let authenticatedUser
|
||||
@ -22,6 +24,9 @@ beforeAll(async () => {
|
||||
driver,
|
||||
neode,
|
||||
user: authenticatedUser,
|
||||
cypherParams: {
|
||||
currentUserId: authenticatedUser ? authenticatedUser.id : null,
|
||||
},
|
||||
}
|
||||
},
|
||||
})
|
||||
@ -34,43 +39,44 @@ afterAll(async () => {
|
||||
driver.close()
|
||||
})
|
||||
|
||||
|
||||
describe('Message', () => {
|
||||
let roomId: string
|
||||
|
||||
beforeAll(async () => {
|
||||
[chattingUser, otherChattingUser, notChattingUser] = await Promise.all([
|
||||
Factory.build(
|
||||
'user',
|
||||
{
|
||||
id: 'chatting-user',
|
||||
name: 'Chatting User',
|
||||
},
|
||||
),
|
||||
Factory.build(
|
||||
'user',
|
||||
{
|
||||
id: 'other-chatting-user',
|
||||
name: 'Other Chatting User',
|
||||
},
|
||||
),
|
||||
Factory.build(
|
||||
'user',
|
||||
{
|
||||
id: 'not-chatting-user',
|
||||
name: 'Not Chatting User',
|
||||
},
|
||||
),
|
||||
;[chattingUser, otherChattingUser, notChattingUser] = await Promise.all([
|
||||
Factory.build('user', {
|
||||
id: 'chatting-user',
|
||||
name: 'Chatting User',
|
||||
}),
|
||||
Factory.build('user', {
|
||||
id: 'other-chatting-user',
|
||||
name: 'Other Chatting User',
|
||||
}),
|
||||
Factory.build('user', {
|
||||
id: 'not-chatting-user',
|
||||
name: 'Not Chatting User',
|
||||
}),
|
||||
])
|
||||
})
|
||||
|
||||
describe('create message', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
describe('unauthenticated', () => {
|
||||
it('throws authorization error', async () => {
|
||||
await expect(mutate({ mutation: createMessageMutation(), variables: {
|
||||
roomId: 'some-id', content: 'Some bla bla bla', } })).resolves.toMatchObject({
|
||||
errors: [{ message: 'Not Authorized!' }],
|
||||
})
|
||||
await expect(
|
||||
mutate({
|
||||
mutation: createMessageMutation(),
|
||||
variables: {
|
||||
roomId: 'some-id',
|
||||
content: 'Some bla bla bla',
|
||||
},
|
||||
}),
|
||||
).resolves.toMatchObject({
|
||||
errors: [{ message: 'Not Authorized!' }],
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -80,14 +86,22 @@ describe('Message', () => {
|
||||
})
|
||||
|
||||
describe('room does not exist', () => {
|
||||
it('returns null', async () => {
|
||||
await expect(mutate({ mutation: createMessageMutation(), variables: {
|
||||
roomId: 'some-id', content: 'Some bla bla bla', } })).resolves.toMatchObject({
|
||||
errors: undefined,
|
||||
data: {
|
||||
CreateMessage: null,
|
||||
it('returns null and does not publish subscription', async () => {
|
||||
await expect(
|
||||
mutate({
|
||||
mutation: createMessageMutation(),
|
||||
variables: {
|
||||
roomId: 'some-id',
|
||||
content: 'Some bla bla bla',
|
||||
},
|
||||
})
|
||||
}),
|
||||
).resolves.toMatchObject({
|
||||
errors: undefined,
|
||||
data: {
|
||||
CreateMessage: null,
|
||||
},
|
||||
})
|
||||
expect(pubsubSpy).not.toBeCalled()
|
||||
})
|
||||
})
|
||||
|
||||
@ -103,21 +117,107 @@ describe('Message', () => {
|
||||
})
|
||||
|
||||
describe('user chats in room', () => {
|
||||
it('returns the message', async () => {
|
||||
await expect(mutate({
|
||||
mutation: createMessageMutation(),
|
||||
variables: {
|
||||
roomId,
|
||||
it('returns the message and publishes subscriptions', async () => {
|
||||
await expect(
|
||||
mutate({
|
||||
mutation: createMessageMutation(),
|
||||
variables: {
|
||||
roomId,
|
||||
content: 'Some nice message to other chatting user',
|
||||
},
|
||||
}),
|
||||
).resolves.toMatchObject({
|
||||
errors: undefined,
|
||||
data: {
|
||||
CreateMessage: {
|
||||
id: expect.any(String),
|
||||
content: 'Some nice message to other chatting user',
|
||||
senderId: 'chatting-user',
|
||||
username: 'Chatting User',
|
||||
avatar: expect.any(String),
|
||||
date: expect.any(String),
|
||||
saved: true,
|
||||
distributed: false,
|
||||
seen: false,
|
||||
},
|
||||
},
|
||||
})
|
||||
expect(pubsubSpy).toBeCalledWith('ROOM_COUNT_UPDATED', {
|
||||
roomCountUpdated: '1',
|
||||
userId: 'other-chatting-user',
|
||||
})
|
||||
expect(pubsubSpy).toBeCalledWith('CHAT_MESSAGE_ADDED', {
|
||||
chatMessageAdded: expect.objectContaining({
|
||||
id: expect.any(String),
|
||||
content: 'Some nice message to other chatting user',
|
||||
} })).resolves.toMatchObject({
|
||||
senderId: 'chatting-user',
|
||||
username: 'Chatting User',
|
||||
avatar: expect.any(String),
|
||||
date: expect.any(String),
|
||||
saved: true,
|
||||
distributed: false,
|
||||
seen: false,
|
||||
}),
|
||||
userId: 'other-chatting-user',
|
||||
})
|
||||
})
|
||||
|
||||
describe('room is updated as well', () => {
|
||||
it('has last message set', async () => {
|
||||
const result = await query({ query: roomQuery() })
|
||||
await expect(result).toMatchObject({
|
||||
errors: undefined,
|
||||
data: {
|
||||
CreateMessage: {
|
||||
id: expect.any(String),
|
||||
content: 'Some nice message to other chatting user',
|
||||
},
|
||||
Room: [
|
||||
expect.objectContaining({
|
||||
lastMessageAt: expect.any(String),
|
||||
unreadCount: 0,
|
||||
lastMessage: expect.objectContaining({
|
||||
_id: result.data.Room[0].lastMessage.id,
|
||||
id: expect.any(String),
|
||||
content: 'Some nice message to other chatting user',
|
||||
senderId: 'chatting-user',
|
||||
username: 'Chatting User',
|
||||
avatar: expect.any(String),
|
||||
date: expect.any(String),
|
||||
saved: true,
|
||||
distributed: false,
|
||||
seen: false,
|
||||
}),
|
||||
}),
|
||||
],
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('unread count for other user', () => {
|
||||
it('has unread count = 1', async () => {
|
||||
authenticatedUser = await otherChattingUser.toJson()
|
||||
await expect(query({ query: roomQuery() })).resolves.toMatchObject({
|
||||
errors: undefined,
|
||||
data: {
|
||||
Room: [
|
||||
expect.objectContaining({
|
||||
lastMessageAt: expect.any(String),
|
||||
unreadCount: 1,
|
||||
lastMessage: expect.objectContaining({
|
||||
_id: expect.any(String),
|
||||
id: expect.any(String),
|
||||
content: 'Some nice message to other chatting user',
|
||||
senderId: 'chatting-user',
|
||||
username: 'Chatting User',
|
||||
avatar: expect.any(String),
|
||||
date: expect.any(String),
|
||||
saved: true,
|
||||
distributed: false,
|
||||
seen: false,
|
||||
}),
|
||||
}),
|
||||
],
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -125,19 +225,22 @@ describe('Message', () => {
|
||||
beforeAll(async () => {
|
||||
authenticatedUser = await notChattingUser.toJson()
|
||||
})
|
||||
|
||||
|
||||
it('returns null', async () => {
|
||||
await expect(mutate({
|
||||
mutation: createMessageMutation(),
|
||||
variables: {
|
||||
roomId,
|
||||
content: 'I have no access to this room!',
|
||||
} })).resolves.toMatchObject({
|
||||
errors: undefined,
|
||||
data: {
|
||||
CreateMessage: null,
|
||||
await expect(
|
||||
mutate({
|
||||
mutation: createMessageMutation(),
|
||||
variables: {
|
||||
roomId,
|
||||
content: 'I have no access to this room!',
|
||||
},
|
||||
})
|
||||
}),
|
||||
).resolves.toMatchObject({
|
||||
errors: undefined,
|
||||
data: {
|
||||
CreateMessage: null,
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -151,14 +254,17 @@ describe('Message', () => {
|
||||
})
|
||||
|
||||
it('throws authorization error', async () => {
|
||||
await expect(query({
|
||||
query: messageQuery(),
|
||||
variables: {
|
||||
roomId: 'some-id' }
|
||||
})).resolves.toMatchObject({
|
||||
await expect(
|
||||
query({
|
||||
query: messageQuery(),
|
||||
variables: {
|
||||
roomId: 'some-id',
|
||||
},
|
||||
}),
|
||||
).resolves.toMatchObject({
|
||||
errors: [{ message: 'Not Authorized!' }],
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('authenticated', () => {
|
||||
@ -168,12 +274,14 @@ describe('Message', () => {
|
||||
|
||||
describe('room does not exists', () => {
|
||||
it('returns null', async () => {
|
||||
await expect(query({
|
||||
query: messageQuery(),
|
||||
variables: {
|
||||
roomId: 'some-id'
|
||||
},
|
||||
})).resolves.toMatchObject({
|
||||
await expect(
|
||||
query({
|
||||
query: messageQuery(),
|
||||
variables: {
|
||||
roomId: 'some-id',
|
||||
},
|
||||
}),
|
||||
).resolves.toMatchObject({
|
||||
errors: undefined,
|
||||
data: {
|
||||
Message: [],
|
||||
@ -193,15 +301,21 @@ describe('Message', () => {
|
||||
expect(result).toMatchObject({
|
||||
errors: undefined,
|
||||
data: {
|
||||
Message: [{
|
||||
id: expect.any(String),
|
||||
_id: result.data.Message[0].id,
|
||||
content: 'Some nice message to other chatting user',
|
||||
senderId: 'chatting-user',
|
||||
username: 'Chatting User',
|
||||
avatar: expect.any(String),
|
||||
date: expect.any(String),
|
||||
}],
|
||||
Message: [
|
||||
{
|
||||
id: expect.any(String),
|
||||
_id: result.data.Message[0].id,
|
||||
indexId: 0,
|
||||
content: 'Some nice message to other chatting user',
|
||||
senderId: 'chatting-user',
|
||||
username: 'Chatting User',
|
||||
avatar: expect.any(String),
|
||||
date: expect.any(String),
|
||||
saved: true,
|
||||
distributed: true,
|
||||
seen: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
})
|
||||
@ -213,7 +327,7 @@ describe('Message', () => {
|
||||
variables: {
|
||||
roomId,
|
||||
content: 'A nice response message to chatting user',
|
||||
}
|
||||
},
|
||||
})
|
||||
authenticatedUser = await chattingUser.toJson()
|
||||
await mutate({
|
||||
@ -221,49 +335,126 @@ describe('Message', () => {
|
||||
variables: {
|
||||
roomId,
|
||||
content: 'And another nice message to other chatting user',
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
it('returns the messages', async () => {
|
||||
await expect(query({
|
||||
query: messageQuery(),
|
||||
variables: {
|
||||
roomId,
|
||||
},
|
||||
})).resolves.toMatchObject({
|
||||
})
|
||||
})
|
||||
|
||||
it('returns the messages', async () => {
|
||||
await expect(
|
||||
query({
|
||||
query: messageQuery(),
|
||||
variables: {
|
||||
roomId,
|
||||
},
|
||||
}),
|
||||
).resolves.toMatchObject({
|
||||
errors: undefined,
|
||||
data: {
|
||||
Message: [
|
||||
{
|
||||
expect.objectContaining({
|
||||
id: expect.any(String),
|
||||
indexId: 0,
|
||||
content: 'Some nice message to other chatting user',
|
||||
senderId: 'chatting-user',
|
||||
username: 'Chatting User',
|
||||
avatar: expect.any(String),
|
||||
date: expect.any(String),
|
||||
},
|
||||
{
|
||||
saved: true,
|
||||
distributed: true,
|
||||
seen: false,
|
||||
}),
|
||||
expect.objectContaining({
|
||||
id: expect.any(String),
|
||||
indexId: 1,
|
||||
content: 'A nice response message to chatting user',
|
||||
senderId: 'other-chatting-user',
|
||||
username: 'Other Chatting User',
|
||||
avatar: expect.any(String),
|
||||
date: expect.any(String),
|
||||
},
|
||||
{
|
||||
saved: true,
|
||||
distributed: true,
|
||||
seen: false,
|
||||
}),
|
||||
expect.objectContaining({
|
||||
id: expect.any(String),
|
||||
indexId: 2,
|
||||
content: 'And another nice message to other chatting user',
|
||||
senderId: 'chatting-user',
|
||||
username: 'Chatting User',
|
||||
avatar: expect.any(String),
|
||||
date: expect.any(String),
|
||||
},
|
||||
saved: true,
|
||||
distributed: false,
|
||||
seen: false,
|
||||
}),
|
||||
],
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('returns the messages paginated', async () => {
|
||||
await expect(
|
||||
query({
|
||||
query: messageQuery(),
|
||||
variables: {
|
||||
roomId,
|
||||
first: 2,
|
||||
offset: 0,
|
||||
},
|
||||
}),
|
||||
).resolves.toMatchObject({
|
||||
errors: undefined,
|
||||
data: {
|
||||
Message: [
|
||||
expect.objectContaining({
|
||||
id: expect.any(String),
|
||||
indexId: 1,
|
||||
content: 'A nice response message to chatting user',
|
||||
senderId: 'other-chatting-user',
|
||||
username: 'Other Chatting User',
|
||||
avatar: expect.any(String),
|
||||
date: expect.any(String),
|
||||
}),
|
||||
expect.objectContaining({
|
||||
id: expect.any(String),
|
||||
indexId: 2,
|
||||
content: 'And another nice message to other chatting user',
|
||||
senderId: 'chatting-user',
|
||||
username: 'Chatting User',
|
||||
avatar: expect.any(String),
|
||||
date: expect.any(String),
|
||||
}),
|
||||
],
|
||||
},
|
||||
})
|
||||
|
||||
await expect(
|
||||
query({
|
||||
query: messageQuery(),
|
||||
variables: {
|
||||
roomId,
|
||||
first: 2,
|
||||
offset: 2,
|
||||
},
|
||||
}),
|
||||
).resolves.toMatchObject({
|
||||
errors: undefined,
|
||||
data: {
|
||||
Message: [
|
||||
expect.objectContaining({
|
||||
id: expect.any(String),
|
||||
indexId: 0,
|
||||
content: 'Some nice message to other chatting user',
|
||||
senderId: 'chatting-user',
|
||||
username: 'Chatting User',
|
||||
avatar: expect.any(String),
|
||||
date: expect.any(String),
|
||||
}),
|
||||
],
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('room exists, authenticated user not in room', () => {
|
||||
@ -272,19 +463,91 @@ describe('Message', () => {
|
||||
})
|
||||
|
||||
it('returns null', async () => {
|
||||
await expect(query({
|
||||
query: messageQuery(),
|
||||
variables: {
|
||||
roomId,
|
||||
},
|
||||
})).resolves.toMatchObject({
|
||||
await expect(
|
||||
query({
|
||||
query: messageQuery(),
|
||||
variables: {
|
||||
roomId,
|
||||
},
|
||||
}),
|
||||
).resolves.toMatchObject({
|
||||
errors: undefined,
|
||||
data: {
|
||||
Message: [],
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('marks massges as seen', () => {
|
||||
describe('unauthenticated', () => {
|
||||
beforeAll(() => {
|
||||
authenticatedUser = null
|
||||
})
|
||||
|
||||
it('throws authorization error', async () => {
|
||||
await expect(
|
||||
mutate({
|
||||
mutation: markMessagesAsSeen(),
|
||||
variables: {
|
||||
messageIds: ['some-id'],
|
||||
},
|
||||
}),
|
||||
).resolves.toMatchObject({
|
||||
errors: [{ message: 'Not Authorized!' }],
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('authenticated', () => {
|
||||
const messageIds: string[] = []
|
||||
beforeAll(async () => {
|
||||
authenticatedUser = await otherChattingUser.toJson()
|
||||
const msgs = await query({
|
||||
query: messageQuery(),
|
||||
variables: {
|
||||
roomId,
|
||||
},
|
||||
})
|
||||
msgs.data.Message.forEach((m) => messageIds.push(m.id))
|
||||
})
|
||||
|
||||
it('returns true', async () => {
|
||||
await expect(
|
||||
mutate({
|
||||
mutation: markMessagesAsSeen(),
|
||||
variables: {
|
||||
messageIds,
|
||||
},
|
||||
}),
|
||||
).resolves.toMatchObject({
|
||||
errors: undefined,
|
||||
data: {
|
||||
MarkMessagesAsSeen: true,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('has seen prop set to true', async () => {
|
||||
await expect(
|
||||
query({
|
||||
query: messageQuery(),
|
||||
variables: {
|
||||
roomId,
|
||||
},
|
||||
}),
|
||||
).resolves.toMatchObject({
|
||||
data: {
|
||||
Message: [
|
||||
expect.objectContaining({ seen: true }),
|
||||
expect.objectContaining({ seen: false }),
|
||||
expect.objectContaining({ seen: true }),
|
||||
],
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -1,7 +1,36 @@
|
||||
import { neo4jgraphql } from 'neo4j-graphql-js'
|
||||
import Resolver from './helpers/Resolver'
|
||||
|
||||
import { getUnreadRoomsCount } from './rooms'
|
||||
import { pubsub, ROOM_COUNT_UPDATED, CHAT_MESSAGE_ADDED } from '../../server'
|
||||
import { withFilter } from 'graphql-subscriptions'
|
||||
|
||||
const setMessagesAsDistributed = async (undistributedMessagesIds, session) => {
|
||||
return session.writeTransaction(async (transaction) => {
|
||||
const setDistributedCypher = `
|
||||
MATCH (m:Message) WHERE m.id IN $undistributedMessagesIds
|
||||
SET m.distributed = true
|
||||
RETURN m { .* }
|
||||
`
|
||||
const setDistributedTxResponse = await transaction.run(setDistributedCypher, {
|
||||
undistributedMessagesIds,
|
||||
})
|
||||
const messages = await setDistributedTxResponse.records.map((record) => record.get('m'))
|
||||
return messages
|
||||
})
|
||||
}
|
||||
|
||||
export default {
|
||||
Subscription: {
|
||||
chatMessageAdded: {
|
||||
subscribe: withFilter(
|
||||
() => pubsub.asyncIterator(CHAT_MESSAGE_ADDED),
|
||||
(payload, variables, context) => {
|
||||
return payload.userId === context.user?.id
|
||||
},
|
||||
),
|
||||
},
|
||||
},
|
||||
Query: {
|
||||
Message: async (object, params, context, resolveInfo) => {
|
||||
const { roomId } = params
|
||||
@ -13,47 +42,121 @@ export default {
|
||||
id: context.user.id,
|
||||
},
|
||||
}
|
||||
|
||||
const resolved = await neo4jgraphql(object, params, context, resolveInfo)
|
||||
|
||||
if (resolved) {
|
||||
resolved.forEach((message) => {
|
||||
message._id = message.id
|
||||
})
|
||||
const undistributedMessagesIds = resolved
|
||||
.filter((msg) => !msg.distributed && msg.senderId !== context.user.id)
|
||||
.map((msg) => msg.id)
|
||||
const session = context.driver.session()
|
||||
try {
|
||||
if (undistributedMessagesIds.length > 0) {
|
||||
await setMessagesAsDistributed(undistributedMessagesIds, session)
|
||||
}
|
||||
} finally {
|
||||
session.close()
|
||||
}
|
||||
// send subscription to author to updated the messages
|
||||
}
|
||||
return resolved
|
||||
return resolved.reverse()
|
||||
},
|
||||
},
|
||||
Mutation: {
|
||||
CreateMessage: async (_parent, params, context, _resolveInfo) => {
|
||||
const { roomId, content } = params
|
||||
const { user: { id: currentUserId } } = context
|
||||
const {
|
||||
user: { id: currentUserId },
|
||||
} = context
|
||||
const session = context.driver.session()
|
||||
const writeTxResultPromise = session.writeTransaction(async (transaction) => {
|
||||
const createMessageCypher = `
|
||||
MATCH (currentUser:User { id: $currentUserId })-[:CHATS_IN]->(room:Room { id: $roomId })
|
||||
OPTIONAL MATCH (currentUser)-[:AVATAR_IMAGE]->(image:Image)
|
||||
OPTIONAL MATCH (m:Message)-[:INSIDE]->(room)
|
||||
OPTIONAL MATCH (room)<-[:CHATS_IN]-(recipientUser:User)
|
||||
WHERE NOT recipientUser.id = $currentUserId
|
||||
WITH MAX(m.indexId) as maxIndex, room, currentUser, image, recipientUser
|
||||
CREATE (currentUser)-[:CREATED]->(message:Message {
|
||||
createdAt: toString(datetime()),
|
||||
id: apoc.create.uuid(),
|
||||
content: $content
|
||||
indexId: CASE WHEN maxIndex IS NOT NULL THEN maxIndex + 1 ELSE 0 END,
|
||||
content: LEFT($content,2000),
|
||||
saved: true,
|
||||
distributed: false,
|
||||
seen: false
|
||||
})-[:INSIDE]->(room)
|
||||
RETURN message { .* }
|
||||
SET room.lastMessageAt = toString(datetime())
|
||||
RETURN message {
|
||||
.*,
|
||||
indexId: toString(message.indexId),
|
||||
recipientId: recipientUser.id,
|
||||
senderId: currentUser.id,
|
||||
username: currentUser.name,
|
||||
avatar: image.url,
|
||||
date: message.createdAt
|
||||
}
|
||||
`
|
||||
const createMessageTxResponse = await transaction.run(
|
||||
createMessageCypher,
|
||||
{ currentUserId, roomId, content }
|
||||
)
|
||||
const createMessageTxResponse = await transaction.run(createMessageCypher, {
|
||||
currentUserId,
|
||||
roomId,
|
||||
content,
|
||||
})
|
||||
|
||||
const [message] = await createMessageTxResponse.records.map((record) =>
|
||||
record.get('message'),
|
||||
)
|
||||
record.get('message'),
|
||||
)
|
||||
|
||||
return message
|
||||
})
|
||||
try {
|
||||
const message = await writeTxResultPromise
|
||||
if (message) {
|
||||
const roomCountUpdated = await getUnreadRoomsCount(message.recipientId, session)
|
||||
|
||||
// send subscriptions
|
||||
void pubsub.publish(ROOM_COUNT_UPDATED, {
|
||||
roomCountUpdated,
|
||||
userId: message.recipientId,
|
||||
})
|
||||
void pubsub.publish(CHAT_MESSAGE_ADDED, {
|
||||
chatMessageAdded: message,
|
||||
userId: message.recipientId,
|
||||
})
|
||||
}
|
||||
|
||||
return message
|
||||
} catch (error) {
|
||||
throw new Error(error)
|
||||
} finally {
|
||||
session.close()
|
||||
}
|
||||
}
|
||||
},
|
||||
MarkMessagesAsSeen: async (_parent, params, context, _resolveInfo) => {
|
||||
const { messageIds } = params
|
||||
const currentUserId = context.user.id
|
||||
const session = context.driver.session()
|
||||
const writeTxResultPromise = session.writeTransaction(async (transaction) => {
|
||||
const setSeenCypher = `
|
||||
MATCH (m:Message)<-[:CREATED]-(user:User)
|
||||
WHERE m.id IN $messageIds AND NOT user.id = $currentUserId
|
||||
SET m.seen = true
|
||||
RETURN m { .* }
|
||||
`
|
||||
const setSeenTxResponse = await transaction.run(setSeenCypher, {
|
||||
messageIds,
|
||||
currentUserId,
|
||||
})
|
||||
const messages = await setSeenTxResponse.records.map((record) => record.get('m'))
|
||||
return messages
|
||||
})
|
||||
try {
|
||||
await writeTxResultPromise
|
||||
// send subscription to author to updated the messages
|
||||
return true
|
||||
} finally {
|
||||
session.close()
|
||||
}
|
||||
},
|
||||
},
|
||||
Message: {
|
||||
@ -61,7 +164,7 @@ export default {
|
||||
hasOne: {
|
||||
author: '<-[:CREATED]-(related:User)',
|
||||
room: '-[:INSIDE]->(related:Room)',
|
||||
}
|
||||
},
|
||||
}),
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
@ -238,7 +238,7 @@ describe('given some notifications', () => {
|
||||
variables: { ...variables, read: false },
|
||||
})
|
||||
await expect(response).toMatchObject(expected)
|
||||
await expect(response.data.notifications.length).toEqual(2) // double-check
|
||||
await expect(response.data.notifications).toHaveLength(2) // double-check
|
||||
})
|
||||
|
||||
describe('if a resource gets deleted', () => {
|
||||
|
||||
@ -7,8 +7,8 @@ export default {
|
||||
notificationAdded: {
|
||||
subscribe: withFilter(
|
||||
() => pubsub.asyncIterator(NOTIFICATION_ADDED),
|
||||
(payload, variables) => {
|
||||
return payload.notificationAdded.to.id === variables.userId
|
||||
(payload, variables, context) => {
|
||||
return payload.notificationAdded.to.id === context.user?.id
|
||||
},
|
||||
),
|
||||
},
|
||||
|
||||
@ -907,6 +907,7 @@ describe('UpdatePost', () => {
|
||||
})
|
||||
})
|
||||
|
||||
// eslint-disable-next-line jest/no-disabled-tests
|
||||
describe.skip('params.image', () => {
|
||||
describe('is object', () => {
|
||||
beforeEach(() => {
|
||||
|
||||
@ -28,7 +28,7 @@ export default {
|
||||
},
|
||||
SignupVerification: async (_parent, args, context) => {
|
||||
const { termsAndConditionsAgreedVersion } = args
|
||||
const regEx = new RegExp(/^[0-9]+\.[0-9]+\.[0-9]+$/g)
|
||||
const regEx = /^[0-9]+\.[0-9]+\.[0-9]+$/g
|
||||
if (!regEx.test(termsAndConditionsAgreedVersion)) {
|
||||
throw new UserInputError('Invalid version format!')
|
||||
}
|
||||
|
||||
@ -728,7 +728,7 @@ describe('file a report on a resource', () => {
|
||||
describe('unauthenticated', () => {
|
||||
it('throws authorization error', async () => {
|
||||
authenticatedUser = null
|
||||
expect(query({ query: reportsQuery })).resolves.toMatchObject({
|
||||
await expect(query({ query: reportsQuery })).resolves.toMatchObject({
|
||||
data: { reports: null },
|
||||
errors: [{ message: 'Not Authorized!' }],
|
||||
})
|
||||
@ -738,7 +738,7 @@ describe('file a report on a resource', () => {
|
||||
describe('authenticated', () => {
|
||||
it('role "user" gets no reports', async () => {
|
||||
authenticatedUser = await currentUser.toJson()
|
||||
expect(query({ query: reportsQuery })).resolves.toMatchObject({
|
||||
await expect(query({ query: reportsQuery })).resolves.toMatchObject({
|
||||
data: { reports: null },
|
||||
errors: [{ message: 'Not Authorized!' }],
|
||||
})
|
||||
|
||||
@ -1,7 +1,8 @@
|
||||
import { createTestClient } from 'apollo-server-testing'
|
||||
import Factory, { cleanDatabase } from '../../db/factories'
|
||||
import { getNeode, getDriver } from '../../db/neo4j'
|
||||
import { createRoomMutation, roomQuery } from '../../graphql/rooms'
|
||||
import { createRoomMutation, roomQuery, unreadRoomsQuery } from '../../graphql/rooms'
|
||||
import { createMessageMutation } from '../../graphql/messages'
|
||||
import createServer from '../../server'
|
||||
|
||||
const driver = getDriver()
|
||||
@ -21,6 +22,9 @@ beforeAll(async () => {
|
||||
driver,
|
||||
neode,
|
||||
user: authenticatedUser,
|
||||
cypherParams: {
|
||||
currentUserId: authenticatedUser ? authenticatedUser.id : null,
|
||||
},
|
||||
}
|
||||
},
|
||||
})
|
||||
@ -34,57 +38,64 @@ afterAll(async () => {
|
||||
})
|
||||
|
||||
describe('Room', () => {
|
||||
let roomId: string
|
||||
|
||||
beforeAll(async () => {
|
||||
[chattingUser, otherChattingUser, notChattingUser] = await Promise.all([
|
||||
Factory.build(
|
||||
'user',
|
||||
{
|
||||
id: 'chatting-user',
|
||||
name: 'Chatting User',
|
||||
},
|
||||
),
|
||||
Factory.build(
|
||||
'user',
|
||||
{
|
||||
id: 'other-chatting-user',
|
||||
name: 'Other Chatting User',
|
||||
},
|
||||
),
|
||||
Factory.build(
|
||||
'user',
|
||||
{
|
||||
id: 'not-chatting-user',
|
||||
name: 'Not Chatting User',
|
||||
},
|
||||
),
|
||||
;[chattingUser, otherChattingUser, notChattingUser] = await Promise.all([
|
||||
Factory.build('user', {
|
||||
id: 'chatting-user',
|
||||
name: 'Chatting User',
|
||||
}),
|
||||
Factory.build('user', {
|
||||
id: 'other-chatting-user',
|
||||
name: 'Other Chatting User',
|
||||
}),
|
||||
Factory.build('user', {
|
||||
id: 'not-chatting-user',
|
||||
name: 'Not Chatting User',
|
||||
}),
|
||||
Factory.build('user', {
|
||||
id: 'second-chatting-user',
|
||||
name: 'Second Chatting User',
|
||||
}),
|
||||
Factory.build('user', {
|
||||
id: 'third-chatting-user',
|
||||
name: 'Third Chatting User',
|
||||
}),
|
||||
])
|
||||
})
|
||||
|
||||
describe('create room', () => {
|
||||
describe('unauthenticated', () => {
|
||||
it('throws authorization error', async () => {
|
||||
await expect(mutate({ mutation: createRoomMutation(), variables: {
|
||||
userId: 'some-id' } })).resolves.toMatchObject({
|
||||
errors: [{ message: 'Not Authorized!' }],
|
||||
})
|
||||
await expect(
|
||||
mutate({
|
||||
mutation: createRoomMutation(),
|
||||
variables: {
|
||||
userId: 'some-id',
|
||||
},
|
||||
}),
|
||||
).resolves.toMatchObject({
|
||||
errors: [{ message: 'Not Authorized!' }],
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('authenticated', () => {
|
||||
let roomId: string
|
||||
|
||||
beforeAll(async () => {
|
||||
authenticatedUser = await chattingUser.toJson()
|
||||
})
|
||||
|
||||
describe('user id does not exist', () => {
|
||||
it('returns null', async () => {
|
||||
await expect(mutate({
|
||||
mutation: createRoomMutation(),
|
||||
variables: {
|
||||
userId: 'not-existing-user',
|
||||
},
|
||||
})).resolves.toMatchObject({
|
||||
await expect(
|
||||
mutate({
|
||||
mutation: createRoomMutation(),
|
||||
variables: {
|
||||
userId: 'not-existing-user',
|
||||
},
|
||||
}),
|
||||
).resolves.toMatchObject({
|
||||
errors: undefined,
|
||||
data: {
|
||||
CreateRoom: null,
|
||||
@ -92,7 +103,22 @@ describe('Room', () => {
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
describe('user id is self', () => {
|
||||
it('throws error', async () => {
|
||||
await expect(
|
||||
mutate({
|
||||
mutation: createRoomMutation(),
|
||||
variables: {
|
||||
userId: 'chatting-user',
|
||||
},
|
||||
}),
|
||||
).resolves.toMatchObject({
|
||||
errors: [{ message: 'Cannot create a room with self' }],
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('user id exists', () => {
|
||||
it('returns the id of the room', async () => {
|
||||
const result = await mutate({
|
||||
@ -108,6 +134,26 @@ describe('Room', () => {
|
||||
CreateRoom: {
|
||||
id: expect.any(String),
|
||||
roomId: result.data.CreateRoom.id,
|
||||
roomName: 'Other Chatting User',
|
||||
unreadCount: 0,
|
||||
users: expect.arrayContaining([
|
||||
{
|
||||
_id: 'chatting-user',
|
||||
id: 'chatting-user',
|
||||
name: 'Chatting User',
|
||||
avatar: {
|
||||
url: expect.any(String),
|
||||
},
|
||||
},
|
||||
{
|
||||
_id: 'other-chatting-user',
|
||||
id: 'other-chatting-user',
|
||||
name: 'Other Chatting User',
|
||||
avatar: {
|
||||
url: expect.any(String),
|
||||
},
|
||||
},
|
||||
]),
|
||||
},
|
||||
},
|
||||
})
|
||||
@ -116,12 +162,14 @@ describe('Room', () => {
|
||||
|
||||
describe('create room with same user id', () => {
|
||||
it('returns the id of the room', async () => {
|
||||
await expect(mutate({
|
||||
mutation: createRoomMutation(),
|
||||
variables: {
|
||||
userId: 'other-chatting-user',
|
||||
},
|
||||
})).resolves.toMatchObject({
|
||||
await expect(
|
||||
mutate({
|
||||
mutation: createRoomMutation(),
|
||||
variables: {
|
||||
userId: 'other-chatting-user',
|
||||
},
|
||||
}),
|
||||
).resolves.toMatchObject({
|
||||
errors: undefined,
|
||||
data: {
|
||||
CreateRoom: {
|
||||
@ -130,7 +178,7 @@ describe('Room', () => {
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -139,11 +187,11 @@ describe('Room', () => {
|
||||
beforeAll(() => {
|
||||
authenticatedUser = null
|
||||
})
|
||||
|
||||
|
||||
it('throws authorization error', async () => {
|
||||
await expect(query({ query: roomQuery() })).resolves.toMatchObject({
|
||||
errors: [{ message: 'Not Authorized!' }],
|
||||
})
|
||||
errors: [{ message: 'Not Authorized!' }],
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -194,7 +242,7 @@ describe('Room', () => {
|
||||
})
|
||||
|
||||
it('returns the room', async () => {
|
||||
const result = await query({ query: roomQuery() })
|
||||
const result = await query({ query: roomQuery() })
|
||||
expect(result).toMatchObject({
|
||||
errors: undefined,
|
||||
data: {
|
||||
@ -203,6 +251,7 @@ describe('Room', () => {
|
||||
id: expect.any(String),
|
||||
roomId: result.data.Room[0].id,
|
||||
roomName: 'Chatting User',
|
||||
unreadCount: 0,
|
||||
users: expect.arrayContaining([
|
||||
{
|
||||
_id: 'chatting-user',
|
||||
@ -241,7 +290,322 @@ describe('Room', () => {
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('unread rooms query', () => {
|
||||
describe('unauthenticated', () => {
|
||||
it('throws authorization error', async () => {
|
||||
authenticatedUser = null
|
||||
await expect(
|
||||
query({
|
||||
query: unreadRoomsQuery(),
|
||||
}),
|
||||
).resolves.toMatchObject({
|
||||
errors: [{ message: 'Not Authorized!' }],
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('authenticated', () => {
|
||||
let otherRoomId: string
|
||||
|
||||
beforeAll(async () => {
|
||||
authenticatedUser = await chattingUser.toJson()
|
||||
const result = await mutate({
|
||||
mutation: createRoomMutation(),
|
||||
variables: {
|
||||
userId: 'not-chatting-user',
|
||||
},
|
||||
})
|
||||
otherRoomId = result.data.CreateRoom.roomId
|
||||
await mutate({
|
||||
mutation: createMessageMutation(),
|
||||
variables: {
|
||||
roomId: otherRoomId,
|
||||
content: 'Message to not chatting user',
|
||||
},
|
||||
})
|
||||
await mutate({
|
||||
mutation: createMessageMutation(),
|
||||
variables: {
|
||||
roomId,
|
||||
content: '1st message to other chatting user',
|
||||
},
|
||||
})
|
||||
await mutate({
|
||||
mutation: createMessageMutation(),
|
||||
variables: {
|
||||
roomId,
|
||||
content: '2nd message to other chatting user',
|
||||
},
|
||||
})
|
||||
authenticatedUser = await otherChattingUser.toJson()
|
||||
const result2 = await mutate({
|
||||
mutation: createRoomMutation(),
|
||||
variables: {
|
||||
userId: 'not-chatting-user',
|
||||
},
|
||||
})
|
||||
otherRoomId = result2.data.CreateRoom.roomId
|
||||
await mutate({
|
||||
mutation: createMessageMutation(),
|
||||
variables: {
|
||||
roomId: otherRoomId,
|
||||
content: 'Other message to not chatting user',
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
describe('as chatting user', () => {
|
||||
it('has 0 unread rooms', async () => {
|
||||
authenticatedUser = await chattingUser.toJson()
|
||||
await expect(
|
||||
query({
|
||||
query: unreadRoomsQuery(),
|
||||
}),
|
||||
).resolves.toMatchObject({
|
||||
data: {
|
||||
UnreadRooms: 0,
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('as other chatting user', () => {
|
||||
it('has 1 unread rooms', async () => {
|
||||
authenticatedUser = await otherChattingUser.toJson()
|
||||
await expect(
|
||||
query({
|
||||
query: unreadRoomsQuery(),
|
||||
}),
|
||||
).resolves.toMatchObject({
|
||||
data: {
|
||||
UnreadRooms: 1,
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('as not chatting user', () => {
|
||||
it('has 2 unread rooms', async () => {
|
||||
authenticatedUser = await notChattingUser.toJson()
|
||||
await expect(
|
||||
query({
|
||||
query: unreadRoomsQuery(),
|
||||
}),
|
||||
).resolves.toMatchObject({
|
||||
data: {
|
||||
UnreadRooms: 2,
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('query several rooms', () => {
|
||||
beforeAll(async () => {
|
||||
authenticatedUser = await chattingUser.toJson()
|
||||
await mutate({
|
||||
mutation: createRoomMutation(),
|
||||
variables: {
|
||||
userId: 'second-chatting-user',
|
||||
},
|
||||
})
|
||||
await mutate({
|
||||
mutation: createRoomMutation(),
|
||||
variables: {
|
||||
userId: 'third-chatting-user',
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('returns the rooms paginated', async () => {
|
||||
await expect(
|
||||
query({ query: roomQuery(), variables: { first: 3, offset: 0 } }),
|
||||
).resolves.toMatchObject({
|
||||
errors: undefined,
|
||||
data: {
|
||||
Room: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
id: expect.any(String),
|
||||
roomId: expect.any(String),
|
||||
roomName: 'Third Chatting User',
|
||||
lastMessageAt: null,
|
||||
unreadCount: 0,
|
||||
lastMessage: null,
|
||||
users: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
_id: 'chatting-user',
|
||||
id: 'chatting-user',
|
||||
name: 'Chatting User',
|
||||
avatar: {
|
||||
url: expect.any(String),
|
||||
},
|
||||
}),
|
||||
expect.objectContaining({
|
||||
_id: 'third-chatting-user',
|
||||
id: 'third-chatting-user',
|
||||
name: 'Third Chatting User',
|
||||
avatar: {
|
||||
url: expect.any(String),
|
||||
},
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
expect.objectContaining({
|
||||
id: expect.any(String),
|
||||
roomId: expect.any(String),
|
||||
roomName: 'Second Chatting User',
|
||||
lastMessageAt: null,
|
||||
unreadCount: 0,
|
||||
lastMessage: null,
|
||||
users: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
_id: 'chatting-user',
|
||||
id: 'chatting-user',
|
||||
name: 'Chatting User',
|
||||
avatar: {
|
||||
url: expect.any(String),
|
||||
},
|
||||
}),
|
||||
expect.objectContaining({
|
||||
_id: 'second-chatting-user',
|
||||
id: 'second-chatting-user',
|
||||
name: 'Second Chatting User',
|
||||
avatar: {
|
||||
url: expect.any(String),
|
||||
},
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
expect.objectContaining({
|
||||
id: expect.any(String),
|
||||
roomId: expect.any(String),
|
||||
roomName: 'Other Chatting User',
|
||||
lastMessageAt: expect.any(String),
|
||||
unreadCount: 0,
|
||||
lastMessage: {
|
||||
_id: expect.any(String),
|
||||
id: expect.any(String),
|
||||
content: '2nd message to other chatting user',
|
||||
senderId: 'chatting-user',
|
||||
username: 'Chatting User',
|
||||
avatar: expect.any(String),
|
||||
date: expect.any(String),
|
||||
saved: true,
|
||||
distributed: false,
|
||||
seen: false,
|
||||
},
|
||||
users: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
_id: 'chatting-user',
|
||||
id: 'chatting-user',
|
||||
name: 'Chatting User',
|
||||
avatar: {
|
||||
url: expect.any(String),
|
||||
},
|
||||
}),
|
||||
expect.objectContaining({
|
||||
_id: 'other-chatting-user',
|
||||
id: 'other-chatting-user',
|
||||
name: 'Other Chatting User',
|
||||
avatar: {
|
||||
url: expect.any(String),
|
||||
},
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
]),
|
||||
},
|
||||
})
|
||||
await expect(
|
||||
query({ query: roomQuery(), variables: { first: 3, offset: 3 } }),
|
||||
).resolves.toMatchObject({
|
||||
errors: undefined,
|
||||
data: {
|
||||
Room: [
|
||||
expect.objectContaining({
|
||||
id: expect.any(String),
|
||||
roomId: expect.any(String),
|
||||
roomName: 'Not Chatting User',
|
||||
users: expect.arrayContaining([
|
||||
{
|
||||
_id: 'chatting-user',
|
||||
id: 'chatting-user',
|
||||
name: 'Chatting User',
|
||||
avatar: {
|
||||
url: expect.any(String),
|
||||
},
|
||||
},
|
||||
{
|
||||
_id: 'not-chatting-user',
|
||||
id: 'not-chatting-user',
|
||||
name: 'Not Chatting User',
|
||||
avatar: {
|
||||
url: expect.any(String),
|
||||
},
|
||||
},
|
||||
]),
|
||||
}),
|
||||
],
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('query single room', () => {
|
||||
let result: any = null
|
||||
|
||||
beforeAll(async () => {
|
||||
authenticatedUser = await chattingUser.toJson()
|
||||
result = await query({ query: roomQuery() })
|
||||
})
|
||||
|
||||
describe('as chatter of room', () => {
|
||||
it('returns the room', async () => {
|
||||
expect(
|
||||
await query({
|
||||
query: roomQuery(),
|
||||
variables: { first: 2, offset: 0, id: result.data.Room[0].id },
|
||||
}),
|
||||
).toMatchObject({
|
||||
errors: undefined,
|
||||
data: {
|
||||
Room: [
|
||||
{
|
||||
id: expect.any(String),
|
||||
roomId: expect.any(String),
|
||||
roomName: result.data.Room[0].roomName,
|
||||
users: expect.any(Array),
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
describe('as not chatter of room', () => {
|
||||
beforeAll(async () => {
|
||||
authenticatedUser = await notChattingUser.toJson()
|
||||
})
|
||||
|
||||
it('returns no room', async () => {
|
||||
authenticatedUser = await notChattingUser.toJson()
|
||||
expect(
|
||||
await query({
|
||||
query: roomQuery(),
|
||||
variables: { first: 2, offset: 0, id: result.data.Room[0].id },
|
||||
}),
|
||||
).toMatchObject({
|
||||
errors: undefined,
|
||||
data: {
|
||||
Room: [],
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -1,31 +1,61 @@
|
||||
import { neo4jgraphql } from 'neo4j-graphql-js'
|
||||
import Resolver from './helpers/Resolver'
|
||||
import { pubsub, ROOM_COUNT_UPDATED } from '../../server'
|
||||
import { withFilter } from 'graphql-subscriptions'
|
||||
|
||||
export const getUnreadRoomsCount = async (userId, session) => {
|
||||
return session.readTransaction(async (transaction) => {
|
||||
const unreadRoomsCypher = `
|
||||
MATCH (:User { id: $userId })-[:CHATS_IN]->(room:Room)<-[:INSIDE]-(message:Message)<-[:CREATED]-(sender:User)
|
||||
WHERE NOT sender.id = $userId AND NOT message.seen
|
||||
RETURN toString(COUNT(DISTINCT room)) AS count
|
||||
`
|
||||
const unreadRoomsTxResponse = await transaction.run(unreadRoomsCypher, { userId })
|
||||
return unreadRoomsTxResponse.records.map((record) => record.get('count'))[0]
|
||||
})
|
||||
}
|
||||
|
||||
export default {
|
||||
Subscription: {
|
||||
roomCountUpdated: {
|
||||
subscribe: withFilter(
|
||||
() => pubsub.asyncIterator(ROOM_COUNT_UPDATED),
|
||||
(payload, variables, context) => {
|
||||
return payload.userId === context.user?.id
|
||||
},
|
||||
),
|
||||
},
|
||||
},
|
||||
Query: {
|
||||
Room: async (object, params, context, resolveInfo) => {
|
||||
Room: async (object, params, context, resolveInfo) => {
|
||||
if (!params.filter) params.filter = {}
|
||||
params.filter.users_some = {
|
||||
id: context.user.id,
|
||||
}
|
||||
const resolved = await neo4jgraphql(object, params, context, resolveInfo)
|
||||
if (resolved) {
|
||||
resolved.forEach((room) => {
|
||||
if (room.users) {
|
||||
room.roomName = room.users.filter((user) => user.id !== context.user.id)[0].name
|
||||
room.users.forEach((user) => {
|
||||
user._id = user.id
|
||||
})
|
||||
}
|
||||
})
|
||||
return neo4jgraphql(object, params, context, resolveInfo)
|
||||
},
|
||||
UnreadRooms: async (object, params, context, resolveInfo) => {
|
||||
const {
|
||||
user: { id: currentUserId },
|
||||
} = context
|
||||
const session = context.driver.session()
|
||||
try {
|
||||
const count = await getUnreadRoomsCount(currentUserId, session)
|
||||
return count
|
||||
} finally {
|
||||
session.close()
|
||||
}
|
||||
return resolved
|
||||
},
|
||||
},
|
||||
Mutation: {
|
||||
CreateRoom: async (_parent, params, context, _resolveInfo) => {
|
||||
const { userId } = params
|
||||
const { user: { id: currentUserId } } = context
|
||||
const {
|
||||
user: { id: currentUserId },
|
||||
} = context
|
||||
if (userId === currentUserId) {
|
||||
throw new Error('Cannot create a room with self')
|
||||
}
|
||||
const session = context.driver.session()
|
||||
const writeTxResultPromise = session.writeTransaction(async (transaction) => {
|
||||
const createRoomCypher = `
|
||||
@ -35,15 +65,23 @@ export default {
|
||||
ON CREATE SET
|
||||
room.createdAt = toString(datetime()),
|
||||
room.id = apoc.create.uuid()
|
||||
RETURN room { .* }
|
||||
WITH room, user, currentUser
|
||||
OPTIONAL MATCH (room)<-[:INSIDE]-(message:Message)<-[:CREATED]-(sender:User)
|
||||
WHERE NOT sender.id = $currentUserId AND NOT message.seen
|
||||
WITH room, user, currentUser, message,
|
||||
user.name AS roomName
|
||||
RETURN room {
|
||||
.*,
|
||||
users: [properties(currentUser), properties(user)],
|
||||
roomName: roomName,
|
||||
unreadCount: toString(COUNT(DISTINCT message))
|
||||
}
|
||||
`
|
||||
const createRommTxResponse = await transaction.run(
|
||||
createRoomCypher,
|
||||
{ userId, currentUserId }
|
||||
)
|
||||
const [room] = await createRommTxResponse.records.map((record) =>
|
||||
record.get('room'),
|
||||
)
|
||||
const createRommTxResponse = await transaction.run(createRoomCypher, {
|
||||
userId,
|
||||
currentUserId,
|
||||
})
|
||||
const [room] = await createRommTxResponse.records.map((record) => record.get('room'))
|
||||
return room
|
||||
})
|
||||
try {
|
||||
@ -56,14 +94,15 @@ export default {
|
||||
throw new Error(error)
|
||||
} finally {
|
||||
session.close()
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
Room: {
|
||||
...Resolver('Room', {
|
||||
undefinedToNull: ['lastMessageAt'],
|
||||
hasMany: {
|
||||
users: '<-[:CHATS_IN]-(related:User)',
|
||||
}
|
||||
},
|
||||
}),
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
@ -590,7 +590,7 @@ describe('save category settings', () => {
|
||||
beforeEach(async () => {
|
||||
await Promise.all(
|
||||
categories.map(({ icon, name }, index) => {
|
||||
Factory.build('category', {
|
||||
return Factory.build('category', {
|
||||
id: `cat${index + 1}`,
|
||||
slug: name,
|
||||
name,
|
||||
|
||||
@ -144,7 +144,7 @@ export default {
|
||||
params.locationName = params.locationName === '' ? null : params.locationName
|
||||
const { termsAndConditionsAgreedVersion } = params
|
||||
if (termsAndConditionsAgreedVersion) {
|
||||
const regEx = new RegExp(/^[0-9]+\.[0-9]+\.[0-9]+$/g)
|
||||
const regEx = /^[0-9]+\.[0-9]+\.[0-9]+$/g
|
||||
if (!regEx.test(termsAndConditionsAgreedVersion)) {
|
||||
throw new ForbiddenError('Invalid version format!')
|
||||
}
|
||||
|
||||
@ -25,4 +25,3 @@ type LocationMapBox {
|
||||
type Query {
|
||||
queryLocations(place: String!, lang: String!): [LocationMapBox]!
|
||||
}
|
||||
|
||||
|
||||
@ -2,8 +2,13 @@
|
||||
# room: _RoomFilter
|
||||
# }
|
||||
|
||||
enum _MessageOrdering {
|
||||
indexId_desc
|
||||
}
|
||||
|
||||
type Message {
|
||||
id: ID!
|
||||
indexId: Int!
|
||||
createdAt: String
|
||||
updatedAt: String
|
||||
|
||||
@ -16,6 +21,10 @@ type Message {
|
||||
username: String! @cypher(statement: "MATCH (this)<-[:CREATED]-(user:User) RETURN user.name")
|
||||
avatar: String @cypher(statement: "MATCH (this)<-[:CREATED]-(:User)-[:AVATAR_IMAGE]->(image:Image) RETURN image.url")
|
||||
date: String! @cypher(statement: "RETURN this.createdAt")
|
||||
|
||||
saved: Boolean
|
||||
distributed: Boolean
|
||||
seen: Boolean
|
||||
}
|
||||
|
||||
type Mutation {
|
||||
@ -23,8 +32,19 @@ type Mutation {
|
||||
roomId: ID!
|
||||
content: String!
|
||||
): Message
|
||||
|
||||
MarkMessagesAsSeen(messageIds: [String!]): Boolean
|
||||
}
|
||||
|
||||
type Query {
|
||||
Message(roomId: ID!): [Message]
|
||||
Message(
|
||||
roomId: ID!,
|
||||
first: Int
|
||||
offset: Int
|
||||
orderBy: [_MessageOrdering]
|
||||
): [Message]
|
||||
}
|
||||
|
||||
type Subscription {
|
||||
chatMessageAdded: Message
|
||||
}
|
||||
|
||||
@ -38,5 +38,5 @@ type Mutation {
|
||||
}
|
||||
|
||||
type Subscription {
|
||||
notificationAdded(userId: ID!): NOTIFIED
|
||||
notificationAdded: NOTIFIED
|
||||
}
|
||||
|
||||
@ -84,8 +84,8 @@ input _PostFilter {
|
||||
group: _GroupFilter
|
||||
postsInMyGroups: Boolean
|
||||
postType_in: [PostType]
|
||||
eventStart_gte: String
|
||||
eventEnd_gte: String
|
||||
eventStart_gte: String
|
||||
eventEnd_gte: String
|
||||
}
|
||||
|
||||
enum _PostOrdering {
|
||||
|
||||
@ -5,6 +5,12 @@
|
||||
# users_some: _UserFilter
|
||||
# }
|
||||
|
||||
# TODO change this to last message date
|
||||
enum _RoomOrdering {
|
||||
lastMessageAt_desc
|
||||
createdAt_desc
|
||||
}
|
||||
|
||||
type Room {
|
||||
id: ID!
|
||||
createdAt: String
|
||||
@ -13,7 +19,28 @@ type Room {
|
||||
users: [User]! @relation(name: "CHATS_IN", direction: "IN")
|
||||
|
||||
roomId: String! @cypher(statement: "RETURN this.id")
|
||||
roomName: String! ## @cypher(statement: "MATCH (this)<-[:CHATS_IN]-(user:User) WHERE NOT user.id = $cypherParams.currentUserId RETURN user[0].name")
|
||||
roomName: String! @cypher(statement: "MATCH (this)<-[:CHATS_IN]-(user:User) WHERE NOT user.id = $cypherParams.currentUserId RETURN user.name")
|
||||
avatar: String @cypher(statement: """
|
||||
MATCH (this)<-[:CHATS_IN]-(user:User)
|
||||
WHERE NOT user.id = $cypherParams.currentUserId
|
||||
OPTIONAL MATCH (user)-[:AVATAR_IMAGE]->(image:Image)
|
||||
RETURN image.url
|
||||
""")
|
||||
|
||||
lastMessageAt: String
|
||||
|
||||
lastMessage: Message @cypher(statement: """
|
||||
MATCH (this)<-[:INSIDE]-(message:Message)
|
||||
WITH message ORDER BY message.indexId DESC LIMIT 1
|
||||
RETURN message
|
||||
""")
|
||||
|
||||
unreadCount: Int @cypher(statement: """
|
||||
MATCH (this)<-[:INSIDE]-(message:Message)<-[:CREATED]-(user:User)
|
||||
WHERE NOT user.id = $cypherParams.currentUserId
|
||||
AND NOT message.seen
|
||||
RETURN count(message)
|
||||
""")
|
||||
}
|
||||
|
||||
type Mutation {
|
||||
@ -23,5 +50,13 @@ type Mutation {
|
||||
}
|
||||
|
||||
type Query {
|
||||
Room: [Room]
|
||||
Room(
|
||||
id: ID
|
||||
orderBy: [_RoomOrdering]
|
||||
): [Room]
|
||||
UnreadRooms: Int
|
||||
}
|
||||
|
||||
type Subscription {
|
||||
roomCountUpdated: Int
|
||||
}
|
||||
|
||||
@ -14,6 +14,8 @@ import bodyParser from 'body-parser'
|
||||
import { graphqlUploadExpress } from 'graphql-upload'
|
||||
|
||||
export const NOTIFICATION_ADDED = 'NOTIFICATION_ADDED'
|
||||
export const CHAT_MESSAGE_ADDED = 'CHAT_MESSAGE_ADDED'
|
||||
export const ROOM_COUNT_UPDATED = 'ROOM_COUNT_UPDATED'
|
||||
const { REDIS_DOMAIN, REDIS_PORT, REDIS_PASSWORD } = CONFIG
|
||||
let prodPubsub, devPubsub
|
||||
const options = {
|
||||
|
||||
@ -106,6 +106,4 @@
|
||||
// "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
|
||||
"skipLibCheck": true /* Skip type checking all .d.ts files. */
|
||||
},
|
||||
"include": ["./src/**/*"],
|
||||
"exclude": ["./src/**/*.spec.ts"]
|
||||
}
|
||||
|
||||
3441
backend/yarn.lock
12
cypress/create-cucumber-html-report.js
Normal file
@ -0,0 +1,12 @@
|
||||
const report = require("multiple-cucumber-html-reporter");
|
||||
|
||||
const reportTitle = "Ocelot webapp end-to-end test report"
|
||||
|
||||
report.generate({
|
||||
jsonDir: "reports/json_logs",
|
||||
reportPath: "./reports/cucumber_html_report",
|
||||
pageTitle: reportTitle,
|
||||
reportName: reportTitle,
|
||||
pageFooter: "<div></div>",
|
||||
hideMetadata: true
|
||||
});
|
||||
@ -21,13 +21,7 @@ async function setupNodeEvents(on, config) {
|
||||
return testStore[name]
|
||||
},
|
||||
});
|
||||
|
||||
on("after:run", (results) => {
|
||||
if (results) {
|
||||
console.log(results.status);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
@ -42,10 +36,7 @@ module.exports = defineConfig({
|
||||
baseUrl: "http://localhost:3000",
|
||||
specPattern: "cypress/e2e/**/*.feature",
|
||||
supportFile: "cypress/support/e2e.js",
|
||||
retries: {
|
||||
runMode: 2,
|
||||
openMode: 0,
|
||||
},
|
||||
retries: 0,
|
||||
video: false,
|
||||
setupNodeEvents,
|
||||
},
|
||||
|
||||
@ -13,9 +13,8 @@
|
||||
// Cypress.Commands.add('login', (email, password) => { ... })
|
||||
|
||||
/* globals Cypress cy */
|
||||
import "cypress-file-upload";
|
||||
import { GraphQLClient, request } from 'graphql-request'
|
||||
import CONFIG from '../../backend/build/config'
|
||||
import CONFIG from '../../backend/build/src/config'
|
||||
|
||||
const authenticatedHeaders = (variables) => {
|
||||
const mutation = `
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import Factory from '../../backend/build/db/factories'
|
||||
import { getNeode } from '../../backend/build/db/neo4j'
|
||||
import Factory from '../../backend/build/src/db/factories'
|
||||
import { getNeode } from '../../backend/build/src/db/neo4j'
|
||||
|
||||
const neodeInstance = getNeode()
|
||||
|
||||
|
||||
@ -1,28 +1,27 @@
|
||||
import { Then } from "@badeball/cypress-cucumber-preprocessor";
|
||||
|
||||
Then("I should be able to {string} a teaser image", condition => {
|
||||
// cy.reload()
|
||||
let postTeaserImage = ""
|
||||
|
||||
switch(condition){
|
||||
case 'change':
|
||||
cy.get('.delete-image-button')
|
||||
case "change":
|
||||
postTeaserImage = "humanconnection.png"
|
||||
cy.get(".delete-image-button")
|
||||
.click()
|
||||
cy.fixture('humanconnection.png').as('postTeaserImage').then(function() {
|
||||
cy.get("#postdropzone").upload(
|
||||
{ fileContent: this.postTeaserImage, fileName: 'humanconnection.png', mimeType: "image/png" },
|
||||
{ subjectType: "drag-n-drop", force: true }
|
||||
).wait(750);
|
||||
})
|
||||
cy.get("#postdropzone").selectFile(
|
||||
{ contents: `cypress/fixtures/${postTeaserImage}`, fileName: postTeaserImage, mimeType: "image/png" },
|
||||
{ action: "drag-drop", force: true }
|
||||
).wait(750);
|
||||
break;
|
||||
case 'add':
|
||||
cy.fixture('onourjourney.png').as('postTeaserImage').then(function() {
|
||||
cy.get("#postdropzone").upload(
|
||||
{ fileContent: this.postTeaserImage, fileName: 'onourjourney.png', mimeType: "image/png" },
|
||||
{ subjectType: "drag-n-drop", force: true }
|
||||
).wait(750);
|
||||
})
|
||||
case "add":
|
||||
postTeaserImage = "onourjourney.png"
|
||||
cy.get("#postdropzone").selectFile(
|
||||
{ contents: `cypress/fixtures/${postTeaserImage}`, fileName: postTeaserImage, mimeType: "image/png" },
|
||||
{ action: "drag-drop", force: true }
|
||||
).wait(750);
|
||||
break;
|
||||
case 'remove':
|
||||
cy.get('.delete-image-button')
|
||||
case "remove":
|
||||
cy.get(".delete-image-button")
|
||||
.click()
|
||||
break;
|
||||
}
|
||||
|
||||
@ -2,13 +2,11 @@ import { Then } from "@badeball/cypress-cucumber-preprocessor";
|
||||
|
||||
Then("I should be able to change my profile picture", () => {
|
||||
const avatarUpload = "onourjourney.png";
|
||||
|
||||
cy.fixture(avatarUpload, "base64").then(fileContent => {
|
||||
cy.get("#customdropzone").upload(
|
||||
{ fileContent, fileName: avatarUpload, mimeType: "image/png" },
|
||||
{ subjectType: "drag-n-drop", force: true }
|
||||
);
|
||||
});
|
||||
|
||||
cy.get("#customdropzone").selectFile(
|
||||
{ contents: `cypress/fixtures/${avatarUpload}`, fileName: avatarUpload, mimeType: "image/png" },
|
||||
{ action: "drag-drop" }
|
||||
);
|
||||
cy.get(".profile-page-avatar img")
|
||||
.should("have.attr", "src")
|
||||
.and("contains", "onourjourney");
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { Given } from "@badeball/cypress-cucumber-preprocessor";
|
||||
import encode from '../../../../backend/build/jwt/encode'
|
||||
import encode from '../../../../backend/build/src/jwt/encode'
|
||||
|
||||
Given("I am logged in as {string}", slug => {
|
||||
cy.neode()
|
||||
|
||||
@ -2,6 +2,10 @@
|
||||
|
||||
When you overtake this deploy and rebrand repo to your network you have to recognize the following changes and doings:
|
||||
|
||||
## Version >= 2.7.0 with 'ocelotDockerVersionTag' 2.7.0-470
|
||||
|
||||
- You have to rename all `.js` files to `.ts` in `branding/constants`
|
||||
|
||||
## Version >= 2.4.0 with 'ocelotDockerVersionTag' 2.4.0-298
|
||||
|
||||
- You have to set `SHOW_CONTENT_FILTER_HEADER_MENU` and `SHOW_CONTENT_FILTER_MASONRY_GRID` in `branding/constants/filter.js` originally in main code file `webapp/constants/filter.js` to your preferred value.
|
||||
|
||||
@ -1 +1 @@
|
||||
Subproject commit 3056eec040cf7a052a5d08ab4cac7129355ab652
|
||||
Subproject commit fdc2e52fa444b300e1c4736600bc0e9ae3314222
|
||||
@ -15,4 +15,4 @@ echo "Using CONFIGURATION=${CONFIGURATION}"
|
||||
KUBECONFIG=${KUBECONFIG:-${SCRIPT_DIR}/../configurations/${CONFIGURATION}/kubeconfig.yaml}
|
||||
|
||||
# clean & seed
|
||||
kubectl --kubeconfig=${KUBECONFIG} -n default exec -it $(kubectl --kubeconfig=${KUBECONFIG} -n default get pods | grep ocelot-backend | awk '{ print $1 }') -- /bin/sh -c "node --experimental-repl-await build/db/clean.js && node --experimental-repl-await build/db/seed.js"
|
||||
kubectl --kubeconfig=${KUBECONFIG} -n default exec -it $(kubectl --kubeconfig=${KUBECONFIG} -n default get pods | grep ocelot-backend | awk '{ print $1 }') -- /bin/sh -c "node --experimental-repl-await build/src/db/clean.js && node --experimental-repl-await build/src/db/seed.js"
|
||||
@ -12,9 +12,9 @@ FROM $APP_IMAGE_CODE as code
|
||||
ARG CONFIGURATION=example
|
||||
|
||||
# copy public constants and email templates into the Docker image to brand it
|
||||
COPY configurations/${CONFIGURATION}/branding/constants/emails.js src/config/
|
||||
COPY configurations/${CONFIGURATION}/branding/constants/logos.js src/config/
|
||||
COPY configurations/${CONFIGURATION}/branding/constants/metadata.js src/config/
|
||||
COPY configurations/${CONFIGURATION}/branding/constants/emails.ts src/config/
|
||||
COPY configurations/${CONFIGURATION}/branding/constants/logos.ts src/config/
|
||||
COPY configurations/${CONFIGURATION}/branding/constants/metadata.ts src/config/
|
||||
COPY configurations/${CONFIGURATION}/branding/email/ src/middleware/helpers/email/
|
||||
|
||||
##################################################################################
|
||||
@ -38,7 +38,7 @@ COPY --from=build ${DOCKER_WORKDIR}/build ./build
|
||||
COPY --from=build ${DOCKER_WORKDIR}/node_modules ./node_modules
|
||||
# TODO - externalize the uploads so we can copy the whole folder
|
||||
COPY --from=build ${DOCKER_WORKDIR}/public/img/ ./public/img/
|
||||
COPY --from=build ${DOCKER_WORKDIR}/public/providers.json ./public/providers.json
|
||||
COPY --from=build ${DOCKER_WORKDIR}/public/providers.json ./build/public/providers.json
|
||||
# Copy package.json for script definitions (lock file should not be needed)
|
||||
COPY --from=build ${DOCKER_WORKDIR}/package.json ./package.json
|
||||
|
||||
|
||||
@ -14,6 +14,7 @@ ARG CONFIGURATION=example
|
||||
# copy public constants into the Docker image to brand it
|
||||
COPY configurations/${CONFIGURATION}/branding/static/ static/
|
||||
COPY configurations/${CONFIGURATION}/branding/constants/ constants/
|
||||
RUN /bin/sh -c 'cd constants && for f in *.ts; do mv -- "$f" "${f%.ts}.js"; done'
|
||||
|
||||
# locales
|
||||
COPY configurations/${CONFIGURATION}/branding/locales/*.json locales/tmp/
|
||||
|
||||
@ -14,6 +14,7 @@ ARG CONFIGURATION=example
|
||||
# copy public constants into the Docker image to brand it
|
||||
COPY configurations/${CONFIGURATION}/branding/static/ static/
|
||||
COPY configurations/${CONFIGURATION}/branding/constants/ constants/
|
||||
RUN /bin/sh -c 'cd constants && for f in *.ts; do mv -- "$f" "${f%.ts}.js"; done'
|
||||
COPY configurations/${CONFIGURATION}/branding/locales/html/ locales/html/
|
||||
COPY configurations/${CONFIGURATION}/branding/assets/styles/imports/ assets/styles/imports/
|
||||
COPY configurations/${CONFIGURATION}/branding/assets/fonts/ assets/fonts/
|
||||
|
||||
@ -293,7 +293,7 @@ $ kubectl -n default rollout status deployment/ocelot-neo4j --timeout=240s
|
||||
$ kubectl config get-contexts
|
||||
|
||||
# reset and seed Neo4j database via backend for staging
|
||||
$ kubectl -n default exec -it $(kubectl -n default get pods | grep ocelot-backend | awk '{ print $1 }') -- /bin/sh -c "node --experimental-repl-await build/db/clean.js && node --experimental-repl-await build/db/seed.js"
|
||||
$ kubectl -n default exec -it $(kubectl -n default get pods | grep ocelot-backend | awk '{ print $1 }') -- /bin/sh -c "node --experimental-repl-await build/src/db/clean.js && node --experimental-repl-await build/src/db/seed.js"
|
||||
|
||||
|
||||
```
|
||||
|
||||
24
package.json
@ -10,13 +10,25 @@
|
||||
"url": "https://github.com/Ocelot-Social-Community/Ocelot-Social.git"
|
||||
},
|
||||
"cypress-cucumber-preprocessor": {
|
||||
"nonGlobalStepDefinitions": true
|
||||
"stepDefinitions": "cypress/support/step_definitions/**/*.js",
|
||||
"json": {
|
||||
"enabled": true,
|
||||
"output": "cypress/reports/json_logs/cucumber_log.json",
|
||||
"formatter": "cucumber-json-formatter"
|
||||
},
|
||||
"messages": {
|
||||
"enabled": true,
|
||||
"output": "cypress/reports/json_logs/messages.ndjson"
|
||||
},
|
||||
"html": {
|
||||
"enabled": false
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"db:seed": "cd backend && yarn run db:seed",
|
||||
"db:reset": "cd backend && yarn run db:reset",
|
||||
"cypress:run": "cypress run --browser electron --config-file ./cypress/cypress.config.js",
|
||||
"cypress:open": "cypress open --browser electron --config-file ./cypress/cypress.config.js",
|
||||
"cypress:run": "cypress run --e2e --browser electron --config-file ./cypress/cypress.config.js",
|
||||
"cypress:open": "cypress open --e2e --browser electron --config-file ./cypress/cypress.config.js",
|
||||
"cucumber:setup": "cd backend && yarn run dev",
|
||||
"cucumber": "wait-on tcp:4000 && cucumber-js --require-module @babel/register --exit",
|
||||
"release": "yarn version --no-git-tag-version --no-commit-hooks --no-commit && auto-changelog --latest-version $(node -p -e \"require('./package.json').version\") && cd backend && yarn version --no-git-tag-version --no-commit-hooks --no-commit --new-version $(node -p -e \"require('./../package.json').version\") && cd ../webapp && yarn version --no-git-tag-version --no-commit-hooks --no-commit --new-version $(node -p -e \"require('./../package.json').version\") && cd ../webapp/maintenance/source && yarn version --no-git-tag-version --no-commit-hooks --no-commit --new-version $(node -p -e \"require('./../../../package.json').version\")"
|
||||
@ -27,13 +39,12 @@
|
||||
"@babel/register": "^7.12.10",
|
||||
"@badeball/cypress-cucumber-preprocessor": "^15.1.4",
|
||||
"@cypress/browserify-preprocessor": "^3.0.2",
|
||||
"@faker-js/faker": "7.6.0",
|
||||
"@faker-js/faker": "8.0.2",
|
||||
"auto-changelog": "^2.3.0",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"cross-env": "^7.0.3",
|
||||
"cucumber": "^6.0.5",
|
||||
"cypress": "^12.14.0",
|
||||
"cypress-file-upload": "^3.5.3",
|
||||
"cypress": "^12.17.0",
|
||||
"cypress-network-idle": "^1.14.2",
|
||||
"date-fns": "^2.25.0",
|
||||
"dotenv": "^16.3.1",
|
||||
@ -42,6 +53,7 @@
|
||||
"import": "^0.0.6",
|
||||
"jsonwebtoken": "^8.5.1",
|
||||
"mock-socket": "^9.0.3",
|
||||
"multiple-cucumber-html-reporter": "^3.4.0",
|
||||
"neo4j-driver": "^4.3.4",
|
||||
"neode": "^0.4.8",
|
||||
"rosie": "^2.1.0",
|
||||
|
||||
5
webapp/assets/_new/icons/svgs/align-center.svg
Executable file
@ -0,0 +1,5 @@
|
||||
<!-- Generated by IcoMoon.io -->
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
|
||||
<title>align-center</title>
|
||||
<path d="M3 7h26v2h-26v-2zM7 11h18v2h-18v-2zM3 15h26v2h-26v-2zM7 19h18v2h-18v-2zM3 23h26v2h-26v-2z"></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 274 B |
5
webapp/assets/_new/icons/svgs/align-justify.svg
Executable file
@ -0,0 +1,5 @@
|
||||
<!-- Generated by IcoMoon.io -->
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
|
||||
<title>align-justify</title>
|
||||
<path d="M3 7h26v2h-26v-2zM3 11h26v2h-26v-2zM3 15h26v2h-26v-2zM3 19h26v2h-26v-2zM3 23h26v2h-26v-2z"></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 275 B |
5
webapp/assets/_new/icons/svgs/align-left.svg
Executable file
@ -0,0 +1,5 @@
|
||||
<!-- Generated by IcoMoon.io -->
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
|
||||
<title>align-left</title>
|
||||
<path d="M3 7h26v2h-26v-2zM3 11h18v2h-18v-2zM3 15h26v2h-26v-2zM3 19h18v2h-18v-2zM3 23h26v2h-26v-2z"></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 272 B |
5
webapp/assets/_new/icons/svgs/align-right.svg
Executable file
@ -0,0 +1,5 @@
|
||||
<!-- Generated by IcoMoon.io -->
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
|
||||
<title>align-right</title>
|
||||
<path d="M3 7h26v2h-26v-2zM11 11h18v2h-18v-2zM3 15h26v2h-26v-2zM11 19h18v2h-18v-2zM3 23h26v2h-26v-2z"></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 275 B |
5
webapp/assets/_new/icons/svgs/angle-left.svg
Executable file
@ -0,0 +1,5 @@
|
||||
<!-- Generated by IcoMoon.io -->
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
|
||||
<title>angle-left</title>
|
||||
<path d="M19.031 4.281l1.438 1.438-10.281 10.281 10.281 10.281-1.438 1.438-11-11-0.688-0.719 0.688-0.719z"></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 279 B |
5
webapp/assets/_new/icons/svgs/angle-right.svg
Executable file
@ -0,0 +1,5 @@
|
||||
<!-- Generated by IcoMoon.io -->
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
|
||||
<title>angle-right</title>
|
||||
<path d="M12.969 4.281l11 11 0.688 0.719-0.688 0.719-11 11-1.438-1.438 10.281-10.281-10.281-10.281z"></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 274 B |
5
webapp/assets/_new/icons/svgs/archive.svg
Executable file
@ -0,0 +1,5 @@
|
||||
<!-- Generated by IcoMoon.io -->
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
|
||||
<title>archive</title>
|
||||
<path d="M4 5h24v6h-1v16h-22v-16h-1v-6zM6 7v2h20v-2h-20zM7 11v14h18v-14h-18zM12.813 13c0.013-0.001 0.034-0.001 0.047-0.001s0.034 0.001 0.047 0.001c0.013-0.001 0.034-0.001 0.047-0.001s0.034 0.001 0.047 0.001l6.014-0c0.552 0 1 0.448 1 1s-0.448 1-1 1l-0.014-0h-6c-0.026 0.002-0.068 0.004-0.094 0.004-0.554 0-1.004-0.45-1.004-1.004 0-0.505 0.408-0.953 0.911-1z"></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 527 B |
5
webapp/assets/_new/icons/svgs/arrow-up.svg
Executable file
@ -0,0 +1,5 @@
|
||||
<!-- Generated by IcoMoon.io -->
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
|
||||
<title>arrow-up</title>
|
||||
<path d="M16 4.094l0.719 0.688 8.5 8.5-1.438 1.438-6.781-6.781v20.063h-2v-20.063l-6.781 6.781-1.438-1.438 8.5-8.5z"></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 286 B |
5
webapp/assets/_new/icons/svgs/bar-chart.svg
Executable file
@ -0,0 +1,5 @@
|
||||
<!-- Generated by IcoMoon.io -->
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
|
||||
<title>bar-chart</title>
|
||||
<path d="M21 4h8v24h-8v-24zM23 6v20h4v-20h-4zM3 10h8v18h-8v-18zM5 12v14h4v-14h-4zM12 16h8v12h-8v-12zM14 18v8h4v-8h-4z"></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 290 B |
5
webapp/assets/_new/icons/svgs/briefcase.svg
Executable file
@ -0,0 +1,5 @@
|
||||
<!-- Generated by IcoMoon.io -->
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
|
||||
<title>briefcase</title>
|
||||
<path d="M16 3c1.864 0 3.399 1.275 3.844 3h9.156v20h-26v-20h9.156c0.445-1.725 1.98-3 3.844-3zM16 5c-0.81 0-1.428 0.385-1.75 1h3.5c-0.322-0.615-0.94-1-1.75-1zM5 8v9h22v-9h-22zM16 14c0.552 0 1 0.448 1 1s-0.448 1-1 1-1-0.448-1-1 0.448-1 1-1zM5 19v5h22v-5h-22z"></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 429 B |
5
webapp/assets/_new/icons/svgs/bug.svg
Executable file
@ -0,0 +1,5 @@
|
||||
<!-- Generated by IcoMoon.io -->
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
|
||||
<title>bug</title>
|
||||
<path d="M10.719 3.281l2.313 2.313c0.923-0.39 1.922-0.594 2.969-0.594s2.046 0.203 2.969 0.594l2.313-2.313 1.438 1.438-1.938 1.938c1.462 1.119 2.61 2.755 3.344 4.656l2.438-1.219 0.875 1.813-2.75 1.375c0.183 0.876 0.313 1.782 0.313 2.719 0 0.34-0.006 0.666-0.031 1h3.031v2h-3.375c-0.242 1.043-0.561 2.039-1.031 2.938l3 2.25-1.188 1.625-2.938-2.188c-1.618 2.056-3.885 3.375-6.469 3.375s-4.851-1.319-6.469-3.375l-2.938 2.188-1.188-1.625 3-2.25c-0.47-0.898-0.789-1.894-1.031-2.938h-3.375v-2h3.031c-0.025-0.334-0.031-0.66-0.031-1 0-0.937 0.13-1.843 0.313-2.719l-2.75-1.375 0.875-1.813 2.438 1.219c0.734-1.901 1.882-3.538 3.344-4.656l-1.938-1.938zM16 7c-1.978 0-3.827 1.094-5.125 2.875 1.134 0.511 2.924 1.125 5.125 1.125s3.991-0.614 5.125-1.125c-1.298-1.781-3.147-2.875-5.125-2.875zM9.906 11.594c-0.569 1.292-0.906 2.788-0.906 4.406 0 4.629 2.698 8.282 6 8.906v-11.969c-2.17-0.162-3.941-0.801-5.094-1.344zM22.094 11.594c-1.153 0.542-2.924 1.182-5.094 1.344v11.969c3.302-0.625 6-4.278 6-8.906 0-1.618-0.337-3.115-0.906-4.406z"></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
5
webapp/assets/_new/icons/svgs/calculator.svg
Executable file
@ -0,0 +1,5 @@
|
||||
<!-- Generated by IcoMoon.io -->
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
|
||||
<title>calculator</title>
|
||||
<path d="M5 3h20v26h-20v-26zM7 5v22h16v-22h-16zM9 7h12v6h-12v-6zM11 9v2h8v-2h-8zM10 15h2v2h-2v-2zM14 15h2v2h-2v-2zM18 15h2v2h-2v-2zM10 19h2v2h-2v-2zM14 19h2v2h-2v-2zM18 19h2v2h-2v-2zM10 23h2v2h-2v-2zM14 23h2v2h-2v-2zM18 23h2v2h-2v-2z"></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 407 B |
5
webapp/assets/_new/icons/svgs/camera.svg
Executable file
@ -0,0 +1,5 @@
|
||||
<!-- Generated by IcoMoon.io -->
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
|
||||
<title>camera</title>
|
||||
<path d="M11.5 6h9l0.313 0.406 1.188 1.594h7v18h-26v-18h7l1.188-1.594zM12.5 8l-1.188 1.594-0.313 0.406h-6v14h22v-14h-6l-0.313-0.406-1.188-1.594h-7zM8 11c0.552 0 1 0.448 1 1s-0.448 1-1 1-1-0.448-1-1 0.448-1 1-1zM16 11c3.302 0 6 2.698 6 6s-2.698 6-6 6-6-2.698-6-6 2.698-6 6-6zM16 13c-2.221 0-4 1.779-4 4s1.779 4 4 4 4-1.779 4-4-1.779-4-4-4z"></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 508 B |
5
webapp/assets/_new/icons/svgs/cart-plus.svg
Executable file
@ -0,0 +1,5 @@
|
||||
<!-- Generated by IcoMoon.io -->
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
|
||||
<title>cart-plus</title>
|
||||
<path d="M4 7h2.219c0.918 0 1.716 0.61 1.938 1.5l2.625 10.5h12.469l2.406-9h2.094l-2.594 9.531c-0.238 0.87-1.004 1.469-1.906 1.469h-12.469c-0.918 0-1.714-0.61-1.938-1.5l-2.625-10.5h-2.219c-0.552 0-1-0.448-1-1s0.448-1 1-1zM22 21c1.645 0 3 1.355 3 3s-1.355 3-3 3-3-1.355-3-3 1.355-3 3-3zM13 21c1.645 0 3 1.355 3 3s-1.355 3-3 3-3-1.355-3-3 1.355-3 3-3zM16 7h2v3h3v2h-3v3h-2v-3h-3v-2h3v-3zM13 23c-0.564 0-1 0.436-1 1s0.436 1 1 1 1-0.436 1-1-0.436-1-1-1zM22 23c-0.564 0-1 0.436-1 1s0.436 1 1 1 1-0.436 1-1-0.436-1-1-1z"></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 685 B |
5
webapp/assets/_new/icons/svgs/certificate.svg
Executable file
@ -0,0 +1,5 @@
|
||||
<!-- Generated by IcoMoon.io -->
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
|
||||
<title>certificate</title>
|
||||
<path d="M16 3c0.624 0 1.248 0.213 1.781 0.594l1.656 1.156 1.875 0.25h0.031c1.314 0.16 2.352 1.223 2.531 2.531 0.003 0.024 0.029 0.038 0.031 0.063h-0.031l0.375 1.875 1.156 1.656c0.762 1.067 0.73 2.476 0.031 3.594v0.031l-0.031 0.031-1.156 1.656-0.25 1.875 3.125 4.75 1.031 1.531h-4.781l-1.156 2.688-0.719 1.719-1.031-1.563-3.156-4.75c-0.818 0.379-1.779 0.349-2.625 0l-3.156 4.75-1.031 1.563-0.719-1.719-1.156-2.688h-4.781l1.031-1.531 3.219-4.906-0.313-1.719-1.188-1.656c-0.762-1.067-0.73-2.507-0.031-3.625v-0.031l0.031-0.031 1.156-1.5 0.25-1.938v-0.031l0.031-0.031c0.283-1.275 1.287-2.279 2.563-2.563l0.031-0.031h0.031l1.906-0.25 1.656-1.156c0.533-0.381 1.157-0.594 1.781-0.594zM16 5.031c-0.229 0-0.458 0.068-0.625 0.188l-2 1.438-0.25 0.031-2.094 0.281c-0.015 0.003-0.016 0.027-0.031 0.031-0.487 0.125-0.875 0.513-1 1-0.004 0.015-0.028 0.016-0.031 0.031l-0.281 2.094-0.031 0.281-0.156 0.188-1.25 1.625c-0.301 0.482-0.269 1.073-0.031 1.406l1.281 1.781 0.156 0.188 0.031 0.25 0.406 2.281v0.063c0.015 0.138 0.063 0.266 0.125 0.375 0.139 0.244 0.378 0.403 0.688 0.438h0.031l2.188 0.313 0.281 0.031 0.188 0.156 1.625 1.25c0.482 0.302 1.073 0.269 1.406 0.031l1.781-1.281 0.188-0.156 0.25-0.031 2.281-0.406h0.063c0.25-0.028 0.45-0.142 0.594-0.313v-0.031l0.063-0.031c0.084-0.122 0.138-0.273 0.156-0.438v-0.031l0.313-2.188 0.031-0.25 1.406-1.969c0.302-0.482 0.269-1.042 0.031-1.375l-1.281-1.781-0.156-0.188-0.031-0.219-0.406-2.219v-0.063c-0.050-0.447-0.365-0.763-0.813-0.813h-0.031l-2.188-0.313-0.25-0.031-0.219-0.156-1.781-1.281c-0.167-0.119-0.396-0.188-0.625-0.188zM22.906 20.25c-0.409 0.323-0.9 0.552-1.438 0.625-0.024 0.003-0.038 0.029-0.063 0.031v-0.031l-1.969 0.344-0.469 0.344 2.125 3.25 0.688-1.594 0.25-0.625h2.406zM9.094 20.281l-1.531 2.313h2.406l0.25 0.625 0.688 1.594 2.125-3.219-0.438-0.344-1.906-0.25c-0.010-0.001-0.021 0.001-0.031 0-0.595-0.072-1.135-0.338-1.563-0.719z"></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.0 KiB |
5
webapp/assets/_new/icons/svgs/chain-broken.svg
Executable file
@ -0,0 +1,5 @@
|
||||
<!-- Generated by IcoMoon.io -->
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
|
||||
<title>chain-broken</title>
|
||||
<path d="M21.75 4c1.603 0 3.189 0.626 4.406 1.844 2.435 2.435 2.435 6.409 0 8.844l-1.469 1.469c-1.015 1.016-2.304 1.618-3.625 1.781l-0.25-2c0.901-0.111 1.745-0.494 2.438-1.188h0.031l1.469-1.469c1.671-1.671 1.671-4.36 0-6.031s-4.36-1.671-6.031 0l-1.469 1.469c-0.692 0.692-1.076 1.564-1.188 2.469l-2-0.25c0.162-1.319 0.765-2.609 1.781-3.625l1.469-1.469c1.218-1.218 2.834-1.844 4.438-1.844zM7.719 6.281l4 4-1.438 1.438-4-4zM10.938 14.063l0.25 2c-0.901 0.111-1.745 0.494-2.438 1.188h-0.031l-1.469 1.469c-1.671 1.671-1.671 4.36 0 6.031s4.36 1.671 6.031 0l1.469-1.469c0.692-0.692 1.076-1.564 1.188-2.469l2 0.25c-0.162 1.319-0.765 2.609-1.781 3.625l-1.469 1.469c-2.435 2.435-6.409 2.435-8.844 0s-2.435-6.409 0-8.844l1.469-1.469c1.015-1.016 2.304-1.618 3.625-1.781zM21.719 20.281l4 4-1.438 1.438-4-4z"></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 968 B |
5
webapp/assets/_new/icons/svgs/chain.svg
Executable file
@ -0,0 +1,5 @@
|
||||
<!-- Generated by IcoMoon.io -->
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
|
||||
<title>chain</title>
|
||||
<path d="M21.75 4c1.671 0 3.225 0.661 4.406 1.844s1.844 2.735 1.844 4.406-0.662 3.255-1.844 4.438l-1.469 1.469c-1.181 1.183-2.766 1.844-4.438 1.844-0.793 0-1.565-0.153-2.281-0.438l1.625-1.625c0.215 0.038 0.432 0.063 0.656 0.063 1.138 0 2.226-0.445 3.031-1.25l1.469-1.469c1.66-1.66 1.66-4.372 0-6.031-0.804-0.805-1.863-1.25-3-1.25s-2.227 0.444-3.031 1.25l-1.469 1.469c-0.997 0.996-1.391 2.393-1.188 3.688l-1.625 1.625c-0.285-0.716-0.438-1.487-0.438-2.281 0-1.671 0.662-3.255 1.844-4.438l1.469-1.469c1.181-1.183 2.766-1.844 4.438-1.844zM19.281 11.281l1.438 1.438-8 8-1.438-1.438zM11.75 14c0.793 0 1.565 0.153 2.281 0.438l-1.625 1.625c-0.215-0.038-0.432-0.063-0.656-0.063-1.138 0-2.226 0.445-3.031 1.25l-1.469 1.469c-1.66 1.66-1.66 4.372 0 6.031 0.804 0.805 1.863 1.25 3 1.25s2.227-0.444 3.031-1.25l1.469-1.469c0.997-0.996 1.391-2.393 1.188-3.688l1.625-1.625c0.285 0.716 0.438 1.487 0.438 2.281 0 1.671-0.662 3.256-1.844 4.438l-1.469 1.469c-1.181 1.183-2.766 1.844-4.438 1.844s-3.225-0.661-4.406-1.844c-1.182-1.182-1.844-2.735-1.844-4.406s0.662-3.256 1.844-4.438l1.469-1.469c1.181-1.183 2.766-1.844 4.438-1.844z"></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
4
webapp/assets/_new/icons/svgs/chat-bubble.svg
Normal file
@ -0,0 +1,4 @@
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
|
||||
<title>chat-bubble</title>
|
||||
<g data-name="Chat"><path d="M27.23,24.91A9,9,0,0,0,22.44,9.52,8.57,8.57,0,0,0,21,9.41a8.94,8.94,0,0,0-8.92,10.14c0,.1,0,.2,0,.29a9,9,0,0,0,.22,1l0,.11a8.93,8.93,0,0,0,.38,1l.13.28a9,9,0,0,0,.45.83l.07.14a9.13,9.13,0,0,1-2.94-.36,1,1,0,0,0-.68,0L7,24.13l.54-1.9a1,1,0,0,0-.32-1,9,9,0,0,1,11.27-14,1,1,0,0,0,1.23-1.58A10.89,10.89,0,0,0,13,3.25a11,11,0,0,0-7.5,19l-1,3.34A1,1,0,0,0,5.9,26.82l4.35-1.93a11,11,0,0,0,4.68.16A9,9,0,0,0,21,27.41a8.81,8.81,0,0,0,2.18-.27l3.41,1.52A1,1,0,0,0,28,27.48Zm-1.77-1.1a1,1,0,0,0-.32,1L25.45,26l-1.79-.8a1,1,0,0,0-.41-.09,1,1,0,0,0-.29,0,6.64,6.64,0,0,1-2,.29,7,7,0,0,1,0-14,6.65,6.65,0,0,1,1.11.09,7,7,0,0,1,3.35,12.31Z"></path><path d="M17.82 17.08H17a1 1 0 0 0 0 2h.82a1 1 0 0 0 0-2zM21.41 17.08h-.82a1 1 0 0 0 0 2h.82a1 1 0 0 0 0-2zM25 17.08h-.82a1 1 0 0 0 0 2H25a1 1 0 0 0 0-2z"></path></g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 961 B |
5
webapp/assets/_new/icons/svgs/cloud-download.svg
Executable file
@ -0,0 +1,5 @@
|
||||
<!-- Generated by IcoMoon.io -->
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
|
||||
<title>cloud-download</title>
|
||||
<path d="M16 5c3.378 0 6.14 2.131 7.344 5.063 3.527 0.182 6.33 2.986 6.563 6.5 1.239 1.102 2.094 2.677 2.094 4.438 0 3.324-2.676 6-6 6h-20c-3.324 0-6-2.676-6-6 0-2.751 1.884-4.944 4.344-5.656 0.597-1.699 2.050-2.919 3.844-3.219 0.454-3.994 3.694-7.125 7.813-7.125zM16 7c-3.37 0-6 2.63-6 6v1h-1c-1.444 0-2.638 0.964-2.938 2.313l-0.125 0.656-0.656 0.125c-1.832 0.319-3.281 1.886-3.281 3.906 0 2.276 1.724 4 4 4h20c2.276 0 4-1.724 4-4 0-1.267-0.65-2.48-1.594-3.188l-0.406-0.313v-0.5c0-2.755-2.245-5-5-5h-1.031l-0.219-0.719c-0.779-2.51-2.988-4.281-5.75-4.281zM15 12h2v6.563l2.281-2.281 1.438 1.438-4 4-0.719 0.688-0.719-0.688-4-4 1.438-1.438 2.281 2.281v-6.563z"></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 835 B |
5
webapp/assets/_new/icons/svgs/cloud-upload.svg
Executable file
@ -0,0 +1,5 @@
|
||||
<!-- Generated by IcoMoon.io -->
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
|
||||
<title>cloud-upload</title>
|
||||
<path d="M16 4c3.378 0 6.14 2.131 7.344 5.063 3.527 0.182 6.33 2.986 6.563 6.5 1.239 1.102 2.094 2.677 2.094 4.438 0 3.324-2.676 6-6 6h-20c-3.324 0-6-2.676-6-6 0-2.751 1.884-4.944 4.344-5.656 0.597-1.699 2.050-2.919 3.844-3.219 0.454-3.994 3.694-7.125 7.813-7.125zM16 6c-3.37 0-6 2.63-6 6v1h-1c-1.444 0-2.638 0.964-2.938 2.313l-0.125 0.656-0.656 0.125c-1.832 0.319-3.281 1.886-3.281 3.906 0 2.276 1.724 4 4 4h20c2.276 0 4-1.724 4-4 0-1.267-0.65-2.48-1.594-3.188l-0.406-0.313v-0.5c0-2.755-2.245-5-5-5h-1.031l-0.219-0.719c-0.779-2.51-2.988-4.281-5.75-4.281zM16 11.594l0.719 0.688 4 4-1.438 1.438-2.281-2.281v6.563h-2v-6.563l-2.281 2.281-1.438-1.438 4-4z"></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 827 B |
5
webapp/assets/_new/icons/svgs/cloud.svg
Executable file
@ -0,0 +1,5 @@
|
||||
<!-- Generated by IcoMoon.io -->
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
|
||||
<title>cloud</title>
|
||||
<path d="M16 5c2.451 0 4.563 1.302 5.813 3.219 0.392-0.089 0.755-0.219 1.188-0.219 3.302 0 6 2.698 6 6 1.73 1.055 3 2.835 3 5 0 3.302-2.698 6-6 6h-20c-3.302 0-6-2.698-6-6s2.698-6 6-6c0.211 0 0.394 0.040 0.594 0.063 0.531-1.191 1.439-2.083 2.656-2.563 0.698-3.129 3.419-5.5 6.75-5.5zM16 7c-2.522 0-4.581 1.836-4.938 4.25l-0.094 0.688-0.656 0.156c-1.11 0.265-2.002 1.136-2.25 2.25l-0.188 0.969-1-0.219c-0.298-0.067-0.584-0.094-0.875-0.094-2.22 0-4 1.78-4 4s1.78 4 4 4h20c2.22 0 4-1.78 4-4 0-1.662-1.009-3.078-2.438-3.688l-0.656-0.281 0.063-0.719c0.018-0.235 0.031-0.321 0.031-0.313 0-2.22-1.78-4-4-4-0.444 0-0.875 0.096-1.313 0.25l-0.844 0.281-0.375-0.781c-0.824-1.631-2.511-2.75-4.469-2.75z"></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 858 B |
5
webapp/assets/_new/icons/svgs/code.svg
Executable file
@ -0,0 +1,5 @@
|
||||
<!-- Generated by IcoMoon.io -->
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
|
||||
<title>code</title>
|
||||
<path d="M18 5h2l-6 22h-2zM7.938 6.406l1.625 1.188-6.313 8.406 6.313 8.406-1.625 1.188-6.75-9-0.438-0.594 0.438-0.594zM24.063 6.406l6.75 9 0.438 0.594-0.438 0.594-6.75 9-1.625-1.188 6.313-8.406-6.313-8.406z"></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 374 B |
5
webapp/assets/_new/icons/svgs/coffee.svg
Executable file
@ -0,0 +1,5 @@
|
||||
<!-- Generated by IcoMoon.io -->
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
|
||||
<title>coffee</title>
|
||||
<path d="M16-2h2v4h-2v-4zM20-1h2v3h-2v-3zM10 3h18v4h2c1.645 0 3 1.355 3 3v3c0 1.645-1.355 3-3 3h-2v5c0 1.645-1.355 3-3 3h-12c-1.645 0-3-1.355-3-3v-18zM12 5v16c0 0.555 0.445 1 1 1h12c0.555 0 1-0.445 1-1v-16h-14zM28 9v5h2c0.555 0 1-0.445 1-1v-3c0-0.555-0.445-1-1-1h-2z"></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 436 B |
5
webapp/assets/_new/icons/svgs/columns.svg
Executable file
@ -0,0 +1,5 @@
|
||||
<!-- Generated by IcoMoon.io -->
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
|
||||
<title>columns</title>
|
||||
<path d="M5 5h22v22h-22v-22zM7 7v18h8v-18h-8zM17 7v18h8v-18h-8z"></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 234 B |
5
webapp/assets/_new/icons/svgs/compass.svg
Executable file
@ -0,0 +1,5 @@
|
||||
<!-- Generated by IcoMoon.io -->
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
|
||||
<title>compass</title>
|
||||
<path d="M16 3c7.168 0 13 5.832 13 13s-5.832 13-13 13-13-5.832-13-13 5.832-13 13-13zM14.875 5.063c-5.226 0.529-9.341 4.695-9.813 9.938h0.938v2h-0.938c0.475 5.284 4.653 9.462 9.938 9.938v-0.938h2v0.938c5.284-0.475 9.462-4.653 9.938-9.938h-0.938v-2h0.938c-0.475-5.284-4.653-9.462-9.938-9.938v0.938h-2v-0.938c-0.041 0.004-0.084-0.004-0.125 0zM22.094 9.906l-3.688 8.5-8.5 3.688 3.688-8.5zM16 14.5c-0.8 0-1.5 0.7-1.5 1.5s0.7 1.5 1.5 1.5 1.5-0.7 1.5-1.5-0.7-1.5-1.5-1.5z"></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 635 B |
5
webapp/assets/_new/icons/svgs/credit-card.svg
Executable file
@ -0,0 +1,5 @@
|
||||
<!-- Generated by IcoMoon.io -->
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
|
||||
<title>credit-card</title>
|
||||
<path d="M5 5h18c1.645 0 3 1.355 3 3v1h1c1.645 0 3 1.355 3 3v12c0 1.645-1.355 3-3 3h-18c-1.645 0-3-1.355-3-3v-1h-1c-1.645 0-3-1.355-3-3v-12c0-1.645 1.355-3 3-3zM5 7c-0.565 0-1 0.435-1 1v12c0 0.565 0.435 1 1 1h18c0.565 0 1-0.435 1-1v-9h-19v-2h19v-1c0-0.565-0.435-1-1-1h-18zM26 11v2h2v-1c0-0.565-0.435-1-1-1h-1zM26 15v5c0 1.645-1.355 3-3 3h-15v1c0 0.565 0.435 1 1 1h18c0.565 0 1-0.435 1-1v-9h-2z"></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 568 B |
5
webapp/assets/_new/icons/svgs/crop.svg
Executable file
@ -0,0 +1,5 @@
|
||||
<!-- Generated by IcoMoon.io -->
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
|
||||
<title>crop</title>
|
||||
<path d="M8 4h2v16.563l10.563-10.563h-9.563v-2h11.563l3.719-3.719 1.438 1.438-3.719 3.719v11.563h-2v-9.563l-10.563 10.563h16.563v2h-4v4h-2v-4h-14v-14h-4v-2h4v-4z"></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 329 B |
5
webapp/assets/_new/icons/svgs/crosshairs.svg
Executable file
@ -0,0 +1,5 @@
|
||||
<!-- Generated by IcoMoon.io -->
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
|
||||
<title>crosshairs</title>
|
||||
<path d="M15 3h2v2.063c5.268 0.477 9.46 4.67 9.938 9.938h2.063v2h-2.063c-0.477 5.268-4.67 9.46-9.938 9.938v2.063h-2v-2.063c-5.268-0.477-9.46-4.67-9.938-9.938h-2.063v-2h2.063c0.477-5.268 4.67-9.46 9.938-9.938v-2.063zM15 7.031c-4.193 0.453-7.515 3.776-7.969 7.969h1.969v2h-1.969c0.453 4.193 3.776 7.515 7.969 7.969v-1.969h2v1.969c4.193-0.453 7.515-3.776 7.969-7.969h-1.969v-2h1.969c-0.453-4.193-3.776-7.515-7.969-7.969v1.969h-2v-1.969z"></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 607 B |
5
webapp/assets/_new/icons/svgs/cube.svg
Executable file
@ -0,0 +1,5 @@
|
||||
<!-- Generated by IcoMoon.io -->
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
|
||||
<title>cube</title>
|
||||
<path d="M16 4.406l0.406 0.188 10 4.5 0.594 0.25v12.688l-0.5 0.281-10.5 5.844-0.5-0.281-10.5-5.844v-12.688l0.594-0.25 10-4.5zM16 6.594l-7.688 3.438 7.688 3.844 7.688-3.844zM7 11.625v9.219l8 4.438v-9.656zM25 11.625l-8 4v9.656l8-4.438v-9.219z"></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 408 B |
5
webapp/assets/_new/icons/svgs/cubes.svg
Executable file
@ -0,0 +1,5 @@
|
||||
<!-- Generated by IcoMoon.io -->
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
|
||||
<title>cubes</title>
|
||||
<path d="M16 4l0.375 0.156 6.625 2.656v6.719l5.406 2.344 0.594 0.281v8.063l-0.5 0.313-6 3.344-0.469 0.25-0.469-0.219-5.563-2.781-5.563 2.781-0.469 0.219-0.469-0.25-6-3.344-0.5-0.313v-8.063l0.594-0.281 5.406-2.344v-6.719l6.625-2.656zM16 6.188l-3.281 1.281 3.281 1.281 3.281-1.281zM11 8.938v4.625l4 1.781v-4.875zM21 8.938l-4 1.531v4.875l4-1.781v-4.625zM10 15.313l-3.625 1.563 3.625 1.813 3.625-1.781zM22 15.313l-2.5 1.094-1.125 0.5 3.625 1.781 3.625-1.813zM5 18.406v4.656l4 2.25v-4.906zM27 18.406l-4 2v4.906l4-2.25v-4.656zM15 18.469l-4 1.938v4.969l4-2v-4.906zM17 18.469v4.906l4 2v-4.969z"></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 754 B |
5
webapp/assets/_new/icons/svgs/cut.svg
Executable file
@ -0,0 +1,5 @@
|
||||
<!-- Generated by IcoMoon.io -->
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
|
||||
<title>cut</title>
|
||||
<path d="M6 6c2.197 0 4 1.803 4 4 0 0.494-0.115 0.969-0.281 1.406l6.063 3.438 10.219-5.844h4l-20.281 11.594c0.166 0.438 0.281 0.913 0.281 1.406 0 2.197-1.803 4-4 4s-4-1.803-4-4 1.803-4 4-4c0.981 0 1.864 0.375 2.563 0.969l5.156-2.938-5.219-2.969c-0.691 0.568-1.543 0.938-2.5 0.938-2.197 0-4-1.803-4-4s1.803-4 4-4zM6 8c-0.977 0-1.784 0.677-1.969 1.594-0.026 0.131-0.031 0.267-0.031 0.406 0 1.116 0.884 2 2 2s2-0.884 2-2-0.884-2-2-2zM19.094 16.813l10.906 6.188h-4l-8.906-5.094zM6 20c-0.977 0-1.784 0.677-1.969 1.594-0.026 0.131-0.031 0.267-0.031 0.406 0 1.116 0.884 2 2 2s2-0.884 2-2-0.884-2-2-2z"></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 760 B |
5
webapp/assets/_new/icons/svgs/dashboard.svg
Executable file
@ -0,0 +1,5 @@
|
||||
<!-- Generated by IcoMoon.io -->
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
|
||||
<title>dashboard</title>
|
||||
<path d="M16 4c6.616 0 12 5.384 12 12s-5.384 12-12 12-12-5.384-12-12 5.384-12 12-12zM16 6c-5.535 0-10 4.465-10 10s4.465 10 10 10 10-4.465 10-10-4.465-10-10-10zM16 7c0.552 0 1 0.448 1 1s-0.448 1-1 1-1-0.448-1-1 0.448-1 1-1zM10.344 9.344c0.552 0 1 0.448 1 1s-0.448 1-1 1-1-0.448-1-1 0.448-1 1-1zM20.938 9.656l1.438 1.406-4.438 4.438c0.041 0.16 0.063 0.327 0.063 0.5 0 1.105-0.895 2-2 2s-2-0.895-2-2 0.895-2 2-2c0.173 0 0.34 0.021 0.5 0.063zM8 15c0.552 0 1 0.448 1 1s-0.448 1-1 1-1-0.448-1-1 0.448-1 1-1zM24 15c0.552 0 1 0.448 1 1s-0.448 1-1 1-1-0.448-1-1 0.448-1 1-1zM10.344 20.656c0.552 0 1 0.448 1 1s-0.448 1-1 1-1-0.448-1-1 0.448-1 1-1zM21.656 20.656c0.552 0 1 0.448 1 1s-0.448 1-1 1-1-0.448-1-1 0.448-1 1-1z"></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 882 B |
5
webapp/assets/_new/icons/svgs/diamond.svg
Executable file
@ -0,0 +1,5 @@
|
||||
<!-- Generated by IcoMoon.io -->
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
|
||||
<title>diamond</title>
|
||||
<path d="M9.531 6h12.938l5.313 6.375 0.5 0.594-0.5 0.656-11.781 15-0.781-1-11-14-0.5-0.656 0.5-0.594 5-6zM10.469 8l-3.344 4h4.313l2.688-4h-3.656zM17.875 8l2.688 4h4.313l-3.344-4h-3.656zM16 8.844l-2.125 3.156h4.25zM7.031 14l6.594 8.406-2.375-8.406h-4.219zM13.313 14l2.688 9.313 2.656-9.313h-5.344zM20.75 14l-2.375 8.375 6.594-8.375h-4.219z"></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 509 B |
5
webapp/assets/_new/icons/svgs/exchange.svg
Executable file
@ -0,0 +1,5 @@
|
||||
<!-- Generated by IcoMoon.io -->
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
|
||||
<title>exchange</title>
|
||||
<path d="M26.188-1.719l6.719 6.719-6.719 6.719-1.406-1.438 4.281-4.281h-21.063v-2h21.063l-4.281-4.281zM13.813 12.281l1.406 1.438-4.281 4.281h21.063v2h-21.063l4.281 4.281-1.406 1.438-6.719-6.719 0.719-0.719z"></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 378 B |
5
webapp/assets/_new/icons/svgs/exclamation-triangle.svg
Executable file
@ -0,0 +1,5 @@
|
||||
<!-- Generated by IcoMoon.io -->
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
|
||||
<title>exclamation-triangle</title>
|
||||
<path d="M16 3.219l0.875 1.5 12 20.781 0.844 1.5h-27.438l0.844-1.5 12-20.781zM16 7.219l-10.25 17.781h20.5zM15 14h2v6h-2v-6zM15 21h2v2h-2v-2z"></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 324 B |
5
webapp/assets/_new/icons/svgs/expand.svg
Executable file
@ -0,0 +1,5 @@
|
||||
<!-- Generated by IcoMoon.io -->
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
|
||||
<title>expand</title>
|
||||
<path d="M14 5h13v13h-2v-9.563l-16.563 16.563h9.563v2h-13v-13h2v9.563l16.563-16.563h-9.563v-2z"></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 264 B |
5
webapp/assets/_new/icons/svgs/external-link.svg
Executable file
@ -0,0 +1,5 @@
|
||||
<!-- Generated by IcoMoon.io -->
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
|
||||
<title>external-link</title>
|
||||
<path d="M18 5h9v9h-2v-5.563l-12.281 12.281-1.438-1.438 12.281-12.281h-5.563v-2zM5 9h13l-2 2h-9v14h14v-9l2-2v13h-18v-18z"></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 297 B |
5
webapp/assets/_new/icons/svgs/eyedropper.svg
Executable file
@ -0,0 +1,5 @@
|
||||
<!-- Generated by IcoMoon.io -->
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
|
||||
<title>eyedropper</title>
|
||||
<path d="M24.656 3.031c1.108 0 2.222 0.41 3.063 1.25 1.681 1.681 1.681 4.444 0 6.125l-2.813 2.781 1 1-1.406 1.406-1-1-9.5 9.5c-1.064 1.064-1.845 1.684-2.531 2.063s-1.277 0.493-1.688 0.563-0.636 0.113-1.063 0.344-1.040 0.696-2 1.656l-0.719 0.688-0.719-0.688-2-2-0.688-0.719 0.688-0.719c0.986-0.986 1.475-1.621 1.719-2.063s0.276-0.66 0.344-1.063 0.196-1.011 0.563-1.688 0.96-1.429 2-2.469l9.5-9.5-1-1 1.406-1.406 1 1 2.781-2.813c0.84-0.84 1.954-1.25 3.063-1.25zM24.656 5.031c-0.592 0-1.197 0.228-1.656 0.688l-2.781 2.781 3.281 3.281 2.781-2.781c0.919-0.919 0.919-2.362 0-3.281-0.46-0.46-1.033-0.688-1.625-0.688zM18.813 9.906l-9.5 9.5c-0.96 0.96-1.426 1.605-1.656 2.031s-0.274 0.621-0.344 1.031-0.184 1.033-0.563 1.719c-0.259 0.469-0.859 1.1-1.406 1.719l0.75 0.75c0.601-0.529 1.227-1.126 1.688-1.375 0.677-0.366 1.254-0.463 1.656-0.531s0.621-0.1 1.063-0.344 1.108-0.733 2.094-1.719l9.5-9.5z"></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
5
webapp/assets/_new/icons/svgs/facebook.svg
Executable file
@ -0,0 +1,5 @@
|
||||
<!-- Generated by IcoMoon.io -->
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
|
||||
<title>facebook</title>
|
||||
<path d="M7 5h18c1.093 0 2 0.907 2 2v18c0 1.093-0.907 2-2 2h-18c-1.093 0-2-0.907-2-2v-18c0-1.093 0.907-2 2-2zM7 7v18h9.688v-6.75h-2.625v-3h2.625v-2.25c0-2.583 1.571-3.969 3.875-3.969 1.104 0 2.067 0.057 2.344 0.094v2.719h-1.625c-1.253 0-1.469 0.595-1.469 1.469v1.938h2.969l-0.375 3h-2.594v6.75h5.188v-18h-18z"></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 480 B |