├── .dockerignore ├── .editorconfig ├── .env.default ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── dependabot.yml └── workflows │ ├── ci.yml │ └── release.yml ├── .gitignore ├── .prettierignore ├── Dockerfile ├── LICENSE ├── README.md ├── docker-compose-prod.yml ├── docker-compose.yml ├── docs ├── deployment.md ├── teammapper-logo.png └── teammapper-screenshot.png ├── entrypoint.prod.sh ├── package.json ├── teammapper-backend ├── .env.default ├── .gitignore ├── .prettierrc.json ├── LICENSE ├── README.md ├── eslint.config.cjs ├── nest-cli.json ├── package-lock.json ├── package.json ├── src │ ├── app.module.ts │ ├── config.service.ts │ ├── data-source.ts │ ├── filters │ │ └── global-exception.filter.ts │ ├── jobs │ │ ├── deleteMaps.job.ts │ │ └── seedMapData.job.ts │ ├── main.ts │ ├── map │ │ ├── controllers │ │ │ ├── maps.controller.spec.ts │ │ │ ├── maps.controller.ts │ │ │ ├── maps.gateway.spec.ts │ │ │ └── maps.gateway.ts │ │ ├── entities │ │ │ ├── mmpMap.entity.ts │ │ │ └── mmpNode.entity.ts │ │ ├── guards │ │ │ ├── edit.guard.spec.ts │ │ │ └── edit.guard.ts │ │ ├── map.module.ts │ │ ├── services │ │ │ ├── maps.service.spec.ts │ │ │ ├── maps.service.ts │ │ │ ├── tasks.service.ts │ │ │ └── uuid.error.ts │ │ ├── types.ts │ │ └── utils │ │ │ ├── clientServerMapping.ts │ │ │ └── tests │ │ │ └── mapFactories.ts │ └── migrations │ │ ├── 1638048135450-CreateMapsAndNodes.ts │ │ ├── 1640704269037-AddDefaultTimestampToMaps.ts │ │ ├── 1640939564906-AddAdminIdForMaps.ts │ │ ├── 1644079415806-AddNodeMapIdAsPrimaryColumnOnNodes.ts │ │ ├── 1663839669273-AddIndexToForeignKeysOnMmpNode.ts │ │ ├── 1663927754319-AddIndexForNodesParents.ts │ │ ├── 1668360651755-AddOptionsToMap.ts │ │ ├── 1678605712865-AddLinkHrefToNode.ts │ │ ├── 1678976170981-AddModificationSecretToMaps.ts │ │ ├── 1679478438937-AddLastModifiedToNodes.ts │ │ ├── 1701777634545-AddDetachedPropertyToNodes.ts │ │ ├── 1718959806227-AddLastAccessedFieldToMap.ts │ │ ├── 1724314314717-AddCreatedAtToMap.ts │ │ ├── 1724314435583-AddCreatedAtToNode.ts │ │ ├── 1724325535133-AddDefaultToCreatedAtMmpMap.ts │ │ └── 1724325567562-AddDefaultToCreatedAtMmpNode.ts ├── test │ ├── app.e2e-spec.ts │ ├── db.ts │ ├── helper.ts │ └── jest-e2e.json ├── tsconfig.build.json └── tsconfig.json └── teammapper-frontend ├── .browserslistrc ├── .gitignore ├── .prettierrc ├── CHANGELOG.md ├── LICENSE ├── README.md ├── angular.json ├── eslint.config.js ├── mmp ├── .gitignore ├── LICENSE ├── README.md └── src │ ├── index.ts │ ├── map │ ├── handlers │ │ ├── copy-paste.ts │ │ ├── drag.ts │ │ ├── draw.ts │ │ ├── events.ts │ │ ├── export.ts │ │ ├── history.ts │ │ ├── nodes.ts │ │ └── zoom.ts │ ├── map.ts │ ├── models │ │ └── node.ts │ ├── options.ts │ └── types.ts │ ├── typings.d.ts │ └── utils │ ├── log.ts │ └── utils.ts ├── package-lock.json ├── package.json ├── setup-jest.ts ├── src ├── app │ ├── core │ │ ├── http │ │ │ └── http.service.ts │ │ └── services │ │ │ ├── dialog │ │ │ └── dialog.service.ts │ │ │ ├── map-sync │ │ │ ├── map-sync.service.ts │ │ │ └── server-types.ts │ │ │ ├── mmp │ │ │ ├── __mocks__ │ │ │ │ └── mmp.service.ts │ │ │ ├── mmp-utils.ts │ │ │ ├── mmp.service.spec.ts │ │ │ └── mmp.service.ts │ │ │ ├── pictograms │ │ │ ├── picto-types.ts │ │ │ ├── pictogram.service.spec.ts │ │ │ └── pictogram.service.ts │ │ │ ├── settings │ │ │ ├── settings.service.spec.ts │ │ │ └── settings.service.ts │ │ │ ├── shortcuts │ │ │ └── shortcuts.service.ts │ │ │ ├── storage │ │ │ └── storage.service.ts │ │ │ └── utils │ │ │ └── utils.service.ts │ ├── guards │ │ └── toast.guard.ts │ ├── modules │ │ ├── about │ │ │ ├── about-routing.module.ts │ │ │ ├── about.module.ts │ │ │ ├── components │ │ │ │ ├── footer │ │ │ │ │ ├── footer.component.html │ │ │ │ │ ├── footer.component.scss │ │ │ │ │ ├── footer.component.spec.ts │ │ │ │ │ └── footer.component.ts │ │ │ │ ├── header │ │ │ │ │ ├── header.component.html │ │ │ │ │ ├── header.component.scss │ │ │ │ │ ├── header.component.spec.ts │ │ │ │ │ └── header.component.ts │ │ │ │ └── jumbotron │ │ │ │ │ ├── jumbotron.component.html │ │ │ │ │ ├── jumbotron.component.scss │ │ │ │ │ ├── jumbotron.component.spec.ts │ │ │ │ │ └── jumbotron.component.ts │ │ │ └── pages │ │ │ │ └── about │ │ │ │ ├── about.component.html │ │ │ │ ├── about.component.scss │ │ │ │ └── about.component.ts │ │ └── application │ │ │ ├── application-routing.module.ts │ │ │ ├── application.module.ts │ │ │ ├── components │ │ │ ├── client-color-panels │ │ │ │ ├── client-color-panels.component.html │ │ │ │ ├── client-color-panels.component.scss │ │ │ │ └── client-color-panels.component.ts │ │ │ ├── color-panels │ │ │ │ ├── color-panels.component.html │ │ │ │ ├── color-panels.component.scss │ │ │ │ └── color-panels.component.ts │ │ │ ├── dialog-about │ │ │ │ ├── dialog-about.component.html │ │ │ │ ├── dialog-about.component.scss │ │ │ │ └── dialog-about.component.ts │ │ │ ├── dialog-connection-info │ │ │ │ ├── dialog-connection-info.component.html │ │ │ │ └── dialog-connection-info.component.ts │ │ │ ├── dialog-pictograms │ │ │ │ ├── dialog-pictograms.component.html │ │ │ │ ├── dialog-pictograms.component.scss │ │ │ │ ├── dialog-pictograms.component.spec.ts │ │ │ │ └── dialog-pictograms.component.ts │ │ │ ├── dialog-share │ │ │ │ ├── dialog-share.component.html │ │ │ │ ├── dialog-share.component.scss │ │ │ │ ├── dialog-share.component.ts │ │ │ │ └── qrcode-settings.ts │ │ │ ├── floating-buttons │ │ │ │ ├── floating-buttons.component.html │ │ │ │ ├── floating-buttons.component.scss │ │ │ │ └── floating-buttons.component.ts │ │ │ ├── map │ │ │ │ ├── map.component.html │ │ │ │ ├── map.component.scss │ │ │ │ └── map.component.ts │ │ │ ├── slider-panels │ │ │ │ ├── slider-panels.component.html │ │ │ │ ├── slider-panels.component.scss │ │ │ │ └── slider-panels.component.ts │ │ │ └── toolbar │ │ │ │ ├── toolbar.component.html │ │ │ │ ├── toolbar.component.scss │ │ │ │ ├── toolbar.component.spec.ts │ │ │ │ └── toolbar.component.ts │ │ │ └── pages │ │ │ ├── application │ │ │ ├── application.component.html │ │ │ ├── application.component.scss │ │ │ └── application.component.ts │ │ │ ├── settings │ │ │ ├── settings.component.html │ │ │ ├── settings.component.scss │ │ │ └── settings.component.ts │ │ │ └── shortcuts │ │ │ ├── shortcuts.component.html │ │ │ ├── shortcuts.component.scss │ │ │ └── shortcuts.component.ts │ ├── root-routing.module.ts │ ├── root.component.html │ ├── root.component.scss │ ├── root.component.ts │ ├── root.module.ts │ └── shared │ │ ├── animations │ │ └── route.animation.ts │ │ ├── components │ │ └── .gitkeep │ │ ├── models │ │ ├── cached-map.model.ts │ │ └── settings.model.ts │ │ ├── pipes │ │ ├── .gitkeep │ │ └── inverse-bool.pipe.ts │ │ └── shared.module.ts ├── assets │ ├── data │ │ └── settings.json │ ├── font │ │ ├── firasans │ │ │ ├── FiraSans-Bold.ttf │ │ │ ├── FiraSans-Light.ttf │ │ │ ├── FiraSans-Medium.ttf │ │ │ └── FiraSans-Regular.ttf │ │ └── source-sans-pro │ │ │ ├── OFL.txt │ │ │ ├── SourceSansPro-Black.ttf │ │ │ ├── SourceSansPro-BlackItalic.ttf │ │ │ ├── SourceSansPro-Bold.ttf │ │ │ ├── SourceSansPro-BoldItalic.ttf │ │ │ ├── SourceSansPro-ExtraLight.ttf │ │ │ ├── SourceSansPro-ExtraLightItalic.ttf │ │ │ ├── SourceSansPro-Italic.ttf │ │ │ ├── SourceSansPro-Light.ttf │ │ │ ├── SourceSansPro-LightItalic.ttf │ │ │ ├── SourceSansPro-Regular.ttf │ │ │ ├── SourceSansPro-SemiBold.ttf │ │ │ └── SourceSansPro-SemiBoldItalic.ttf │ ├── i18n │ │ ├── de.json │ │ ├── en.json │ │ ├── es.json │ │ ├── fr.json │ │ ├── it.json │ │ ├── pt-br.json │ │ ├── zh-cn.json │ │ └── zh-tw.json │ ├── icons │ │ ├── icon-128x128.png │ │ ├── icon-192x192.png │ │ ├── icon-72x72.png │ │ └── teammapper.svg │ └── images │ │ ├── business-plan.png │ │ ├── logo_ARASAAC.png │ │ ├── radial-tree.png │ │ ├── readme-header.png │ │ ├── screens.png │ │ └── solar-system.png ├── environments │ ├── environment.prod.ts │ └── environment.ts ├── index.html ├── main.ts ├── polyfills.ts ├── proxy.conf.json ├── robots.txt ├── styles.scss ├── test │ └── mocks │ │ ├── mmp-index.ts │ │ └── mmp-map.ts ├── theme.scss ├── tsconfig.app.json ├── tsconfig.spec.json └── typings.d.ts └── tsconfig.json /.dockerignore: -------------------------------------------------------------------------------- 1 | .dockerignore 2 | .editorconfig 3 | .env* 4 | .git 5 | .github 6 | .gitignore 7 | .prettier* 8 | .vscode 9 | ca/*.crt 10 | ca/*.key 11 | ca/*.pem 12 | ca/*.req 13 | docker-compose* 14 | Dockerfile* 15 | LICENSE 16 | teammapper-backend/client-build 17 | teammapper-backend/dist 18 | teammapper-backend/LICENSE 19 | teammapper-backend/node_modules 20 | teammapper-backend/npm-debug.log 21 | teammapper-backend/README.md 22 | teammapper-frontend/dist 23 | teammapper-frontend/LICENSE 24 | teammapper-frontend/node_modules 25 | teammapper-frontend/npm-debug.log 26 | teammapper-frontend/README.md 27 | README.md -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see https://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 4 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | max_line_length = off 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.env.default: -------------------------------------------------------------------------------- 1 | # Environment Variables DEVELOPMENT 2 | APP_BACKEND_PORT=3000 3 | APP_FRONTEND_PORT=4200 4 | 5 | POSTGRES_DB=teammapper-backend-dev 6 | POSTGRES_PASSWORD=teammapper-password 7 | POSTGRES_PORT=5432 8 | POSTGRES_SSL=false 9 | POSTGRES_USER=teammapper-user 10 | 11 | DOCKER_COMPOSE_APP_ENV_POSTGRES_TEST_DATABASE=teammapper-backend-test 12 | DOCKER_COMPOSE_APP_ENV_POSTGRES_TEST_HOST=postgres 13 | DOCKER_COMPOSE_APP_ENV_POSTGRES_TEST_PASSWORD=teammapper-password 14 | DOCKER_COMPOSE_APP_ENV_POSTGRES_TEST_PORT=5432 15 | DOCKER_COMPOSE_APP_ENV_POSTGRES_TEST_USER=teammapper-user 16 | 17 | # Environment Variables PRODUCTION 18 | APP_PROD_PORT=3011 19 | 20 | POSTGRES_PROD_DB= 21 | POSTGRES_PROD_PASSWORD= 22 | POSTGRES_PROD_PORT=5432 23 | # Enable/disable ssl connection to database 24 | POSTGRES_PROD_SSL=false 25 | # Disabling self-signed ssl certificates by default; please use POSTGRES_PROD_SSL_REJECT_UNAUTHORIZED=true to enable self-singed (unauthorized) ssl certificate. 26 | POSTGRES_PROD_SSL_REJECT_UNAUTHORIZED=false 27 | POSTGRES_PROD_USER= 28 | # PROD Requires SSL Connection Support. Use DEV if not available. 29 | PROD_MODE=PROD 30 | 31 | POSTGRES_QUERY_TIMEOUT=100000 32 | POSTGRES_STATEMENT_TIMEOUT=100000 33 | 34 | DELETE_AFTER_DAYS=30 35 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Proposed feature** 11 | A clear and concise description of what you want to happen. 12 | 13 | **Describe alternatives you've considered** 14 | A clear and concise description of any alternative solutions or features you've considered. 15 | 16 | **Additional context** 17 | Add any other context or screenshots about the feature request here. 18 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "npm" # See documentation for possible values 9 | directory: "/teammapper-backend" # Location of package manifests 10 | schedule: 11 | interval: "weekly" 12 | - package-ecosystem: "npm" # See documentation for possible values 13 | directory: "/teammapper-frontend" # Location of package manifests 14 | schedule: 15 | interval: "weekly" 16 | - package-ecosystem: "docker" # See documentation for possible values 17 | directory: "/" # Location of package manifests 18 | schedule: 19 | interval: "weekly" 20 | - package-ecosystem: "github-actions" # See documentation for possible values 21 | directory: "/" # Location of package manifests 22 | schedule: 23 | interval: "weekly" 24 | 25 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | # https://docs.github.com/en/packages/managing-github-packages-using-github-actions-workflows/publishing-and-installing-a-package-with-github-actions#upgrading-a-workflow-that-accesses-ghcrio 2 | 3 | # This workflow uses actions that are not certified by GitHub. 4 | # They are provided by a third-party and are governed by 5 | # separate terms of service, privacy policy, and support 6 | # documentation. 7 | 8 | # GitHub recommends pinning actions to a commit SHA. 9 | # To get a newer version, you will need to update the SHA. 10 | # You can also reference a tag or branch, but the action may change without warning. 11 | 12 | name: Create and publish a Docker image 13 | 14 | on: 15 | release: 16 | types: [published] 17 | 18 | env: 19 | REGISTRY: ghcr.io 20 | IMAGE_NAME: ${{ github.repository }} 21 | 22 | jobs: 23 | build-and-push-image: 24 | runs-on: ubuntu-latest 25 | permissions: 26 | contents: read 27 | packages: write 28 | 29 | steps: 30 | - name: Checkout repository 31 | uses: actions/checkout@v4 32 | - name: Set up QEMU 33 | uses: docker/setup-qemu-action@v3 34 | - name: Set up Docker Buildx 35 | uses: docker/setup-buildx-action@v3 36 | - name: Log in to the Container registry 37 | uses: docker/login-action@v3 38 | with: 39 | registry: ${{ env.REGISTRY }} 40 | username: ${{ github.actor }} 41 | password: ${{ secrets.GITHUB_TOKEN }} 42 | 43 | - name: Extract metadata (tags, labels) for Docker 44 | id: meta 45 | uses: docker/metadata-action@v5 46 | with: 47 | images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} 48 | 49 | - name: Build and push Docker image 50 | uses: docker/build-push-action@v6 51 | with: 52 | platforms: linux/amd64,linux/arm64/v8 53 | target: production 54 | push: true 55 | tags: ${{ steps.meta.outputs.tags }} 56 | labels: ${{ steps.meta.outputs.labels }} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist 5 | /tmp 6 | /out-tsc 7 | /builds 8 | /documentation 9 | 10 | # dependencies 11 | /node_modules 12 | 13 | # IDEs and editors 14 | /.idea 15 | .project 16 | .classpath 17 | .c9/ 18 | *.launch 19 | .settings/ 20 | *.sublime-workspace 21 | 22 | # IDE - VSCode 23 | .vscode/* 24 | !.vscode/settings.json 25 | !.vscode/tasks.json 26 | !.vscode/launch.json 27 | !.vscode/extensions.json 28 | 29 | # misc 30 | /.sass-cache 31 | /connect.lock 32 | /coverage 33 | /libpeerconnection.log 34 | npm-debug.log 35 | testem.log 36 | /typings 37 | 38 | # e2e 39 | /e2e/*.js 40 | /e2e/*.map 41 | 42 | # System Files 43 | .DS_Store 44 | Thumbs.db 45 | docker-compose.override.yml 46 | 47 | # Environment vars 48 | .env 49 | .env.prod 50 | 51 | ca/*.crt 52 | ca/*.pem 53 | ca/*.key 54 | ca/*.req 55 | 56 | .env.prod -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | **/* 2 | !README.md 3 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:22-alpine3.19 as base 2 | 3 | RUN apk add --no-cache postgresql-client make g++ python3 py3-pip 4 | 5 | # Ensuring that all npm packages and commands are executed with a non-root user 6 | USER node 7 | 8 | ENV APP_PATH=/home/node/app 9 | ENV APP_BACKEND_PATH=${APP_PATH}/teammapper-backend 10 | ENV APP_FRONTEND_PATH=${APP_PATH}/teammapper-frontend 11 | 12 | RUN mkdir -p $APP_PATH 13 | WORKDIR $APP_PATH 14 | 15 | FROM base as production 16 | USER node 17 | 18 | COPY --chown=node:node teammapper-backend/package.json teammapper-backend/package-lock.json $APP_BACKEND_PATH/ 19 | RUN npm --prefix teammapper-backend ci 20 | 21 | COPY --chown=node:node teammapper-frontend/package.json teammapper-frontend/package-lock.json $APP_FRONTEND_PATH/ 22 | RUN npm --prefix teammapper-frontend ci 23 | 24 | COPY --chown=node:node package.json $APP_PATH/ 25 | 26 | COPY --chown=node:node teammapper-backend $APP_BACKEND_PATH/ 27 | RUN npm run build:backend:prod 28 | 29 | COPY --chown=node:node teammapper-frontend $APP_FRONTEND_PATH/ 30 | RUN GENERATE_SOURCEMAP=false npm run build:frontend:prod 31 | 32 | RUN mv $APP_FRONTEND_PATH/dist $APP_BACKEND_PATH/client 33 | 34 | COPY --chown=node:node entrypoint.prod.sh $APP_PATH/ 35 | CMD ["./entrypoint.prod.sh"] 36 | 37 | FROM base as development 38 | USER root 39 | RUN apk add --no-cache chromium 40 | USER node 41 | 42 | ENV CHROME_BIN=/usr/bin/chromium-browser 43 | 44 | COPY --chown=node:node teammapper-frontend/package.json teammapper-frontend/package-lock.json $APP_FRONTEND_PATH/ 45 | RUN npm --prefix teammapper-frontend install 46 | 47 | COPY --chown=node:node teammapper-backend/package.json teammapper-backend/package-lock.json $APP_BACKEND_PATH/ 48 | RUN npm --prefix teammapper-backend install 49 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2019 Omar Desogus 4 | 5 | Copyright (c) 2021 b310 digital gmbh 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 8 | 9 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 10 | 11 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /docker-compose-prod.yml: -------------------------------------------------------------------------------- 1 | version: "3.8" 2 | 3 | services: 4 | app_prod: 5 | tty: true 6 | stdin_open: true 7 | build: 8 | context: . 9 | dockerfile: Dockerfile 10 | target: production 11 | environment: 12 | MODE: ${PROD_MODE:-DEV} 13 | BINDING: "0.0.0.0" 14 | POSTGRES_DATABASE: ${POSTGRES_PROD_DB} 15 | POSTGRES_HOST: ${POSTGRES_PROD_HOST:-postgres_prod} 16 | POSTGRES_PASSWORD: ${POSTGRES_PROD_PASSWORD} 17 | POSTGRES_PORT: ${POSTGRES_PROD_PROD_PORT:-5432} 18 | POSTGRES_SSL: ${POSTGRES_PROD_SSL:-true} 19 | POSTGRES_SSL_REJECT_UNAUTHORIZED: ${POSTGRES_PROD_SSL_REJECT_UNAUTHORIZED:-false} 20 | POSTGRES_USER: ${POSTGRES_PROD_USER:-teammapper-user} 21 | POSTGRES_QUERY_TIMEOUT: ${POSTGRES_QUERY_TIMEOUT:-100000} 22 | POSTGRES_STATEMENT_TIMEOUT: ${POSTGRES_STATEMENT_TIMEOUT:-100000} 23 | DELETE_AFTER_DAYS: ${DELETE_AFTER_DAYS:-30} 24 | ports: 25 | - "${APP_PROD_PORT:-80}:3000" 26 | depends_on: 27 | - postgres_prod 28 | 29 | postgres_prod: 30 | image: postgres:15-alpine 31 | # Pass config parameters to the postgres server. 32 | # Find more information below when you need to generate the ssl-relevant file your self 33 | # command: -c ssl=on -c ssl_cert_file=/var/lib/postgresql/server.crt -c ssl_key_file=/var/lib/postgresql/server.key 34 | environment: 35 | PGDATA: /var/lib/postgresql/data/pgdata 36 | POSTGRES_DB: ${POSTGRES_PROD_DB} 37 | POSTGRES_PASSWORD: ${POSTGRES_PROD_PASSWORD} 38 | POSTGRES_PORT: ${POSTGRES_PROD_PORT:-5432} 39 | POSTGRES_USER: ${POSTGRES_PROD_USER} 40 | volumes: 41 | # To setup an ssl-enabled postgres server locally, you need to generate a self-signed ssl certificate. 42 | # See README.md for more information. 43 | # Mount the ssl_cert_file and ssl_key_file into the docker container. 44 | - ./ca/server.crt:/var/lib/postgresql/server.crt 45 | - ./ca/server.key:/var/lib/postgresql/server.key 46 | - postgres_prod_data:/var/lib/postgresql/data/pgdata 47 | 48 | volumes: 49 | postgres_prod_data: 50 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.8" 2 | 3 | services: 4 | app: 5 | build: 6 | context: . 7 | dockerfile: Dockerfile 8 | target: development 9 | tty: true 10 | stdin_open: true 11 | environment: 12 | MODE: DEV 13 | BINDING: "0.0.0.0" 14 | POSTGRES_DATABASE: ${POSTGRES_DB:-teammapper-backend-dev} 15 | POSTGRES_HOST: ${POSTGRES_HOST:-postgres} 16 | POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-teammapper-password} 17 | POSTGRES_PORT: ${POSTGRES_PORT:-5432} 18 | POSTGRES_USER: ${POSTGRES_USER:-teammapper-user} 19 | POSTGRES_SSL: ${POSTGRES_SSL:-false} 20 | POSTGRES_QUERY_TIMEOUT: ${POSTGRES_QUERY_TIMEOUT:-100000} 21 | POSTGRES_STATEMENT_TIMEOUT: ${POSTGRES_STATEMENT_TIMEOUT:-100000} 22 | POSTGRES_TEST_DATABASE: ${DOCKER_COMPOSE_APP_ENV_POSTGRES_TEST_DATABASE:-teammapper-backend-test} 23 | POSTGRES_TEST_HOST: ${DOCKER_COMPOSE_APP_ENV_POSTGRES_HOST:-postgres} 24 | POSTGRES_TEST_PASSWORD: ${DOCKER_COMPOSE_APP_ENV_POSTGRES_PASSWORD:-teammapper-password} 25 | POSTGRES_TEST_PORT: ${DOCKER_COMPOSE_APP_ENV_POSTGRES_PORT:-5432} 26 | POSTGRES_TEST_USER: ${DOCKER_COMPOSE_APP_ENV_POSTGRES_USER:-teammapper-user} 27 | DELETE_AFTER_DAYS: ${DELETE_AFTER_DAYS:-30} 28 | ports: 29 | - "${APP_FRONTEND_PORT:-4200}:4200" 30 | - "${APP_BACKEND_PORT:-3000}:3000" 31 | # karma server 32 | - 9876:9876 33 | volumes: 34 | - .:/home/node/app 35 | - app_backend_node_modules:/home/node/app/teammapper-backend/node_modules 36 | - app_frontend_node_modules:/home/node/app/teammapper-frontend/node_modules 37 | depends_on: 38 | - postgres 39 | 40 | postgres: 41 | image: postgres:15-alpine 42 | environment: 43 | PGDATA: /var/lib/postgresql/data/pgdata 44 | POSTGRES_DB: ${POSTGRES_DB:-teammapper-backend-dev} 45 | POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-teammapper-password} 46 | POSTGRES_USER: ${POSTGRES_USER:-teammapper-user} 47 | # Exposing the port is not needed unless you want to access this database instance from the host. 48 | # Be careful when other postgres docker container are running on the same port 49 | ports: 50 | - "${POSTGRES_PORT:-5432}:5432" 51 | volumes: 52 | - postgres_data:/var/lib/postgresql/data/pgdata 53 | 54 | volumes: 55 | postgres_data: 56 | app_backend_node_modules: 57 | app_frontend_node_modules: 58 | -------------------------------------------------------------------------------- /docs/deployment.md: -------------------------------------------------------------------------------- 1 | # Deployment 2 | ## Behind a Reverse Proxy 3 | 4 | TeamMapper supports deployment behind a reverse proxy. Here's an example configuration using nginx with docker compose, though other reverse proxy setups will work as well. Add these lines to your docker-compose.yml: 5 | 6 | ``` 7 | nginx: 8 | image: nginx:latest 9 | ports: 10 | - 80:80 11 | volumes: 12 | - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro 13 | ``` 14 | 15 | Then create a `nginx.conf` file in the folder `nginx`, with one of the following setups. 16 | 17 | ### Reverse Proxy using a Subdomain 18 | ``` 19 | server { 20 | listen 80; 21 | listen [::]:80; 22 | server_name teammapper.lan.DOMAIN.tld; 23 | 24 | return 301 https://$host$request_uri; 25 | } 26 | server { 27 | listen 443 ssl; 28 | listen [::]:443 ssl; 29 | server_name teammapper.lan.DOMAIN.tld; 30 | 31 | # certificates for ssl, change according to your setup 32 | # ssl_certificate /etc/letsencrypt/live/lan.DOMAIN.tld/fullchain.pem; 33 | # ssl_certificate_key /etc/letsencrypt/live/lan.DOMAIN.tld/privkey.pem; 34 | 35 | location / { 36 | proxy_pass http://teammapper:3000/; 37 | proxy_http_version 1.1; 38 | proxy_set_header Host $http_host; 39 | proxy_set_header X-Real-IP $remote_addr; 40 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 41 | proxy_set_header X-Forwarded-Proto $scheme; 42 | proxy_set_header Access-Control-Allow-Origin *; 43 | proxy_set_header Upgrade $http_upgrade; 44 | proxy_set_header Connection "Upgrade"; 45 | } 46 | } 47 | ``` 48 | 49 | ### Reverse Proxy using a Path 50 | When hosting TeamMapper under a path (e.g. /teammapper/), the `` tag in the HTML needs to be updated to match that path. This ensures TeamMapper can correctly resolve relative URLs for assets, API calls, and navigation. For nginx, this can be achieved using the `sub_filter` directive to dynamically replace the base href. For Apache, the equivalent functionality is provided by the `mod_substitute` module. 51 | 52 | ``` 53 | events { 54 | worker_connections 1024; 55 | } 56 | 57 | http { 58 | server { 59 | listen 80; 60 | server_name localhost; 61 | 62 | location /teammapper/ { 63 | proxy_pass http://teammapper:3000/; 64 | sub_filter '' ''; 65 | sub_filter_types text/html; 66 | sub_filter_once on; 67 | 68 | # Add proper proxy headers 69 | proxy_set_header Host $host; 70 | proxy_set_header X-Real-IP $remote_addr; 71 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 72 | proxy_set_header X-Forwarded-Proto $scheme; 73 | 74 | # WebSocket support 75 | proxy_http_version 1.1; 76 | proxy_set_header Upgrade $http_upgrade; 77 | proxy_set_header Connection "upgrade"; 78 | } 79 | } 80 | } 81 | ``` -------------------------------------------------------------------------------- /docs/teammapper-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/b310-digital/teammapper/92e2d955a816b7e181ca9e2b77dcaaeb19d3e870/docs/teammapper-logo.png -------------------------------------------------------------------------------- /docs/teammapper-screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/b310-digital/teammapper/92e2d955a816b7e181ca9e2b77dcaaeb19d3e870/docs/teammapper-screenshot.png -------------------------------------------------------------------------------- /entrypoint.prod.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | echo "Looking for the database ..." 4 | while ! pg_isready -q -h $POSTGRES_HOST -p $POSTGRES_PORT -U $POSTGRES_USER 5 | do 6 | echo "Waiting for database." 7 | sleep 2 8 | done 9 | echo "Found database." 10 | echo "Starting the application..." 11 | 12 | npm --prefix teammapper-backend run prod:typeorm:migrate 13 | npm run start:prod -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "teammapper", 3 | "version": "0.0.4", 4 | "description": "Web application to create and work on mindmaps with multiple users.", 5 | "author": "b310 digital gmbh", 6 | "private": false, 7 | "license": "MIT", 8 | "engines": { 9 | "npm": "~10.7.0", 10 | "node": "~22.2" 11 | }, 12 | "scripts": { 13 | "build:prod": "npm run build:frontend:prod && cp teammapper-frontend/dist -TRv teammapper-backend/client && npm run build:backend:prod", 14 | "prepare:prod": "npm --prefix teammapper-backend ci && npm --prefix teammapper-frontend ci", 15 | "build:backend:prod": "npm --prefix teammapper-backend run build", 16 | "build:frontend:prod": "GENERATE_SOURCEMAP=false npm --prefix teammapper-frontend run build:prod", 17 | "clear": "rm teammapper-backend/dist teammapper-backend/client teammapper-frontend/dist", 18 | "migrate:prod": "npm --prefix teammapper-backend run prod:typeorm:migrate", 19 | "start:prod": "npm --prefix teammapper-backend run start:prod", 20 | "build": "npm run prepare:prod && npm run build:prod && npm run migrate:prod", 21 | "start": "npm run start:prod" 22 | } 23 | } -------------------------------------------------------------------------------- /teammapper-backend/.env.default: -------------------------------------------------------------------------------- 1 | POSTGRES_PORT=5432 2 | POSTGRES_HOST=localhost 3 | POSTGRES_USER= 4 | POSTGRES_PASSWORD= 5 | POSTGRES_DATABASE= 6 | MODE=DEV 7 | 8 | ## Test 9 | POSTGRES_TEST_HOST= 10 | POSTGRES_TEST_PORT=5432 11 | POSTGRES_TEST_USER= 12 | POSTGRES_TEST_PASSWORD= 13 | POSTGRES_TEST_DATABASE= 14 | 15 | DELETE_AFTER_DAYS= 16 | -------------------------------------------------------------------------------- /teammapper-backend/.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist 5 | /tmp 6 | /out-tsc 7 | /builds 8 | /documentation 9 | 10 | # dependencies 11 | /node_modules 12 | 13 | # IDEs and editors 14 | /.idea 15 | .project 16 | .classpath 17 | .c9/ 18 | *.launch 19 | .settings/ 20 | *.sublime-workspace 21 | 22 | # IDE - VSCode 23 | .vscode/* 24 | !.vscode/settings.json 25 | !.vscode/tasks.json 26 | !.vscode/launch.json 27 | !.vscode/extensions.json 28 | 29 | # misc 30 | /.sass-cache 31 | /connect.lock 32 | /coverage 33 | /libpeerconnection.log 34 | npm-debug.log 35 | testem.log 36 | /typings 37 | 38 | # e2e 39 | /e2e/*.js 40 | /e2e/*.map 41 | 42 | # System Files 43 | .DS_Store 44 | Thumbs.db 45 | 46 | .env 47 | 48 | # We serve the frontend from this folder. 49 | # During production build this directory will be filled with the production-ready frontend code. 50 | client 51 | -------------------------------------------------------------------------------- /teammapper-backend/.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "es5", 3 | "tabWidth": 2, 4 | "semi": false, 5 | "singleQuote": true 6 | } -------------------------------------------------------------------------------- /teammapper-backend/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 b310 digital gmbh 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /teammapper-backend/README.md: -------------------------------------------------------------------------------- 1 | # Backend for TeamMapper 2 | 3 | ## Description 4 | 5 | Backend for TeamMapper, build with [Nest](https://github.com/nestjs/nest) 6 | 7 | ## Installation 8 | 9 | - Install dependencies 10 | ```bash 11 | $ npm install 12 | ``` 13 | 14 | - Duplicate and rename `.env.default` 15 | 16 | ```bash 17 | cp .env.default .env 18 | ``` 19 | 20 | Change variables according to your preference 21 | 22 | ## Running the app 23 | 24 | ```bash 25 | # development 26 | $ npm run start 27 | 28 | # watch mode 29 | $ npm run start:dev 30 | 31 | # start backend and frontend at the same time 32 | # frontend is accessible via localhost:4200 33 | $ npm run dev 34 | 35 | # production mode 36 | $ npm run start:prod 37 | ``` 38 | 39 | ## Test 40 | 41 | ```bash 42 | # unit tests 43 | $ npm run test 44 | 45 | # e2e tests 46 | $ npm run test:e2e 47 | 48 | # test coverage 49 | $ npm run test:cov 50 | ``` 51 | 52 | ## Typeorm 53 | For a list of commands check https://github.com/typeorm/typeorm/blob/master/docs/using-cli.md 54 | 55 | Some useful commands include: 56 | 57 | Drop schema 58 | 59 | ```bash 60 | npm run dev:typeorm schema:drop 61 | ``` 62 | 63 | For development, sync db structure 64 | 65 | ```bash 66 | npm run dev:typeorm schema:sync 67 | ``` 68 | 69 | For pruction environments, run migrations, see https://github.com/typeorm/typeorm/blob/master/docs/migrations.md 70 | 71 | ```bash 72 | npm run prod:typeorm:migrate 73 | ``` 74 | 75 | or run migrations on dev: 76 | 77 | ```bash 78 | npm run dev:typeorm migration:run 79 | ``` 80 | 81 | Generate new migration based on current changes 82 | 83 | ```bash 84 | npm run dev:typeorm migration:generate -n AddSomethingHere 85 | ``` 86 | 87 | ## License 88 | 89 | Nest and teammapper are [MIT licensed](LICENSE). 90 | -------------------------------------------------------------------------------- /teammapper-backend/eslint.config.cjs: -------------------------------------------------------------------------------- 1 | const { fixupConfigRules, fixupPluginRules } = require("@eslint/compat"); 2 | const tseslint = require("typescript-eslint"); 3 | const jest = require("eslint-plugin-jest"); 4 | const importPlugin = require("eslint-plugin-import"); 5 | const globals = require("globals"); 6 | const tsParser = require("@typescript-eslint/parser"); 7 | const js = require("@eslint/js"); 8 | const prettier = require("eslint-plugin-prettier"); 9 | const stylisticTs = require("@stylistic/eslint-plugin-ts"); 10 | 11 | const { FlatCompat } = require("@eslint/eslintrc"); 12 | 13 | const compat = new FlatCompat({ 14 | baseDirectory: __dirname, 15 | recommendedConfig: js.configs.recommended, 16 | allConfig: js.configs.all 17 | }); 18 | 19 | module.exports = [ 20 | { 21 | ignores: ["src/migrations/**/*", "dist/**/*", "src/jobs/**/*"], 22 | }, 23 | js.configs.recommended, 24 | importPlugin.flatConfigs.recommended, 25 | ...tseslint.configs.recommended, 26 | { 27 | files: ["**/*.ts"], 28 | plugins: { 29 | jest: jest, 30 | prettier: prettier, 31 | "@stylistic/ts": stylisticTs 32 | }, 33 | languageOptions: { 34 | globals: { 35 | ...jest.environments.globals.globals, 36 | ...globals.node, 37 | }, 38 | parser: tsParser, 39 | parserOptions: { 40 | project: ["tsconfig.json"], 41 | ecmaVersion: 12, 42 | sourceType: "module", 43 | }, 44 | }, 45 | settings: { 46 | "import/resolver": { 47 | typescript: {}, 48 | node: { 49 | extensions: [".js", ".ts"], 50 | moduleDirectory: ["node_modules", "src/"], 51 | }, 52 | }, 53 | }, 54 | rules: { 55 | "@typescript-eslint/no-explicit-any": "warn", 56 | "prettier/prettier": "error", 57 | "no-unused-vars": "off", 58 | "@stylistic/ts/member-delimiter-style": ["error", { 59 | "multiline": { 60 | "delimiter": "none", 61 | "requireLast": true 62 | }, 63 | "singleline": { 64 | "delimiter": "semi", 65 | "requireLast": false 66 | }, 67 | "multilineDetection": "brackets" 68 | }], 69 | "@typescript-eslint/no-unused-vars": ["error", { 70 | argsIgnorePattern: "^_", 71 | }], 72 | "import/extensions": ["error", "ignorePackages", { 73 | js: "never", 74 | ts: "never", 75 | }], 76 | }, 77 | }, 78 | ]; -------------------------------------------------------------------------------- /teammapper-backend/nest-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "collection": "@nestjs/schematics", 3 | "sourceRoot": "src" 4 | } 5 | -------------------------------------------------------------------------------- /teammapper-backend/src/app.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common' 2 | import { ConfigModule } from '@nestjs/config' 3 | import { TypeOrmModule } from '@nestjs/typeorm' 4 | import { ServeStaticModule } from '@nestjs/serve-static' 5 | import { join } from 'path' 6 | import configService from './config.service' 7 | import { MapModule } from './map/map.module' 8 | 9 | @Module({ 10 | imports: [ 11 | ConfigModule.forRoot(), 12 | TypeOrmModule.forRoot(configService.getTypeOrmConfig()), 13 | MapModule, 14 | ServeStaticModule.forRoot({ 15 | rootPath: join(__dirname, '..', 'client', 'browser'), 16 | }), 17 | ], 18 | }) 19 | export default class AppModule {} 20 | -------------------------------------------------------------------------------- /teammapper-backend/src/config.service.ts: -------------------------------------------------------------------------------- 1 | // Taken from https://github.com/GauSim/nestjs-typeorm 2 | 3 | import { DataSourceOptions } from 'typeorm' 4 | import { join } from 'path' 5 | 6 | interface EnvProps { 7 | [k: string]: string | undefined 8 | } 9 | 10 | require('dotenv').config() // eslint-disable-line @typescript-eslint/no-require-imports 11 | 12 | class ConfigService { 13 | private env: EnvProps 14 | 15 | constructor(env: EnvProps) { 16 | this.env = env 17 | } 18 | 19 | private getValue(key: string, throwOnMissing = true): string | undefined { 20 | const value = this.env[key] 21 | if (!value && throwOnMissing) { 22 | throw new Error(`config error - missing env.${key}`) 23 | } 24 | 25 | return value 26 | } 27 | 28 | public ensureValues(keys: string[]) { 29 | keys.forEach((k) => this.getValue(k, true)) 30 | return this 31 | } 32 | 33 | public getPort(): number { 34 | return parseInt(this.getValue('PORT', false) || '3000', 10) 35 | } 36 | 37 | public isProduction() { 38 | const mode = this.getValue('MODE', false) 39 | return mode !== 'DEV' 40 | } 41 | 42 | public deleteAfterDays() { 43 | return parseInt(this.getValue('DELETE_AFTER_DAYS', false) || '30') 44 | } 45 | 46 | public getTypeOrmConfig(): DataSourceOptions { 47 | return { 48 | type: 'postgres', 49 | host: this.getValue('POSTGRES_HOST'), 50 | port: Number(this.getValue('POSTGRES_PORT')), 51 | username: this.getValue('POSTGRES_USER'), 52 | password: this.getValue('POSTGRES_PASSWORD'), 53 | database: this.getValue('POSTGRES_DATABASE'), 54 | 55 | entities: [join(__dirname, '**', '*.entity.{ts,js}')], 56 | 57 | migrationsTableName: 'migration', 58 | migrations: [join(__dirname, 'migrations', '*.{ts,js}')], 59 | 60 | extra: { 61 | query_timeout: this.getValue('POSTGRES_QUERY_TIMEOUT') || 100000, 62 | statement_timeout: 63 | this.getValue('POSTGRES_STATEMENT_TIMEOUT') || 100000, 64 | }, 65 | 66 | synchronize: !this.isProduction(), 67 | 68 | // As reported in https://github.com/brianc/node-postgres/issues/2009, implicit disabling of unauthorized certificates has been deprecated. 69 | // You either need to configure a custom certificate provided by yourself that is signed by an official certification authority, or connections will be refused. 70 | // This behaviour may be disabled by changing rejectUnauthorized: false in the ssl configuration. 71 | // 72 | // See https://www.andronio.me/2020/08/20/connecting-typeorm-to-a-postgres-database-on-heroku/ 73 | // See https://github.com/typeorm/typeorm/issues/278 74 | ssl: this.getValue('POSTGRES_SSL') !== 'false' && { 75 | rejectUnauthorized: 76 | this.getValue('POSTGRES_SSL_REJECT_UNAUTHORIZED') !== 'false', 77 | }, 78 | } 79 | } 80 | } 81 | 82 | const configService = new ConfigService(process.env).ensureValues([ 83 | 'POSTGRES_DATABASE', 84 | 'POSTGRES_HOST', 85 | 'POSTGRES_PASSWORD', 86 | 'POSTGRES_PORT', 87 | 'POSTGRES_USER', 88 | ]) 89 | 90 | export default configService 91 | -------------------------------------------------------------------------------- /teammapper-backend/src/data-source.ts: -------------------------------------------------------------------------------- 1 | import { DataSource } from 'typeorm' 2 | import ConfigService from './config.service' 3 | 4 | const AppDataSource = new DataSource(ConfigService.getTypeOrmConfig()) 5 | 6 | export default AppDataSource 7 | -------------------------------------------------------------------------------- /teammapper-backend/src/filters/global-exception.filter.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ExceptionFilter, 3 | Catch, 4 | ArgumentsHost, 5 | Logger, 6 | NotFoundException, 7 | } from '@nestjs/common' 8 | 9 | // This is for any unhandled gateway and "internal" NestJS related errors - like if the gateway can't reach clients or things like that. 10 | // It will try to always keep clients and their websockets alive and gracefully send errors over the wire, without revealing internal error reasons. 11 | @Catch() 12 | export class GlobalExceptionFilter implements ExceptionFilter { 13 | private readonly logger = new Logger(GlobalExceptionFilter.name) 14 | 15 | catch(exception: unknown, host: ArgumentsHost) { 16 | const ctx = host.getType() 17 | 18 | // Skip logging for NotFoundException in HTTP context 19 | // This is handled before anything else (and explicitly outside of ctx switch) to prevent _any_ error from logging 20 | if (ctx === 'http' && exception instanceof NotFoundException) { 21 | const response = host.switchToHttp().getResponse() 22 | return response.status(404).json({ 23 | statusCode: 404, 24 | message: 'Not Found', 25 | timestamp: new Date().toISOString(), 26 | }) 27 | } 28 | 29 | const errorDetails = { 30 | error: exception, 31 | type: exception?.constructor?.name || typeof exception, 32 | message: exception instanceof Error ? exception.message : 'Unknown error', 33 | stack: exception instanceof Error ? exception.stack : undefined, 34 | context: ctx, 35 | } 36 | 37 | this.logger.error(errorDetails) 38 | 39 | try { 40 | switch (ctx) { 41 | case 'http': { 42 | const response = host.switchToHttp().getResponse() 43 | return response.status(500).json({ 44 | statusCode: 500, 45 | message: 'Internal server error', 46 | timestamp: new Date().toISOString(), 47 | }) 48 | } 49 | 50 | case 'ws': { 51 | const client = host.switchToWs().getClient() 52 | const error = { 53 | event: 'error', 54 | data: { 55 | message: 'Internal server error', 56 | timestamp: new Date().toISOString(), 57 | }, 58 | } 59 | 60 | if (typeof client.emit === 'function') { 61 | client.emit('error', error) 62 | } else if (typeof client.send === 'function') { 63 | client.send(JSON.stringify(error)) 64 | } 65 | break 66 | } 67 | 68 | default: { 69 | // Handle any runtime errors outside HTTP/WS contexts 70 | this.logger.error(`Unhandled exception type: ${ctx}`) 71 | // Forward to the global error handler 72 | if (exception instanceof Error) { 73 | process.emitWarning(exception) 74 | } else { 75 | process.emitWarning(new Error(String(exception)), 'UnhandledError') 76 | } 77 | } 78 | } 79 | } catch (handlerError) { 80 | // If the error handler itself fails, log it and emit to process 81 | this.logger.error('Global exception handler failed: ', handlerError) 82 | if (handlerError instanceof Error) { 83 | process.emitWarning(handlerError) 84 | } else { 85 | process.emitWarning(new Error(String(handlerError)), 'HandlerError') 86 | } 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /teammapper-backend/src/jobs/deleteMaps.job.ts: -------------------------------------------------------------------------------- 1 | import { NestFactory } from '@nestjs/core' 2 | import { MapsService } from '../map/services/maps.service' 3 | import AppModule from '../app.module' 4 | import { Logger } from '@nestjs/common' 5 | import configService from '../config.service' 6 | 7 | async function bootstrap() { 8 | const application = await NestFactory.createApplicationContext(AppModule) 9 | 10 | const logger = new Logger('TaskRunner') 11 | const mapsService = application.get(MapsService) 12 | 13 | logger.log('--- Deleting old maps ... ---') 14 | const result = await mapsService.deleteOutdatedMaps( 15 | configService.deleteAfterDays() 16 | ) 17 | logger.log('Deleted rows: ' + result) 18 | logger.log('--- Finished deleting maps ---') 19 | 20 | await application.close() 21 | process.exit(0) 22 | } 23 | 24 | bootstrap() 25 | -------------------------------------------------------------------------------- /teammapper-backend/src/jobs/seedMapData.job.ts: -------------------------------------------------------------------------------- 1 | import { NestFactory } from '@nestjs/core' 2 | import { MapsService } from '../map/services/maps.service' 3 | import AppModule from '../app.module' 4 | import { Logger } from '@nestjs/common' 5 | import { IMmpClientMap, IMmpClientNode } from 'src/map/types' 6 | import * as crypto from 'crypto' 7 | 8 | const createNode: any = ( 9 | isRoot: boolean, 10 | parentId: string, 11 | x: number, 12 | y: number 13 | ) => { 14 | return { 15 | colors: { 16 | name: '#000000', 17 | background: '#FF0000', 18 | branch: '#000000', 19 | }, 20 | coordinates: { x: x, y: y }, 21 | font: { 22 | style: '', 23 | size: 10, 24 | weight: 'bold', 25 | }, 26 | id: crypto.randomUUID(), 27 | image: { 28 | src: '', 29 | size: 0, 30 | }, 31 | k: 0, 32 | locked: false, 33 | name: 'Seed Data', 34 | parent: parentId, 35 | isRoot: isRoot, 36 | } 37 | } 38 | 39 | const createMap = (nodes: IMmpClientNode[]): IMmpClientMap => { 40 | return { 41 | uuid: crypto.randomUUID(), 42 | lastModified: new Date(), 43 | lastAccessed: new Date(), 44 | createdAt: new Date(), 45 | deleteAfterDays: 30, 46 | data: nodes, 47 | deletedAt: new Date(), 48 | options: { fontMaxSize: 10, fontIncrement: 5, fontMinSize: 10 }, 49 | } 50 | } 51 | 52 | async function bootstrap() { 53 | const application = await NestFactory.createApplicationContext(AppModule) 54 | 55 | const logger = new Logger('TaskRunner') 56 | const mapsService = application.get(MapsService) 57 | 58 | logger.log('--- Creating map ... ---') 59 | 60 | const rootNode: IMmpClientNode = createNode(true, '', 0, 0) 61 | const childNode: IMmpClientNode = createNode(false, rootNode.id, 150, 150) 62 | const secondChildNode: IMmpClientNode = createNode( 63 | false, 64 | childNode.id, 65 | 250, 66 | 250 67 | ) 68 | const thirdChildNode: IMmpClientNode = createNode( 69 | false, 70 | childNode.id, 71 | 350, 72 | 350 73 | ) 74 | const fourthChildNode: IMmpClientNode = createNode( 75 | false, 76 | childNode.id, 77 | 450, 78 | 450 79 | ) 80 | const fifthChildNode: IMmpClientNode = createNode( 81 | false, 82 | childNode.id, 83 | 550, 84 | 550 85 | ) 86 | const mapData: IMmpClientMap = createMap([ 87 | rootNode, 88 | childNode, 89 | secondChildNode, 90 | thirdChildNode, 91 | fourthChildNode, 92 | fifthChildNode, 93 | ]) 94 | await mapsService.updateMap(mapData) 95 | 96 | logger.log(`--- Map created with id ${mapData.uuid} ---`) 97 | 98 | logger.log('--- Finished creating map ---') 99 | 100 | await application.close() 101 | process.exit(0) 102 | } 103 | 104 | bootstrap() 105 | -------------------------------------------------------------------------------- /teammapper-backend/src/main.ts: -------------------------------------------------------------------------------- 1 | import { NestFactory } from '@nestjs/core' 2 | import AppModule from './app.module' 3 | import configService from './config.service' 4 | import { createProxyMiddleware } from 'http-proxy-middleware' 5 | import { GlobalExceptionFilter } from './filters/global-exception.filter' 6 | import { Logger } from '@nestjs/common' 7 | import { NestExpressApplication } from '@nestjs/platform-express' 8 | 9 | async function bootstrap() { 10 | const logger = new Logger('Main Process') 11 | 12 | // Process-level handlers for uncaught errors - anything that happens outside of NestJS, such as type errors. 13 | // This is only logged server-side so we log the whole stack for better review. 14 | process.on('warning', (error) => { 15 | logger.error('Possible uncaught exception: ', error) 16 | }) 17 | 18 | process.on('unhandledRejection', (reason: unknown) => { 19 | const stack = 20 | reason instanceof Error ? reason.stack : 'No stack trace available' 21 | logger.error('Unhandled Rejection. Stack trace: ', stack) 22 | }) 23 | 24 | const app = await NestFactory.create(AppModule, { 25 | logger: ['log', 'error', 'warn', 'debug'], 26 | }) 27 | 28 | app.useGlobalFilters(new GlobalExceptionFilter()) 29 | 30 | app.use( 31 | '/arasaac/api', 32 | createProxyMiddleware({ 33 | target: 'https://api.arasaac.org/api', 34 | changeOrigin: true, 35 | pathRewrite: { 36 | '^/arasaac/api': '/', 37 | }, 38 | }) 39 | ) 40 | 41 | app.use( 42 | '/arasaac/images', 43 | createProxyMiddleware({ 44 | target: 'https://static.arasaac.org/', 45 | changeOrigin: true, 46 | pathRewrite: { 47 | '^/arasaac/images': '/', 48 | }, 49 | }) 50 | ) 51 | 52 | await app.listen(configService.getPort()) 53 | } 54 | bootstrap() 55 | -------------------------------------------------------------------------------- /teammapper-backend/src/map/controllers/maps.controller.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Body, 3 | Controller, 4 | Get, 5 | Delete, 6 | NotFoundException, 7 | Param, 8 | Post, 9 | Logger, 10 | } from '@nestjs/common' 11 | import { MapsService } from '../services/maps.service' 12 | import { 13 | IMmpClientDeleteRequest, 14 | IMmpClientMap, 15 | IMmpClientMapCreateRequest, 16 | IMmpClientPrivateMap, 17 | } from '../types' 18 | import MalformedUUIDError from '../services/uuid.error' 19 | import { EntityNotFoundError } from 'typeorm' 20 | 21 | @Controller('api/maps') 22 | export default class MapsController { 23 | private readonly logger = new Logger(MapsController.name) 24 | constructor(private mapsService: MapsService) {} 25 | 26 | @Get(':id') 27 | async findOne(@Param('id') mapId: string): Promise { 28 | try { 29 | // If we update lastAccessed first, we guarantee that the exportMapToClient returns a fresh map that includes an up-to-date lastAccessed field 30 | await this.mapsService.updateLastAccessed(mapId) 31 | const map = await this.mapsService.exportMapToClient(mapId) 32 | if (!map) throw new NotFoundException() 33 | 34 | return map 35 | } catch (e) { 36 | if (e instanceof MalformedUUIDError || e instanceof EntityNotFoundError) { 37 | throw new NotFoundException() 38 | } else { 39 | throw e 40 | } 41 | } 42 | } 43 | 44 | @Delete(':id') 45 | async delete( 46 | @Param('id') mapId: string, 47 | @Body() body: IMmpClientDeleteRequest 48 | ): Promise { 49 | const mmpMap = await this.mapsService.findMap(mapId) 50 | if (mmpMap && mmpMap.adminId === body.adminId) 51 | this.mapsService.deleteMap(mapId) 52 | } 53 | 54 | @Post() 55 | async create( 56 | @Body() body: IMmpClientMapCreateRequest 57 | ): Promise { 58 | const newMap = await this.mapsService.createEmptyMap(body.rootNode) 59 | const exportedMap = await this.mapsService.exportMapToClient(newMap.id) 60 | 61 | if (exportedMap) { 62 | return { 63 | map: exportedMap, 64 | adminId: newMap.adminId, 65 | modificationSecret: newMap.modificationSecret, 66 | } 67 | } 68 | } 69 | 70 | @Post(':id/duplicate') 71 | async duplicate( 72 | @Param('id') mapId: string 73 | ): Promise { 74 | const oldMap = await this.mapsService.findMap(mapId).catch((e: Error) => { 75 | if (e.name === 'MalformedUUIDError') { 76 | this.logger.warn( 77 | `:id/duplicate(): Wrong/no UUID provided for findMap() with mapId ${mapId}` 78 | ) 79 | return 80 | } 81 | }) 82 | 83 | if (!oldMap) throw new NotFoundException() 84 | 85 | const newMap = await this.mapsService.createEmptyMap() 86 | 87 | const oldNodes = await this.mapsService.findNodes(oldMap.id) 88 | 89 | await this.mapsService.addNodes(newMap.id, oldNodes) 90 | 91 | const exportedMap = await this.mapsService.exportMapToClient(newMap.id) 92 | 93 | if (exportedMap) { 94 | return { 95 | map: exportedMap, 96 | adminId: newMap.adminId, 97 | modificationSecret: newMap.modificationSecret, 98 | } 99 | } 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /teammapper-backend/src/map/entities/mmpMap.entity.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Entity, 3 | Column, 4 | PrimaryGeneratedColumn, 5 | OneToMany, 6 | Generated, 7 | } from 'typeorm' 8 | import { MapOptions } from '../types' 9 | import { MmpNode } from './mmpNode.entity' 10 | 11 | @Entity() 12 | export class MmpMap { 13 | @PrimaryGeneratedColumn('uuid') 14 | id: string 15 | 16 | @Column({ 17 | type: 'timestamptz', 18 | nullable: true, 19 | default: () => 'CURRENT_TIMESTAMP', 20 | }) 21 | lastModified: Date | null 22 | 23 | @Column({ type: 'timestamptz', nullable: true }) 24 | lastAccessed: Date | null 25 | 26 | @Column({ type: 'uuid', nullable: true }) 27 | @Generated('uuid') 28 | adminId: string | null 29 | 30 | @Column({ type: 'uuid', nullable: true, default: null }) 31 | @Generated('uuid') 32 | modificationSecret: string | null 33 | 34 | @Column({ type: 'varchar', nullable: true }) 35 | name: string | null 36 | 37 | @Column('jsonb', { 38 | nullable: false, 39 | default: {}, 40 | }) 41 | options: MapOptions 42 | 43 | @OneToMany(() => MmpNode, (node) => node.nodeMap, { 44 | cascade: true, 45 | }) 46 | nodes: MmpNode[] 47 | 48 | @Column({ 49 | type: 'timestamptz', 50 | nullable: true, 51 | default: () => 'CURRENT_TIMESTAMP', 52 | }) 53 | createdAt: Date | null 54 | } 55 | -------------------------------------------------------------------------------- /teammapper-backend/src/map/entities/mmpNode.entity.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Entity, 3 | Column, 4 | Index, 5 | PrimaryGeneratedColumn, 6 | PrimaryColumn, 7 | ManyToOne, 8 | JoinColumn, 9 | Generated, 10 | OneToMany, 11 | BeforeInsert, 12 | BeforeUpdate, 13 | } from 'typeorm' 14 | import { MmpMap } from './mmpMap.entity' 15 | import { validateOrReject, IsDefined } from 'class-validator' 16 | 17 | @Entity() 18 | export class MmpNode { 19 | @PrimaryGeneratedColumn('uuid') 20 | id: string 21 | 22 | @Column({ type: 'varchar', nullable: true }) 23 | name: string | null 24 | 25 | @ManyToOne(() => MmpMap, (map) => map.nodes, { 26 | onDelete: 'CASCADE', 27 | }) 28 | @JoinColumn() 29 | nodeMap: MmpMap 30 | 31 | @ManyToOne(() => MmpNode, (node) => node.children, { 32 | onDelete: 'CASCADE', 33 | }) 34 | @JoinColumn([ 35 | { name: 'nodeMapId', referencedColumnName: 'nodeMapId' }, 36 | { name: 'nodeParentId', referencedColumnName: 'id' }, 37 | ]) 38 | @Index() 39 | nodeParent: MmpNode 40 | 41 | @OneToMany(() => MmpNode, (node) => node.nodeParent) 42 | children: MmpNode[] 43 | 44 | @Column({ type: 'boolean', default: false }) 45 | @IsDefined() 46 | root: boolean 47 | 48 | @Column({ type: 'float' }) 49 | @IsDefined() 50 | coordinatesX: number 51 | 52 | @Column({ type: 'float' }) 53 | @IsDefined() 54 | coordinatesY: number 55 | 56 | @Column({ type: 'varchar', nullable: true }) 57 | colorsName: string | null 58 | 59 | @Column({ type: 'varchar', nullable: true }) 60 | colorsBackground: string | null 61 | 62 | @Column({ type: 'varchar', nullable: true }) 63 | colorsBranch: string | null 64 | 65 | @Column({ type: 'integer', nullable: true }) 66 | fontSize: number | null 67 | 68 | @Column({ type: 'varchar', nullable: true }) 69 | fontStyle: string | null 70 | 71 | @Column({ type: 'varchar', nullable: true }) 72 | fontWeight: string | null 73 | 74 | @Column({ type: 'varchar', nullable: true }) 75 | imageSrc: string | null 76 | 77 | @Column({ type: 'integer', nullable: true, default: 60 }) 78 | imageSize: number | null 79 | 80 | @Column({ type: 'varchar', nullable: true }) 81 | linkHref: string | null 82 | 83 | @Column({ type: 'boolean', nullable: true }) 84 | locked: boolean | null 85 | 86 | @Column({ type: 'boolean', default: false }) 87 | @IsDefined() 88 | detached: boolean 89 | 90 | @Column({ type: 'float', nullable: true }) 91 | k: number | null 92 | 93 | @PrimaryColumn('uuid') 94 | @Index() 95 | @IsDefined() 96 | nodeMapId: string 97 | 98 | @Column({ type: 'uuid', nullable: true }) 99 | nodeParentId: string | null 100 | 101 | @Column({ type: 'integer' }) 102 | @Generated('increment') 103 | orderNumber: number 104 | 105 | @Column({ 106 | type: 'timestamptz', 107 | nullable: true, 108 | default: () => 'CURRENT_TIMESTAMP', 109 | }) 110 | lastModified: Date | null 111 | 112 | @Column({ 113 | type: 'timestamptz', 114 | nullable: true, 115 | default: () => 'CURRENT_TIMESTAMP', 116 | }) 117 | createdAt: Date | null 118 | 119 | @BeforeInsert() 120 | @BeforeUpdate() 121 | async validate() { 122 | await validateOrReject(this) 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /teammapper-backend/src/map/guards/edit.guard.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test } from '@nestjs/testing' 2 | import { EditGuard } from './edit.guard' 3 | import { MapsService } from '../services/maps.service' 4 | import { MmpMap } from '../entities/mmpMap.entity' 5 | import { createMock } from '@golevelup/ts-jest' 6 | import { ExecutionContext } from '@nestjs/common' 7 | 8 | describe('EditGuard', () => { 9 | let guard: EditGuard 10 | let mapsService: MapsService 11 | 12 | beforeEach(() => { 13 | guard = new EditGuard(mapsService) 14 | }) 15 | 16 | describe('canActivate', () => { 17 | describe('with modificationSecret', () => { 18 | const map: MmpMap = new MmpMap() 19 | map.id = '123' 20 | map.modificationSecret = 'abc' 21 | 22 | beforeAll(async () => { 23 | mapsService = createMock({ 24 | findMap: (_uuid: string) => 25 | new Promise((resolve, _reject) => { 26 | resolve(map) 27 | }), 28 | }) 29 | await Test.createTestingModule({ 30 | providers: [{ provide: MapsService, useValue: mapsService }], 31 | }).compile() 32 | }) 33 | 34 | it('should return true when user provides correct credentials', async () => { 35 | const mockContext = createMock({ 36 | switchToWs: () => ({ 37 | getData: () => ({ 38 | modificationSecret: 'abc', 39 | mapId: '123', 40 | }), 41 | }), 42 | }) 43 | const canActivate = await guard.canActivate(mockContext) 44 | 45 | expect(canActivate).toBe(true) 46 | }) 47 | 48 | it('should return false when user is not provided correct credentials', async () => { 49 | const mockContext = createMock({ 50 | switchToWs: () => ({ 51 | getData: () => ({ 52 | modificationSecret: 'wrong', 53 | mapId: '123', 54 | }), 55 | }), 56 | }) 57 | const canActivate = await guard.canActivate(mockContext) 58 | 59 | expect(canActivate).toBe(false) 60 | }) 61 | }) 62 | 63 | describe('without modificationSecret', () => { 64 | const map: MmpMap = new MmpMap() 65 | map.id = '123' 66 | 67 | beforeAll(async () => { 68 | mapsService = createMock({ 69 | findMap: (_uuid: string) => 70 | new Promise((resolve, _reject) => { 71 | resolve(map) 72 | }), 73 | }) 74 | await Test.createTestingModule({ 75 | providers: [{ provide: MapsService, useValue: mapsService }], 76 | }).compile() 77 | }) 78 | 79 | it('should return true when map has no modification secret', async () => { 80 | const mockContext = createMock({ 81 | switchToWs: () => ({ 82 | getData: () => ({ mapId: '123' }), 83 | }), 84 | }) 85 | const canActivate = await guard.canActivate(mockContext) 86 | 87 | expect(canActivate).toBe(true) 88 | }) 89 | }) 90 | }) 91 | }) 92 | -------------------------------------------------------------------------------- /teammapper-backend/src/map/guards/edit.guard.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common' 2 | import { MapsService } from '../services/maps.service' 3 | import { IMmpClientEditingRequest } from '../types' 4 | 5 | @Injectable() 6 | export class EditGuard implements CanActivate { 7 | constructor(private readonly mapsService: MapsService) {} 8 | 9 | canActivate(context: ExecutionContext): Promise { 10 | const request = context.switchToWs().getData() 11 | return this.validateRequest(request.mapId, request.modificationSecret) 12 | } 13 | 14 | async validateRequest( 15 | mapId: string, 16 | givenModificationSecret: string 17 | ): Promise { 18 | const map = await this.mapsService.findMap(mapId) 19 | if (!map || !map.modificationSecret) return true 20 | 21 | return givenModificationSecret === map.modificationSecret 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /teammapper-backend/src/map/map.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common' 2 | import { CacheModule } from '@nestjs/cache-manager' 3 | import { ScheduleModule } from '@nestjs/schedule' 4 | import { TypeOrmModule } from '@nestjs/typeorm' 5 | import MapsController from './controllers/maps.controller' 6 | import { MapsGateway } from './controllers/maps.gateway' 7 | import { MmpMap } from './entities/mmpMap.entity' 8 | import { MmpNode } from './entities/mmpNode.entity' 9 | import { MapsService } from './services/maps.service' 10 | import { TasksService } from './services/tasks.service' 11 | 12 | @Module({ 13 | imports: [ 14 | TypeOrmModule.forFeature([MmpMap, MmpNode]), 15 | CacheModule.register(), 16 | ScheduleModule.forRoot(), 17 | ], 18 | controllers: [MapsController], 19 | providers: [MapsService, MapsGateway, TasksService], 20 | exports: [MapsService], 21 | }) 22 | export class MapModule {} 23 | -------------------------------------------------------------------------------- /teammapper-backend/src/map/services/tasks.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, Logger } from '@nestjs/common' 2 | import { Cron } from '@nestjs/schedule' 3 | import { MapsService } from './maps.service' 4 | import configService from '../../config.service' 5 | 6 | /* 7 | * This service takes care of _very_ simple job scheduling. 8 | * Note: Jobs might execute multiple times - depending on your deployment and the container count. 9 | * Do not schedule jobs, that can't be executed multiple times, e.g. sending mails. Use a different solution for these kind of tasks. 10 | */ 11 | @Injectable() 12 | export class TasksService { 13 | private readonly logger = new Logger(TasksService.name) 14 | 15 | constructor(private mapsService: MapsService) {} 16 | 17 | // every day midnight 18 | @Cron('0 0 * * *') 19 | async handleCron() { 20 | this.logger.log('--- Deleting old maps ... ---') 21 | const affected = await this.mapsService.deleteOutdatedMaps( 22 | configService.deleteAfterDays() 23 | ) 24 | this.logger.log('Deleted rows: ' + affected) 25 | this.logger.log('--- Finished deleting maps ---') 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /teammapper-backend/src/map/services/uuid.error.ts: -------------------------------------------------------------------------------- 1 | export default class MalformedUUIDError extends Error { 2 | constructor(message: string) { 3 | super(message) 4 | this.name = 'MalformedUUIDError' 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /teammapper-backend/src/map/types.ts: -------------------------------------------------------------------------------- 1 | export interface MapOptions { 2 | fontIncrement: number 3 | fontMaxSize: number 4 | fontMinSize: number 5 | } 6 | 7 | export interface IMmpClientColor { 8 | name: string | null 9 | background: string | null 10 | branch: string | null 11 | } 12 | 13 | export interface IMmpClientCoordinates { 14 | x: number 15 | y: number 16 | } 17 | 18 | export interface IMmpClientFont { 19 | style: string | null 20 | size: number | null 21 | weight: string | null 22 | } 23 | 24 | export interface IMmpClientMap { 25 | uuid: string 26 | lastModified: Date | null 27 | lastAccessed: Date | null 28 | deleteAfterDays: number 29 | deletedAt: Date 30 | data: IMmpClientNode[] 31 | options: IMmpClientMapOptions 32 | createdAt: Date | null 33 | } 34 | 35 | export interface IMmpClientPrivateMap { 36 | map: IMmpClientMap 37 | adminId: string | null 38 | modificationSecret: string | null 39 | } 40 | 41 | export interface IMmpClientNodeBasics { 42 | colors: IMmpClientColor 43 | font: IMmpClientFont 44 | name: string | null 45 | image: { src: string | null; size: number | null } 46 | } 47 | 48 | export interface IMmpClientNode extends IMmpClientNodeBasics { 49 | coordinates: IMmpClientCoordinates 50 | detached: boolean 51 | id: string 52 | k: number 53 | link: { href: string | null } 54 | locked: boolean 55 | parent: string | null 56 | isRoot: boolean 57 | } 58 | 59 | export interface IMmpClientMapOptions { 60 | fontMaxSize: number 61 | fontMinSize: number 62 | fontIncrement: number 63 | } 64 | 65 | export interface IClientCache { 66 | [clientId: string]: string 67 | } 68 | 69 | export interface IMmpClientJoinRequest { 70 | mapId: string 71 | color: string 72 | } 73 | 74 | export interface IMmpClientNodeSelectionRequest { 75 | mapId: string 76 | nodeId: string 77 | selected: boolean 78 | } 79 | 80 | export interface IMmpClientEditingRequest { 81 | modificationSecret: string 82 | mapId: string 83 | } 84 | 85 | export interface IMmpClientNodeRequest extends IMmpClientEditingRequest { 86 | node: IMmpClientNode 87 | updatedProperty: string 88 | } 89 | 90 | export interface IMmpClientNodeAddRequest extends IMmpClientEditingRequest { 91 | nodes: IMmpClientNode[] 92 | } 93 | 94 | export interface IMmpClientUpdateMapOptionsRequest 95 | extends IMmpClientEditingRequest { 96 | options: IMmpClientMapOptions 97 | } 98 | 99 | export interface IMmpClientSnapshotChanges { 100 | [k: string]: Partial | undefined 101 | } 102 | 103 | export interface IMmpClientMapDiff { 104 | added: IMmpClientSnapshotChanges 105 | deleted: IMmpClientSnapshotChanges 106 | updated: IMmpClientSnapshotChanges 107 | } 108 | 109 | export interface IMmpClientMapRequest extends IMmpClientEditingRequest { 110 | map: IMmpClientMap 111 | } 112 | 113 | export interface IMmpClientUndoRedoRequest extends IMmpClientEditingRequest { 114 | diff: IMmpClientMapDiff 115 | } 116 | 117 | export interface IMmpClientMapCreateRequest { 118 | rootNode: IMmpClientNodeBasics 119 | } 120 | 121 | export interface IMmpClientDeleteRequest { 122 | adminId: string | null 123 | mapId: string 124 | } 125 | -------------------------------------------------------------------------------- /teammapper-backend/src/map/utils/tests/mapFactories.ts: -------------------------------------------------------------------------------- 1 | import { MmpMap } from 'src/map/entities/mmpMap.entity' 2 | import { MmpNode } from 'src/map/entities/mmpNode.entity' 3 | import * as crypto from 'crypto' 4 | import { IMmpClientMap, IMmpClientNodeBasics } from 'src/map/types' 5 | 6 | export const createMmpMap = (overrides = {}): MmpMap => ({ 7 | id: crypto.randomUUID(), 8 | adminId: 'admin-id', 9 | modificationSecret: 'modification-secret', 10 | name: 'Test Map', 11 | lastModified: new Date('1970-01-01'), 12 | lastAccessed: new Date('1970-01-01'), 13 | options: { 14 | fontMaxSize: 1, 15 | fontMinSize: 1, 16 | fontIncrement: 1, 17 | }, 18 | createdAt: new Date('1970-01-01'), 19 | nodes: Array(), 20 | ...overrides, 21 | }) 22 | 23 | export const createMmpClientMap = (overrides = {}): IMmpClientMap => ({ 24 | uuid: crypto.randomUUID(), 25 | data: [], 26 | deleteAfterDays: 30, 27 | deletedAt: new Date('1970-01-01'), 28 | lastModified: new Date('1970-01-01'), 29 | lastAccessed: new Date('1970-01-01'), 30 | options: { 31 | fontMaxSize: 1, 32 | fontMinSize: 1, 33 | fontIncrement: 1, 34 | }, 35 | createdAt: new Date('1970-01-01'), 36 | ...overrides, 37 | }) 38 | 39 | export const createClientRootNode = (overrides = {}): IMmpClientNodeBasics => ({ 40 | colors: { 41 | name: '', 42 | background: '', 43 | branch: '', 44 | }, 45 | font: { 46 | style: '', 47 | size: 0, 48 | weight: '', 49 | }, 50 | name: 'Root node', 51 | image: { 52 | src: '', 53 | size: 0, 54 | }, 55 | ...overrides, 56 | }) 57 | -------------------------------------------------------------------------------- /teammapper-backend/src/migrations/1638048135450-CreateMapsAndNodes.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner } from 'typeorm' 2 | 3 | export class CreateMapsAndNodes1638048135450 implements MigrationInterface { 4 | name = 'CreateMapsAndNodes1638048135450' 5 | 6 | public async up(queryRunner: QueryRunner): Promise { 7 | await queryRunner.query( 8 | 'CREATE TABLE "mmp_node" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "name" character varying, "root" boolean NOT NULL DEFAULT false, "coordinatesX" double precision NOT NULL, "coordinatesY" double precision NOT NULL, "colorsName" character varying, "colorsBackground" character varying, "colorsBranch" character varying, "fontSize" integer, "fontStyle" character varying, "fontWeight" character varying, "imageSrc" character varying, "imageSize" integer DEFAULT \'60\', "locked" boolean, "k" double precision, "nodeMapId" uuid, "nodeParentId" uuid, "orderNumber" SERIAL, CONSTRAINT "PK_70f65b529c3e785462fb35c05cf" PRIMARY KEY ("id"))' 9 | ) 10 | await queryRunner.query( 11 | 'CREATE TABLE "mmp_map" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "lastModified" TIMESTAMP WITH TIME ZONE, "name" character varying, CONSTRAINT "PK_a76c9b36fe9df0143f9870ad139" PRIMARY KEY ("id"))' 12 | ) 13 | await queryRunner.query( 14 | 'ALTER TABLE "mmp_node" ADD CONSTRAINT "FK_19c5208da416d32ea491315716e" FOREIGN KEY ("nodeMapId") REFERENCES "mmp_map"("id") ON DELETE CASCADE ON UPDATE NO ACTION' 15 | ) 16 | await queryRunner.query( 17 | 'ALTER TABLE "mmp_node" ADD CONSTRAINT "FK_9919032f542cee8cab97749e1a5" FOREIGN KEY ("nodeParentId") REFERENCES "mmp_node"("id") ON DELETE CASCADE ON UPDATE NO ACTION' 18 | ) 19 | } 20 | 21 | public async down(queryRunner: QueryRunner): Promise { 22 | await queryRunner.query( 23 | 'ALTER TABLE "mmp_node" DROP CONSTRAINT "FK_9919032f542cee8cab97749e1a5"' 24 | ) 25 | await queryRunner.query( 26 | 'ALTER TABLE "mmp_node" DROP CONSTRAINT "FK_19c5208da416d32ea491315716e"' 27 | ) 28 | await queryRunner.query('DROP TABLE "mmp_map"') 29 | await queryRunner.query('DROP TABLE "mmp_node"') 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /teammapper-backend/src/migrations/1640704269037-AddDefaultTimestampToMaps.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner } from 'typeorm' 2 | 3 | export class AddDefaultTimestampToMaps1640704269037 4 | implements MigrationInterface 5 | { 6 | name = 'AddDefaultTimestampToMaps1640704269037' 7 | 8 | public async up(queryRunner: QueryRunner): Promise { 9 | await queryRunner.query( 10 | 'ALTER TABLE "public"."mmp_map" ALTER COLUMN "lastModified" SET DEFAULT now()' 11 | ) 12 | } 13 | 14 | public async down(queryRunner: QueryRunner): Promise { 15 | await queryRunner.query( 16 | 'ALTER TABLE "public"."mmp_map" ALTER COLUMN "lastModified" DROP DEFAULT' 17 | ) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /teammapper-backend/src/migrations/1640939564906-AddAdminIdForMaps.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner } from 'typeorm' 2 | 3 | export class AddAdminIdForMaps1640939564906 implements MigrationInterface { 4 | name = 'AddAdminIdForMaps1640939564906' 5 | 6 | public async up(queryRunner: QueryRunner): Promise { 7 | await queryRunner.query( 8 | 'ALTER TABLE "public"."mmp_map" ADD "adminId" uuid DEFAULT uuid_generate_v4()' 9 | ) 10 | } 11 | 12 | public async down(queryRunner: QueryRunner): Promise { 13 | await queryRunner.query( 14 | 'ALTER TABLE "public"."mmp_map" DROP COLUMN "adminId"' 15 | ) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /teammapper-backend/src/migrations/1644079415806-AddNodeMapIdAsPrimaryColumnOnNodes.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner } from 'typeorm' 2 | 3 | export class AddNodeMapIdAsPrimaryColumnOnNodes1644079415806 4 | implements MigrationInterface 5 | { 6 | name = 'AddNodeMapIdAsPrimaryColumnOnNodes1644079415806' 7 | 8 | public async up(queryRunner: QueryRunner): Promise { 9 | await queryRunner.query( 10 | 'DELETE FROM "public"."mmp_node" WHERE id IN (SELECT node_1.id FROM "public"."mmp_node" node_1 INNER JOIN "public"."mmp_node" AS node_2 ON "node_1"."nodeParentId" = "node_2"."id" WHERE "node_1"."nodeMapId" != "node_2"."nodeMapId")' 11 | ) 12 | await queryRunner.query( 13 | 'DELETE FROM "public"."mmp_node" WHERE "nodeMapId" IS NULL' 14 | ) 15 | await queryRunner.query( 16 | 'ALTER TABLE "public"."mmp_node" DROP CONSTRAINT "FK_9919032f542cee8cab97749e1a5"' 17 | ) 18 | await queryRunner.query( 19 | 'ALTER TABLE "public"."mmp_node" DROP CONSTRAINT "PK_70f65b529c3e785462fb35c05cf"' 20 | ) 21 | await queryRunner.query( 22 | 'ALTER TABLE "public"."mmp_node" ADD CONSTRAINT "PK_1a1ff8a3417cc16c8f5151cb124" PRIMARY KEY ("id", "nodeMapId")' 23 | ) 24 | await queryRunner.query( 25 | 'ALTER TABLE "public"."mmp_node" DROP CONSTRAINT "FK_19c5208da416d32ea491315716e"' 26 | ) 27 | await queryRunner.query( 28 | 'ALTER TABLE "public"."mmp_node" ALTER COLUMN "nodeMapId" SET NOT NULL' 29 | ) 30 | await queryRunner.query( 31 | 'ALTER TABLE "public"."mmp_node" ADD CONSTRAINT "FK_19c5208da416d32ea491315716e" FOREIGN KEY ("nodeMapId") REFERENCES "mmp_map"("id") ON DELETE CASCADE ON UPDATE NO ACTION' 32 | ) 33 | await queryRunner.query( 34 | 'ALTER TABLE "public"."mmp_node" ADD CONSTRAINT "FK_336300b82c56a05f0317f229420" FOREIGN KEY ("nodeMapId", "nodeParentId") REFERENCES "mmp_node"("nodeMapId","id") ON DELETE CASCADE ON UPDATE NO ACTION' 35 | ) 36 | } 37 | 38 | public async down(queryRunner: QueryRunner): Promise { 39 | await queryRunner.query( 40 | 'ALTER TABLE "public"."mmp_node" DROP CONSTRAINT "FK_336300b82c56a05f0317f229420"' 41 | ) 42 | await queryRunner.query( 43 | 'ALTER TABLE "public"."mmp_node" DROP CONSTRAINT "FK_19c5208da416d32ea491315716e"' 44 | ) 45 | await queryRunner.query( 46 | 'ALTER TABLE "public"."mmp_node" ALTER COLUMN "nodeMapId" DROP NOT NULL' 47 | ) 48 | await queryRunner.query( 49 | 'ALTER TABLE "public"."mmp_node" ADD CONSTRAINT "FK_19c5208da416d32ea491315716e" FOREIGN KEY ("nodeMapId") REFERENCES "mmp_map"("id") ON DELETE CASCADE ON UPDATE NO ACTION' 50 | ) 51 | await queryRunner.query( 52 | 'ALTER TABLE "public"."mmp_node" DROP CONSTRAINT "PK_1a1ff8a3417cc16c8f5151cb124"' 53 | ) 54 | await queryRunner.query( 55 | 'ALTER TABLE "public"."mmp_node" ADD CONSTRAINT "PK_70f65b529c3e785462fb35c05cf" PRIMARY KEY ("id")' 56 | ) 57 | await queryRunner.query( 58 | 'ALTER TABLE "public"."mmp_node" ADD CONSTRAINT "FK_9919032f542cee8cab97749e1a5" FOREIGN KEY ("nodeParentId") REFERENCES "mmp_node"("id") ON DELETE CASCADE ON UPDATE NO ACTION' 59 | ) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /teammapper-backend/src/migrations/1663839669273-AddIndexToForeignKeysOnMmpNode.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner } from 'typeorm' 2 | 3 | export class AddIndexToForeignKeysOnMmpNode1663839669273 4 | implements MigrationInterface 5 | { 6 | name = 'AddIndexToForeignKeysOnMmpNode1663839669273' 7 | 8 | public async up(queryRunner: QueryRunner): Promise { 9 | await queryRunner.query( 10 | 'CREATE INDEX "IDX_19c5208da416d32ea491315716" ON "mmp_node" ("nodeMapId") ' 11 | ) 12 | } 13 | 14 | public async down(queryRunner: QueryRunner): Promise { 15 | await queryRunner.query( 16 | 'DROP INDEX "public"."IDX_19c5208da416d32ea491315716"' 17 | ) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /teammapper-backend/src/migrations/1663927754319-AddIndexForNodesParents.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner } from 'typeorm' 2 | 3 | export class AddIndexForNodesParents1663927754319 4 | implements MigrationInterface 5 | { 6 | name = 'AddIndexForNodesParents1663927754319' 7 | 8 | public async up(queryRunner: QueryRunner): Promise { 9 | await queryRunner.query( 10 | 'CREATE INDEX "IDX_336300b82c56a05f0317f22942" ON "mmp_node" ("nodeMapId", "nodeParentId") ' 11 | ) 12 | } 13 | 14 | public async down(queryRunner: QueryRunner): Promise { 15 | await queryRunner.query( 16 | 'DROP INDEX "public"."IDX_336300b82c56a05f0317f22942"' 17 | ) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /teammapper-backend/src/migrations/1668360651755-AddOptionsToMap.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner } from 'typeorm' 2 | 3 | export class AddOptionsToMap1668360651755 implements MigrationInterface { 4 | name = 'AddOptionsToMap1668360651755' 5 | 6 | public async up(queryRunner: QueryRunner): Promise { 7 | await queryRunner.query( 8 | `ALTER TABLE "mmp_map" ADD "options" jsonb NOT NULL DEFAULT '{}'` 9 | ) 10 | } 11 | 12 | public async down(queryRunner: QueryRunner): Promise { 13 | await queryRunner.query(`ALTER TABLE "mmp_map" DROP COLUMN "options"`) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /teammapper-backend/src/migrations/1678605712865-AddLinkHrefToNode.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner } from 'typeorm' 2 | 3 | export class AddLinkHrefToNode1678605712865 implements MigrationInterface { 4 | name = 'AddLinkHrefToNode1678605712865' 5 | 6 | public async up(queryRunner: QueryRunner): Promise { 7 | await queryRunner.query( 8 | `ALTER TABLE "mmp_node" ADD "linkHref" character varying` 9 | ) 10 | } 11 | 12 | public async down(queryRunner: QueryRunner): Promise { 13 | await queryRunner.query(`ALTER TABLE "mmp_node" DROP COLUMN "linkHref"`) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /teammapper-backend/src/migrations/1678976170981-AddModificationSecretToMaps.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner } from 'typeorm' 2 | 3 | export class AddModificationSecretToMaps1678976170981 4 | implements MigrationInterface 5 | { 6 | name = 'AddModificationSecretToMaps1678976170981' 7 | 8 | public async up(queryRunner: QueryRunner): Promise { 9 | await queryRunner.query( 10 | `ALTER TABLE "mmp_map" ADD "modificationSecret" uuid NULL` 11 | ) 12 | await queryRunner.query( 13 | `ALTER TABLE "mmp_map" ALTER "modificationSecret" SET DEFAULT uuid_generate_v4()` 14 | ) 15 | } 16 | 17 | public async down(queryRunner: QueryRunner): Promise { 18 | await queryRunner.query( 19 | `ALTER TABLE "mmp_map" DROP COLUMN "modificationSecret"` 20 | ) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /teammapper-backend/src/migrations/1679478438937-AddLastModifiedToNodes.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner } from 'typeorm' 2 | 3 | export class AddLastModifiedToNodes1679478438937 implements MigrationInterface { 4 | name = 'AddLastModifiedToNodes1679478438937' 5 | 6 | public async up(queryRunner: QueryRunner): Promise { 7 | await queryRunner.query( 8 | `ALTER TABLE "mmp_node" ADD "lastModified" TIMESTAMP WITH TIME ZONE DEFAULT now()` 9 | ) 10 | } 11 | 12 | public async down(queryRunner: QueryRunner): Promise { 13 | await queryRunner.query(`ALTER TABLE "mmp_node" DROP COLUMN "lastModified"`) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /teammapper-backend/src/migrations/1701777634545-AddDetachedPropertyToNodes.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner } from 'typeorm' 2 | 3 | export class AddDetachedPropertyToNodes1701777634545 4 | implements MigrationInterface 5 | { 6 | name = 'AddDetachedPropertyToNodes1701777634545' 7 | 8 | public async up(queryRunner: QueryRunner): Promise { 9 | await queryRunner.query( 10 | `ALTER TABLE "mmp_node" ADD "detached" boolean NOT NULL DEFAULT false` 11 | ) 12 | } 13 | 14 | public async down(queryRunner: QueryRunner): Promise { 15 | await queryRunner.query(`ALTER TABLE "mmp_node" DROP COLUMN "detached"`) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /teammapper-backend/src/migrations/1718959806227-AddLastAccessedFieldToMap.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner } from 'typeorm' 2 | 3 | export class AddLastAccessedFieldToMap1718959806227 4 | implements MigrationInterface 5 | { 6 | name = 'AddLastAccessedFieldToMap1718959806227' 7 | 8 | public async up(queryRunner: QueryRunner): Promise { 9 | await queryRunner.query( 10 | `ALTER TABLE "mmp_map" ADD "lastAccessed" TIMESTAMP WITH TIME ZONE` 11 | ) 12 | } 13 | 14 | public async down(queryRunner: QueryRunner): Promise { 15 | await queryRunner.query(`ALTER TABLE "mmp_map" DROP COLUMN "lastAccessed"`) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /teammapper-backend/src/migrations/1724314314717-AddCreatedAtToMap.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner } from 'typeorm' 2 | 3 | export class AddCreatedAtToMap1724314314717 implements MigrationInterface { 4 | public async up(queryRunner: QueryRunner): Promise { 5 | await queryRunner.query( 6 | `ALTER TABLE "mmp_map" ADD "createdAt" TIMESTAMP WITH TIME ZONE` 7 | ) 8 | } 9 | 10 | public async down(queryRunner: QueryRunner): Promise { 11 | await queryRunner.query(`ALTER TABLE "mmp_map" DROP COLUMN "createdAt"`) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /teammapper-backend/src/migrations/1724314435583-AddCreatedAtToNode.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner } from 'typeorm' 2 | 3 | export class AddCreatedAtToNode1724314435583 implements MigrationInterface { 4 | public async up(queryRunner: QueryRunner): Promise { 5 | await queryRunner.query( 6 | `ALTER TABLE "mmp_node" ADD "createdAt" TIMESTAMP WITH TIME ZONE` 7 | ) 8 | } 9 | 10 | public async down(queryRunner: QueryRunner): Promise { 11 | await queryRunner.query(`ALTER TABLE "mmp_node" DROP COLUMN "createdAt"`) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /teammapper-backend/src/migrations/1724325535133-AddDefaultToCreatedAtMmpMap.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner } from 'typeorm' 2 | 3 | export class AddDefaultToCreatedAtMmpMap1724325535133 4 | implements MigrationInterface 5 | { 6 | name = 'AddDefaultToCreatedAtMmpMap1724325535133' 7 | 8 | public async up(queryRunner: QueryRunner): Promise { 9 | await queryRunner.query( 10 | `ALTER TABLE "mmp_map" ALTER COLUMN "createdAt" SET DEFAULT now()` 11 | ) 12 | } 13 | 14 | public async down(queryRunner: QueryRunner): Promise { 15 | await queryRunner.query( 16 | `ALTER TABLE "mmp_map" ALTER COLUMN "createdAt" DROP DEFAULT` 17 | ) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /teammapper-backend/src/migrations/1724325567562-AddDefaultToCreatedAtMmpNode.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner } from 'typeorm' 2 | 3 | export class AddDefaultToCreatedAtMmpNode1724325567562 4 | implements MigrationInterface 5 | { 6 | name = 'AddDefaultToCreatedAtMmpNode1724325567562' 7 | 8 | public async up(queryRunner: QueryRunner): Promise { 9 | await queryRunner.query( 10 | `ALTER TABLE "mmp_node" ALTER COLUMN "createdAt" SET DEFAULT now()` 11 | ) 12 | } 13 | 14 | public async down(queryRunner: QueryRunner): Promise { 15 | await queryRunner.query( 16 | `ALTER TABLE "mmp_node" ALTER COLUMN "createdAt" DROP DEFAULT` 17 | ) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /teammapper-backend/test/db.ts: -------------------------------------------------------------------------------- 1 | import { TypeOrmModuleOptions } from '@nestjs/typeorm' 2 | import { DataSource } from 'typeorm' 3 | import { PostgresConnectionOptions } from 'typeorm/driver/postgres/PostgresConnectionOptions' 4 | 5 | require('dotenv').config() // eslint-disable-line @typescript-eslint/no-require-imports 6 | 7 | // we create a database for each worker: 8 | const setupWorkerDatabase = async (workerId: string): Promise => { 9 | const connection = await mainTestDataSource() 10 | const databaseName = buildDatabaseName(workerId) 11 | await connection.query(`DROP DATABASE IF EXISTS "${databaseName}"`) 12 | await connection.query(`CREATE DATABASE "${databaseName}"`) 13 | await connection.destroy() 14 | 15 | return databaseName 16 | } 17 | 18 | const createDataSourceConfig: PostgresConnectionOptions = { 19 | type: 'postgres', 20 | host: process.env.POSTGRES_TEST_HOST, 21 | port: parseInt(process.env.POSTGRES_TEST_PORT || '3000', 10), 22 | username: process.env.POSTGRES_TEST_USER, 23 | password: process.env.POSTGRES_TEST_PASSWORD, 24 | database: process.env.POSTGRES_TEST_DATABASE, 25 | synchronize: false, 26 | } 27 | 28 | // this is the configuration for the main test database. this database is not used for actual tests. 29 | // it is the entrypoint for each worker to be able to create their own worker database 30 | const mainTestDataSource = async () => { 31 | // we don't need to synchronise the tables for the main test database - # 32 | // we cannot use CREATE DATABASE ... TEMPLATE ...;, since this method does 33 | // not work when multiple connections are accessing the template database. 34 | // However, as soon as we have more than one worker, there are multiple 35 | // connections to the main database, concurrently trying to create their worker databases. 36 | const connection = new DataSource(createDataSourceConfig) 37 | await connection.initialize() 38 | 39 | return connection 40 | } 41 | 42 | const workerDataSourceConfig = (databaseName: string): TypeOrmModuleOptions => { 43 | return { 44 | ...createDataSourceConfig, 45 | database: databaseName, 46 | synchronize: true, 47 | autoLoadEntities: true, 48 | dropSchema: true, 49 | extra: { 50 | query_timeout: 1000, 51 | statement_timeout: 1000, 52 | }, 53 | } 54 | } 55 | 56 | export const destroyWorkerDatabase = async ( 57 | workerDataSource: DataSource, 58 | workerId: string 59 | ): Promise => { 60 | // first, drop connection to test database - we cannot use the connection from the worker, since the worker is connected to the database we want to delete. 61 | await workerDataSource.destroy() 62 | 63 | // get a connection to the main test database 64 | const databaseName = buildDatabaseName(workerId) 65 | // delete worker database: 66 | const connection = await mainTestDataSource() 67 | await connection.query(`DROP DATABASE IF EXISTS "${databaseName}"`) 68 | await connection.destroy() 69 | } 70 | 71 | const buildDatabaseName = (workerId: string): string => { 72 | return `${process.env.POSTGRES_TEST_DATABASE}-${workerId}` 73 | } 74 | 75 | export const createTestConfiguration = async ( 76 | workerId: string 77 | ): Promise => { 78 | const databaseName = await setupWorkerDatabase(workerId) 79 | return workerDataSourceConfig(databaseName) 80 | } 81 | -------------------------------------------------------------------------------- /teammapper-backend/test/helper.ts: -------------------------------------------------------------------------------- 1 | import { DataSource } from 'typeorm' 2 | 3 | export async function truncateDatabase(dataSource: DataSource) { 4 | // loosely based on https://blog.tooljet.com/clearing-tables-before-each-test-nestjs-typeorm/ 5 | const entities = dataSource.entityMetadatas 6 | for (const entity of entities) { 7 | const repository = dataSource.getRepository(entity.name) 8 | await repository.query( 9 | `TRUNCATE TABLE ${entity.tableName} RESTART IDENTITY CASCADE;` 10 | ) 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /teammapper-backend/test/jest-e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "moduleFileExtensions": ["js", "json", "ts"], 3 | "rootDir": "../", 4 | "testEnvironment": "node", 5 | "testRegex": ".e2e-spec.ts$", 6 | "transform": { 7 | "^.+\\.(t|j)s$": "ts-jest" 8 | }, 9 | "modulePaths": [""], 10 | "testRunner": "jest-jasmine2" 11 | } 12 | -------------------------------------------------------------------------------- /teammapper-backend/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["node_modules", "test", "dist", "**/*spec.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /teammapper-backend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "declaration": true, 5 | "removeComments": true, 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true, 8 | "allowSyntheticDefaultImports": true, 9 | "target": "es2017", 10 | "sourceMap": true, 11 | "outDir": "./dist", 12 | "baseUrl": "./", 13 | "incremental": true, 14 | "noImplicitAny": true, 15 | "strictNullChecks": true, 16 | "esModuleInterop": true, 17 | "skipLibCheck": true 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /teammapper-frontend/.browserslistrc: -------------------------------------------------------------------------------- 1 | # This file is used by the build system to adjust CSS and JS output to support the specified browsers below. 2 | # For additional information regarding the format and rule options, please see: 3 | # https://github.com/browserslist/browserslist#queries 4 | 5 | # For the full list of supported browsers by the Angular framework, please see: 6 | # https://angular.dev/reference/versions#browser-support 7 | 8 | # You can see what browsers were selected by your queries by running: 9 | # npx browserslist 10 | 11 | last 2 Chrome versions 12 | last 1 Firefox version 13 | last 2 Edge major versions 14 | last 3 Safari major versions 15 | last 2 iOS major versions 16 | last 2 Android major versions 17 | Firefox ESR 18 | -------------------------------------------------------------------------------- /teammapper-frontend/.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist 5 | /tmp 6 | /out-tsc 7 | /builds 8 | /documentation 9 | 10 | # dependencies 11 | /node_modules 12 | 13 | # IDEs and editors 14 | /.idea 15 | .project 16 | .classpath 17 | .c9/ 18 | *.launch 19 | .settings/ 20 | *.sublime-workspace 21 | 22 | # IDE - VSCode 23 | .vscode/* 24 | !.vscode/settings.json 25 | !.vscode/tasks.json 26 | !.vscode/launch.json 27 | !.vscode/extensions.json 28 | 29 | # misc 30 | /.sass-cache 31 | /connect.lock 32 | /coverage 33 | /libpeerconnection.log 34 | npm-debug.log 35 | testem.log 36 | /typings 37 | /.angular 38 | 39 | # e2e 40 | /e2e/*.js 41 | /e2e/*.map 42 | 43 | # System Files 44 | .DS_Store 45 | Thumbs.db 46 | -------------------------------------------------------------------------------- /teammapper-frontend/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 2, 3 | "useTabs": false, 4 | "singleQuote": true, 5 | "semi": true, 6 | "bracketSpacing": true, 7 | "arrowParens": "avoid", 8 | "trailingComma": "es5", 9 | "bracketSameLine": true, 10 | "printWidth": 80 11 | } -------------------------------------------------------------------------------- /teammapper-frontend/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](http://keepachangelog.com/) 5 | and this project adheres to [Semantic Versioning](http://semver.org/). 6 | 7 | ## [Unreleased] 8 | 9 | ### Added 10 | 11 | ### Changed 12 | 13 | ### Fixed 14 | 15 | ## [0.13.0] - 2021-02-08 16 | ### Added 17 | - Added version number in information component 18 | - Added check when last remaining map has been removed, add a new map 19 | 20 | ### Changed 21 | - Changed localStorage to localForage module 22 | 23 | ### Fixed 24 | - Fixed initialisation of app with APP_INITIALIZER 25 | ## [0.12.0] - 2020-12-17 26 | 27 | ### Changed 28 | - Removed welcome message 29 | - Set autoBranchColors to true 30 | - UI/UX Adjustments 31 | 32 | ## [0.11.0] - 2020-11-17 33 | ### Changed 34 | - Changed url of repository 35 | ## [0.11.0] - 2020-09-28 36 | ### Added 37 | - Added libs folder for customized libraries 38 | - Added stripTags Pipe 39 | 40 | ### Changed 41 | - Upgraded dependencies for core-js and mmp 42 | - Changed logos and icons 43 | - Changed CI -------------------------------------------------------------------------------- /teammapper-frontend/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2019 Omar Desogus 4 | 5 | Copyright (c) 2021 b310 digital gmbh 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 8 | 9 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 10 | 11 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /teammapper-frontend/README.md: -------------------------------------------------------------------------------- 1 | # TeamMapper frontend 2 | 3 | ## Description 4 | 5 | Based on https://github.com/cedoor/mindmapp (discontinued) 6 | 7 | ## Setup 8 | 9 | ``` 10 | npm install 11 | 12 | npm run start 13 | ``` 14 | 15 | ## Dev 16 | 17 | ### Lint 18 | 19 | ``` 20 | npx prettier --write src 21 | npm run lint -- --fix 22 | ``` -------------------------------------------------------------------------------- /teammapper-frontend/eslint.config.js: -------------------------------------------------------------------------------- 1 | const prettier = require("eslint-plugin-prettier"); 2 | const globals = require("globals"); 3 | const js = require("@eslint/js"); 4 | const tseslint = require("typescript-eslint"); 5 | // Allows us to bring in the recommended rules for Angular projects from angular-eslint 6 | const angular = require('angular-eslint'); 7 | 8 | // Export our config array, which is composed together thanks to the typed utility function from typescript-eslint 9 | module.exports = tseslint.config( 10 | { 11 | // Everything in this config object targets our TypeScript files (Components, Directives, Pipes etc) 12 | files: ['**/*.ts'], 13 | extends: [ 14 | // Apply the recommended core rules 15 | js.configs.recommended, 16 | // Apply the recommended TypeScript rules 17 | ...tseslint.configs.recommended, 18 | // Optionally apply stylistic rules from typescript-eslint that improve code consistency 19 | ...tseslint.configs.stylistic, 20 | // Apply the recommended Angular rules 21 | ...angular.configs.tsRecommended, 22 | ], 23 | plugins: { 24 | prettier, 25 | }, 26 | // Set the custom processor which will allow us to have our inline Component templates extracted 27 | // and treated as if they are HTML files (and therefore have the .html config below applied to them) 28 | processor: angular.processInlineTemplates, 29 | // Override specific rules for TypeScript files (these will take priority over the extended configs above) 30 | rules: { 31 | '@angular-eslint/prefer-standalone': 'off', 32 | '@angular-eslint/directive-selector': [ 33 | 'error', 34 | { 35 | type: 'attribute', 36 | prefix: 'teammapper', 37 | style: 'camelCase', 38 | }, 39 | ], 40 | '@angular-eslint/component-selector': [ 41 | 'error', 42 | { 43 | type: 'element', 44 | prefix: 'teammapper', 45 | style: 'kebab-case', 46 | }, 47 | ], 48 | "prettier/prettier": "error", 49 | "no-unused-vars": "off", 50 | "@typescript-eslint/no-explicit-any": "warn", 51 | 52 | "@typescript-eslint/no-unused-vars": ["error", { 53 | args: "all", 54 | argsIgnorePattern: "^_", 55 | caughtErrors: "all", 56 | caughtErrorsIgnorePattern: "^_", 57 | }], 58 | }, 59 | }, 60 | { 61 | // Everything in this config object targets our HTML files (external templates, 62 | // and inline templates as long as we have the `processor` set on our TypeScript config above) 63 | files: ['**/*.html'], 64 | extends: [ 65 | // Apply the recommended Angular template rules 66 | ...angular.configs.templateRecommended, 67 | // Apply the Angular template rules which focus on accessibility of our apps 68 | ...angular.configs.templateAccessibility, 69 | ], 70 | rules: {}, 71 | }, 72 | ); -------------------------------------------------------------------------------- /teammapper-frontend/mmp/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | package-lock.json 3 | npm-debug.log 4 | build 5 | docs 6 | .rpt2_cache 7 | .idea 8 | -------------------------------------------------------------------------------- /teammapper-frontend/mmp/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018 Omar Desogus 4 | 5 | Copyright (c) 2021 b310 digital gmbh 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 8 | 9 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 10 | 11 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /teammapper-frontend/mmp/README.md: -------------------------------------------------------------------------------- 1 | # MMP Library 2 | Support for this library was unfortunately discontinued and therefore copied to this project for further development and to make needed changes. 3 | 4 | Original library can be found here: https://github.com/cedoor/mmp 5 | 6 | 7 | ## Third-party libs 8 | 9 | | Library | Authors or maintainers | License | Link | 10 | |-------------------|:------------------------------------:|:----------:|:----:| 11 | | D3 | Mike Bostock | [BSD-3-Clause](https://github.com/d3/d3/blob/master/LICENSE) | https://d3js.org/ | 12 | 13 | ## License 14 | * See [LICENSE](https://github.com/cedoor/mmp/blob/master/LICENSE) file 15 | 16 | ## Contacts 17 | #### Initial Developer 18 | * e-mail : me@cedoor.dev 19 | * github : @cedoor 20 | * website : https://cedoor.dev 21 | 22 | #### Further development, refactoring and adjustments in this project 23 | 24 | * e-mail : jannik@b310.de 25 | * github : @jannikstreek 26 | * website : https://b310.de 27 | 28 | -------------------------------------------------------------------------------- /teammapper-frontend/mmp/src/index.ts: -------------------------------------------------------------------------------- 1 | import {OptionParameters} from './map/options' 2 | import MmpMap from './map/map' 3 | import { PropertyMapping } from './map/handlers/nodes' 4 | 5 | /** 6 | * Return a mmp object with all mmp functions. 7 | */ 8 | export function create (id: string, ref: HTMLElement, options?: OptionParameters) { 9 | return new MmpMap(id, ref, options) 10 | } 11 | 12 | export const NodePropertyMapping = PropertyMapping 13 | -------------------------------------------------------------------------------- /teammapper-frontend/mmp/src/map/handlers/events.ts: -------------------------------------------------------------------------------- 1 | import {dispatch} from 'd3' 2 | import {Dispatch} from 'd3-dispatch' 3 | import Utils from '../../utils/utils' 4 | import Log from '../../utils/log' 5 | 6 | /** 7 | * Manage the events of the map. 8 | */ 9 | export default class Events { 10 | 11 | private dispatcher: Dispatch 12 | 13 | /** 14 | * Initialize the events. 15 | */ 16 | constructor() { 17 | const events = Utils.fromObjectToArray(Event) 18 | 19 | this.dispatcher = dispatch(...events) 20 | } 21 | 22 | /** 23 | * Call all registered callbacks for specified map event. 24 | * @param {Event} event 25 | * @param parameters 26 | */ 27 | public call(event: Event, ...parameters) { 28 | return this.dispatcher.call(event, ...parameters) 29 | } 30 | 31 | /** 32 | * Add a callback for specific map event. 33 | * @param {string} event 34 | * @param {Function} callback 35 | */ 36 | public on = (event: string, callback: Function) => { 37 | if (typeof event !== 'string') { 38 | Log.error('The event must be a string', 'type') 39 | } 40 | 41 | if (!Event[event]) { 42 | Log.error('The event does not exist') 43 | } 44 | 45 | this.dispatcher.on(Event[event], callback as any) 46 | } 47 | 48 | /** 49 | * Removes / resets all callbacks 50 | */ 51 | public unsubscribeAll = () => { 52 | Object.values(Event).forEach((event: string) => { 53 | this.dispatcher.on(event, null) 54 | }) 55 | } 56 | } 57 | 58 | export enum Event { 59 | create = 'mmp-create', 60 | center = 'mmp-center', 61 | undo = 'mmp-undo', 62 | redo = 'mmp-redo', 63 | exportJSON = 'mmp-export-json', 64 | exportImage = 'mmp-export-image', 65 | zoomIn = 'mmp-zoom-in', 66 | zoomOut = 'mmp-zoom-out', 67 | nodeSelect = 'mmp-node-select', 68 | nodeDeselect = 'mmp-node-deselect', 69 | nodeUpdate = 'mmp-node-update', 70 | nodeCreate = 'mmp-node-create', 71 | nodePaste = 'mmp-node-paste', 72 | nodeRemove = 'mmp-node-remove' 73 | } 74 | -------------------------------------------------------------------------------- /teammapper-frontend/mmp/src/map/handlers/zoom.ts: -------------------------------------------------------------------------------- 1 | import * as d3 from 'd3' 2 | import Map from '../map' 3 | import {Event} from './events' 4 | import {ZoomBehavior, D3ZoomEvent} from 'd3-zoom' 5 | import Log from '../../utils/log' 6 | 7 | /** 8 | * Manage the zoom events of the map. 9 | */ 10 | export default class Zoom { 11 | 12 | private map: Map 13 | 14 | private zoomBehavior: ZoomBehavior 15 | 16 | /** 17 | * Get the associated map instance and initialize the d3 zoom behavior. 18 | * @param {Map} map 19 | */ 20 | constructor(map: Map) { 21 | this.map = map 22 | 23 | this.zoomBehavior = d3.zoom().scaleExtent([0.1, 2]).on('zoom', (event: D3ZoomEvent) => { 24 | this.map.dom.g.attr('transform', event.transform) 25 | }) 26 | } 27 | 28 | /** 29 | * Zoom in the map. 30 | * @param {number} duration 31 | */ 32 | public zoomIn = (duration?: number) => { 33 | if (duration && typeof duration !== 'number') { 34 | Log.error('The parameter must be a number', 'type') 35 | } 36 | 37 | this.move(true, duration) 38 | this.map.events.call(Event.zoomIn) 39 | } 40 | 41 | /** 42 | * Zoom out the map. 43 | * @param {number} duration 44 | */ 45 | public zoomOut = (duration?: number) => { 46 | if (duration && typeof duration !== 'number') { 47 | Log.error('The parameter must be a number', 'type') 48 | } 49 | 50 | this.move(false, duration) 51 | this.map.events.call(Event.zoomOut) 52 | } 53 | 54 | /** 55 | * Center the root node in the mind map. 56 | * @param {number} duration 57 | * @param {number} type 58 | */ 59 | public center = (type?: 'zoom' | 'position', duration: number = 500) => { 60 | if (type && type !== 'zoom' && type !== 'position') { 61 | Log.error('The type must be a string ("zoom" or "position")', 'type') 62 | } 63 | 64 | if (duration && typeof duration !== 'number') { 65 | Log.error('The duration must be a number', 'type') 66 | } 67 | 68 | const root = this.map.nodes.getRoot(), 69 | x = root.coordinates.x, 70 | y = root.coordinates.y, 71 | svg = this.map.dom.svg.transition().duration(duration) 72 | 73 | switch (type) { 74 | case 'zoom': 75 | this.zoomBehavior.scaleTo(svg, 1) 76 | break 77 | case 'position': 78 | default: 79 | this.zoomBehavior.translateTo(svg, x, y) 80 | } 81 | 82 | this.map.events.call(Event.center) 83 | } 84 | 85 | /** 86 | * Return the d3 zoom behavior. 87 | * @returns {ZoomBehavior} zoom 88 | */ 89 | public getZoomBehavior(): ZoomBehavior { 90 | return this.zoomBehavior 91 | } 92 | 93 | /** 94 | * Move the zoom in a direction (true: in, false: out). 95 | * @param {boolean} direction 96 | * @param {number} duration 97 | */ 98 | private move(direction: boolean, duration: number = 50) { 99 | const svg = this.map.dom.svg.transition().duration(duration) 100 | 101 | this.zoomBehavior.scaleBy(svg, direction ? 4 / 3 : 3 / 4) 102 | } 103 | 104 | } 105 | -------------------------------------------------------------------------------- /teammapper-frontend/mmp/src/map/types.ts: -------------------------------------------------------------------------------- 1 | import { ExportNodeProperties, NodeProperties, UserNodeProperties } from './models/node' 2 | import { ExportHistory, MapSnapshot } from './handlers/history' 3 | import { DefaultNodeProperties, OptionParameters } from './options' 4 | 5 | interface MapCreateEvent { 6 | previousMapData: MapSnapshot 7 | } 8 | 9 | interface MapProperties { 10 | uuid: string, 11 | lastModified: number, 12 | createdAt: number, 13 | data: MapSnapshot, 14 | deletedAt: number, 15 | deleteAfterDays: number 16 | } 17 | 18 | interface NodeUpdateEvent { 19 | nodeProperties: ExportNodeProperties, 20 | previousValue: any, 21 | changedProperty: string 22 | } 23 | 24 | export { 25 | DefaultNodeProperties, 26 | ExportHistory, 27 | ExportNodeProperties, 28 | MapCreateEvent, 29 | MapProperties, 30 | MapSnapshot, 31 | NodeProperties, 32 | NodeUpdateEvent, 33 | OptionParameters, 34 | UserNodeProperties 35 | } -------------------------------------------------------------------------------- /teammapper-frontend/mmp/src/typings.d.ts: -------------------------------------------------------------------------------- 1 | declare module "*.json" { 2 | const value: any; 3 | export default value; 4 | } -------------------------------------------------------------------------------- /teammapper-frontend/mmp/src/utils/log.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Manage console messages and errors. 3 | */ 4 | export default class Log { 5 | 6 | /** 7 | * Throw an Error with a message. 8 | * @param {string} message 9 | * @param {string} type 10 | */ 11 | static error(message: string, 12 | type?: 'eval' | 'range' | 'reference' | 'syntax' | 'type' | 'uri') { 13 | switch (type) { 14 | case 'eval': 15 | throw new EvalError(message) 16 | case 'range': 17 | throw new RangeError(message) 18 | case 'reference': 19 | throw new ReferenceError(message) 20 | case 'syntax': 21 | throw new SyntaxError(message) 22 | case 'type': 23 | throw new TypeError(message) 24 | case 'uri': 25 | throw new URIError(message) 26 | default: 27 | throw new Error(message) 28 | } 29 | } 30 | 31 | /** 32 | * Print an info message. 33 | * @param {string} message 34 | */ 35 | static info(message: string) { 36 | console.log(message) 37 | } 38 | 39 | /** 40 | * Print a debug message. 41 | * @param {string} message 42 | */ 43 | static debug(message: string) { 44 | console.debug(message) 45 | } 46 | 47 | } -------------------------------------------------------------------------------- /teammapper-frontend/setup-jest.ts: -------------------------------------------------------------------------------- 1 | import 'jest-canvas-mock'; -------------------------------------------------------------------------------- /teammapper-frontend/src/app/core/http/http.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | 3 | /** 4 | * All the API urls used with the http service. 5 | */ 6 | export enum API_URL { 7 | LOCAL_ASSETS = './assets/data/', 8 | ROOT = 'api', 9 | } 10 | 11 | @Injectable({ 12 | providedIn: 'root', 13 | }) 14 | export class HttpService { 15 | /** 16 | * Constructs a `GET` request that returns the response body as a JSON object. 17 | */ 18 | public async get(apiUrl: API_URL, endpoint: string): Promise { 19 | return fetch(`${apiUrl}${endpoint}`); 20 | } 21 | 22 | public delete(apiUrl: API_URL, endpoint: string, body = ''): Promise { 23 | return fetch(`${apiUrl}${endpoint}`, { 24 | method: 'DELETE', 25 | body, 26 | headers: { 'Content-Type': 'application/json' }, 27 | }); 28 | } 29 | 30 | /** 31 | * Constructs a `POST` request that interprets the body as a JSON object and 32 | * returns the response body as a JSON object. 33 | */ 34 | public async post( 35 | apiUrl: API_URL, 36 | endpoint: string, 37 | body = '' 38 | ): Promise { 39 | return fetch(`${apiUrl}${endpoint}`, { 40 | method: 'POST', 41 | body, 42 | headers: { 'Content-Type': 'application/json' }, 43 | }); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /teammapper-frontend/src/app/core/services/dialog/dialog.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { MatDialog, MatDialogRef } from '@angular/material/dialog'; 3 | import { DialogAboutComponent } from 'src/app/modules/application/components/dialog-about/dialog-about.component'; 4 | import { DialogConnectionInfoComponent } from 'src/app/modules/application/components/dialog-connection-info/dialog-connection-info.component'; 5 | import { DialogPictogramsComponent } from 'src/app/modules/application/components/dialog-pictograms/dialog-pictograms.component'; 6 | import { DialogShareComponent } from 'src/app/modules/application/components/dialog-share/dialog-share.component'; 7 | 8 | @Injectable({ 9 | providedIn: 'root', 10 | }) 11 | export class DialogService { 12 | private disconnectModalRef: MatDialogRef; 13 | private shareModalRef: MatDialogRef; 14 | private aboutModalRef: MatDialogRef; 15 | private pictogramsModalRef: MatDialogRef; 16 | 17 | constructor(private dialog: MatDialog) {} 18 | 19 | openPictogramDialog() { 20 | this.pictogramsModalRef = this.dialog.open(DialogPictogramsComponent); 21 | this.pictogramsModalRef.componentInstance.onPictogramAdd.subscribe(() => { 22 | this.closePictogramDialog(); 23 | }); 24 | } 25 | 26 | closePictogramDialog() { 27 | if (!this.pictogramsModalRef) return; 28 | 29 | this.pictogramsModalRef.close(); 30 | } 31 | 32 | openDisconnectDialog() { 33 | this.disconnectModalRef = this.dialog.open(DialogConnectionInfoComponent); 34 | } 35 | 36 | closeDisconnectDialog() { 37 | if (!this.disconnectModalRef) return; 38 | 39 | this.disconnectModalRef.close(); 40 | } 41 | 42 | openAboutDialog() { 43 | this.aboutModalRef = this.dialog.open(DialogAboutComponent, { 44 | maxHeight: '90vh', 45 | }); 46 | } 47 | 48 | closeAboutDialog() { 49 | if (!this.aboutModalRef) return; 50 | 51 | this.aboutModalRef.close(); 52 | } 53 | 54 | openShareDialog() { 55 | this.shareModalRef = this.dialog.open(DialogShareComponent); 56 | } 57 | 58 | closeShareDialog() { 59 | if (!this.shareModalRef) return; 60 | 61 | this.shareModalRef.close(); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /teammapper-frontend/src/app/core/services/map-sync/server-types.ts: -------------------------------------------------------------------------------- 1 | import { MapSnapshot, ExportNodeProperties } from '@mmp/map/types'; 2 | import { CachedMapOptions } from 'src/app/shared/models/cached-map.model'; 3 | 4 | interface ResponseServer { 5 | // socket id of the triggering client, to prevent endless update loops 6 | clientId: string; 7 | } 8 | 9 | interface ResponseMapUpdated extends ResponseServer { 10 | map: ServerMap; 11 | } 12 | 13 | type ResponseSnapshotChanges = Record< 14 | string, 15 | Partial | undefined 16 | >; 17 | 18 | interface ResponseMapDiff { 19 | added: ResponseSnapshotChanges; 20 | deleted: ResponseSnapshotChanges; 21 | updated: ResponseSnapshotChanges; 22 | } 23 | 24 | interface ResponseUndoRedoChanges extends ResponseServer { 25 | diff: ResponseMapDiff; 26 | } 27 | 28 | interface ResponseMapOptionsUpdated extends ResponseServer { 29 | options: CachedMapOptions; 30 | } 31 | 32 | interface ResponseNodeUpdated extends ResponseServer { 33 | node: ExportNodeProperties; 34 | property: string; 35 | } 36 | 37 | interface ResponseNodesAdded extends ResponseServer { 38 | nodes: ExportNodeProperties[]; 39 | } 40 | 41 | interface ResponseClientNotification { 42 | clientId: string; 43 | message: string; 44 | type: 'error' | 'warning' | 'success'; 45 | } 46 | 47 | interface ResponseNodeRemoved extends ResponseServer { 48 | nodeId: string; 49 | } 50 | 51 | interface ResponseSelectionUpdated extends ResponseServer { 52 | nodeId: string; 53 | selected: boolean; 54 | } 55 | 56 | interface ServerMap { 57 | uuid: string; 58 | lastModified: string; 59 | deletedAt: string; 60 | deleteAfterDays: number; 61 | data: MapSnapshot; 62 | options: CachedMapOptions; 63 | createdAt: string; 64 | } 65 | 66 | interface PrivateServerMap { 67 | map: ServerMap; 68 | adminId: string; 69 | modificationSecret: string; 70 | } 71 | 72 | const ReversePropertyMapping = { 73 | name: 'name', 74 | locked: 'locked', 75 | coordinates: 'coordinates', 76 | image: { 77 | src: 'imageSrc', 78 | size: 'imageSize', 79 | }, 80 | link: { 81 | href: 'linkHref', 82 | }, 83 | colors: { 84 | background: 'backgroundColor', 85 | branch: 'branchColor', 86 | name: 'nameColor', 87 | }, 88 | font: { 89 | weight: 'fontWeight', 90 | style: 'fontStyle', 91 | size: 'fontSize', 92 | }, 93 | hidden: 'hidden', 94 | } as const; 95 | 96 | export { 97 | ResponseMapUpdated, 98 | ResponseUndoRedoChanges, 99 | ResponseMapOptionsUpdated, 100 | ResponseNodesAdded, 101 | ResponseNodeRemoved, 102 | ResponseNodeUpdated, 103 | ResponseSelectionUpdated, 104 | ResponseClientNotification, 105 | ServerMap, 106 | PrivateServerMap, 107 | ReversePropertyMapping, 108 | }; 109 | -------------------------------------------------------------------------------- /teammapper-frontend/src/app/core/services/mmp/__mocks__/mmp.service.ts: -------------------------------------------------------------------------------- 1 | export const mockMmpService = { 2 | new: jest.fn(), 3 | addNodeImage: jest.fn(), 4 | create: jest.fn(), 5 | map: { 6 | on: jest.fn(), 7 | remove: jest.fn(), 8 | center: jest.fn(), 9 | new: jest.fn(), 10 | }, 11 | }; 12 | -------------------------------------------------------------------------------- /teammapper-frontend/src/app/core/services/mmp/mmp-utils.ts: -------------------------------------------------------------------------------- 1 | const COLORS: string[] = [ 2 | '#FFC107', 3 | '#2196F3', 4 | '#9C27B0', 5 | '#f44336', 6 | '#4CAF50', 7 | '#3F51B5', 8 | '#FF9800', 9 | '#CDDC39', 10 | '#795548', 11 | '#673AB7', 12 | '#009688', 13 | '#E91E63', 14 | '#03A9F4', 15 | '#8BC34A', 16 | '#00BCD4', 17 | '#607D8B', 18 | '#FFEB3B', 19 | '#FF5722', 20 | ]; 21 | const EMPTY_IMAGE_DATA = 'data:,'; 22 | 23 | export { COLORS, EMPTY_IMAGE_DATA }; 24 | -------------------------------------------------------------------------------- /teammapper-frontend/src/app/core/services/pictograms/picto-types.ts: -------------------------------------------------------------------------------- 1 | export interface IPictogramResponse { 2 | schematic: boolean; 3 | sex: boolean; 4 | violence: boolean; 5 | aac: boolean; 6 | aacColor: boolean; 7 | skin: boolean; 8 | hair: boolean; 9 | downloads: number; 10 | categories: string[]; 11 | synsets: string[]; 12 | tags: string[]; 13 | _id: number; 14 | keywords: { 15 | keyword: string; 16 | type: number; 17 | plural: string; 18 | hasLocation: boolean; 19 | }[]; 20 | created: Date; 21 | lastUpdated: Date; 22 | } 23 | -------------------------------------------------------------------------------- /teammapper-frontend/src/app/core/services/pictograms/pictogram.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { PictogramService } from './pictogram.service'; 2 | import { HttpClient, provideHttpClient } from '@angular/common/http'; 3 | import { SettingsService } from '../settings/settings.service'; 4 | import { IPictogramResponse } from './picto-types'; 5 | import { TestBed } from '@angular/core/testing'; 6 | import { 7 | HttpTestingController, 8 | provideHttpClientTesting, 9 | } from '@angular/common/http/testing'; 10 | 11 | const testData: IPictogramResponse = { 12 | schematic: false, 13 | sex: false, 14 | violence: false, 15 | aac: false, 16 | aacColor: false, 17 | skin: false, 18 | hair: false, 19 | downloads: 0, 20 | categories: [], 21 | synsets: [], 22 | tags: [], 23 | _id: 1, 24 | created: new Date(), 25 | lastUpdated: new Date(), 26 | keywords: [], 27 | }; 28 | 29 | describe('PictogramService', () => { 30 | let httpClient: HttpClient; 31 | let service: PictogramService; 32 | let httpTesting: HttpTestingController; 33 | 34 | beforeEach(() => { 35 | TestBed.configureTestingModule({ 36 | providers: [ 37 | provideHttpClient(), 38 | provideHttpClientTesting(), 39 | SettingsService, 40 | ], 41 | }); 42 | 43 | httpClient = TestBed.inject(HttpClient); 44 | const settingsService = TestBed.inject(SettingsService); 45 | 46 | service = new PictogramService(httpClient, settingsService); 47 | httpTesting = TestBed.inject(HttpTestingController); 48 | }); 49 | 50 | it('fetches pictos', done => { 51 | const searchTerm = 'House'; 52 | const expectedUrl = 'https://api.arasaac.org/v1/pictograms/en/search/House'; 53 | service.getPictos(searchTerm).subscribe({ 54 | next: data => { 55 | expect(data).toEqual([testData]); 56 | done(); 57 | }, 58 | error: done.fail, 59 | }); 60 | httpTesting.expectOne(expectedUrl).flush([testData]); 61 | }); 62 | 63 | it('constructs the asset url', () => { 64 | const imageUrl = service.getPictoImageUrl(3); 65 | 66 | expect(imageUrl).toEqual( 67 | 'https://static.arasaac.org/pictograms/3/3_300.png' 68 | ); 69 | }); 70 | 71 | it('gets the image', done => { 72 | const blob = new Blob(); 73 | const expectedUrl = 'https://static.arasaac.org/pictograms/3/3_300.png'; 74 | 75 | service.getPictoImage(3).subscribe({ 76 | next: data => { 77 | expect(data).toEqual(blob); 78 | done(); 79 | }, 80 | error: done.fail, 81 | }); 82 | httpTesting.expectOne(expectedUrl).flush(blob); 83 | }); 84 | 85 | it('uses default language when settings are not available', done => { 86 | const searchTerm = 'House'; 87 | const expectedUrl = 'https://api.arasaac.org/v1/pictograms/en/search/House'; 88 | 89 | service.getPictos(searchTerm).subscribe({ 90 | next: _ => { 91 | done(); 92 | }, 93 | error: done.fail, 94 | }); 95 | httpTesting.expectOne(expectedUrl).flush([testData]); 96 | }); 97 | 98 | it('constructs the asset url with custom size and file type', () => { 99 | const imageUrl = service.getPictoImageUrl(3, 500, 'jpg'); 100 | 101 | expect(imageUrl).toEqual( 102 | 'https://static.arasaac.org/pictograms/3/3_500.jpg' 103 | ); 104 | }); 105 | }); 106 | -------------------------------------------------------------------------------- /teammapper-frontend/src/app/core/services/pictograms/pictogram.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { HttpClient } from '@angular/common/http'; 3 | import { Observable } from 'rxjs/internal/Observable'; 4 | import { IPictogramResponse } from './picto-types'; 5 | import { environment } from 'src/environments/environment'; 6 | import { SettingsService } from '../settings/settings.service'; 7 | 8 | @Injectable({ 9 | providedIn: 'root', 10 | }) 11 | export class PictogramService { 12 | private apirUrl = 13 | environment.pictogramApiUrl || 'https://api.arasaac.org/v1/pictograms'; 14 | private staticAssetUrl = 15 | environment.pictogramStaticUrl || 'https://static.arasaac.org/pictograms'; 16 | private apiResource = 'search'; 17 | 18 | constructor( 19 | private http: HttpClient, 20 | private settingsSerivce: SettingsService 21 | ) {} 22 | 23 | getPictos(seachTerm: string): Observable { 24 | const language = 25 | this.settingsSerivce.getCachedSettings()?.general?.language || 'en'; 26 | const url = `${this.apirUrl}/${language}/${this.apiResource}/${seachTerm}`; 27 | return this.http.get(url); 28 | } 29 | 30 | getPictoImageUrl(id: number, size = 300, fileType = 'png') { 31 | return `${this.staticAssetUrl}/${id}/${id}_${size}.${fileType}`; 32 | } 33 | 34 | getPictoImage(id: number): Observable { 35 | const url = this.getPictoImageUrl(id); 36 | return this.http.get(url, { responseType: 'blob' }); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /teammapper-frontend/src/app/core/services/settings/settings.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { BehaviorSubject, Observable } from 'rxjs'; 3 | import { CachedAdminMapEntry } from 'src/app/shared/models/cached-map.model'; 4 | import { Settings } from '../../../shared/models/settings.model'; 5 | import { API_URL, HttpService } from '../../http/http.service'; 6 | import { StorageService, STORAGE_KEYS } from '../storage/storage.service'; 7 | 8 | @Injectable({ 9 | providedIn: 'root', 10 | }) 11 | // Global per user settings service 12 | export class SettingsService { 13 | public static readonly LANGUAGES = [ 14 | 'en', 15 | 'fr', 16 | 'de', 17 | 'it', 18 | 'zh-tw', 19 | 'zh-cn', 20 | 'es', 21 | 'pt-br', 22 | ]; 23 | 24 | public settings: Observable; 25 | private settingsSubject: BehaviorSubject; 26 | private readonly editModeSubject: BehaviorSubject; 27 | 28 | constructor( 29 | private storageService: StorageService, 30 | private httpService: HttpService 31 | ) { 32 | // Initialization of the behavior subjects. 33 | this.settingsSubject = new BehaviorSubject(null); 34 | this.editModeSubject = new BehaviorSubject(null); 35 | this.settings = this.settingsSubject.asObservable(); 36 | } 37 | 38 | /** 39 | * Initialize settings with the default or cached values and return them. 40 | */ 41 | public async init() { 42 | const defaultSettings: Settings = await this.getDefaultSettings(); 43 | const loadedSettings: Settings = await this.storageService.get( 44 | STORAGE_KEYS.SETTINGS 45 | ); 46 | const settings = loadedSettings || defaultSettings; 47 | 48 | // Save the default settings. 49 | await this.storageService.set(STORAGE_KEYS.SETTINGS, settings); 50 | this.settingsSubject.next(settings); 51 | return true; 52 | } 53 | 54 | /** 55 | * Update the settings in the storage. 56 | */ 57 | public async updateCachedSettings(settings: Settings): Promise { 58 | await this.storageService.set(STORAGE_KEYS.SETTINGS, settings); 59 | this.settingsSubject.next(settings); 60 | } 61 | 62 | public async getCachedAdminMapEntries(): Promise { 63 | return (await this.storageService.getAllCreatedMapsFromStorage()) 64 | .map(result => { 65 | return { 66 | id: result[0], 67 | cachedAdminMapValue: result[1], 68 | }; 69 | }) 70 | .filter( 71 | (result: CachedAdminMapEntry) => 72 | new Date(result.cachedAdminMapValue.ttl).getTime() > Date.now() 73 | ) 74 | .sort( 75 | (a, b) => 76 | new Date(b.cachedAdminMapValue.ttl).getTime() - 77 | new Date(a.cachedAdminMapValue.ttl).getTime() 78 | ) 79 | .slice(0, 100); 80 | } 81 | 82 | /** 83 | * Return the current settings. 84 | */ 85 | public getCachedSettings(): Settings | null { 86 | return this.settingsSubject.getValue(); 87 | } 88 | 89 | public getEditModeObservable(): Observable { 90 | return this.editModeSubject.asObservable(); 91 | } 92 | 93 | public setEditMode(value: boolean) { 94 | return this.editModeSubject.next(value); 95 | } 96 | 97 | /** 98 | * Return the default settings. 99 | */ 100 | public async getDefaultSettings(): Promise { 101 | const response = await this.httpService.get( 102 | API_URL.LOCAL_ASSETS, 103 | 'settings.json' 104 | ); 105 | return response.json(); 106 | } 107 | } 108 | 109 | export function appSettingsFactory(settingsService: SettingsService) { 110 | return () => settingsService.init(); 111 | } 112 | -------------------------------------------------------------------------------- /teammapper-frontend/src/app/guards/toast.guard.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { 3 | CanActivate, 4 | ActivatedRouteSnapshot, 5 | RouterStateSnapshot, 6 | Router, 7 | } from '@angular/router'; 8 | import { ToastrService } from 'ngx-toastr'; 9 | 10 | @Injectable({ 11 | providedIn: 'root', 12 | }) 13 | export class ToastGuard implements CanActivate { 14 | constructor( 15 | private router: Router, 16 | private toastrService: ToastrService 17 | ) {} 18 | 19 | canActivate( 20 | next: ActivatedRouteSnapshot, 21 | state: RouterStateSnapshot 22 | ): boolean { 23 | const toastMessage = next.queryParamMap.get('toastMessage'); 24 | const toastError = next.queryParamMap.get('toastIsError'); 25 | 26 | if (toastMessage) { 27 | if (!toastError) { 28 | this.toastrService.success(toastMessage); 29 | } else { 30 | this.toastrService.error(toastMessage); 31 | } 32 | 33 | // This preserves both map UUID and the all important fragment whilst deleting anything toast-related. 34 | const urlTree = this.router.parseUrl(state.url); 35 | delete urlTree.queryParams['toastMessage']; 36 | this.router.navigateByUrl(urlTree, { replaceUrl: true }); 37 | 38 | return true; 39 | } 40 | return true; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /teammapper-frontend/src/app/modules/about/about-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { RouterModule, Routes } from '@angular/router'; 3 | import { AboutComponent } from './pages/about/about.component'; 4 | 5 | const routes: Routes = [ 6 | { 7 | path: '', 8 | component: AboutComponent, 9 | }, 10 | ]; 11 | 12 | @NgModule({ 13 | imports: [RouterModule.forChild(routes)], 14 | exports: [RouterModule], 15 | }) 16 | export class AboutRoutingModule {} 17 | -------------------------------------------------------------------------------- /teammapper-frontend/src/app/modules/about/about.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { AboutRoutingModule } from './about-routing.module'; 3 | import { SharedModule } from '../../shared/shared.module'; 4 | import { AboutComponent } from './pages/about/about.component'; 5 | import { JumbotronComponent } from './components/jumbotron/jumbotron.component'; 6 | import { HeaderComponent } from './components/header/header.component'; 7 | import { FooterComponent } from './components/footer/footer.component'; 8 | 9 | @NgModule({ 10 | imports: [SharedModule, AboutRoutingModule], 11 | declarations: [ 12 | AboutComponent, 13 | JumbotronComponent, 14 | HeaderComponent, 15 | FooterComponent, 16 | ], 17 | }) 18 | export class AboutModule {} 19 | -------------------------------------------------------------------------------- /teammapper-frontend/src/app/modules/about/components/footer/footer.component.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /teammapper-frontend/src/app/modules/about/components/footer/footer.component.scss: -------------------------------------------------------------------------------- 1 | footer.footer { 2 | padding: 20px 0; 3 | background-color: #2c2c2c; 4 | color: whitesmoke; 5 | 6 | div.container { 7 | display: flex; 8 | justify-content: space-between; 9 | align-items: center; 10 | flex-wrap: wrap; 11 | font-size: 16px; 12 | 13 | a { 14 | text-decoration: none; 15 | color: whitesmoke; 16 | } 17 | 18 | a:hover { 19 | opacity: 0.8; 20 | } 21 | 22 | div.spacer { 23 | flex: 1 1 auto; 24 | } 25 | 26 | div.license { 27 | display: inline-block; 28 | white-space: nowrap; 29 | padding: 8px 30px 8px 0; 30 | } 31 | 32 | div.links { 33 | padding: 8px 0; 34 | 35 | > a { 36 | display: inline-block; 37 | white-space: nowrap; 38 | padding-right: 15px; 39 | } 40 | } 41 | 42 | div.languages { 43 | padding: 8px 0; 44 | 45 | ::ng-deep { 46 | .mat-mdc-select { 47 | min-width: 150px; 48 | } 49 | 50 | .mat-mdc-select-value { 51 | max-width: 100%; 52 | width: auto; 53 | } 54 | 55 | .mat-mdc-select-value-text { 56 | color: whitesmoke; 57 | } 58 | 59 | .mat-mdc-select-arrow { 60 | color: whitesmoke; 61 | } 62 | 63 | .mat-mdc-select-placeholder { 64 | color: rgba(255, 255, 255, 0.7); 65 | } 66 | } 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /teammapper-frontend/src/app/modules/about/components/footer/footer.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | import { MatIconModule } from '@angular/material/icon'; 3 | import { MatSelectModule } from '@angular/material/select'; 4 | import { TranslateModule, TranslateService } from '@ngx-translate/core'; 5 | import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; 6 | import { SettingsService } from 'src/app/core/services/settings/settings.service'; 7 | import { of } from 'rxjs'; 8 | import { FooterComponent } from './footer.component'; 9 | 10 | describe('FooterComponent', () => { 11 | let component: FooterComponent; 12 | let fixture: ComponentFixture; 13 | let mockSettingsService: Partial; 14 | let mockTranslateService: jest.Mocked; 15 | 16 | const mockSettings = { 17 | general: { language: 'en' }, 18 | mapOptions: { 19 | autoBranchColors: true, 20 | fontMaxSize: 16, 21 | fontMinSize: 12, 22 | fontIncrement: 2, 23 | }, 24 | }; 25 | 26 | beforeEach(async () => { 27 | mockSettingsService = { 28 | getCachedSettings: jest.fn().mockReturnValue(mockSettings), 29 | updateCachedSettings: jest.fn().mockResolvedValue(undefined), 30 | }; 31 | mockTranslateService = { 32 | use: jest.fn().mockReturnValue(Promise.resolve('en')), 33 | get: jest.fn().mockReturnValue(of('translated value')), 34 | instant: jest.fn().mockReturnValue('translated value'), 35 | onLangChange: of({ lang: 'en' }), 36 | onTranslationChange: of({}), 37 | onDefaultLangChange: of({}), 38 | } as unknown as jest.Mocked; 39 | 40 | await TestBed.configureTestingModule({ 41 | declarations: [FooterComponent], 42 | providers: [ 43 | { provide: SettingsService, useValue: mockSettingsService }, 44 | { provide: TranslateService, useValue: mockTranslateService }, 45 | ], 46 | imports: [ 47 | TranslateModule.forRoot({ 48 | defaultLanguage: 'en', 49 | }), 50 | MatIconModule, 51 | MatSelectModule, 52 | BrowserAnimationsModule, 53 | ], 54 | }).compileComponents(); 55 | 56 | fixture = TestBed.createComponent(FooterComponent); 57 | component = fixture.componentInstance; 58 | fixture.detectChanges(); 59 | }); 60 | 61 | describe('Initialization', () => { 62 | it('should create the component', () => { 63 | expect(component).toBeTruthy(); 64 | }); 65 | 66 | it('should initialize with correct settings and languages', () => { 67 | expect(mockSettingsService.getCachedSettings).toHaveBeenCalled(); 68 | expect(component.languages).toEqual([ 69 | 'en', 70 | 'fr', 71 | 'de', 72 | 'it', 73 | 'zh-tw', 74 | 'zh-cn', 75 | 'es', 76 | 'pt-br', 77 | ]); 78 | expect(component.currentYear).toBe(new Date().getFullYear().toString()); 79 | }); 80 | }); 81 | 82 | describe('Behavior', () => { 83 | it('should update language', async () => { 84 | const newSettings = { 85 | ...mockSettings, 86 | general: { language: 'fr' }, 87 | }; 88 | component.settings = newSettings; 89 | await component.updateLanguage(); 90 | 91 | expect(mockSettingsService.updateCachedSettings).toHaveBeenCalledWith( 92 | newSettings 93 | ); 94 | expect(mockTranslateService.use).toHaveBeenCalledWith('fr'); 95 | }); 96 | }); 97 | }); 98 | -------------------------------------------------------------------------------- /teammapper-frontend/src/app/modules/about/components/footer/footer.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { SettingsService } from '../../../../core/services/settings/settings.service'; 3 | import { TranslateService } from '@ngx-translate/core'; 4 | import { Settings } from '../../../../shared/models/settings.model'; 5 | 6 | @Component({ 7 | selector: 'teammapper-footer', 8 | templateUrl: './footer.component.html', 9 | styleUrls: ['./footer.component.scss'], 10 | standalone: false, 11 | }) 12 | export class FooterComponent implements OnInit { 13 | public settings: Settings; 14 | public languages: string[]; 15 | 16 | public currentYear: string; 17 | 18 | constructor( 19 | private settingsService: SettingsService, 20 | private translateService: TranslateService 21 | ) {} 22 | 23 | public ngOnInit() { 24 | this.settings = this.settingsService.getCachedSettings(); 25 | this.languages = SettingsService.LANGUAGES; 26 | 27 | this.currentYear = new Date().getFullYear().toString(); 28 | } 29 | 30 | public async updateLanguage() { 31 | await this.settingsService.updateCachedSettings(this.settings); 32 | 33 | this.translateService.use(this.settings.general.language); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /teammapper-frontend/src/app/modules/about/components/header/header.component.html: -------------------------------------------------------------------------------- 1 |
2 | 15 |
16 | -------------------------------------------------------------------------------- /teammapper-frontend/src/app/modules/about/components/header/header.component.scss: -------------------------------------------------------------------------------- 1 | @use '@angular/material' as mat; 2 | @use 'src/theme.scss'; 3 | 4 | header.header { 5 | z-index: 99; 6 | position: fixed; 7 | top: 0; 8 | left: 0; 9 | right: 0; 10 | background-color: white; 11 | 12 | nav { 13 | display: flex; 14 | flex-wrap: wrap; 15 | align-items: center; 16 | padding: 16px; 17 | 18 | span.toolbar-spacer { 19 | flex: 1 1 auto; 20 | } 21 | 22 | div.vertical-line { 23 | width: 1px; 24 | height: 35px; 25 | margin: 0 10px; 26 | background-color: #c2c2c2; 27 | } 28 | 29 | a.link { 30 | cursor: pointer; 31 | color: mat.m2-get-color-from-palette(theme.$app-primary); 32 | padding-right: 12px; 33 | } 34 | 35 | img.logo { 36 | padding-right: 10px; 37 | } 38 | 39 | h3 { 40 | margin: 0 20px 3px 0; 41 | font-weight: normal; 42 | } 43 | 44 | a.anchor { 45 | font-size: 16px; 46 | margin-top: 2px; 47 | margin-left: 5px; 48 | } 49 | 50 | @media only screen and (max-width: 600px) { 51 | a.anchor { 52 | display: none; 53 | } 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /teammapper-frontend/src/app/modules/about/components/header/header.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; 2 | import { MatIconModule } from '@angular/material/icon'; 3 | import { TranslateModule } from '@ngx-translate/core'; 4 | import { FontAwesomeModule } from '@fortawesome/angular-fontawesome'; 5 | 6 | import { HeaderComponent } from './header.component'; 7 | 8 | describe('HeaderComponent', () => { 9 | let component: HeaderComponent; 10 | let fixture: ComponentFixture; 11 | 12 | beforeEach(waitForAsync(() => { 13 | TestBed.configureTestingModule({ 14 | declarations: [HeaderComponent], 15 | imports: [TranslateModule.forRoot(), MatIconModule, FontAwesomeModule], 16 | }).compileComponents(); 17 | })); 18 | 19 | beforeEach(() => { 20 | fixture = TestBed.createComponent(HeaderComponent); 21 | component = fixture.componentInstance; 22 | fixture.detectChanges(); 23 | }); 24 | 25 | it('should create', () => { 26 | expect(component).toBeTruthy(); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /teammapper-frontend/src/app/modules/about/components/header/header.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { faGithub, faGitter } from '@fortawesome/free-brands-svg-icons'; 3 | 4 | @Component({ 5 | selector: 'teammapper-header', 6 | templateUrl: './header.component.html', 7 | styleUrls: ['./header.component.scss'], 8 | standalone: false, 9 | }) 10 | export class HeaderComponent { 11 | public faGithub = faGithub; 12 | public faGitter = faGitter; 13 | } 14 | -------------------------------------------------------------------------------- /teammapper-frontend/src/app/modules/about/components/jumbotron/jumbotron.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |

4 | Teammapper Logo
5 | TeamMapper 6 |

7 |
8 |

9 | {{ 'PAGES.ABOUT.INTRODUCTION.APPLICATION_DESCRIPTION' | translate }} 10 |

11 |
12 | 13 | 14 | check 15 | 16 | {{ 17 | 'PAGES.ABOUT.INTRODUCTION.APPLICATION_PROPERTIES.0' | translate 18 | }} 19 | 20 | 21 | 22 | check 23 | 24 | {{ 25 | 'PAGES.ABOUT.INTRODUCTION.APPLICATION_PROPERTIES.1' | translate 26 | }} 27 | 28 | 29 | 30 | check 31 | 32 | {{ 33 | 'PAGES.ABOUT.INTRODUCTION.APPLICATION_PROPERTIES.2' | translate 34 | }} 35 | 36 | 37 | 38 | check 39 | 40 | {{ 41 | 'PAGES.ABOUT.INTRODUCTION.APPLICATION_PROPERTIES.3' | translate 42 | }} 43 | 44 | 45 | 46 | check 47 | 48 | {{ 49 | 'PAGES.ABOUT.INTRODUCTION.APPLICATION_PROPERTIES.4' | translate 50 | }} 51 | 52 | 53 | 54 |
55 | 56 | 59 |
60 |
61 | Screens 62 |
63 |
64 | -------------------------------------------------------------------------------- /teammapper-frontend/src/app/modules/about/components/jumbotron/jumbotron.component.scss: -------------------------------------------------------------------------------- 1 | @use '@angular/material' as mat; 2 | @use 'src/theme.scss'; 3 | 4 | .m-2 { 5 | margin: 2rem; 6 | } 7 | 8 | section.jumbotron { 9 | background-color: mat.m2-get-color-from-palette(theme.$app-primary); 10 | padding: 100px 0 40px 0; 11 | text-align: center; 12 | align-items: center; 13 | display: flex; 14 | justify-content: center; 15 | flex-wrap: wrap; 16 | 17 | h1, 18 | h4, 19 | p { 20 | color: whitesmoke; 21 | word-wrap: break-word !important; 22 | white-space: pre-wrap !important; 23 | line-height: 150%; 24 | overflow-wrap: break-word !important; 25 | } 26 | 27 | > div { 28 | padding: 20px 0; 29 | 30 | > div { 31 | padding-bottom: 20px; 32 | 33 | > p { 34 | margin: 0; 35 | } 36 | 37 | > mat-list { 38 | text-align: left; 39 | margin-bottom: 10px; 40 | max-width: 100%; 41 | 42 | .mdc-list-item { 43 | height: auto; 44 | } 45 | 46 | mat-icon { 47 | color: #74c13f; 48 | } 49 | } 50 | } 51 | 52 | > h1 { 53 | font-family: Fira Sans; 54 | margin: 0 0 24px; 55 | font-size: 42px; 56 | font-weight: 400; 57 | line-height: 54px; 58 | 59 | > img { 60 | width: 150px; 61 | } 62 | } 63 | 64 | > a { 65 | font-family: Fira Sans; 66 | width: 170px; 67 | font-weight: bold; 68 | color: mat.m2-get-color-from-palette(theme.$app-primary); 69 | margin-bottom: 10px; 70 | } 71 | 72 | > img { 73 | max-width: 400px; 74 | width: 100%; 75 | } 76 | } 77 | 78 | .alert { 79 | padding: 20px; 80 | color: white; 81 | margin-bottom: 15px; 82 | text-align: center; 83 | font-size: 0.7em; 84 | } 85 | 86 | .white-list { 87 | font-family: Fira Sans; 88 | color: white; 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /teammapper-frontend/src/app/modules/about/components/jumbotron/jumbotron.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; 2 | import { MatIconModule } from '@angular/material/icon'; 3 | import { TranslateModule } from '@ngx-translate/core'; 4 | import { MatListModule } from '@angular/material/list'; 5 | 6 | import { JumbotronComponent } from './jumbotron.component'; 7 | 8 | describe('JumbotronComponent', () => { 9 | let component: JumbotronComponent; 10 | let fixture: ComponentFixture; 11 | 12 | beforeEach(waitForAsync(() => { 13 | TestBed.configureTestingModule({ 14 | declarations: [JumbotronComponent], 15 | imports: [TranslateModule.forRoot(), MatIconModule, MatListModule], 16 | }).compileComponents(); 17 | })); 18 | 19 | beforeEach(() => { 20 | fixture = TestBed.createComponent(JumbotronComponent); 21 | component = fixture.componentInstance; 22 | fixture.detectChanges(); 23 | }); 24 | 25 | it('should create', () => { 26 | expect(component).toBeTruthy(); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /teammapper-frontend/src/app/modules/about/components/jumbotron/jumbotron.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'teammapper-jumbotron', 5 | templateUrl: './jumbotron.component.html', 6 | styleUrls: ['./jumbotron.component.scss'], 7 | standalone: false, 8 | }) 9 | export class JumbotronComponent { 10 | public projectName: string; 11 | } 12 | -------------------------------------------------------------------------------- /teammapper-frontend/src/app/modules/about/pages/about/about.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 5 |
6 |
7 |

8 | 9 |   {{ 'PAGES.ABOUT.SECTIONS.0.TITLE' | translate }} 10 |

11 |

{{ 'PAGES.ABOUT.SECTIONS.0.SUBTITLE' | translate }}

12 |
13 | 14 | 15 | 16 | {{ 'PAGES.ABOUT.SECTIONS.0.CARDS.0.TITLE' | translate }} 17 | 18 | 19 | Solar system 23 | 24 |

25 | {{ 'PAGES.ABOUT.SECTIONS.0.CARDS.0.CONTENT' | translate }} 26 |

27 |
28 |
29 | 30 | 31 | 32 | {{ 'PAGES.ABOUT.SECTIONS.0.CARDS.1.TITLE' | translate }} 33 | 34 | 35 | Radial tree 39 | 40 |

41 | {{ 'PAGES.ABOUT.SECTIONS.0.CARDS.1.CONTENT' | translate }} 42 |

43 |
44 |
45 | 46 | 47 | 48 | {{ 'PAGES.ABOUT.SECTIONS.0.CARDS.2.TITLE' | translate }} 49 | 50 | 51 | Business plan 55 | 56 |

57 | {{ 'PAGES.ABOUT.SECTIONS.0.CARDS.2.CONTENT' | translate }} 58 |

59 |
60 |
61 |
62 |
63 |
64 | 65 | 66 |
67 | -------------------------------------------------------------------------------- /teammapper-frontend/src/app/modules/about/pages/about/about.component.scss: -------------------------------------------------------------------------------- 1 | @use '@angular/material' as mat; 2 | @use 'src/theme.scss'; 3 | 4 | div.about { 5 | height: 100%; 6 | 7 | section { 8 | position: relative; 9 | } 10 | 11 | section:nth-child(odd) { 12 | background-color: white; 13 | } 14 | 15 | section:nth-child(even) { 16 | background-color: #fafafa; 17 | } 18 | 19 | section.about-section { 20 | > div { 21 | padding: 20px 15px 40px 15px; 22 | 23 | h1 { 24 | color: mat.m2-get-color-from-palette(theme.$app-primary); 25 | } 26 | 27 | div.cards { 28 | display: flex; 29 | flex-wrap: wrap; 30 | align-items: center; 31 | 32 | mat-card { 33 | flex: 1; 34 | min-width: 200px; 35 | margin: 10px; 36 | 37 | fa-icon { 38 | color: #565656; 39 | } 40 | 41 | ::ng-deep mat-card-header > div { 42 | margin-left: 0 !important; 43 | } 44 | } 45 | } 46 | 47 | mat-list { 48 | margin-top: -10px; 49 | 50 | mat-list-item { 51 | height: 40px; 52 | 53 | a { 54 | color: mat.m2-get-color-from-palette(theme.$app-primary); 55 | text-decoration: none; 56 | padding-left: 10px; 57 | } 58 | } 59 | } 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /teammapper-frontend/src/app/modules/about/pages/about/about.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { 3 | faBrain, 4 | faChartLine, 5 | faCheck, 6 | faCogs, 7 | faHeart, 8 | faRocket, 9 | } from '@fortawesome/free-solid-svg-icons'; 10 | 11 | @Component({ 12 | selector: 'teammapper-about', 13 | templateUrl: './about.component.html', 14 | styleUrls: ['./about.component.scss'], 15 | standalone: false, 16 | }) 17 | export class AboutComponent { 18 | public faBrain = faBrain; 19 | public faRocket = faRocket; 20 | public faHeart = faHeart; 21 | public faChartLine = faChartLine; 22 | public faCogs = faCogs; 23 | public faCheck = faCheck; 24 | } 25 | -------------------------------------------------------------------------------- /teammapper-frontend/src/app/modules/application/application-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { RouterModule, Routes } from '@angular/router'; 3 | import { ApplicationComponent } from './pages/application/application.component'; 4 | import { SettingsComponent } from './pages/settings/settings.component'; 5 | import { ShortcutsComponent } from './pages/shortcuts/shortcuts.component'; 6 | 7 | const routes: Routes = [ 8 | { 9 | path: '', 10 | component: ApplicationComponent, 11 | }, 12 | { 13 | path: 'settings', 14 | component: SettingsComponent, 15 | }, 16 | { 17 | path: 'shortcuts', 18 | component: ShortcutsComponent, 19 | }, 20 | ]; 21 | 22 | @NgModule({ 23 | imports: [RouterModule.forChild(routes)], 24 | exports: [RouterModule], 25 | }) 26 | export class ApplicationRoutingModule {} 27 | -------------------------------------------------------------------------------- /teammapper-frontend/src/app/modules/application/application.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { ApplicationRoutingModule } from './application-routing.module'; 3 | import { SharedModule } from '../../shared/shared.module'; 4 | import { MatTabsModule } from '@angular/material/tabs'; 5 | import { ColorPanelsComponent } from './components/color-panels/color-panels.component'; 6 | import { SliderPanelsComponent } from './components/slider-panels/slider-panels.component'; 7 | import { FloatingButtonsComponent } from './components/floating-buttons/floating-buttons.component'; 8 | import { MapComponent } from './components/map/map.component'; 9 | import { ApplicationComponent } from './pages/application/application.component'; 10 | import { SettingsComponent } from './pages/settings/settings.component'; 11 | import { ShortcutsComponent } from './pages/shortcuts/shortcuts.component'; 12 | import { ToolbarComponent } from './components/toolbar/toolbar.component'; 13 | import { ClientColorPanelsComponent } from './components/client-color-panels/client-color-panels.component'; 14 | import { ColorPickerModule } from 'ngx-color-picker'; 15 | import { DialogAboutComponent } from './components/dialog-about/dialog-about.component'; 16 | import { DialogShareComponent } from './components/dialog-share/dialog-share.component'; 17 | import { DialogConnectionInfoComponent } from './components/dialog-connection-info/dialog-connection-info.component'; 18 | import { DialogPictogramsComponent } from './components/dialog-pictograms/dialog-pictograms.component'; 19 | import { MatMenuModule } from '@angular/material/menu'; 20 | import { MatDialogModule } from '@angular/material/dialog'; 21 | import { MatGridListModule } from '@angular/material/grid-list'; 22 | 23 | @NgModule({ 24 | imports: [ 25 | SharedModule, 26 | MatMenuModule, 27 | MatTabsModule, 28 | ApplicationRoutingModule, 29 | ColorPickerModule, 30 | MatDialogModule, 31 | MatGridListModule, 32 | ], 33 | declarations: [ 34 | ApplicationComponent, 35 | SettingsComponent, 36 | ShortcutsComponent, 37 | ClientColorPanelsComponent, 38 | ColorPanelsComponent, 39 | FloatingButtonsComponent, 40 | MapComponent, 41 | SliderPanelsComponent, 42 | ToolbarComponent, 43 | DialogConnectionInfoComponent, 44 | DialogShareComponent, 45 | DialogPictogramsComponent, 46 | DialogAboutComponent, 47 | ], 48 | }) 49 | export class ApplicationModule {} 50 | -------------------------------------------------------------------------------- /teammapper-frontend/src/app/modules/application/components/client-color-panels/client-color-panels.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 |
5 | 6 | account_circle 9 | 10 |
11 |
12 | -------------------------------------------------------------------------------- /teammapper-frontend/src/app/modules/application/components/client-color-panels/client-color-panels.component.scss: -------------------------------------------------------------------------------- 1 | div.client-colors-panel { 2 | div.panel { 3 | z-index: 3; 4 | position: absolute; 5 | bottom: 5px; 6 | left: 15px; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /teammapper-frontend/src/app/modules/application/components/client-color-panels/client-color-panels.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, ElementRef, ViewChild } from '@angular/core'; 2 | import { MapSyncService } from 'src/app/core/services/map-sync/map-sync.service'; 3 | import { Observable } from 'rxjs'; 4 | 5 | @Component({ 6 | selector: 'teammapper-client-colors-panel', 7 | templateUrl: './client-color-panels.component.html', 8 | styleUrls: ['./client-color-panels.component.scss'], 9 | standalone: false, 10 | }) 11 | export class ClientColorPanelsComponent { 12 | @ViewChild('background') public background: ElementRef; 13 | 14 | public clientColors: Observable; 15 | 16 | constructor(public mapSyncService: MapSyncService) { 17 | this.clientColors = mapSyncService.getClientListObservable(); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /teammapper-frontend/src/app/modules/application/components/color-panels/color-panels.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 |
5 | 26 | 43 | 61 |
62 |
63 | -------------------------------------------------------------------------------- /teammapper-frontend/src/app/modules/application/components/color-panels/color-panels.component.scss: -------------------------------------------------------------------------------- 1 | div.colors-panel { 2 | div.background { 3 | z-index: 2; 4 | position: absolute; 5 | visibility: hidden; 6 | top: 0; 7 | left: 0; 8 | height: 100%; 9 | width: 100%; 10 | } 11 | 12 | div.panel { 13 | z-index: 3; 14 | position: absolute; 15 | right: 20px; 16 | top: 44%; 17 | 18 | input { 19 | touch-action: manipulation; 20 | cursor: pointer; 21 | box-shadow: 0 3px 1px -2px rgba(0, 0, 0, 0.3), 22 | 0 2px 2px 0 rgba(0, 0, 0, 0.18), 0 1px 5px 0 rgba(0, 0, 0, 0.16); 23 | display: block; 24 | margin-bottom: 8px; 25 | font-size: 0; 26 | padding: 16px; 27 | border: 0; 28 | border-radius: 30px; 29 | user-select: none; 30 | 31 | &:disabled { 32 | cursor: default; 33 | } 34 | } 35 | 36 | input:focus { 37 | outline: none; 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /teammapper-frontend/src/app/modules/application/components/color-panels/color-panels.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, ElementRef, Input, OnInit, ViewChild } from '@angular/core'; 2 | import { ExportNodeProperties } from '@mmp/map/types'; 3 | import { MmpService } from '../../../../core/services/mmp/mmp.service'; 4 | 5 | @Component({ 6 | selector: 'teammapper-colors-panel', 7 | templateUrl: './color-panels.component.html', 8 | styleUrls: ['./color-panels.component.scss'], 9 | standalone: false, 10 | }) 11 | export class ColorPanelsComponent implements OnInit { 12 | @Input() public node: ExportNodeProperties; 13 | @Input() public editDisabled: boolean; 14 | 15 | @ViewChild('background') public background: ElementRef; 16 | 17 | public options: any; 18 | 19 | constructor(public mmpService: MmpService) {} 20 | 21 | ngOnInit() { 22 | this.options = { 23 | width: '250px', 24 | presetColors: [ 25 | '#666666', 26 | '#f5f5f5', 27 | '#f44336', 28 | '#E91E63', 29 | '#9C27B0', 30 | '#673AB7', 31 | '#3F51B5', 32 | '#2196F3', 33 | '#03A9F4', 34 | '#00BCD4', 35 | '#009688', 36 | '#4CAF50', 37 | '#8BC34A', 38 | '#CDDC39', 39 | '#FFEB3B', 40 | '#FFC107', 41 | '#FF9800', 42 | '#FF5722', 43 | '#795548', 44 | '#9E9E9E', 45 | '#607D8B', 46 | ], 47 | }; 48 | } 49 | 50 | public colorPickerChange(property, value) { 51 | this.mmpService.updateNode(property, value, true); 52 | } 53 | 54 | public colorPickerToggleChange(opening, property, value) { 55 | this.background.nativeElement.style.visibility = opening 56 | ? 'visible' 57 | : 'hidden'; 58 | 59 | if (!opening) { 60 | this.mmpService.updateNode(property, value); 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /teammapper-frontend/src/app/modules/application/components/dialog-about/dialog-about.component.html: -------------------------------------------------------------------------------- 1 |

{{ applicationName }} {{ version }}

2 | 3 |
4 | Teammapper Logo 5 | {{ 'PAGES.ABOUT.INTRODUCTION.APPLICATION_DESCRIPTION' | translate }} 6 |
7 |
8 |
9 | {{ 10 | 'MODALS.INFO.DELETE_AFTER_DAYS' 11 | | translate : { deleteAfterDays: map.deleteAfterDays } 12 | }} 13 |
14 | {{ 'MODALS.INFO.DELETED_AT' | translate }} 15 | {{ map.deletedAt | date : 'YYYY-MM-dd' }}
16 | 17 |
18 | 22 | {{ applicationName }} 23 | 24 | 25 |
26 |
27 | 28 | 31 |
32 | 35 |
36 |
37 | -------------------------------------------------------------------------------- /teammapper-frontend/src/app/modules/application/components/dialog-about/dialog-about.component.scss: -------------------------------------------------------------------------------- 1 | .about-modal-section { 2 | img { 3 | padding-right: 2rem; 4 | margin-bottom: 2rem; 5 | margin-right: auto; 6 | margin-left: auto; 7 | display: block; 8 | } 9 | } 10 | 11 | .ml-5 { 12 | margin-left: 5pt; 13 | } 14 | 15 | .mat-list-item { 16 | font-family: 'Fira Sans'; 17 | } 18 | -------------------------------------------------------------------------------- /teammapper-frontend/src/app/modules/application/components/dialog-about/dialog-about.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { TranslateService } from '@ngx-translate/core'; 3 | import { faGithub } from '@fortawesome/free-brands-svg-icons'; 4 | import { MapProperties } from '@mmp/map/types'; 5 | import { SettingsService } from 'src/app/core/services/settings/settings.service'; 6 | import { StorageService } from 'src/app/core/services/storage/storage.service'; 7 | import { MapSyncService } from 'src/app/core/services/map-sync/map-sync.service'; 8 | import { environment } from 'src/environments/environment'; 9 | import { Router } from '@angular/router'; 10 | import { MatDialogRef } from '@angular/material/dialog'; 11 | 12 | @Component({ 13 | selector: 'teammapper-dialog-about', 14 | templateUrl: 'dialog-about.component.html', 15 | styleUrls: ['./dialog-about.component.scss'], 16 | standalone: false, 17 | }) 18 | export class DialogAboutComponent { 19 | public faGithub = faGithub; 20 | public version: string; 21 | public applicationName: string; 22 | public map: MapProperties; 23 | public mapAdminId: Promise; 24 | 25 | constructor( 26 | private translateService: TranslateService, 27 | private settingsService: SettingsService, 28 | private storageService: StorageService, 29 | private mapSyncService: MapSyncService, 30 | private dialogRef: MatDialogRef, 31 | private router: Router 32 | ) { 33 | this.version = environment.version; 34 | this.applicationName = environment.name; 35 | this.map = this.mapSyncService.getAttachedMap().cachedMap; 36 | this.mapAdminId = this.getMapAdminId(); 37 | } 38 | 39 | async deleteMap() { 40 | if (confirm(this.translateService.instant('MODALS.INFO.CONFIRM_DELETE'))) { 41 | await this.mapSyncService.deleteMap(await this.mapAdminId); 42 | await this.storageService.remove(this.map.uuid); 43 | 44 | this.dialogRef.close(); 45 | 46 | this.router.navigate([''], { 47 | queryParams: { 48 | toastMessage: this.translateService.instant( 49 | 'TOASTS.DELETE_MAP_SUCCESS' 50 | ), 51 | }, 52 | }); 53 | } 54 | } 55 | 56 | language(): string { 57 | return this.settingsService.getCachedSettings().general.language; 58 | } 59 | 60 | async getMapAdminId(): Promise { 61 | return (await this.storageService.get(this.map.uuid))?.adminId; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /teammapper-frontend/src/app/modules/application/components/dialog-connection-info/dialog-connection-info.component.html: -------------------------------------------------------------------------------- 1 |

{{ 'MODALS.CONNECTION_INFO.TITLE' | translate }}

2 | {{ 3 | 'MODALS.CONNECTION_INFO.CONTENT' | translate 4 | }} 5 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /teammapper-frontend/src/app/modules/application/components/dialog-connection-info/dialog-connection-info.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'teammapper-dialog-connection-info', 5 | templateUrl: 'dialog-connection-info.component.html', 6 | standalone: false, 7 | }) 8 | export class DialogConnectionInfoComponent { 9 | reconnect() { 10 | window.location.reload(); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /teammapper-frontend/src/app/modules/application/components/dialog-pictograms/dialog-pictograms.component.html: -------------------------------------------------------------------------------- 1 |

{{ 'MODALS.PICTOGRAMS.TITLE' | translate }}

2 | 3 | 4 | {{ 5 | 'MODALS.PICTOGRAMS.FORM_INPUT_LABEL_SEARCH' | translate 6 | }} 7 | 8 | 11 | 12 |
13 | 14 | 15 |
{{ 'MODALS.PICTOGRAMS.EMPTY' | translate }}
16 | @for (picto of pictos; track picto) { 17 | 21 | Pictogram 29 | 30 | } 31 |
32 |
33 | 34 | 35 | {{ 'MODALS.PICTOGRAMS.TERMS_TEXT.0' | translate }} 36 | {{ 37 | 'MODALS.PICTOGRAMS.TERMS_TEXT.1' | translate 38 | }} 39 | {{ 'MODALS.PICTOGRAMS.TERMS_TEXT.2' | translate }} 40 | {{ 41 | 'MODALS.PICTOGRAMS.TERMS_TEXT.3' | translate 42 | }} 43 | {{ 'MODALS.PICTOGRAMS.TERMS_TEXT.5' | translate }} 44 | 45 |
46 | ARASAAC Logo
49 |
50 | 51 | 54 | 55 | -------------------------------------------------------------------------------- /teammapper-frontend/src/app/modules/application/components/dialog-pictograms/dialog-pictograms.component.scss: -------------------------------------------------------------------------------- 1 | @use '@angular/material' as mat; 2 | @use 'src/theme.scss'; 3 | 4 | .my-1 { 5 | margin: 1em 0 1em 0; 6 | } 7 | 8 | .text-sm { 9 | font-size: small; 10 | } 11 | 12 | .image-wrapper:hover { 13 | background: mat.m2-get-color-from-palette(theme.$app-primary, 0.05); 14 | cursor: pointer; 15 | } 16 | -------------------------------------------------------------------------------- /teammapper-frontend/src/app/modules/application/components/dialog-pictograms/dialog-pictograms.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, EventEmitter, HostListener } from '@angular/core'; 2 | import { IPictogramResponse } from 'src/app/core/services/pictograms/picto-types'; 3 | import { PictogramService } from 'src/app/core/services/pictograms/pictogram.service'; 4 | import { 5 | Breakpoints, 6 | BreakpointObserver, 7 | BreakpointState, 8 | } from '@angular/cdk/layout'; 9 | import { map } from 'rxjs/operators'; 10 | import { MmpService } from 'src/app/core/services/mmp/mmp.service'; 11 | import { UtilsService } from 'src/app/core/services/utils/utils.service'; 12 | 13 | @Component({ 14 | selector: 'teammapper-dialog-pictograms', 15 | templateUrl: 'dialog-pictograms.component.html', 16 | styleUrls: ['./dialog-pictograms.component.scss'], 17 | standalone: false, 18 | }) 19 | export class DialogPictogramsComponent { 20 | public pictos: IPictogramResponse[]; 21 | public onPictogramAdd = new EventEmitter(); 22 | public searchTerm = ''; 23 | public cardLayout = this.breakpointObserver 24 | .observe([Breakpoints.WebLandscape, Breakpoints.TabletLandscape]) 25 | .pipe( 26 | map((state: BreakpointState) => { 27 | if (state.breakpoints[Breakpoints.TabletLandscape]) { 28 | return { 29 | columns: 2, 30 | miniCard: { cols: 1, rows: 1 }, 31 | }; 32 | } 33 | 34 | if (state.breakpoints[Breakpoints.WebLandscape]) { 35 | return { 36 | columns: 4, 37 | miniCard: { cols: 1, rows: 1 }, 38 | }; 39 | } 40 | 41 | return { 42 | columns: 1, 43 | miniCard: { cols: 1, rows: 1 }, 44 | }; 45 | }) 46 | ); 47 | 48 | constructor( 49 | private pictoService: PictogramService, 50 | private breakpointObserver: BreakpointObserver, 51 | private mmpService: MmpService, 52 | private utilsService: UtilsService 53 | ) {} 54 | 55 | @HostListener('document:keydown.enter') 56 | async search() { 57 | this.pictoService.getPictos(this.searchTerm).subscribe(pictos => { 58 | this.pictos = pictos; 59 | }); 60 | } 61 | 62 | async getImageFileOfId(id: number) { 63 | this.pictoService.getPictoImage(id).subscribe(async img => { 64 | this.mmpService.addNodeImage(await this.utilsService.blobToBase64(img)); 65 | this.onPictogramAdd.emit(); 66 | }); 67 | } 68 | 69 | getImageUrlOfId(id: number): string { 70 | return this.pictoService.getPictoImageUrl(id); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /teammapper-frontend/src/app/modules/application/components/dialog-share/dialog-share.component.html: -------------------------------------------------------------------------------- 1 |

{{ 'MODALS.SHARE.TITLE' | translate }}

2 | 3 |
4 | 8 |
9 | {{ 'MODALS.SHARE.LINK_TYPE_EDIT' | translate }} 10 |
11 | {{ 12 | 'MODALS.SHARE.LINK_TYPE_VIEW' | translate 13 | }} 14 |
15 |

16 | 17 | {{ 'MODALS.SHARE.CONTENT' | translate }} 18 | 24 | 27 | 28 |
29 | 30 |
31 |
32 | 33 | 36 |
37 | 40 |
41 | 44 | 47 |
48 | -------------------------------------------------------------------------------- /teammapper-frontend/src/app/modules/application/components/dialog-share/dialog-share.component.scss: -------------------------------------------------------------------------------- 1 | @use '@angular/material' as mat; 2 | @use 'src/theme.scss'; 3 | 4 | #qr-code-canvas { 5 | display: flex; 6 | justify-content: center; 7 | } 8 | 9 | mat-form-field { 10 | width: 100%; 11 | } 12 | 13 | .divider-margin { 14 | margin: 20px 0 20px 0; 15 | } 16 | 17 | .hidden { 18 | display: none; 19 | } 20 | 21 | :host 22 | ::ng-deep 23 | .mat-slide-toggle.mat-checked:not(.mat-disabled) 24 | .mat-slide-toggle-bar { 25 | background-color: mat.m2-get-color-from-palette(theme.$app-primary); 26 | } 27 | 28 | :host ::ng-deep .mat-slide-toggle.mat-disabled .mat-slide-toggle-bar { 29 | background-color: mat.m2-get-color-from-palette(theme.$app-primary); 30 | } 31 | -------------------------------------------------------------------------------- /teammapper-frontend/src/app/modules/application/components/dialog-share/qrcode-settings.ts: -------------------------------------------------------------------------------- 1 | import { Options } from 'qr-code-styling'; 2 | 3 | export const qrcodeStyling: Options = { 4 | type: 'svg', 5 | image: '', 6 | dotsOptions: { 7 | color: '#000000', 8 | type: 'dots', 9 | }, 10 | cornersSquareOptions: { 11 | type: 'square', 12 | }, 13 | cornersDotOptions: { 14 | type: 'dot', 15 | }, 16 | backgroundOptions: { 17 | color: '#fff', 18 | }, 19 | imageOptions: { 20 | crossOrigin: 'anonymous', 21 | margin: 20, 22 | }, 23 | }; 24 | -------------------------------------------------------------------------------- /teammapper-frontend/src/app/modules/application/components/floating-buttons/floating-buttons.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 7 |
8 |
9 | 13 |
14 | 15 |
16 | 20 |
21 | 22 |
23 | 27 |
28 | 29 |
30 | 34 |
35 |
-------------------------------------------------------------------------------- /teammapper-frontend/src/app/modules/application/components/floating-buttons/floating-buttons.component.scss: -------------------------------------------------------------------------------- 1 | div.floating-buttons { 2 | .no-touch { 3 | width: 50px; 4 | height: 50px; 5 | z-index: 2; 6 | position: absolute; 7 | display: flex; 8 | justify-content: center; 9 | align-items: center; 10 | } 11 | 12 | button { 13 | touch-action: manipulation; 14 | } 15 | 16 | .add-node, 17 | .remove-node, 18 | .undo, 19 | .redo { 20 | top: 80px; 21 | } 22 | 23 | .add-node button, 24 | .remove-node button { 25 | background-color: #00a3d3; 26 | color: #fff; 27 | 28 | &:disabled { 29 | color: rgba(0, 0, 0, 0.26); 30 | background: rgba(0, 0, 0, 0.26) !important; 31 | } 32 | } 33 | 34 | .add-node { 35 | left: 15px; 36 | } 37 | 38 | .remove-node { 39 | left: 65px; 40 | } 41 | 42 | .undo { 43 | right: 65px; 44 | } 45 | 46 | .redo { 47 | right: 15px; 48 | } 49 | 50 | .zoom-in, 51 | .zoom-out, 52 | .center { 53 | right: 15px; 54 | } 55 | 56 | .zoom-out { 57 | bottom: 60px; 58 | } 59 | 60 | .zoom-in { 61 | bottom: 110px; 62 | } 63 | 64 | .center { 65 | bottom: 5px; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /teammapper-frontend/src/app/modules/application/components/floating-buttons/floating-buttons.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input } from '@angular/core'; 2 | import { MmpService } from '../../../../core/services/mmp/mmp.service'; 3 | 4 | @Component({ 5 | selector: 'teammapper-floating-buttons', 6 | templateUrl: './floating-buttons.component.html', 7 | styleUrls: ['./floating-buttons.component.scss'], 8 | standalone: false, 9 | }) 10 | export class FloatingButtonsComponent { 11 | @Input() public editDisabled: boolean; 12 | 13 | constructor(public mmpService: MmpService) {} 14 | } 15 | -------------------------------------------------------------------------------- /teammapper-frontend/src/app/modules/application/components/map/map.component.html: -------------------------------------------------------------------------------- 1 |
2 | -------------------------------------------------------------------------------- /teammapper-frontend/src/app/modules/application/components/map/map.component.scss: -------------------------------------------------------------------------------- 1 | div.map { 2 | height: 100%; 3 | } 4 | -------------------------------------------------------------------------------- /teammapper-frontend/src/app/modules/application/components/map/map.component.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Component, 3 | ElementRef, 4 | ViewChild, 5 | OnDestroy, 6 | AfterViewInit, 7 | } from '@angular/core'; 8 | import { MapSyncService } from 'src/app/core/services/map-sync/map-sync.service'; 9 | import { MmpService } from 'src/app/core/services/mmp/mmp.service'; 10 | import { SettingsService } from 'src/app/core/services/settings/settings.service'; 11 | import { CachedMapEntry } from 'src/app/shared/models/cached-map.model'; 12 | 13 | import { first, Subscription } from 'rxjs'; 14 | 15 | @Component({ 16 | selector: 'teammapper-map', 17 | templateUrl: './map.component.html', 18 | styleUrls: ['./map.component.scss'], 19 | standalone: false, 20 | }) 21 | export class MapComponent implements AfterViewInit, OnDestroy { 22 | @ViewChild('map') mapWrapper: ElementRef; 23 | 24 | private mapSyncServiceSubscription: Subscription; 25 | 26 | constructor( 27 | private settingsService: SettingsService, 28 | private mmpService: MmpService, 29 | private mapSyncService: MapSyncService 30 | ) {} 31 | 32 | public async ngAfterViewInit() { 33 | const settings = this.settingsService.getCachedSettings(); 34 | 35 | this.mapSyncServiceSubscription = this.mapSyncService 36 | .getAttachedMapObservable() 37 | .pipe(first((val: CachedMapEntry | null) => val !== null)) 38 | .subscribe(async (_result: CachedMapEntry | null) => { 39 | await this.mmpService.create( 40 | 'map_1', 41 | this.mapWrapper.nativeElement, 42 | settings.mapOptions 43 | ); 44 | this.mapSyncService.initMap(); 45 | }); 46 | } 47 | 48 | ngOnDestroy() { 49 | this.mapSyncService.reset(); 50 | this.mmpService.remove(); 51 | this.mapSyncServiceSubscription.unsubscribe(); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /teammapper-frontend/src/app/modules/application/components/slider-panels/slider-panels.component.html: -------------------------------------------------------------------------------- 1 |
2 | 15 | 20 | 21 | 33 | 38 | 39 |
40 | -------------------------------------------------------------------------------- /teammapper-frontend/src/app/modules/application/components/slider-panels/slider-panels.component.scss: -------------------------------------------------------------------------------- 1 | div.sliders-panel { 2 | .slider-vertical { 3 | z-index: 2; 4 | position: absolute; 5 | left: 15px; 6 | } 7 | 8 | .font-size { 9 | top: 150px; 10 | } 11 | 12 | .image-size { 13 | top: calc(150px + 50px); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /teammapper-frontend/src/app/modules/application/components/slider-panels/slider-panels.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input } from '@angular/core'; 2 | import { ExportNodeProperties } from '@mmp/map/types'; 3 | import { SettingsService } from 'src/app/core/services/settings/settings.service'; 4 | import { CachedMapOptions } from 'src/app/shared/models/cached-map.model'; 5 | import { MmpService } from '../../../../core/services/mmp/mmp.service'; 6 | 7 | @Component({ 8 | selector: 'teammapper-sliders-panel', 9 | templateUrl: './slider-panels.component.html', 10 | styleUrls: ['./slider-panels.component.scss'], 11 | standalone: false, 12 | }) 13 | export class SliderPanelsComponent { 14 | @Input() public node: ExportNodeProperties; 15 | @Input() public editDisabled: boolean; 16 | public mapOptions: CachedMapOptions; 17 | 18 | constructor( 19 | public mmpService: MmpService, 20 | public settingsService: SettingsService 21 | ) { 22 | this.mapOptions = this.mmpService.getAdditionalMapOptions(); 23 | } 24 | 25 | public updateNodeFontSize(event: any) { 26 | const value = parseInt(event.target.value, 10); 27 | 28 | this.mmpService.updateNode('fontSize', value, true); 29 | } 30 | 31 | public updateNodeImageSize(event: any) { 32 | const value = parseInt(event.target.value, 10); 33 | 34 | this.mmpService.updateNode('imageSize', value, true); 35 | } 36 | 37 | public getSettingsFontMaxSize() { 38 | const options: CachedMapOptions = this.mmpService.getAdditionalMapOptions(); 39 | return options.fontMaxSize; 40 | } 41 | 42 | public getSettingsFontMinSize() { 43 | const options: CachedMapOptions = this.mmpService.getAdditionalMapOptions(); 44 | return options.fontMinSize; 45 | } 46 | 47 | public getSettingsFontIncrement() { 48 | const options: CachedMapOptions = this.mmpService.getAdditionalMapOptions(); 49 | return options.fontIncrement; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /teammapper-frontend/src/app/modules/application/components/toolbar/toolbar.component.scss: -------------------------------------------------------------------------------- 1 | @use '@angular/material' as mat; 2 | @use 'src/theme.scss'; 3 | 4 | mat-toolbar { 5 | overflow-x: auto; 6 | touch-action: manipulation; 7 | height: 60px; 8 | padding: 0 8px; 9 | border-bottom: 1px solid lightgray; 10 | background-color: white !important; 11 | 12 | div.vertical-line { 13 | height: 100%; 14 | margin: 0 10px; 15 | 16 | > div { 17 | width: 1px; 18 | background-color: #c2c2c2; 19 | height: 100%; 20 | } 21 | } 22 | 23 | span.toolbar-spacer { 24 | flex: 1 1 auto; 25 | min-width: 50px; 26 | } 27 | } 28 | 29 | // Scrollbar properties. 30 | mat-toolbar::-webkit-scrollbar { 31 | height: 6px; 32 | } 33 | 34 | mat-toolbar::-webkit-scrollbar-track { 35 | background: #f1f1f1; 36 | } 37 | 38 | mat-toolbar::-webkit-scrollbar-thumb { 39 | background: mat.m2-get-color-from-palette(theme.$app-primary, 0.7); 40 | } 41 | 42 | mat-toolbar::-webkit-scrollbar-thumb:hover { 43 | background: mat.m2-get-color-from-palette(theme.$app-primary, 0.8); 44 | } 45 | 46 | .label-file-upload:hover { 47 | background: mat.m2-get-color-from-palette(theme.$app-primary, 0.05); 48 | } 49 | 50 | input[type='file'] { 51 | display: none; 52 | } 53 | -------------------------------------------------------------------------------- /teammapper-frontend/src/app/modules/application/pages/application/application.component.html: -------------------------------------------------------------------------------- 1 |
2 | 5 | 6 | 9 | 13 | 14 | 17 | 18 | 19 |
20 | -------------------------------------------------------------------------------- /teammapper-frontend/src/app/modules/application/pages/application/application.component.scss: -------------------------------------------------------------------------------- 1 | div.teammapper-application { 2 | height: 100%; 3 | display: flex; 4 | flex-direction: column; 5 | 6 | teammapper-map { 7 | flex: 1; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /teammapper-frontend/src/app/modules/application/pages/settings/settings.component.scss: -------------------------------------------------------------------------------- 1 | div.settings { 2 | mat-toolbar { 3 | background-color: transparent; 4 | 5 | div.container { 6 | width: 100%; 7 | text-align: center; 8 | position: relative; 9 | 10 | button.close-button { 11 | position: absolute; 12 | top: -5px; 13 | right: -5px; 14 | } 15 | } 16 | } 17 | 18 | div.content { 19 | mat-card { 20 | margin-bottom: 20px; 21 | 22 | hr { 23 | margin-top: 0; 24 | margin-bottom: 20px; 25 | border: 0; 26 | height: 1px; 27 | background-color: lightgray; 28 | } 29 | } 30 | 31 | mat-card:last-child { 32 | margin-bottom: 0; 33 | } 34 | 35 | div.general-options { 36 | mat-card.general-options-main { 37 | padding-bottom: 15px; 38 | } 39 | } 40 | 41 | div.map-options { 42 | mat-card.map-options-map { 43 | mat-slide-toggle { 44 | display: block; 45 | margin-bottom: 20px; 46 | } 47 | 48 | mat-slide-toggle:last-child { 49 | margin-bottom: 0; 50 | } 51 | } 52 | 53 | mat-card.map-options-nodes { 54 | padding-bottom: 15px; 55 | 56 | mat-form-field { 57 | margin-right: 20px; 58 | } 59 | } 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /teammapper-frontend/src/app/modules/application/pages/settings/settings.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { Settings } from '../../../../shared/models/settings.model'; 3 | import { SettingsService } from '../../../../core/services/settings/settings.service'; 4 | import { MmpService } from '../../../../core/services/mmp/mmp.service'; 5 | import { TranslateService } from '@ngx-translate/core'; 6 | import { Location } from '@angular/common'; 7 | import { Router } from '@angular/router'; 8 | import { Observable } from 'rxjs'; 9 | import { MapSyncService } from 'src/app/core/services/map-sync/map-sync.service'; 10 | import { 11 | CachedAdminMapEntry, 12 | CachedMapOptions, 13 | } from 'src/app/shared/models/cached-map.model'; 14 | 15 | @Component({ 16 | selector: 'teammapper-settings', 17 | templateUrl: './settings.component.html', 18 | styleUrls: ['./settings.component.scss'], 19 | standalone: false, 20 | }) 21 | export class SettingsComponent implements OnInit { 22 | public readonly languages: string[]; 23 | public settings: Settings; 24 | public mapOptions: CachedMapOptions; 25 | public editMode: Observable; 26 | public cachedAdminMapEntries: CachedAdminMapEntry[]; 27 | 28 | constructor( 29 | private settingsService: SettingsService, 30 | private mmpService: MmpService, 31 | private mapSyncService: MapSyncService, 32 | private translateService: TranslateService, 33 | private router: Router, 34 | private location: Location 35 | ) { 36 | this.languages = SettingsService.LANGUAGES; 37 | this.settings = this.settingsService.getCachedSettings(); 38 | this.mapOptions = this.mmpService.getAdditionalMapOptions(); 39 | this.editMode = this.settingsService.getEditModeObservable(); 40 | this.cachedAdminMapEntries = []; 41 | } 42 | 43 | public async updateGeneralMapOptions() { 44 | await this.settingsService.updateCachedSettings(this.settings); 45 | } 46 | 47 | public async ngOnInit() { 48 | this.cachedAdminMapEntries = 49 | await this.settingsService.getCachedAdminMapEntries(); 50 | } 51 | 52 | public async updateMapOptions() { 53 | await this.validateMapOptionsInput(); 54 | this.mapSyncService.updateMapOptions(this.mapOptions); 55 | } 56 | 57 | public async updateLanguage() { 58 | await this.settingsService.updateCachedSettings(this.settings); 59 | 60 | this.translateService.use(this.settings.general.language); 61 | } 62 | 63 | public back() { 64 | this.location.back(); 65 | } 66 | 67 | public getMapUrl(entry: CachedAdminMapEntry): string { 68 | return this.router 69 | .createUrlTree([`/map/${entry.id}`], { 70 | fragment: entry.cachedAdminMapValue.modificationSecret, 71 | }) 72 | .toString(); 73 | } 74 | 75 | public getMapTitle(entry: CachedAdminMapEntry): string { 76 | return entry.cachedAdminMapValue.rootName || entry.id; 77 | } 78 | 79 | private async validateMapOptionsInput() { 80 | const defaultSettings: Settings = 81 | await this.settingsService.getDefaultSettings(); 82 | if ( 83 | this.mapOptions.fontIncrement > this.mapOptions.fontMaxSize || 84 | this.mapOptions.fontIncrement < 1 85 | ) 86 | this.mapOptions.fontIncrement = defaultSettings.mapOptions.fontIncrement; 87 | if (this.mapOptions.fontMaxSize > 99 || this.mapOptions.fontMaxSize < 15) 88 | this.mapOptions.fontMaxSize = defaultSettings.mapOptions.fontMaxSize; 89 | if (this.mapOptions.fontMinSize > 99 || this.mapOptions.fontMinSize < 15) 90 | this.mapOptions.fontMinSize = defaultSettings.mapOptions.fontMinSize; 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /teammapper-frontend/src/app/modules/application/pages/shortcuts/shortcuts.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 |

{{ 'PAGES.SHORTCUTS.TITLE' | translate }}

5 | 12 |
13 |
14 | 15 |
16 | 17 | 19 | 20 | {{ key }} +  22 | 23 | 24 | {{ shortcut.description | translate }} 25 | 26 | 27 | 28 | 29 | 31 | 32 | {{ key }} +  34 | 35 | 36 | {{ shortcut.description | translate }} 37 | 38 | 39 | 40 |
41 |
42 | -------------------------------------------------------------------------------- /teammapper-frontend/src/app/modules/application/pages/shortcuts/shortcuts.component.scss: -------------------------------------------------------------------------------- 1 | div.shortcuts { 2 | mat-toolbar { 3 | background-color: transparent; 4 | 5 | div.container { 6 | width: 100%; 7 | text-align: center; 8 | position: relative; 9 | 10 | button.close-button { 11 | position: absolute; 12 | top: -5px; 13 | right: -5px; 14 | } 15 | } 16 | } 17 | 18 | div.content { 19 | display: flex; 20 | justify-content: space-around; 21 | flex-wrap: wrap; 22 | padding-bottom: 10px; 23 | 24 | mat-list-item { 25 | color: #555555; 26 | } 27 | 28 | .key { 29 | padding: 6px 12px; 30 | background: gainsboro; 31 | border-radius: 3px; 32 | font-size: 15px; 33 | } 34 | 35 | .description { 36 | padding-left: 10px; 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /teammapper-frontend/src/app/modules/application/pages/shortcuts/shortcuts.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { ShortcutsService } from '../../../../core/services/shortcuts/shortcuts.service'; 3 | import { Hotkey } from 'angular2-hotkeys'; 4 | import { Location } from '@angular/common'; 5 | 6 | interface Shortcut { 7 | keys: string[]; 8 | // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type 9 | description: string | Function; 10 | } 11 | 12 | @Component({ 13 | selector: 'teammapper-shortcuts', 14 | templateUrl: './shortcuts.component.html', 15 | styleUrls: ['./shortcuts.component.scss'], 16 | standalone: false, 17 | }) 18 | export class ShortcutsComponent implements OnInit { 19 | public shortcuts: Shortcut[]; 20 | 21 | constructor( 22 | private shortcutsService: ShortcutsService, 23 | private location: Location 24 | ) {} 25 | 26 | public ngOnInit() { 27 | const hotKeys: Hotkey[] = this.shortcutsService.getHotKeys(); 28 | this.shortcuts = hotKeys.map((hotKey: Hotkey) => { 29 | const keys = hotKey.combo[0]; 30 | 31 | return { 32 | keys: keys === '+' ? [keys] : keys.split('+'), 33 | description: hotKey.description, 34 | }; 35 | }); 36 | } 37 | 38 | public back() { 39 | this.location.back(); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /teammapper-frontend/src/app/root-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { RouterModule, Routes } from '@angular/router'; 3 | import { ToastGuard } from './guards/toast.guard'; 4 | 5 | const routes: Routes = [ 6 | { 7 | path: '', 8 | loadChildren: () => 9 | import('./modules/about/about.module').then(m => m.AboutModule), 10 | canActivate: [ToastGuard], 11 | }, 12 | { 13 | path: 'map', 14 | loadChildren: () => 15 | import('./modules/application/application.module').then( 16 | m => m.ApplicationModule 17 | ), 18 | canActivate: [ToastGuard], 19 | }, 20 | { 21 | path: 'map/:id', 22 | loadChildren: () => 23 | import('./modules/application/application.module').then( 24 | m => m.ApplicationModule 25 | ), 26 | canActivate: [ToastGuard], 27 | }, 28 | { 29 | path: 'app', 30 | loadChildren: () => 31 | import('./modules/application/application.module').then( 32 | m => m.ApplicationModule 33 | ), 34 | }, 35 | { 36 | path: '**', 37 | redirectTo: '', 38 | }, 39 | ]; 40 | 41 | @NgModule({ 42 | imports: [ 43 | RouterModule.forRoot(routes, { 44 | onSameUrlNavigation: 'reload', 45 | scrollPositionRestoration: 'enabled', 46 | }), 47 | ], 48 | exports: [RouterModule], 49 | }) 50 | export class RootRoutingModule {} 51 | -------------------------------------------------------------------------------- /teammapper-frontend/src/app/root.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
-------------------------------------------------------------------------------- /teammapper-frontend/src/app/root.component.scss: -------------------------------------------------------------------------------- 1 | main { 2 | height: 100%; 3 | } 4 | -------------------------------------------------------------------------------- /teammapper-frontend/src/app/root.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { Router } from '@angular/router'; 3 | import { TranslateService } from '@ngx-translate/core'; 4 | import { SettingsService } from './core/services/settings/settings.service'; 5 | import { ShortcutsService } from './core/services/shortcuts/shortcuts.service'; 6 | import { routeAnimation } from './shared/animations/route.animation'; 7 | 8 | @Component({ 9 | selector: 'teammapper-root', 10 | templateUrl: 'root.component.html', 11 | styleUrls: ['./root.component.scss'], 12 | standalone: false, 13 | animations: [routeAnimation], 14 | }) 15 | export class RootComponent implements OnInit { 16 | public tapCounter = 0; 17 | 18 | constructor( 19 | private translateService: TranslateService, 20 | private router: Router, 21 | private settingsService: SettingsService, 22 | private shortcutsService: ShortcutsService 23 | ) {} 24 | 25 | public async ngOnInit() { 26 | const settings = this.settingsService.getCachedSettings(); 27 | 28 | const browserLang = this.translateService.getBrowserLang(); 29 | if (settings.general.language !== browserLang) { 30 | settings.general.language = browserLang; 31 | await this.settingsService.updateCachedSettings(settings); 32 | } 33 | 34 | await this.initTranslations(settings.general.language); 35 | 36 | this.shortcutsService.init(); 37 | 38 | // If there is a PWA environment go to application page as default. 39 | if (window.matchMedia('(display-mode: standalone)').matches) { 40 | this.router.navigate(['app'], { 41 | replaceUrl: true, 42 | }); 43 | } 44 | 45 | // Fix for #347: Force reload of pages in bfcache to prevent broken sync states on macOS where URL and internal state don't match 46 | window.addEventListener('pageshow', event => { 47 | if (event.persisted && window.location.pathname.includes('/map')) { 48 | window.location.reload(); 49 | } 50 | }); 51 | } 52 | 53 | private async initTranslations(language: string): Promise { 54 | this.translateService.setDefaultLang(language); 55 | await this.translateService.use(language).toPromise(); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /teammapper-frontend/src/app/root.module.ts: -------------------------------------------------------------------------------- 1 | import { 2 | HttpClient, 3 | provideHttpClient, 4 | withInterceptorsFromDi, 5 | } from '@angular/common/http'; 6 | import { APP_INITIALIZER, NgModule } from '@angular/core'; 7 | import { BrowserModule } from '@angular/platform-browser'; 8 | import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; 9 | import { TranslateLoader, TranslateModule } from '@ngx-translate/core'; 10 | import { TranslateHttpLoader } from '@ngx-translate/http-loader'; 11 | import { HotkeyModule } from 'angular2-hotkeys'; 12 | import { 13 | appSettingsFactory, 14 | SettingsService, 15 | } from './core/services/settings/settings.service'; 16 | import { RootRoutingModule } from './root-routing.module'; 17 | import { RootComponent } from './root.component'; 18 | import { SharedModule } from './shared/shared.module'; 19 | import { ToastrModule } from 'ngx-toastr'; 20 | 21 | export function createTranslateLoader(http: HttpClient) { 22 | return new TranslateHttpLoader(http, './assets/i18n/', '.json'); 23 | } 24 | 25 | @NgModule({ 26 | declarations: [RootComponent], 27 | bootstrap: [RootComponent], 28 | imports: [ 29 | BrowserModule, 30 | SharedModule, 31 | BrowserAnimationsModule, 32 | ToastrModule.forRoot(), 33 | RootRoutingModule, 34 | TranslateModule.forRoot({ 35 | loader: { 36 | provide: TranslateLoader, 37 | useFactory: createTranslateLoader, 38 | deps: [HttpClient], 39 | }, 40 | }), 41 | HotkeyModule.forRoot(), 42 | ], 43 | providers: [ 44 | { 45 | provide: APP_INITIALIZER, 46 | useFactory: appSettingsFactory, 47 | deps: [SettingsService], 48 | multi: true, 49 | }, 50 | provideHttpClient(withInterceptorsFromDi()), 51 | ], 52 | }) 53 | export class RootModule {} 54 | -------------------------------------------------------------------------------- /teammapper-frontend/src/app/shared/animations/route.animation.ts: -------------------------------------------------------------------------------- 1 | import { 2 | animate, 3 | query, 4 | style, 5 | transition, 6 | trigger, 7 | } from '@angular/animations'; 8 | 9 | export const routeAnimation = trigger('routeAnimation', [ 10 | transition('* => *', [ 11 | // Set a default style for enter and leave 12 | query( 13 | ':enter, :leave', 14 | [ 15 | style({ 16 | position: 'absolute', 17 | width: '100%', 18 | height: '100%', 19 | opacity: 0, 20 | }), 21 | ], 22 | { 23 | optional: true, 24 | } 25 | ), 26 | // Animate the new page in 27 | query( 28 | ':enter', 29 | [ 30 | animate( 31 | '600ms ease', 32 | style({ 33 | opacity: 1, 34 | }) 35 | ), 36 | ], 37 | { 38 | optional: true, 39 | } 40 | ), 41 | ]), 42 | ]); 43 | -------------------------------------------------------------------------------- /teammapper-frontend/src/app/shared/components/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/b310-digital/teammapper/92e2d955a816b7e181ca9e2b77dcaaeb19d3e870/teammapper-frontend/src/app/shared/components/.gitkeep -------------------------------------------------------------------------------- /teammapper-frontend/src/app/shared/models/cached-map.model.ts: -------------------------------------------------------------------------------- 1 | import { MapSnapshot } from '@mmp/map/types'; 2 | 3 | export interface CachedMapEntry { 4 | cachedMap: CachedMap; 5 | key: string; 6 | } 7 | 8 | export interface CachedMap { 9 | lastModified: number; 10 | createdAt: number; 11 | data: MapSnapshot; 12 | uuid: string; 13 | deleteAfterDays: number; 14 | deletedAt: number; 15 | options: CachedMapOptions; 16 | } 17 | 18 | export interface CachedMapOptions { 19 | fontMaxSize: number; 20 | fontMinSize: number; 21 | fontIncrement: number; 22 | } 23 | 24 | export interface CachedAdminMapValue { 25 | adminId: string; 26 | modificationSecret: string; 27 | ttl: Date; 28 | rootName: string | null; 29 | } 30 | 31 | export interface CachedAdminMapEntry { 32 | id: string; 33 | cachedAdminMapValue: CachedAdminMapValue; 34 | } 35 | -------------------------------------------------------------------------------- /teammapper-frontend/src/app/shared/models/settings.model.ts: -------------------------------------------------------------------------------- 1 | import { OptionParameters } from '@mmp/map/types'; 2 | 3 | // Options for the service 4 | // Mmp itself only takes the options parameters. The service can be sugered with additonal options though. 5 | export interface MmpOptions extends OptionParameters { 6 | // single attribute that is not contained in OptionParameters interface 7 | autoBranchColors: boolean; 8 | fontMaxSize: number; 9 | fontMinSize: number; 10 | fontIncrement: number; 11 | } 12 | 13 | export interface Settings { 14 | general: General; 15 | mapOptions: MmpOptions; 16 | } 17 | 18 | interface General { 19 | language: string; 20 | } 21 | -------------------------------------------------------------------------------- /teammapper-frontend/src/app/shared/pipes/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/b310-digital/teammapper/92e2d955a816b7e181ca9e2b77dcaaeb19d3e870/teammapper-frontend/src/app/shared/pipes/.gitkeep -------------------------------------------------------------------------------- /teammapper-frontend/src/app/shared/pipes/inverse-bool.pipe.ts: -------------------------------------------------------------------------------- 1 | import { Pipe, PipeTransform } from '@angular/core'; 2 | 3 | @Pipe({ 4 | name: 'inverseBool', 5 | pure: false, 6 | standalone: false, 7 | }) 8 | export class InverseBoolPipe implements PipeTransform { 9 | transform(value: boolean | null): boolean { 10 | return !value; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /teammapper-frontend/src/app/shared/shared.module.ts: -------------------------------------------------------------------------------- 1 | import { TranslateModule } from '@ngx-translate/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { NgModule } from '@angular/core'; 4 | import { FormsModule } from '@angular/forms'; 5 | import { RouterModule } from '@angular/router'; 6 | import { FontAwesomeModule } from '@fortawesome/angular-fontawesome'; 7 | import { MatIconModule } from '@angular/material/icon'; 8 | import { MatToolbarModule } from '@angular/material/toolbar'; 9 | import { InverseBoolPipe } from './pipes/inverse-bool.pipe'; 10 | import { MatSnackBarModule } from '@angular/material/snack-bar'; 11 | import { MatButtonModule } from '@angular/material/button'; 12 | import { MatCardModule } from '@angular/material/card'; 13 | import { MatListModule } from '@angular/material/list'; 14 | import { MatFormFieldModule } from '@angular/material/form-field'; 15 | import { MatInputModule } from '@angular/material/input'; 16 | import { MatSelectModule } from '@angular/material/select'; 17 | import { MatSlideToggleModule } from '@angular/material/slide-toggle'; 18 | import { MatSliderModule } from '@angular/material/slider'; 19 | 20 | const PIPES = [InverseBoolPipe]; 21 | 22 | @NgModule({ 23 | imports: [ 24 | CommonModule, 25 | FormsModule, 26 | FontAwesomeModule, 27 | MatSliderModule, 28 | RouterModule, 29 | TranslateModule, 30 | MatIconModule, 31 | MatSnackBarModule, 32 | MatButtonModule, 33 | MatCardModule, 34 | MatListModule, 35 | MatToolbarModule, 36 | MatFormFieldModule, 37 | MatInputModule, 38 | MatSelectModule, 39 | MatSlideToggleModule, 40 | ], 41 | declarations: [...PIPES], 42 | exports: [ 43 | CommonModule, 44 | FormsModule, 45 | FontAwesomeModule, 46 | MatSliderModule, 47 | RouterModule, 48 | TranslateModule, 49 | MatIconModule, 50 | MatSnackBarModule, 51 | MatButtonModule, 52 | MatCardModule, 53 | MatListModule, 54 | MatToolbarModule, 55 | MatFormFieldModule, 56 | MatInputModule, 57 | MatSelectModule, 58 | MatSlideToggleModule, 59 | ...PIPES, 60 | ], 61 | }) 62 | export class SharedModule {} 63 | -------------------------------------------------------------------------------- /teammapper-frontend/src/assets/data/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "general": { 3 | "language": "en" 4 | }, 5 | "mapOptions": { 6 | "centerOnResize": false, 7 | "autoBranchColors": true, 8 | "fontMaxSize": 70, 9 | "fontMinSize": 15, 10 | "fontIncrement": 5, 11 | "defaultNode": { 12 | "name": "", 13 | "link": { 14 | "href": "" 15 | }, 16 | "image": { 17 | "src": "", 18 | "size": 60 19 | }, 20 | "colors": { 21 | "name": "#666666", 22 | "background": "#f5f5f5", 23 | "branch": "#546e7a" 24 | }, 25 | "font": { 26 | "size": 22, 27 | "style": "normal", 28 | "weight": "normal" 29 | }, 30 | "locked": true 31 | }, 32 | "rootNode": { 33 | "name": "Root node", 34 | "link": { 35 | "href": "" 36 | }, 37 | "image": { 38 | "src": "", 39 | "size": 70 40 | }, 41 | "colors": { 42 | "name": "#666666", 43 | "background": "#f5f5f5" 44 | }, 45 | "font": { 46 | "size": 26, 47 | "style": "normal", 48 | "weight": "normal" 49 | } 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /teammapper-frontend/src/assets/font/firasans/FiraSans-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/b310-digital/teammapper/92e2d955a816b7e181ca9e2b77dcaaeb19d3e870/teammapper-frontend/src/assets/font/firasans/FiraSans-Bold.ttf -------------------------------------------------------------------------------- /teammapper-frontend/src/assets/font/firasans/FiraSans-Light.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/b310-digital/teammapper/92e2d955a816b7e181ca9e2b77dcaaeb19d3e870/teammapper-frontend/src/assets/font/firasans/FiraSans-Light.ttf -------------------------------------------------------------------------------- /teammapper-frontend/src/assets/font/firasans/FiraSans-Medium.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/b310-digital/teammapper/92e2d955a816b7e181ca9e2b77dcaaeb19d3e870/teammapper-frontend/src/assets/font/firasans/FiraSans-Medium.ttf -------------------------------------------------------------------------------- /teammapper-frontend/src/assets/font/firasans/FiraSans-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/b310-digital/teammapper/92e2d955a816b7e181ca9e2b77dcaaeb19d3e870/teammapper-frontend/src/assets/font/firasans/FiraSans-Regular.ttf -------------------------------------------------------------------------------- /teammapper-frontend/src/assets/font/source-sans-pro/SourceSansPro-Black.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/b310-digital/teammapper/92e2d955a816b7e181ca9e2b77dcaaeb19d3e870/teammapper-frontend/src/assets/font/source-sans-pro/SourceSansPro-Black.ttf -------------------------------------------------------------------------------- /teammapper-frontend/src/assets/font/source-sans-pro/SourceSansPro-BlackItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/b310-digital/teammapper/92e2d955a816b7e181ca9e2b77dcaaeb19d3e870/teammapper-frontend/src/assets/font/source-sans-pro/SourceSansPro-BlackItalic.ttf -------------------------------------------------------------------------------- /teammapper-frontend/src/assets/font/source-sans-pro/SourceSansPro-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/b310-digital/teammapper/92e2d955a816b7e181ca9e2b77dcaaeb19d3e870/teammapper-frontend/src/assets/font/source-sans-pro/SourceSansPro-Bold.ttf -------------------------------------------------------------------------------- /teammapper-frontend/src/assets/font/source-sans-pro/SourceSansPro-BoldItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/b310-digital/teammapper/92e2d955a816b7e181ca9e2b77dcaaeb19d3e870/teammapper-frontend/src/assets/font/source-sans-pro/SourceSansPro-BoldItalic.ttf -------------------------------------------------------------------------------- /teammapper-frontend/src/assets/font/source-sans-pro/SourceSansPro-ExtraLight.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/b310-digital/teammapper/92e2d955a816b7e181ca9e2b77dcaaeb19d3e870/teammapper-frontend/src/assets/font/source-sans-pro/SourceSansPro-ExtraLight.ttf -------------------------------------------------------------------------------- /teammapper-frontend/src/assets/font/source-sans-pro/SourceSansPro-ExtraLightItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/b310-digital/teammapper/92e2d955a816b7e181ca9e2b77dcaaeb19d3e870/teammapper-frontend/src/assets/font/source-sans-pro/SourceSansPro-ExtraLightItalic.ttf -------------------------------------------------------------------------------- /teammapper-frontend/src/assets/font/source-sans-pro/SourceSansPro-Italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/b310-digital/teammapper/92e2d955a816b7e181ca9e2b77dcaaeb19d3e870/teammapper-frontend/src/assets/font/source-sans-pro/SourceSansPro-Italic.ttf -------------------------------------------------------------------------------- /teammapper-frontend/src/assets/font/source-sans-pro/SourceSansPro-Light.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/b310-digital/teammapper/92e2d955a816b7e181ca9e2b77dcaaeb19d3e870/teammapper-frontend/src/assets/font/source-sans-pro/SourceSansPro-Light.ttf -------------------------------------------------------------------------------- /teammapper-frontend/src/assets/font/source-sans-pro/SourceSansPro-LightItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/b310-digital/teammapper/92e2d955a816b7e181ca9e2b77dcaaeb19d3e870/teammapper-frontend/src/assets/font/source-sans-pro/SourceSansPro-LightItalic.ttf -------------------------------------------------------------------------------- /teammapper-frontend/src/assets/font/source-sans-pro/SourceSansPro-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/b310-digital/teammapper/92e2d955a816b7e181ca9e2b77dcaaeb19d3e870/teammapper-frontend/src/assets/font/source-sans-pro/SourceSansPro-Regular.ttf -------------------------------------------------------------------------------- /teammapper-frontend/src/assets/font/source-sans-pro/SourceSansPro-SemiBold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/b310-digital/teammapper/92e2d955a816b7e181ca9e2b77dcaaeb19d3e870/teammapper-frontend/src/assets/font/source-sans-pro/SourceSansPro-SemiBold.ttf -------------------------------------------------------------------------------- /teammapper-frontend/src/assets/font/source-sans-pro/SourceSansPro-SemiBoldItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/b310-digital/teammapper/92e2d955a816b7e181ca9e2b77dcaaeb19d3e870/teammapper-frontend/src/assets/font/source-sans-pro/SourceSansPro-SemiBoldItalic.ttf -------------------------------------------------------------------------------- /teammapper-frontend/src/assets/icons/icon-128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/b310-digital/teammapper/92e2d955a816b7e181ca9e2b77dcaaeb19d3e870/teammapper-frontend/src/assets/icons/icon-128x128.png -------------------------------------------------------------------------------- /teammapper-frontend/src/assets/icons/icon-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/b310-digital/teammapper/92e2d955a816b7e181ca9e2b77dcaaeb19d3e870/teammapper-frontend/src/assets/icons/icon-192x192.png -------------------------------------------------------------------------------- /teammapper-frontend/src/assets/icons/icon-72x72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/b310-digital/teammapper/92e2d955a816b7e181ca9e2b77dcaaeb19d3e870/teammapper-frontend/src/assets/icons/icon-72x72.png -------------------------------------------------------------------------------- /teammapper-frontend/src/assets/images/business-plan.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/b310-digital/teammapper/92e2d955a816b7e181ca9e2b77dcaaeb19d3e870/teammapper-frontend/src/assets/images/business-plan.png -------------------------------------------------------------------------------- /teammapper-frontend/src/assets/images/logo_ARASAAC.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/b310-digital/teammapper/92e2d955a816b7e181ca9e2b77dcaaeb19d3e870/teammapper-frontend/src/assets/images/logo_ARASAAC.png -------------------------------------------------------------------------------- /teammapper-frontend/src/assets/images/radial-tree.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/b310-digital/teammapper/92e2d955a816b7e181ca9e2b77dcaaeb19d3e870/teammapper-frontend/src/assets/images/radial-tree.png -------------------------------------------------------------------------------- /teammapper-frontend/src/assets/images/readme-header.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/b310-digital/teammapper/92e2d955a816b7e181ca9e2b77dcaaeb19d3e870/teammapper-frontend/src/assets/images/readme-header.png -------------------------------------------------------------------------------- /teammapper-frontend/src/assets/images/screens.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/b310-digital/teammapper/92e2d955a816b7e181ca9e2b77dcaaeb19d3e870/teammapper-frontend/src/assets/images/screens.png -------------------------------------------------------------------------------- /teammapper-frontend/src/assets/images/solar-system.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/b310-digital/teammapper/92e2d955a816b7e181ca9e2b77dcaaeb19d3e870/teammapper-frontend/src/assets/images/solar-system.png -------------------------------------------------------------------------------- /teammapper-frontend/src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true, 3 | version: '0.1.9', 4 | name: 'TeamMapper', 5 | pictogramApiUrl: '/arasaac/api/pictograms', 6 | pictogramStaticUrl: '/arasaac/images/pictograms', 7 | featureFlagPictograms: false, 8 | }; 9 | -------------------------------------------------------------------------------- /teammapper-frontend/src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: false, 3 | version: '0.4.0', 4 | name: 'TeamMapper', 5 | pictogramApiUrl: 'https://api.arasaac.org/v1/pictograms', 6 | pictogramStaticUrl: 'https://static.arasaac.org/pictograms', 7 | featureFlagPictograms: true, 8 | }; 9 | -------------------------------------------------------------------------------- /teammapper-frontend/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | TeamMapper 7 | 8 | 11 | 12 | 13 | 16 | 17 | 20 | 28 | 33 | 38 | 42 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /teammapper-frontend/src/main.ts: -------------------------------------------------------------------------------- 1 | import { enableProdMode } from '@angular/core'; 2 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 3 | 4 | import { RootModule } from './app/root.module'; 5 | import { environment } from './environments/environment'; 6 | 7 | if (environment.production) { 8 | enableProdMode(); 9 | } 10 | 11 | setTimeout(() => { 12 | platformBrowserDynamic().bootstrapModule(RootModule).catch(console.error); 13 | }, 0); 14 | -------------------------------------------------------------------------------- /teammapper-frontend/src/polyfills.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file includes polyfills needed by Angular and is loaded before the app. 3 | * You can add your own extra polyfills to this file. 4 | * 5 | * This file is divided into 2 sections: 6 | * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. 7 | * 2. Application imports. Files imported after ZoneJS that should be loaded before your main 8 | * file. 9 | * 10 | * The current setup is for so-called "evergreen" browsers; the last versions of browsers that 11 | * automatically update themselves. This includes recent versions of Safari, Chrome (including 12 | * Opera), Edge on the desktop, and iOS and Chrome on mobile. 13 | * 14 | * Learn more in https://angular.io/guide/browser-support 15 | */ 16 | 17 | /*************************************************************************************************** 18 | * BROWSER POLYFILLS 19 | */ 20 | 21 | /** 22 | * By default, zone.js will patch all possible macroTask and DomEvents 23 | * user can disable parts of macroTask/DomEvents patch by setting following flags 24 | * because those flags need to be set before `zone.js` being loaded, and webpack 25 | * will put import in the top of bundle, so user need to create a separate file 26 | * in this directory (for example: zone-flags.ts), and put the following flags 27 | * into that file, and then add the following code before importing zone.js. 28 | * import './zone-flags'; 29 | * 30 | * The flags allowed in zone-flags.ts are listed here. 31 | * 32 | * The following flags will work for all browsers. 33 | * 34 | * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame 35 | * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick 36 | * (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames 37 | * 38 | * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js 39 | * with the following flag, it will bypass `zone.js` patch for IE/Edge 40 | * 41 | * (window as any).__Zone_enable_cross_context_check = true; 42 | * 43 | */ 44 | 45 | /*************************************************************************************************** 46 | * Zone JS is required by default for Angular itself. 47 | */ 48 | import 'zone.js'; // Included with Angular CLI. 49 | 50 | /*************************************************************************************************** 51 | * APPLICATION IMPORTS 52 | */ 53 | -------------------------------------------------------------------------------- /teammapper-frontend/src/proxy.conf.json: -------------------------------------------------------------------------------- 1 | { 2 | "/socket.io": { 3 | "target": "http://localhost:3000", 4 | "secure": false, 5 | "changeOrigin": true, 6 | "ws": true 7 | }, 8 | "/api/**": { 9 | "target": "http://localhost:3000", 10 | "secure": false, 11 | "changeOrigin": true 12 | }, 13 | "/arasaac/*": { 14 | "target": "http://localhost:3000", 15 | "secure": false, 16 | "changeOrigin": true 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /teammapper-frontend/src/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: /map 3 | 4 | User-agent: GPTBot 5 | Disallow: / -------------------------------------------------------------------------------- /teammapper-frontend/src/styles.scss: -------------------------------------------------------------------------------- 1 | //@import '@angular/material' as mat; 2 | @use '@material-design-icons/font'; 3 | @use 'theme.scss'; 4 | 5 | @font-face { 6 | font-family: 'Source Sans Pro'; 7 | src: url('assets/font/source-sans-pro/SourceSansPro-Regular.ttf') 8 | format('truetype'); 9 | } 10 | 11 | @font-face { 12 | font-family: 'Fira Sans'; 13 | src: url('assets/font/firasans/FiraSans-Regular.ttf') format('truetype'); 14 | font-weight: 500; 15 | } 16 | 17 | @font-face { 18 | font-family: 'Fira Sans'; 19 | src: url('assets/font/firasans/FiraSans-Bold.ttf') format('truetype'); 20 | font-weight: 700; 21 | } 22 | 23 | html, 24 | body { 25 | margin: 0; 26 | padding: 0; 27 | height: 100%; 28 | } 29 | 30 | body { 31 | font-family: 'Fira Sans', sans; 32 | font-size: 17px; 33 | } 34 | 35 | .container { 36 | max-width: 1170px; 37 | margin: 0 auto; 38 | padding: 0 15px; 39 | } 40 | 41 | *:focus { 42 | outline: none; 43 | } 44 | 45 | // Color picker override 46 | div.color-picker { 47 | border-radius: 2px; 48 | border: 1px solid #a8a8a8 !important; 49 | 50 | div.box:last-child { 51 | display: none !important; 52 | } 53 | 54 | div.arrow { 55 | top: 170px !important; 56 | } 57 | } 58 | 59 | // Snackbar 60 | snack-bar-container.snackbar { 61 | background-color: whitesmoke; 62 | border: 1px solid lightgray; 63 | border-bottom: 0; 64 | color: #555555; 65 | 66 | simple-snack-bar.mat-simple-snackbar { 67 | font-size: 15px; 68 | 69 | button { 70 | color: #555555; 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /teammapper-frontend/src/test/mocks/mmp-index.ts: -------------------------------------------------------------------------------- 1 | const config = { 2 | Mmp: jest.fn().mockImplementation(() => ({ 3 | on: jest.fn(), 4 | remove: jest.fn(), 5 | center: jest.fn(), 6 | new: jest.fn(), 7 | })), 8 | }; 9 | 10 | export default config; 11 | -------------------------------------------------------------------------------- /teammapper-frontend/src/test/mocks/mmp-map.ts: -------------------------------------------------------------------------------- 1 | module.exports = jest.fn().mockImplementation(() => ({ 2 | on: jest.fn(), 3 | remove: jest.fn(), 4 | center: jest.fn(), 5 | new: jest.fn(), 6 | })); 7 | -------------------------------------------------------------------------------- /teammapper-frontend/src/theme.scss: -------------------------------------------------------------------------------- 1 | @use '@angular/material' as mat; 2 | 3 | @include mat.core(); 4 | 5 | // Define the palettes for your theme using the Material Design palettes available in palette.scss 6 | // (imported above). For each palette, you can optionally specify a default, lighter, and darker 7 | // hue. Available color palettes: https://www.google.com/design/spec/style/color.html 8 | $app-primary: mat.m2-define-palette(mat.$m2-blue-gray-palette, 700); 9 | $app-accent: mat.m2-define-palette(mat.$m2-gray-palette, 50); 10 | 11 | // The warn palette is optional (defaults to red). 12 | $app-warn: mat.m2-define-palette(mat.$m2-red-palette); 13 | 14 | // Create the theme object (a Sass map containing all of the palettes). 15 | $app-theme: mat.m2-define-light-theme( 16 | ( 17 | color: ( 18 | primary: $app-primary, 19 | accent: $app-accent, 20 | warn: $app-warn, 21 | ), 22 | typography: mat.m2-define-typography-config(), 23 | density: 0, 24 | ) 25 | ); 26 | 27 | // Include theme styles for core and each component used in your app. 28 | // Alternatively, you can import and @include the theme mixins for each component 29 | // that you are using. 30 | @include mat.all-component-themes($app-theme); 31 | -------------------------------------------------------------------------------- /teammapper-frontend/src/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "resolveJsonModule": true, 5 | "outDir": "../out-tsc/app", 6 | "types": ["node"] 7 | }, 8 | "include": ["**/*.ts"], 9 | "exclude": ["**/*.spec.ts", "**/__mocks__/*", "test/*"] 10 | } 11 | -------------------------------------------------------------------------------- /teammapper-frontend/src/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "../tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "./out-tsc/spec", 6 | "types": ["jest"] 7 | }, 8 | "files": ["polyfills.ts"], 9 | "include": ["./**/*.spec.ts", "./**/*.d.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /teammapper-frontend/src/typings.d.ts: -------------------------------------------------------------------------------- 1 | interface NodeModule { 2 | id: string; 3 | } 4 | -------------------------------------------------------------------------------- /teammapper-frontend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "baseUrl": "./", 5 | "outDir": "./dist/out-tsc", 6 | "sourceMap": true, 7 | "declaration": false, 8 | "esModuleInterop": true, 9 | "module": "esnext", 10 | "moduleResolution": "node", 11 | "emitDecoratorMetadata": true, 12 | "skipLibCheck": true, 13 | "experimentalDecorators": true, 14 | "importHelpers": true, 15 | "target": "ES2022", 16 | "typeRoots": [ 17 | "node_modules/@types" 18 | ], 19 | "lib": [ 20 | "es2018", 21 | "dom" 22 | ], 23 | "paths": { 24 | "@mmp/*": [ 25 | "mmp/src/*" 26 | ] 27 | }, 28 | "useDefineForClassFields": false 29 | }, 30 | "exclude": [ 31 | "node_modules", 32 | "dist" 33 | ], 34 | } --------------------------------------------------------------------------------