├── .github ├── ISSUE_TEMPLATE │ └── bug_report.md ├── dependabot.yml ├── labeler.yml └── workflows │ ├── build-backend.yml │ ├── build-client.yml │ ├── greetings.yml │ ├── label.yml │ └── stale.yml ├── .gitignore ├── GUIDE.md ├── LICENSE ├── README.md ├── api ├── .mvn │ └── wrapper │ │ ├── maven-wrapper.jar │ │ └── maven-wrapper.properties ├── lombok.config ├── mvnw ├── mvnw.cmd ├── pom.xml └── src │ ├── main │ ├── java │ │ └── com │ │ │ └── example │ │ │ └── demo │ │ │ ├── Application.java │ │ │ ├── application │ │ │ ├── AppConfig.java │ │ │ ├── DataInitializer.java │ │ │ ├── SecurityConfig.java │ │ │ └── SessionConfig.java │ │ │ ├── domain │ │ │ ├── exception │ │ │ │ ├── CommentNotFoundException.java │ │ │ │ └── PostNotFoundException.java │ │ │ ├── model │ │ │ │ ├── Comment.java │ │ │ │ ├── Post.java │ │ │ │ ├── Status.java │ │ │ │ └── User.java │ │ │ └── repository │ │ │ │ ├── CommentRepository.java │ │ │ │ ├── PostRepository.java │ │ │ │ └── UserRepository.java │ │ │ ├── infrastructure │ │ │ ├── MongoConfig.java │ │ │ └── persistence │ │ │ │ ├── MongoCommentRepository.java │ │ │ │ ├── MongoPostRepository.java │ │ │ │ └── MongoUserRepository.java │ │ │ └── interfaces │ │ │ ├── CurrentUserController.java │ │ │ ├── PostController.java │ │ │ ├── RestExceptionHandler.java │ │ │ ├── UserController.java │ │ │ ├── WebConfig.java │ │ │ └── dto │ │ │ ├── CommentForm.java │ │ │ ├── CreatPostCommand.java │ │ │ ├── LoginRequest.java │ │ │ ├── PaginatedResult.java │ │ │ ├── PostSummary.java │ │ │ ├── UpdatePostCommand.java │ │ │ └── UpdatePostStatusCommand.java │ └── resources │ │ └── application.yml │ └── test │ ├── java │ └── com │ │ └── example │ │ └── demo │ │ ├── IntegrationTests.java │ │ ├── PostControllerTest.java │ │ └── PostRepositoryTest.java │ └── resources │ └── application.properties ├── docker-compose.yml ├── start.png └── ui ├── .circleci └── config.yml ├── .editorconfig ├── .eslintrc.js ├── .gitignore ├── .prettierignore ├── .prettierrc ├── .prettierrc.js ├── .storybook ├── main.js └── tsconfig.json ├── .travis.yml ├── Dockerfile ├── README.md ├── apps ├── shared-components-e2e │ ├── .eslintrc.json │ ├── cypress.json │ ├── project.json │ ├── src │ │ ├── fixtures │ │ │ └── example.json │ │ └── support │ │ │ ├── commands.ts │ │ │ └── index.ts │ └── tsconfig.json ├── todolist-e2e │ ├── .eslintrc.json │ ├── cypress.json │ ├── project.json │ ├── src │ │ ├── fixtures │ │ │ └── example.json │ │ ├── integration │ │ │ └── app.spec.ts │ │ └── support │ │ │ ├── app.po.ts │ │ │ ├── commands.ts │ │ │ └── index.ts │ └── tsconfig.json └── todolist │ ├── jest.config.ts │ ├── karma.conf.js │ ├── project.json │ ├── src │ ├── app │ │ ├── app-routing.module.ts │ │ ├── app.component.css │ │ ├── app.component.html │ │ ├── app.component.spec.ts │ │ ├── app.component.ts │ │ ├── app.module.ts │ │ ├── auth │ │ │ ├── auth-routing.module.ts │ │ │ ├── auth.module.ts │ │ │ └── signin │ │ │ │ ├── signin.component.css │ │ │ │ ├── signin.component.html │ │ │ │ └── signin.component.ts │ │ ├── core │ │ │ ├── auth-guard.ts │ │ │ ├── auth-inteceptor.ts │ │ │ ├── auth.service.ts │ │ │ ├── core.module.ts │ │ │ ├── credentials.model.ts │ │ │ ├── load-guard.ts │ │ │ ├── token-inteceptor.ts │ │ │ ├── token-storage.ts │ │ │ └── user.model.ts │ │ ├── dialog │ │ │ ├── dialog.component.css │ │ │ ├── dialog.component.html │ │ │ └── dialog.component.ts │ │ ├── home │ │ │ ├── home-routing.module.ts │ │ │ ├── home.component.css │ │ │ ├── home.component.html │ │ │ ├── home.component.spec.ts │ │ │ ├── home.component.ts │ │ │ └── home.module.ts │ │ ├── http-client.spec.ts │ │ ├── http-error-handler.service.ts │ │ ├── message.service.ts │ │ ├── post │ │ │ ├── edit-post │ │ │ │ ├── edit-post.component.css │ │ │ │ ├── edit-post.component.html │ │ │ │ └── edit-post.component.ts │ │ │ ├── new-post │ │ │ │ ├── new-post.component.css │ │ │ │ ├── new-post.component.html │ │ │ │ └── new-post.component.ts │ │ │ ├── post-details │ │ │ │ ├── comment │ │ │ │ │ ├── comment-form.component.css │ │ │ │ │ ├── comment-form.component.html │ │ │ │ │ ├── comment-form.component.ts │ │ │ │ │ ├── comment-list-item.component.css │ │ │ │ │ ├── comment-list-item.component.html │ │ │ │ │ ├── comment-list-item.component.ts │ │ │ │ │ ├── comment-list.component.css │ │ │ │ │ ├── comment-list.component.html │ │ │ │ │ ├── comment-list.component.ts │ │ │ │ │ ├── comment-panel.component.css │ │ │ │ │ ├── comment-panel.component.html │ │ │ │ │ └── comment-panel.component.ts │ │ │ │ ├── post-details-panel │ │ │ │ │ ├── post-details-panel.component.css │ │ │ │ │ ├── post-details-panel.component.html │ │ │ │ │ └── post-details-panel.component.ts │ │ │ │ ├── post-details.component.css │ │ │ │ ├── post-details.component.html │ │ │ │ └── post-details.component.ts │ │ │ ├── post-list │ │ │ │ ├── post-list.component.css │ │ │ │ ├── post-list.component.html │ │ │ │ └── post-list.component.ts │ │ │ ├── post-routing.module.ts │ │ │ ├── post.module.ts │ │ │ └── shared │ │ │ │ ├── comment.model.ts │ │ │ │ ├── post-details-resolve.ts │ │ │ │ ├── post-form │ │ │ │ ├── post-form.component.css │ │ │ │ ├── post-form.component.html │ │ │ │ ├── post-form.component.spec.ts │ │ │ │ └── post-form.component.ts │ │ │ │ ├── post.model.ts │ │ │ │ ├── post.service.spec.ts │ │ │ │ ├── post.service.ts │ │ │ │ └── username.model.ts │ │ ├── shared │ │ │ ├── nl2br.pipe.spec.ts │ │ │ ├── nl2br.pipe.ts │ │ │ ├── shared.module.ts │ │ │ └── show-authed.directive.ts │ │ └── user │ │ │ ├── profile.service.ts │ │ │ ├── profile │ │ │ ├── profile.component.css │ │ │ ├── profile.component.html │ │ │ └── profile.component.ts │ │ │ ├── user-routing.module.ts │ │ │ └── user.module.ts │ ├── assets │ │ ├── .gitkeep │ │ └── avatars.svg │ ├── environments │ │ ├── environment.cors.ts │ │ ├── environment.prod.ts │ │ └── environment.ts │ ├── favicon.ico │ ├── index.html │ ├── main.ts │ ├── polyfills.ts │ ├── styles.css │ ├── test.ts │ ├── theme.scss │ └── typings.d.ts │ ├── tailwind.config.js │ ├── tsconfig.app.json │ ├── tsconfig.json │ └── tsconfig.spec.json ├── decorate-angular-cli.js ├── jest.config.ts ├── jest.preset.js ├── karma.conf.js ├── libs ├── .gitkeep └── shared │ └── components │ ├── .eslintrc.json │ ├── .storybook │ ├── main.js │ ├── preview.js │ └── tsconfig.json │ ├── README.md │ ├── jest.config.ts │ ├── project.json │ ├── src │ ├── index.ts │ ├── lib │ │ └── shared-components.module.ts │ └── test-setup.ts │ ├── tsconfig.json │ ├── tsconfig.lib.json │ └── tsconfig.spec.json ├── migrations.json ├── nginx └── nginx.conf ├── nx.json ├── package.json ├── proxy.conf.json ├── storybook-migration-summary.md ├── tailwind.config.js ├── testing ├── activated-route-stub.ts ├── async-observable-helpers.ts ├── global-jasmine.ts ├── index.ts ├── jasmine-matchers.d.ts ├── jasmine-matchers.ts └── router-link-directive-stub.ts ├── tools └── tsconfig.tools.json ├── tsconfig.base.json └── tslint.json /.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: 'hantsy' 7 | 8 | --- 9 | 10 | **Development Environment:** 11 | - OS: [e.g. Windows 10, Linux] 12 | - Java version:[8, 11] 13 | - Build tools:[Gradle, Maven] 14 | 15 | **Describe the bug** 16 | A clear and concise description of what the bug is. 17 | 18 | **To Reproduce** 19 | Steps to reproduce the behavior: 20 | 1. Go to '...' 21 | 2. Click on '....' 22 | 3. Scroll down to '....' 23 | 4. See error 24 | 25 | **Expected behavior** 26 | A clear and concise description of what you expected to happen. 27 | 28 | **Screenshots** 29 | If applicable, add screenshots to help explain your problem. 30 | 31 | **Additional context** 32 | Add any other context about the problem here. 33 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | - package-ecosystem: "npm" 8 | directory: "/ui" 9 | schedule: 10 | interval: monthly 11 | - package-ecosystem: "maven" 12 | directory: "/api" 13 | schedule: 14 | interval: monthly 15 | open-pull-requests-limit: 10 16 | reviewers: 17 | - "hantsy" 18 | assignees: 19 | - "hantsy" 20 | labels: 21 | - "dependencies" 22 | - "maven" 23 | -------------------------------------------------------------------------------- /.github/labeler.yml: -------------------------------------------------------------------------------- 1 | # Add 'backend' label 2 | backend: 3 | - server/**/* 4 | 5 | # Add 'client' label 6 | angular: 7 | - client/**/* 8 | -------------------------------------------------------------------------------- /.github/workflows/build-backend.yml: -------------------------------------------------------------------------------- 1 | name: Server 2 | 3 | on: 4 | push: 5 | paths-ignore: 6 | - "docs/**" 7 | - "ui/**" 8 | branches: 9 | - master 10 | - release/* 11 | pull_request: 12 | types: 13 | - opened 14 | - synchronize 15 | - reopened 16 | 17 | jobs: 18 | build-server: 19 | runs-on: ubuntu-latest 20 | 21 | steps: 22 | - uses: actions/checkout@v4 23 | with: 24 | # Disabling shallow clone is recommended for improving relevancy of reporting 25 | fetch-depth: 0 26 | 27 | - name: Set up JDK 17 28 | uses: actions/setup-java@v4 29 | with: 30 | java-version: 17 31 | distribution: "zulu" 32 | cache: "maven" 33 | 34 | - name: Cache Maven packages 35 | uses: actions/cache@v4 36 | with: 37 | path: ~/.m2 38 | key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }} 39 | restore-keys: ${{ runner.os }}-m2 40 | 41 | - name: Resolve dependencies 42 | run: mvn clean dependency:go-offline --file api/pom.xml 43 | 44 | - name: Build with Maven 45 | run: mvn package --file api/pom.xml -DskipTests 46 | 47 | 48 | - name: Build Docker Image 49 | run: mvn spring-boot:build-image --file api/pom.xml -DskipTests 50 | 51 | # - name: Login to DockerHub Registry 52 | # run: echo ${{ secrets.DOCKERHUB_PASSWORD }} | docker login -u ${{ secrets.DOCKERHUB_USERNAME }} --password-stdin 53 | 54 | # - name: Push Docker Image 55 | # run: docker push hantsy/angular-spring-reactive-sample-server 56 | -------------------------------------------------------------------------------- /.github/workflows/build-client.yml: -------------------------------------------------------------------------------- 1 | name: Client 2 | on: 3 | push: 4 | paths-ignore: 5 | - "docs/**" 6 | - "api/**" 7 | branches: 8 | - master 9 | - release/* 10 | pull_request: 11 | types: 12 | - opened 13 | - synchronize 14 | - reopened 15 | 16 | jobs: 17 | build-client: 18 | runs-on: ubuntu-latest 19 | 20 | steps: 21 | - uses: actions/checkout@v4 22 | - name: Setup NodeJS 23 | uses: actions/setup-node@v4 24 | with: 25 | node-version: "14" 26 | 27 | - uses: actions/cache@v4 28 | with: 29 | path: ~/.npm 30 | key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} 31 | restore-keys: | 32 | ${{ runner.os }}-node- 33 | 34 | - name: Install Dependencies & Build Docker Image 35 | run: | 36 | cd ./ui 37 | npm install 38 | npm run build 39 | docker build -t hantsy/angular-spring-reactive-sample-client . 40 | 41 | # - name: Login to DockerHub Registry 42 | # run: echo ${{ secrets.DOCKERHUB_PASSWORD }} | docker login -u ${{ secrets.DOCKERHUB_USERNAME }} --password-stdin 43 | 44 | # - name: Push Docker Image 45 | # run: docker push hantsy/angular-spring-reactive-sample-client 46 | -------------------------------------------------------------------------------- /.github/workflows/greetings.yml: -------------------------------------------------------------------------------- 1 | name: Greetings 2 | 3 | on: [pull_request, issues] 4 | 5 | jobs: 6 | greeting: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/first-interaction@v1 10 | with: 11 | repo-token: ${{ secrets.GITHUB_TOKEN }} 12 | issue-message: "Thanks for reporting issues" 13 | pr-message: "Thanks for sending your prs" 14 | -------------------------------------------------------------------------------- /.github/workflows/label.yml: -------------------------------------------------------------------------------- 1 | # This workflow will triage pull requests and apply a label based on the 2 | # paths that are modified in the pull request. 3 | # 4 | # To use this workflow, you will need to set up a .github/labeler.yml 5 | # file with configuration. For more information, see: 6 | # https://github.com/actions/labeler/blob/master/README.md 7 | 8 | name: "Pull Request Labeler" 9 | on: 10 | - pull_request 11 | 12 | jobs: 13 | label: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/labeler@v5 17 | with: 18 | repo-token: "${{ secrets.GITHUB_TOKEN }}" 19 | -------------------------------------------------------------------------------- /.github/workflows/stale.yml: -------------------------------------------------------------------------------- 1 | name: Mark stale issues and pull requests 2 | 3 | on: 4 | schedule: 5 | - cron: "0 0 * * *" 6 | 7 | jobs: 8 | stale: 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | - uses: actions/stale@v9 13 | with: 14 | repo-token: ${{ secrets.GITHUB_TOKEN }} 15 | stale-issue-message: "Please add extra info to clarify this issue, if none provided, it will be marked as stale" 16 | stale-pr-message: "Please update the pr according to feedbacks, else it will be marked as stale" 17 | stale-issue-label: "no-issue-activity" 18 | stale-pr-label: "no-pr-activity" 19 | days-before-stale: 30 20 | days-before-close: 5 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | !.mvn/wrapper/maven-wrapper.jar 3 | 4 | ### STS ### 5 | .apt_generated 6 | .classpath 7 | .factorypath 8 | .project 9 | .settings 10 | .springBeans 11 | 12 | ### IntelliJ IDEA ### 13 | .idea 14 | *.iws 15 | *.iml 16 | *.ipr 17 | 18 | ### NetBeans ### 19 | nbproject/private/ 20 | build/ 21 | nbbuild/ 22 | dist/ 23 | nbdist/ 24 | .nb-gradle/ 25 | 26 | node_modules 27 | dist/ 28 | package-lock.json 29 | .vscode 30 | .angular/ 31 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Build Client](https://github.com/hantsy/angular-spring-reactive-sample/workflows/Client/badge.svg) 2 | ![Build Server Side](https://github.com/hantsy/angular-spring-reactive-sample/workflows/Server/badge.svg) 3 | 4 | 5 | 6 | **Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)* 7 | 8 | - [Angular Spring Reactive Sample](#angular-spring-reactive-sample) 9 | - [Project structure](#project-structure) 10 | - [Build](#build) 11 | - [Server](#server) 12 | - [Client](#client) 13 | - [Contribute](#contribute) 14 | 15 | 16 | 17 | I've also created a series of projects to demo Angular and Spring WebFlux using other protocols: 18 | 19 | * [Angular and Websocket sample](https://github.com/hantsy/angular-spring-websocket-sample) 20 | * [Angular and Server Sent Event sample](https://github.com/hantsy/angular-spring-sse-sample) 21 | * [Angular and RSocket sample](https://github.com/hantsy/angular-spring-rsocket-sample) 22 | 23 | # Angular Spring Reactive Sample 24 | 25 | This application demonstrate building backend RESTful APIs with the newest Reactive stack introduced in Spring 5, and creating the frontend SPA with Angular 5. 26 | 27 | **Read the [comprehensive step by step guide](GUIDE.md) to get more details**. 28 | 29 | ## Project structure 30 | 31 | * client - The client application built with Angular CLI. 32 | * server - The backend RESTful APIs. 33 | 34 | 35 | ## Build and Run 36 | 37 | Clone the source codes into your local system. 38 | 39 | ``` 40 | git clone https://github.com/hantsy/angular-spring-reactive-sample 41 | ``` 42 | 43 | ### Server 44 | 45 | The backend is a Spring Boot based application, make sure you have installed the following software: 46 | 47 | * Apache Maven 48 | * Oracle JDK 8 49 | * Docker & Docker Compose 50 | 51 | There is a *docker-compose.yml* file in the project root folder. 52 | 53 | Starts up required MongoDb and Reids service in the background by executing the following command. 54 | 55 | ``` 56 | docker-compose up 57 | ``` 58 | 59 | > NOTE: You can also install a local MongoDb and Redis instead of using Docker. 60 | 61 | Then run the application by Spring boot maven plugin directly. 62 | 63 | ``` 64 | mvn spring-boot:run 65 | ``` 66 | 67 | ### Client 68 | 69 | The **client** application is generated by Angular CLI. 70 | 71 | Enter **client** folder, execute the following command to run the frontend UI. 72 | 73 | ``` 74 | npm install 75 | npm run start 76 | ``` 77 | 78 | Open your favorite browser, and navigate to http://localhost:4200. 79 | 80 | ## CORS 81 | 82 | By default, I do not use a CORS config to run the Server side in this sample application. 83 | 84 | But if you do not like to use a `proxy.conf.js` in the Angular config, follow the following steps to enable **cors** support in the backend, and connect to the backend directly in the client side. 85 | 86 | ### Server 87 | 88 | Activate the **cors** profile when running the Spring Boot application. 89 | 90 | ```bash 91 | java -jar target/app.jar --spring.profiles.active=cors 92 | ``` 93 | 94 | ### Client 95 | 96 | There is a standalone configuration **cors** added in the Angular config to connect to the backend directly. 97 | 98 | ```bash 99 | npm run start:cors 100 | ``` 101 | 102 | ## Contribute 103 | 104 | Welcome to contribute this project. If you have some ideas do not hesitate to file an issue or send a PR directly. 105 | -------------------------------------------------------------------------------- /api/.mvn/wrapper/maven-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hantsy/angular-spring-reactive-sample/c931f218f69f576da27da2b2d48bdf0c43352b4b/api/.mvn/wrapper/maven-wrapper.jar -------------------------------------------------------------------------------- /api/.mvn/wrapper/maven-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionUrl=https://repo1.maven.org/maven2/org/apache/maven/apache-maven/3.5.0/apache-maven-3.5.0-bin.zip 2 | -------------------------------------------------------------------------------- /api/lombok.config: -------------------------------------------------------------------------------- 1 | lombok.noArgsConstructor.extraPrivate=true 2 | -------------------------------------------------------------------------------- /api/mvnw.cmd: -------------------------------------------------------------------------------- 1 | @REM ---------------------------------------------------------------------------- 2 | @REM Licensed to the Apache Software Foundation (ASF) under one 3 | @REM or more contributor license agreements. See the NOTICE file 4 | @REM distributed with this work for additional information 5 | @REM regarding copyright ownership. The ASF licenses this file 6 | @REM to you under the Apache License, Version 2.0 (the 7 | @REM "License"); you may not use this file except in compliance 8 | @REM with the License. You may obtain a copy of the License at 9 | @REM 10 | @REM http://www.apache.org/licenses/LICENSE-2.0 11 | @REM 12 | @REM Unless required by applicable law or agreed to in writing, 13 | @REM software distributed under the License is distributed on an 14 | @REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | @REM KIND, either express or implied. See the License for the 16 | @REM specific language governing permissions and limitations 17 | @REM under the License. 18 | @REM ---------------------------------------------------------------------------- 19 | 20 | @REM ---------------------------------------------------------------------------- 21 | @REM Maven2 Start Up Batch script 22 | @REM 23 | @REM Required ENV vars: 24 | @REM JAVA_HOME - location of a JDK home dir 25 | @REM 26 | @REM Optional ENV vars 27 | @REM M2_HOME - location of maven2's installed home dir 28 | @REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands 29 | @REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a key stroke before ending 30 | @REM MAVEN_OPTS - parameters passed to the Java VM when running Maven 31 | @REM e.g. to debug Maven itself, use 32 | @REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 33 | @REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files 34 | @REM ---------------------------------------------------------------------------- 35 | 36 | @REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' 37 | @echo off 38 | @REM enable echoing my setting MAVEN_BATCH_ECHO to 'on' 39 | @if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% 40 | 41 | @REM set %HOME% to equivalent of $HOME 42 | if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") 43 | 44 | @REM Execute a user defined script before this one 45 | if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre 46 | @REM check for pre script, once with legacy .bat ending and once with .cmd ending 47 | if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" 48 | if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" 49 | :skipRcPre 50 | 51 | @setlocal 52 | 53 | set ERROR_CODE=0 54 | 55 | @REM To isolate internal variables from possible post scripts, we use another setlocal 56 | @setlocal 57 | 58 | @REM ==== START VALIDATION ==== 59 | if not "%JAVA_HOME%" == "" goto OkJHome 60 | 61 | echo. 62 | echo Error: JAVA_HOME not found in your environment. >&2 63 | echo Please set the JAVA_HOME variable in your environment to match the >&2 64 | echo location of your Java installation. >&2 65 | echo. 66 | goto error 67 | 68 | :OkJHome 69 | if exist "%JAVA_HOME%\bin\java.exe" goto init 70 | 71 | echo. 72 | echo Error: JAVA_HOME is set to an invalid directory. >&2 73 | echo JAVA_HOME = "%JAVA_HOME%" >&2 74 | echo Please set the JAVA_HOME variable in your environment to match the >&2 75 | echo location of your Java installation. >&2 76 | echo. 77 | goto error 78 | 79 | @REM ==== END VALIDATION ==== 80 | 81 | :init 82 | 83 | @REM Find the project base dir, i.e. the directory that contains the folder ".mvn". 84 | @REM Fallback to current working directory if not found. 85 | 86 | set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% 87 | IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir 88 | 89 | set EXEC_DIR=%CD% 90 | set WDIR=%EXEC_DIR% 91 | :findBaseDir 92 | IF EXIST "%WDIR%"\.mvn goto baseDirFound 93 | cd .. 94 | IF "%WDIR%"=="%CD%" goto baseDirNotFound 95 | set WDIR=%CD% 96 | goto findBaseDir 97 | 98 | :baseDirFound 99 | set MAVEN_PROJECTBASEDIR=%WDIR% 100 | cd "%EXEC_DIR%" 101 | goto endDetectBaseDir 102 | 103 | :baseDirNotFound 104 | set MAVEN_PROJECTBASEDIR=%EXEC_DIR% 105 | cd "%EXEC_DIR%" 106 | 107 | :endDetectBaseDir 108 | 109 | IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig 110 | 111 | @setlocal EnableExtensions EnableDelayedExpansion 112 | for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a 113 | @endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% 114 | 115 | :endReadAdditionalConfig 116 | 117 | SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" 118 | 119 | set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" 120 | set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain 121 | 122 | %MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* 123 | if ERRORLEVEL 1 goto error 124 | goto end 125 | 126 | :error 127 | set ERROR_CODE=1 128 | 129 | :end 130 | @endlocal & set ERROR_CODE=%ERROR_CODE% 131 | 132 | if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost 133 | @REM check for post script, once with legacy .bat ending and once with .cmd ending 134 | if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" 135 | if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" 136 | :skipRcPost 137 | 138 | @REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' 139 | if "%MAVEN_BATCH_PAUSE%" == "on" pause 140 | 141 | if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% 142 | 143 | exit /B %ERROR_CODE% 144 | -------------------------------------------------------------------------------- /api/src/main/java/com/example/demo/Application.java: -------------------------------------------------------------------------------- 1 | package com.example.demo; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | 6 | @SpringBootApplication 7 | public class Application { 8 | 9 | public static void main(String[] args) { 10 | SpringApplication.run(Application.class, args); 11 | } 12 | 13 | } 14 | 15 | -------------------------------------------------------------------------------- /api/src/main/java/com/example/demo/application/AppConfig.java: -------------------------------------------------------------------------------- 1 | package com.example.demo.application; 2 | 3 | import org.springframework.context.annotation.Bean; 4 | import org.springframework.context.annotation.Configuration; 5 | import org.springframework.security.crypto.factory.PasswordEncoderFactories; 6 | import org.springframework.security.crypto.password.PasswordEncoder; 7 | 8 | @Configuration 9 | public class AppConfig { 10 | 11 | @Bean 12 | public PasswordEncoder passwordEncoder() { 13 | return PasswordEncoderFactories.createDelegatingPasswordEncoder(); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /api/src/main/java/com/example/demo/application/DataInitializer.java: -------------------------------------------------------------------------------- 1 | /* 2 | * To change this license header, choose License Headers in Project Properties. 3 | * To change this template file, choose Tools | Templates 4 | * and open the template in the editor. 5 | */ 6 | package com.example.demo.application; 7 | 8 | import com.example.demo.domain.model.User; 9 | import com.example.demo.domain.repository.PostRepository; 10 | import com.example.demo.domain.repository.UserRepository; 11 | import lombok.RequiredArgsConstructor; 12 | import lombok.extern.slf4j.Slf4j; 13 | import org.springframework.boot.context.event.ApplicationReadyEvent; 14 | import org.springframework.context.event.EventListener; 15 | import org.springframework.security.crypto.password.PasswordEncoder; 16 | import org.springframework.stereotype.Component; 17 | import reactor.core.publisher.Flux; 18 | 19 | import java.util.Arrays; 20 | import java.util.List; 21 | 22 | /** 23 | * @author hantsy 24 | */ 25 | @Component 26 | @Slf4j 27 | @RequiredArgsConstructor 28 | class DataInitializer { 29 | 30 | private final PostRepository posts; 31 | private final UserRepository users; 32 | private final PasswordEncoder passwordEncoder; 33 | 34 | @EventListener(value = ApplicationReadyEvent.class) 35 | public void init() { 36 | initPosts(); 37 | initUsers(); 38 | } 39 | 40 | private void initUsers() { 41 | log.info("start users initialization ..."); 42 | this.users 43 | .deleteAll() 44 | .thenMany( 45 | Flux 46 | .just("user", "admin") 47 | .flatMap( 48 | username -> { 49 | List roles = "user".equals(username) 50 | ? Arrays.asList("ROLE_USER") 51 | : Arrays.asList("ROLE_USER", "ROLE_ADMIN"); 52 | 53 | User user = User.builder() 54 | .roles(roles) 55 | .username(username) 56 | .password(passwordEncoder.encode("password")) 57 | .email(username + "@example.com") 58 | .build(); 59 | return this.users.create(user); 60 | } 61 | ) 62 | ) 63 | .log() 64 | .subscribe( 65 | null, 66 | null, 67 | () -> log.info("done users initialization...") 68 | ); 69 | } 70 | 71 | private void initPosts() { 72 | log.info("start post data initialization ..."); 73 | this.posts 74 | .deleteAll() 75 | .thenMany( 76 | Flux 77 | .just("Post one", "Post two") 78 | .flatMap( 79 | title -> this.posts.create(title, "content of " + title) 80 | ) 81 | ) 82 | .log() 83 | .subscribe( 84 | null, 85 | null, 86 | () -> log.info("done post initialization...") 87 | ); 88 | } 89 | 90 | 91 | } 92 | -------------------------------------------------------------------------------- /api/src/main/java/com/example/demo/application/SessionConfig.java: -------------------------------------------------------------------------------- 1 | package com.example.demo.application; 2 | 3 | import org.springframework.context.annotation.Bean; 4 | import org.springframework.context.annotation.Configuration; 5 | import org.springframework.web.server.session.HeaderWebSessionIdResolver; 6 | import org.springframework.web.server.session.WebSessionIdResolver; 7 | 8 | @Configuration 9 | public 10 | class SessionConfig { 11 | public final static String xAuthToken = "X-AUTH-TOKEN"; 12 | 13 | @Bean 14 | public WebSessionIdResolver webSessionIdResolver() { 15 | var resolver = new HeaderWebSessionIdResolver(); 16 | resolver.setHeaderName(xAuthToken); 17 | return resolver; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /api/src/main/java/com/example/demo/domain/exception/CommentNotFoundException.java: -------------------------------------------------------------------------------- 1 | package com.example.demo.domain.exception; 2 | 3 | public class CommentNotFoundException extends RuntimeException { 4 | public CommentNotFoundException(String id) { 5 | super("Comment #" + id + " was not found."); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /api/src/main/java/com/example/demo/domain/exception/PostNotFoundException.java: -------------------------------------------------------------------------------- 1 | package com.example.demo.domain.exception; 2 | 3 | public class PostNotFoundException extends RuntimeException { 4 | public PostNotFoundException(String id) { 5 | super("Post #" + id + " was not found."); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /api/src/main/java/com/example/demo/domain/model/Comment.java: -------------------------------------------------------------------------------- 1 | package com.example.demo.domain.model; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Builder; 5 | import lombok.Data; 6 | import lombok.NoArgsConstructor; 7 | import org.springframework.data.annotation.*; 8 | import org.springframework.data.mongodb.core.mapping.Document; 9 | 10 | import jakarta.validation.constraints.NotBlank; 11 | import java.io.Serializable; 12 | import java.time.LocalDateTime; 13 | 14 | @Document(collection = "comments") 15 | @Data 16 | @Builder 17 | @NoArgsConstructor 18 | @AllArgsConstructor 19 | public class Comment implements Serializable { 20 | 21 | @Id 22 | private String id; 23 | 24 | @NotBlank 25 | private String content; 26 | 27 | @CreatedDate 28 | private LocalDateTime createdDate; 29 | 30 | @CreatedBy 31 | private String createdBy; 32 | 33 | @LastModifiedDate 34 | private LocalDateTime lastModifiedDate; 35 | 36 | @LastModifiedBy 37 | private String lastModifiedBy; 38 | 39 | } 40 | -------------------------------------------------------------------------------- /api/src/main/java/com/example/demo/domain/model/Post.java: -------------------------------------------------------------------------------- 1 | package com.example.demo.domain.model; 2 | 3 | import jakarta.validation.constraints.NotBlank; 4 | import lombok.*; 5 | import org.springframework.data.annotation.*; 6 | import org.springframework.data.mongodb.core.mapping.Document; 7 | import org.springframework.data.mongodb.core.mapping.DocumentReference; 8 | 9 | import java.io.Serializable; 10 | import java.time.LocalDateTime; 11 | import java.util.Collections; 12 | import java.util.List; 13 | 14 | import static com.example.demo.domain.model.Status.DRAFT; 15 | 16 | @Document(collection = "posts") 17 | @Data 18 | @ToString 19 | @Builder 20 | @NoArgsConstructor 21 | @AllArgsConstructor 22 | public class Post implements Serializable { 23 | 24 | @Id 25 | private String id; 26 | 27 | @NotBlank 28 | private String title; 29 | 30 | @NotBlank 31 | private String content; 32 | 33 | @Builder.Default 34 | private Status status = DRAFT; 35 | 36 | @DocumentReference 37 | @Builder.Default 38 | List comments = Collections.emptyList(); 39 | 40 | // @Version 41 | // @Builder.Default 42 | // Long version = null; 43 | 44 | @CreatedDate 45 | private LocalDateTime createdDate; 46 | 47 | @CreatedBy 48 | private String createdBy; 49 | 50 | @LastModifiedDate 51 | private LocalDateTime lastModifiedDate; 52 | 53 | @LastModifiedBy 54 | private String lastModifiedBy; 55 | } 56 | -------------------------------------------------------------------------------- /api/src/main/java/com/example/demo/domain/model/Status.java: -------------------------------------------------------------------------------- 1 | package com.example.demo.domain.model; 2 | 3 | public enum Status { 4 | DRAFT, 5 | PENDING_MODERATED, 6 | PUBLISHED 7 | } 8 | -------------------------------------------------------------------------------- /api/src/main/java/com/example/demo/domain/model/User.java: -------------------------------------------------------------------------------- 1 | /* 2 | * To change this license header, choose License Headers in Project Properties. 3 | * To change this template file, choose Tools | Templates 4 | * and open the template in the editor. 5 | */ 6 | package com.example.demo.domain.model; 7 | 8 | import com.fasterxml.jackson.annotation.JsonIgnore; 9 | import jakarta.validation.constraints.Email; 10 | import lombok.*; 11 | import org.springframework.data.annotation.Id; 12 | import org.springframework.data.mongodb.core.mapping.Document; 13 | 14 | import java.util.Collections; 15 | import java.util.List; 16 | 17 | /** 18 | * @author hantsy 19 | */ 20 | @Document(collection = "users") 21 | @Data 22 | @ToString 23 | @Builder 24 | @NoArgsConstructor 25 | @AllArgsConstructor 26 | public class User { 27 | 28 | @Id 29 | private String id; 30 | private String username; 31 | 32 | @JsonIgnore 33 | private String password; 34 | 35 | @Email 36 | private String email; 37 | 38 | @Builder.Default() 39 | private boolean active = true; 40 | 41 | @Builder.Default() 42 | private List roles = Collections.emptyList(); 43 | 44 | } 45 | -------------------------------------------------------------------------------- /api/src/main/java/com/example/demo/domain/repository/CommentRepository.java: -------------------------------------------------------------------------------- 1 | package com.example.demo.domain.repository; 2 | 3 | import com.example.demo.domain.model.Comment; 4 | import reactor.core.publisher.Mono; 5 | 6 | public interface CommentRepository { 7 | 8 | Mono findById(String id); 9 | 10 | Mono update(String id, String content); 11 | 12 | } 13 | -------------------------------------------------------------------------------- /api/src/main/java/com/example/demo/domain/repository/PostRepository.java: -------------------------------------------------------------------------------- 1 | package com.example.demo.domain.repository; 2 | 3 | import com.example.demo.domain.model.Post; 4 | import com.example.demo.domain.model.Status; 5 | import com.example.demo.interfaces.dto.PostSummary; 6 | import reactor.core.publisher.Flux; 7 | import reactor.core.publisher.Mono; 8 | 9 | public interface PostRepository { 10 | 11 | Flux findAll(); 12 | Flux findByKeyword(String keyword, int offset, int limit); 13 | 14 | Mono countByKeyword(String keyword); 15 | 16 | Mono findById(String id); 17 | 18 | Mono create(String title, String content); 19 | 20 | Mono update(String id, String title, String content); 21 | 22 | Mono updateStatus(String id, Status status); 23 | 24 | Mono deleteById(String id); 25 | 26 | Mono deleteAll(); 27 | 28 | Mono addComment(String id, String content); 29 | 30 | Mono removeComment(String id, String commentId); 31 | } 32 | -------------------------------------------------------------------------------- /api/src/main/java/com/example/demo/domain/repository/UserRepository.java: -------------------------------------------------------------------------------- 1 | /* 2 | * To change this license header, choose License Headers in Project Properties. 3 | * To change this template file, choose Tools | Templates 4 | * and open the template in the editor. 5 | */ 6 | package com.example.demo.domain.repository; 7 | 8 | 9 | import com.example.demo.domain.model.User; 10 | import org.springframework.data.mongodb.repository.ReactiveMongoRepository; 11 | import reactor.core.publisher.Mono; 12 | 13 | /** 14 | * 15 | * @author hantsy 16 | */ 17 | public interface UserRepository { 18 | 19 | Mono findByUsername(String username); 20 | 21 | Mono create(User user); 22 | 23 | Mono deleteAll(); 24 | } 25 | -------------------------------------------------------------------------------- /api/src/main/java/com/example/demo/infrastructure/MongoConfig.java: -------------------------------------------------------------------------------- 1 | package com.example.demo.infrastructure; 2 | 3 | import org.springframework.context.annotation.Bean; 4 | import org.springframework.context.annotation.Configuration; 5 | import org.springframework.data.domain.ReactiveAuditorAware; 6 | import org.springframework.data.mongodb.config.EnableReactiveMongoAuditing; 7 | import org.springframework.security.core.Authentication; 8 | import org.springframework.security.core.context.ReactiveSecurityContextHolder; 9 | import org.springframework.security.core.context.SecurityContext; 10 | import reactor.core.publisher.Mono; 11 | 12 | @Configuration 13 | @EnableReactiveMongoAuditing 14 | public class MongoConfig { 15 | 16 | @Bean 17 | public ReactiveAuditorAware reactiveAuditorAware() { 18 | return () -> ReactiveSecurityContextHolder.getContext() 19 | .map(SecurityContext::getAuthentication) 20 | .filter(Authentication::isAuthenticated) 21 | .map(Authentication::getName) 22 | .switchIfEmpty(Mono.empty()); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /api/src/main/java/com/example/demo/infrastructure/persistence/MongoCommentRepository.java: -------------------------------------------------------------------------------- 1 | package com.example.demo.infrastructure.persistence; 2 | 3 | import com.example.demo.domain.model.Comment; 4 | import com.example.demo.domain.repository.CommentRepository; 5 | import lombok.RequiredArgsConstructor; 6 | import lombok.extern.slf4j.Slf4j; 7 | import org.springframework.data.mongodb.core.ReactiveMongoTemplate; 8 | import org.springframework.data.mongodb.core.query.Update; 9 | import org.springframework.stereotype.Repository; 10 | import reactor.core.publisher.Mono; 11 | 12 | import static org.springframework.data.mongodb.core.query.Criteria.where; 13 | import static org.springframework.data.mongodb.core.query.Query.query; 14 | 15 | @Repository 16 | @Slf4j 17 | @RequiredArgsConstructor 18 | public class MongoCommentRepository implements CommentRepository { 19 | private final ReactiveMongoTemplate mongoTemplate; 20 | 21 | @Override 22 | public Mono findById(String id) { 23 | return mongoTemplate.findById(id, Comment.class); 24 | } 25 | 26 | @Override 27 | public Mono update(String id, String content) { 28 | return mongoTemplate.update(Comment.class) 29 | .matching(query(where("id").is(id))) 30 | .apply(Update.update("content", content)) 31 | .first() 32 | .map(it -> it.getModifiedCount() > 0); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /api/src/main/java/com/example/demo/infrastructure/persistence/MongoPostRepository.java: -------------------------------------------------------------------------------- 1 | package com.example.demo.infrastructure.persistence; 2 | 3 | import com.example.demo.domain.model.Comment; 4 | import com.example.demo.domain.model.Post; 5 | import com.example.demo.domain.model.Status; 6 | import com.example.demo.domain.repository.PostRepository; 7 | import com.example.demo.interfaces.dto.PostSummary; 8 | import com.mongodb.client.result.DeleteResult; 9 | import lombok.RequiredArgsConstructor; 10 | import lombok.extern.slf4j.Slf4j; 11 | import org.springframework.data.mongodb.core.ReactiveMongoTemplate; 12 | import org.springframework.data.mongodb.core.query.Update; 13 | import org.springframework.stereotype.Repository; 14 | import reactor.core.publisher.Flux; 15 | import reactor.core.publisher.Mono; 16 | 17 | import static org.springframework.data.mongodb.core.query.Criteria.where; 18 | import static org.springframework.data.mongodb.core.query.Query.query; 19 | 20 | @Repository 21 | @Slf4j 22 | @RequiredArgsConstructor 23 | public class MongoPostRepository implements PostRepository { 24 | private final ReactiveMongoTemplate mongoTemplate; 25 | 26 | @Override 27 | public Flux findAll() { 28 | return mongoTemplate.findAll(Post.class); 29 | } 30 | 31 | @Override 32 | public Flux findByKeyword(String keyword, int offset, int limit) { 33 | return mongoTemplate 34 | .find( 35 | query(where("title").regex(".*" + keyword + ".*", "i")) 36 | .skip(offset) 37 | .limit(limit), 38 | Post.class 39 | ) 40 | .map(it -> new PostSummary(it.getId(), it.getTitle(), it.getCreatedDate())); 41 | } 42 | 43 | @Override 44 | public Mono countByKeyword(String keyword) { 45 | return mongoTemplate.count(query(where("title").regex(".*" + keyword + ".*", "i")), Post.class); 46 | } 47 | 48 | @Override 49 | public Mono findById(String id) { 50 | return mongoTemplate.findById(id, Post.class); 51 | } 52 | 53 | @Override 54 | public Mono create(String title, String content) { 55 | return mongoTemplate.insert(Post.builder().title(title).content(content).build()); 56 | } 57 | 58 | @Override 59 | public Mono update(String id, String title, String content) { 60 | return mongoTemplate.update(Post.class) 61 | .matching(where("id").is(id)) 62 | .apply(Update.update("title", title).set("content", content)) 63 | .all() 64 | .map(result -> result.getModifiedCount() == 1L); 65 | } 66 | 67 | @Override 68 | public Mono updateStatus(String id, Status status) { 69 | return mongoTemplate.update(Post.class) 70 | .matching(where("id").is(id)) 71 | .apply(Update.update("status", status)) 72 | .all() 73 | .map(result -> result.getModifiedCount() == 1L); 74 | } 75 | 76 | @Override 77 | public Mono deleteById(String id) { 78 | return mongoTemplate.remove(Post.class) 79 | .matching(where("id").is(id)) 80 | .all() 81 | .map(result -> result.getDeletedCount() == 1L); 82 | } 83 | 84 | @Override 85 | public Mono deleteAll() { 86 | return mongoTemplate.remove(Post.class) 87 | .all() 88 | .map(DeleteResult::getDeletedCount); 89 | } 90 | 91 | @Override 92 | public Mono addComment(String id, String content) { 93 | var comment = mongoTemplate.insert(Comment.builder().content(content).build()); 94 | return comment.flatMap(c -> mongoTemplate.update(Post.class) 95 | .matching(where("id").is(id)) 96 | .apply(new Update().push("comments", c)) 97 | .all() 98 | .map(result -> result.getModifiedCount() == 1L) 99 | ); 100 | } 101 | 102 | @Override 103 | public Mono removeComment(String id, String commentId) { 104 | var comment = mongoTemplate.findById(commentId, Comment.class); 105 | return comment.flatMap(c -> mongoTemplate.update(Post.class) 106 | .matching(where("id").is(id)) 107 | .apply(new Update().pull("comments", c)) 108 | .all() 109 | .map(result -> result.getModifiedCount() == 1L) 110 | ); 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /api/src/main/java/com/example/demo/infrastructure/persistence/MongoUserRepository.java: -------------------------------------------------------------------------------- 1 | package com.example.demo.infrastructure.persistence; 2 | 3 | import com.example.demo.domain.model.User; 4 | import com.example.demo.domain.repository.UserRepository; 5 | import com.mongodb.client.result.DeleteResult; 6 | import lombok.RequiredArgsConstructor; 7 | import lombok.extern.slf4j.Slf4j; 8 | import org.springframework.data.mongodb.core.ReactiveMongoTemplate; 9 | import org.springframework.stereotype.Repository; 10 | import reactor.core.publisher.Mono; 11 | 12 | import static org.springframework.data.mongodb.core.query.Criteria.where; 13 | import static org.springframework.data.mongodb.core.query.Query.query; 14 | 15 | @Repository 16 | @Slf4j 17 | @RequiredArgsConstructor 18 | public class MongoUserRepository implements UserRepository { 19 | private final ReactiveMongoTemplate mongoTemplate; 20 | @Override 21 | public Mono findByUsername(String username) { 22 | return this.mongoTemplate.findOne(query(where("username").is(username)), User.class); 23 | } 24 | 25 | @Override 26 | public Mono create(User user) { 27 | return this.mongoTemplate.insert(user); 28 | } 29 | 30 | @Override 31 | public Mono deleteAll() { 32 | return this.mongoTemplate.remove(User.class) 33 | .all() 34 | .map(DeleteResult::getDeletedCount); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /api/src/main/java/com/example/demo/interfaces/CurrentUserController.java: -------------------------------------------------------------------------------- 1 | /* 2 | * To change this license header, choose License Headers in Project Properties. 3 | * To change this template file, choose Tools | Templates 4 | * and open the template in the editor. 5 | */ 6 | package com.example.demo.interfaces; 7 | 8 | import org.springframework.security.core.Authentication; 9 | import org.springframework.security.core.annotation.AuthenticationPrincipal; 10 | import org.springframework.security.core.authority.AuthorityUtils; 11 | import org.springframework.web.bind.annotation.GetMapping; 12 | import org.springframework.web.bind.annotation.RequestMapping; 13 | import org.springframework.web.bind.annotation.RestController; 14 | import reactor.core.publisher.Mono; 15 | 16 | import java.security.Principal; 17 | import java.util.Map; 18 | 19 | /** 20 | * @author hantsy 21 | */ 22 | @RestController 23 | @RequestMapping("/me") 24 | public class CurrentUserController { 25 | 26 | @GetMapping("") 27 | public Mono> current(@AuthenticationPrincipal Mono principal) { 28 | return principal 29 | .map(user -> 30 | Map.of( 31 | "name", user.getName(), 32 | "roles", AuthorityUtils.authorityListToSet(((Authentication) user) 33 | .getAuthorities()) 34 | ) 35 | ); 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /api/src/main/java/com/example/demo/interfaces/PostController.java: -------------------------------------------------------------------------------- 1 | package com.example.demo.interfaces; 2 | 3 | import com.example.demo.domain.exception.PostNotFoundException; 4 | import com.example.demo.domain.model.Comment; 5 | import com.example.demo.domain.model.Post; 6 | import com.example.demo.domain.model.Status; 7 | import com.example.demo.domain.repository.CommentRepository; 8 | import com.example.demo.domain.repository.PostRepository; 9 | import com.example.demo.interfaces.dto.*; 10 | import jakarta.validation.Valid; 11 | import lombok.RequiredArgsConstructor; 12 | import org.springframework.http.ResponseEntity; 13 | import org.springframework.validation.annotation.Validated; 14 | import org.springframework.web.bind.annotation.*; 15 | import reactor.core.publisher.Flux; 16 | import reactor.core.publisher.Mono; 17 | 18 | import java.net.URI; 19 | 20 | import static org.springframework.http.ResponseEntity.created; 21 | import static org.springframework.http.ResponseEntity.noContent; 22 | 23 | @RestController() 24 | @RequestMapping(value = "/posts") 25 | @RequiredArgsConstructor 26 | @Validated 27 | public class PostController { 28 | 29 | private final PostRepository posts; 30 | 31 | private final CommentRepository comments; 32 | 33 | @GetMapping("") 34 | public Mono> all(@RequestParam(value = "q", required = false) String q, 35 | @RequestParam(value = "offset", defaultValue = "0") int offset, 36 | @RequestParam(value = "limit", defaultValue = "10") int limit) { 37 | 38 | return this.posts.findByKeyword(q, offset, limit).collectList() 39 | .zipWith(this.posts.countByKeyword(q), PaginatedResult::new); 40 | } 41 | 42 | @PostMapping("") 43 | public Mono create(@RequestBody @Valid CreatPostCommand post) { 44 | return this.posts.create(post.title(), post.content()) 45 | .map(saved -> created(URI.create("/posts/" + saved.getId())).build()); 46 | } 47 | 48 | @GetMapping("/{id}") 49 | public Mono get(@PathVariable("id") String id) { 50 | return this.posts.findById(id).switchIfEmpty(Mono.error(new PostNotFoundException(id))); 51 | } 52 | 53 | @PutMapping("/{id}") 54 | public Mono update(@PathVariable("id") String id, @RequestBody @Valid UpdatePostCommand post) { 55 | return this.posts.update(id, post.title(), post.content()) 56 | .handle((result, sink) -> { 57 | if (true) { 58 | sink.next(noContent().build()); 59 | } else { 60 | sink.error(new PostNotFoundException(id)); 61 | } 62 | }); 63 | } 64 | 65 | @PutMapping("/{id}/status") 66 | public Mono updateStatus(@PathVariable("id") String id, @RequestBody @Valid UpdatePostStatusCommand body) { 67 | return this.posts.updateStatus(id, Status.valueOf(body.status())) 68 | .handle((result, sink) -> { 69 | if (true) { 70 | sink.next(noContent().build()); 71 | } else { 72 | sink.error(new PostNotFoundException(id)); 73 | } 74 | }); 75 | } 76 | 77 | @DeleteMapping("/{id}") 78 | public Mono delete(@PathVariable("id") String id) { 79 | return this.posts.deleteById(id) 80 | .handle((result, sink) -> { 81 | if (true) { 82 | sink.next(noContent().build()); 83 | } else { 84 | sink.error(new PostNotFoundException(id)); 85 | } 86 | }); 87 | } 88 | 89 | @GetMapping("/{id}/comments") 90 | public Flux getCommentsOf(@PathVariable("id") String id) { 91 | return this.posts.findById(id) 92 | .flatMapMany(p -> Flux.fromIterable(p.getComments())); 93 | } 94 | 95 | @PostMapping("/{id}/comments") 96 | public Mono createCommentsOf(@PathVariable("id") String id, @RequestBody @Valid CommentForm form) { 97 | return this.posts.addComment(id, form.content()) 98 | .map(saved -> noContent().build()); 99 | } 100 | 101 | } 102 | -------------------------------------------------------------------------------- /api/src/main/java/com/example/demo/interfaces/RestExceptionHandler.java: -------------------------------------------------------------------------------- 1 | package com.example.demo.interfaces; 2 | 3 | import com.example.demo.domain.exception.PostNotFoundException; 4 | import com.fasterxml.jackson.annotation.JsonCreator; 5 | import com.fasterxml.jackson.core.JsonProcessingException; 6 | import com.fasterxml.jackson.databind.ObjectMapper; 7 | import lombok.Getter; 8 | import lombok.RequiredArgsConstructor; 9 | import lombok.ToString; 10 | import lombok.extern.slf4j.Slf4j; 11 | import org.springframework.core.annotation.Order; 12 | import org.springframework.core.io.buffer.DefaultDataBufferFactory; 13 | import org.springframework.http.HttpStatus; 14 | import org.springframework.http.MediaType; 15 | import org.springframework.stereotype.Component; 16 | import org.springframework.web.bind.support.WebExchangeBindException; 17 | import org.springframework.web.server.ServerWebExchange; 18 | import org.springframework.web.server.WebExceptionHandler; 19 | import reactor.core.publisher.Mono; 20 | 21 | import java.io.Serializable; 22 | import java.util.ArrayList; 23 | import java.util.List; 24 | 25 | // see: https://stackoverflow.com/questions/47631243/spring-5-reactive-webexceptionhandler-is-not-getting-called 26 | // and https://docs.spring.io/spring-boot/docs/2.0.0.M7/reference/html/boot-features-developing-web-applications.html#boot-features-webflux-error-handling 27 | // and https://stackoverflow.com/questions/48047645/how-to-write-messages-to-http-body-in-spring-webflux-webexceptionhandlder/48057896#48057896 28 | @Component 29 | @Order(-2) 30 | @Slf4j 31 | @RequiredArgsConstructor 32 | public class RestExceptionHandler implements WebExceptionHandler { 33 | 34 | private final ObjectMapper objectMapper; 35 | 36 | @Override 37 | public Mono handle(ServerWebExchange exchange, Throwable ex) { 38 | if (ex instanceof WebExchangeBindException) { 39 | var webExchangeBindException = (WebExchangeBindException) ex; 40 | 41 | log.debug("errors:" + webExchangeBindException.getFieldErrors()); 42 | var errors = new Errors("validation_failure", "Validation failed."); 43 | webExchangeBindException.getFieldErrors().forEach(e -> errors.add(e.getField(), e.getCode(), e.getDefaultMessage())); 44 | 45 | log.debug("handled errors::" + errors); 46 | try { 47 | exchange.getResponse().setStatusCode(HttpStatus.UNPROCESSABLE_ENTITY); 48 | exchange.getResponse().getHeaders().setContentType(MediaType.APPLICATION_JSON); 49 | 50 | var db = new DefaultDataBufferFactory().wrap(objectMapper.writeValueAsBytes(errors)); 51 | 52 | // write the given data buffer to the response 53 | // and return a Mono that signals when it's done 54 | return exchange.getResponse().writeWith(Mono.just(db)); 55 | 56 | } catch (JsonProcessingException e) { 57 | e.printStackTrace(); 58 | return Mono.empty(); 59 | } 60 | } else if (ex instanceof PostNotFoundException) { 61 | exchange.getResponse().setStatusCode(HttpStatus.NOT_FOUND); 62 | 63 | // marks the response as complete and forbids writing to it 64 | return exchange.getResponse().setComplete(); 65 | } 66 | return Mono.error(ex); 67 | } 68 | } 69 | 70 | @Getter 71 | @ToString 72 | class Errors implements Serializable { 73 | private String code; 74 | private String message; 75 | private List errors = new ArrayList<>(); 76 | 77 | @JsonCreator 78 | Errors(String code, String message) { 79 | this.code = code; 80 | this.message = message; 81 | } 82 | 83 | public void add(String path, String code, String message) { 84 | this.errors.add(new Error(path, code, message)); 85 | } 86 | } 87 | 88 | @Getter 89 | @ToString 90 | class Error implements Serializable { 91 | private String path; 92 | private String code; 93 | private String message; 94 | 95 | @JsonCreator 96 | Error(String path, String code, String message) { 97 | this.path = path; 98 | this.code = code; 99 | this.message = message; 100 | } 101 | 102 | } 103 | -------------------------------------------------------------------------------- /api/src/main/java/com/example/demo/interfaces/UserController.java: -------------------------------------------------------------------------------- 1 | /* 2 | * To change this license header, choose License Headers in Project Properties. 3 | * To change this template file, choose Tools | Templates 4 | * and open the template in the editor. 5 | */ 6 | package com.example.demo.interfaces; 7 | 8 | 9 | import com.example.demo.domain.model.User; 10 | import com.example.demo.domain.repository.UserRepository; 11 | import lombok.RequiredArgsConstructor; 12 | import org.springframework.web.bind.annotation.GetMapping; 13 | import org.springframework.web.bind.annotation.PathVariable; 14 | import org.springframework.web.bind.annotation.RestController; 15 | import reactor.core.publisher.Mono; 16 | 17 | /** 18 | * 19 | * @author hantsy 20 | */ 21 | @RestController 22 | @RequiredArgsConstructor 23 | public class UserController { 24 | 25 | private final UserRepository users; 26 | 27 | @GetMapping("/users/{username}") 28 | public Mono get(@PathVariable() String username) { 29 | return this.users.findByUsername(username); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /api/src/main/java/com/example/demo/interfaces/WebConfig.java: -------------------------------------------------------------------------------- 1 | package com.example.demo.interfaces; 2 | 3 | import lombok.extern.slf4j.Slf4j; 4 | import org.springframework.context.annotation.Bean; 5 | import org.springframework.context.annotation.Configuration; 6 | import org.springframework.context.annotation.Profile; 7 | import org.springframework.web.cors.CorsConfiguration; 8 | import org.springframework.web.cors.reactive.CorsConfigurationSource; 9 | import org.springframework.web.cors.reactive.UrlBasedCorsConfigurationSource; 10 | 11 | import java.util.List; 12 | 13 | @Configuration 14 | @Profile("cors") 15 | @Slf4j 16 | class WebConfig { 17 | 18 | @Bean 19 | CorsConfigurationSource corsConfigurationSource() { 20 | var corsConfiguration = new CorsConfiguration().applyPermitDefaultValues(); 21 | corsConfiguration.setAllowedOrigins(List.of("http://localhost:4200")); 22 | var source = new UrlBasedCorsConfigurationSource(); 23 | source.registerCorsConfiguration("/**", corsConfiguration); 24 | log.info("configured cors: {}", source); 25 | return source; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /api/src/main/java/com/example/demo/interfaces/dto/CommentForm.java: -------------------------------------------------------------------------------- 1 | package com.example.demo.interfaces.dto; 2 | 3 | import jakarta.validation.constraints.NotBlank; 4 | 5 | public record CommentForm(@NotBlank String content) { 6 | } 7 | -------------------------------------------------------------------------------- /api/src/main/java/com/example/demo/interfaces/dto/CreatPostCommand.java: -------------------------------------------------------------------------------- 1 | package com.example.demo.interfaces.dto; 2 | 3 | import jakarta.validation.constraints.NotBlank; 4 | 5 | public record CreatPostCommand(@NotBlank String title, @NotBlank String content) { 6 | } 7 | -------------------------------------------------------------------------------- /api/src/main/java/com/example/demo/interfaces/dto/LoginRequest.java: -------------------------------------------------------------------------------- 1 | package com.example.demo.interfaces.dto; 2 | 3 | import jakarta.validation.constraints.NotBlank; 4 | 5 | public record LoginRequest(@NotBlank String username, 6 | @NotBlank String password) { 7 | } 8 | -------------------------------------------------------------------------------- /api/src/main/java/com/example/demo/interfaces/dto/PaginatedResult.java: -------------------------------------------------------------------------------- 1 | package com.example.demo.interfaces.dto; 2 | 3 | import java.util.List; 4 | 5 | public record PaginatedResult(List data, Long count) { 6 | } 7 | -------------------------------------------------------------------------------- /api/src/main/java/com/example/demo/interfaces/dto/PostSummary.java: -------------------------------------------------------------------------------- 1 | package com.example.demo.interfaces.dto; 2 | 3 | import java.time.LocalDateTime; 4 | 5 | public record PostSummary(String id, String title, LocalDateTime createdAt) { 6 | } 7 | -------------------------------------------------------------------------------- /api/src/main/java/com/example/demo/interfaces/dto/UpdatePostCommand.java: -------------------------------------------------------------------------------- 1 | package com.example.demo.interfaces.dto; 2 | 3 | import jakarta.validation.constraints.NotBlank; 4 | 5 | public record UpdatePostCommand(@NotBlank String title, @NotBlank String content) { 6 | } 7 | -------------------------------------------------------------------------------- /api/src/main/java/com/example/demo/interfaces/dto/UpdatePostStatusCommand.java: -------------------------------------------------------------------------------- 1 | package com.example.demo.interfaces.dto; 2 | 3 | import jakarta.validation.constraints.NotBlank; 4 | 5 | public record UpdatePostStatusCommand(@NotBlank String status) { 6 | } 7 | -------------------------------------------------------------------------------- /api/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | data: 3 | mongodb: 4 | # host: localhost 5 | # port: 27017 6 | # uri: mongodb://user:secret@mongo1.example.com:12345,mongo2.example.com:23456/test 7 | uri: mongodb://localhost:27017/blog 8 | grid-fs-database: images 9 | jackson: 10 | default-property-inclusion: non_null 11 | serialization: 12 | indent-output: true 13 | logging: 14 | level: 15 | root: INFO 16 | sql: DEBUG 17 | web: TRACE 18 | com.example: DEBUG 19 | org.springframework.security: DEBUG 20 | -------------------------------------------------------------------------------- /api/src/test/java/com/example/demo/PostRepositoryTest.java: -------------------------------------------------------------------------------- 1 | package com.example.demo; 2 | 3 | import com.example.demo.domain.model.Post; 4 | import com.example.demo.domain.repository.PostRepository; 5 | import com.example.demo.infrastructure.persistence.MongoPostRepository; 6 | import lombok.SneakyThrows; 7 | import lombok.extern.slf4j.Slf4j; 8 | import org.junit.jupiter.api.BeforeEach; 9 | import org.junit.jupiter.api.Test; 10 | import org.springframework.beans.factory.annotation.Autowired; 11 | import org.springframework.boot.test.autoconfigure.data.mongo.DataMongoTest; 12 | import org.springframework.boot.test.context.TestConfiguration; 13 | import org.springframework.context.annotation.Import; 14 | import org.springframework.data.mongodb.core.ReactiveFluentMongoOperations; 15 | import org.springframework.data.mongodb.core.ReactiveMongoTemplate; 16 | import org.springframework.test.context.DynamicPropertyRegistry; 17 | import org.springframework.test.context.DynamicPropertySource; 18 | import org.testcontainers.containers.MongoDBContainer; 19 | import org.testcontainers.junit.jupiter.Container; 20 | import org.testcontainers.junit.jupiter.Testcontainers; 21 | import reactor.test.StepVerifier; 22 | 23 | import java.util.List; 24 | import java.util.concurrent.CountDownLatch; 25 | import java.util.concurrent.TimeUnit; 26 | 27 | import static org.assertj.core.api.Assertions.assertThat; 28 | 29 | @DataMongoTest 30 | @Slf4j 31 | @Testcontainers 32 | public class PostRepositoryTest { 33 | 34 | @TestConfiguration 35 | @Import({MongoPostRepository.class}) 36 | static class TestConfig { 37 | } 38 | 39 | @Container 40 | static MongoDBContainer mongoDBContainer = new MongoDBContainer("mongo:4"); 41 | 42 | @DynamicPropertySource 43 | static void registerMongoProperties(DynamicPropertyRegistry registry) { 44 | registry.add("spring.data.mongodb.uri", () -> mongoDBContainer.getReplicaSetUrl()); 45 | } 46 | 47 | @Autowired 48 | PostRepository postRepository; 49 | 50 | @Autowired 51 | ReactiveMongoTemplate reactiveMongoTemplate; 52 | 53 | @Autowired 54 | ReactiveFluentMongoOperations fluentMongoOperations; 55 | 56 | @SneakyThrows 57 | @BeforeEach 58 | public void setup() { 59 | var latch = new CountDownLatch(1); 60 | this.reactiveMongoTemplate.remove(Post.class).all() 61 | .doOnTerminate(latch::countDown) 62 | .subscribe( 63 | r -> log.debug("delete all posts: " + r), 64 | e -> log.debug("error: " + e), 65 | () -> log.debug("done") 66 | ); 67 | latch.await(5000, TimeUnit.MILLISECONDS); 68 | } 69 | 70 | 71 | @Test 72 | public void testSavePost() { 73 | var content = "my test content"; 74 | var title = "my test title"; 75 | var saved = this.postRepository.create(title, content); 76 | 77 | StepVerifier.create(saved) 78 | .consumeNextWith(p -> { 79 | log.debug("consuming post:: {}", p); 80 | assertThat(p.getTitle()).isEqualTo(title); 81 | }) 82 | .expectComplete() 83 | .verify(); 84 | 85 | var id = saved.block().getId(); 86 | 87 | this.postRepository.addComment(id, "comment1") 88 | .then(this.postRepository.findById(id)) 89 | .as(StepVerifier::create) 90 | .consumeNextWith(p -> { 91 | log.debug("after add comments: {}", p); 92 | assertThat(p.getComments()).isNotNull(); 93 | }) 94 | .expectComplete() 95 | .verify(); 96 | } 97 | 98 | @Test 99 | public void testFluentOperations() { 100 | this.fluentMongoOperations.insert(Post.class) 101 | .all(List.of( 102 | Post.builder().title("my test title").content("my test content").build(), 103 | Post.builder().title("my second title").content("my second content").build() 104 | ) 105 | ) 106 | .as(StepVerifier::create) 107 | .expectNextCount(2) 108 | .verifyComplete(); 109 | 110 | // this.fluentMongoOperations.update(Post.class) 111 | // .matching(Query.query(Criteria.where("id").is(""))) 112 | // .apply(new Update().pull()) 113 | 114 | 115 | // this.fluentMongoOperations.remove(Post.class) 116 | // .matching(Query.query(Criteria.where("id").is(""))) 117 | // .all() 118 | 119 | } 120 | 121 | 122 | } 123 | -------------------------------------------------------------------------------- /api/src/test/resources/application.properties: -------------------------------------------------------------------------------- 1 | logging.level.com.example=DEBUG 2 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | # see https://docs.docker.com/compose/compose-file/compose-versioning/ 2 | version: "3.5" # specify docker-compose version, v3.5 is compatible with docker 17.12.0+ 3 | 4 | # Define the services/containers to be run 5 | services: 6 | redis: 7 | image: redis 8 | ports: 9 | - "6379:6379" 10 | networks: 11 | - backend 12 | 13 | mongodb: 14 | image: mongo 15 | volumes: 16 | - mongodata:/data/db 17 | ports: 18 | - "27017:27017" 19 | networks: 20 | - backend 21 | 22 | client: 23 | image: hantsy/angular-spring-reactive-sample-client 24 | environment: 25 | - "BACKEND_API_URL=http://api:8080" 26 | ports: 27 | - "80:80" 28 | depends_on: 29 | - api 30 | networks: 31 | - frontend 32 | - backend 33 | 34 | api: 35 | image: hantsy/angular-spring-reactive-sample-server 36 | environment: 37 | - "SPRING_DATA_MONGODB_URI=mongodb://mongodb:27017/blog" 38 | ports: 39 | - "8080:8080" 40 | depends_on: 41 | - mongodb 42 | networks: 43 | - backend 44 | 45 | volumes: 46 | mongodata: 47 | # driver: local-persist 48 | # driver_opts: 49 | # mountpoint: ./data 50 | 51 | networks: 52 | frontend: 53 | backend: 54 | -------------------------------------------------------------------------------- /start.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hantsy/angular-spring-reactive-sample/c931f218f69f576da27da2b2d48bdf0c43352b4b/start.png -------------------------------------------------------------------------------- /ui/.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | jobs: 3 | build: 4 | working_directory: ~/my-project 5 | docker: 6 | - image: circleci/node:8-browsers 7 | steps: 8 | - checkout 9 | - restore_cache: 10 | key: my-project-{{ .Branch }}-{{ checksum "package-lock.json" }} 11 | - run: npm install 12 | - save_cache: 13 | key: my-project-{{ .Branch }}-{{ checksum "package-lock.json" }} 14 | paths: 15 | - "node_modules" 16 | - run: npm run test -- --single-run --no-progress --browser=ChromeHeadlessCI 17 | - run: npm run e2e -- --no-progress --config=protractor-ci.conf.js 18 | -------------------------------------------------------------------------------- /ui/.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see https://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.ts] 12 | quote_type = single 13 | 14 | [*.md] 15 | max_line_length = off 16 | trim_trailing_whitespace = false 17 | -------------------------------------------------------------------------------- /ui/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', // Specifies the ESLint parser 3 | parserOptions: { 4 | ecmaVersion: 2020, // Allows for the parsing of modern ECMAScript features 5 | sourceType: 'module', // Allows for the use of imports 6 | }, 7 | extends: ['plugin:@angular-eslint/recommended', 'plugin:@typescript-eslint/recommended', 'prettier/@typescript-eslint', 'plugin:prettier/recommended', 'plugin:storybook/recommended'], 8 | rules: { 9 | // ORIGINAL tslint.json -> "directive-selector": [true, "attribute", "app", "camelCase"], 10 | '@angular-eslint/directive-selector': [ 11 | 'error', 12 | { type: 'attribute', prefix: 'app', style: 'camelCase' }, 13 | ], 14 | 15 | // ORIGINAL tslint.json -> "component-selector": [true, "element", "app", "kebab-case"], 16 | '@angular-eslint/component-selector': [ 17 | 'error', 18 | { type: 'element', prefix: 'app', style: 'kebab-case' }, 19 | ], 20 | // Place to specify ESLint rules. Can be used to overwrite rules specified from the extended configs 21 | // e.g. "@typescript-eslint/explicit-function-return-type": "off", 22 | '@typescript-eslint/explicit-function-return-type': 'off', 23 | }, 24 | overrides: [ 25 | /** 26 | * This extra piece of configuration is only necessary if you make use of inline 27 | * templates within Component metadata, e.g.: 28 | * 29 | * @Component({ 30 | * template: `

Hello, World!

` 31 | * }) 32 | * ... 33 | * 34 | * It is not necessary if you only use .html files for templates. 35 | */ 36 | { 37 | files: ['*.component.ts'], 38 | parser: '@typescript-eslint/parser', 39 | parserOptions: { 40 | ecmaVersion: 2020, 41 | sourceType: 'module', 42 | }, 43 | plugins: ['@angular-eslint/template'], 44 | processor: '@angular-eslint/template/extract-inline-html', 45 | }, 46 | ], 47 | }; 48 | -------------------------------------------------------------------------------- /ui/.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 | /bazel-out 8 | 9 | # Node 10 | /node_modules 11 | npm-debug.log 12 | yarn-error.log 13 | 14 | # IDEs and editors 15 | .idea/ 16 | .project 17 | .classpath 18 | .c9/ 19 | *.launch 20 | .settings/ 21 | *.sublime-workspace 22 | 23 | # Visual Studio Code 24 | .vscode/* 25 | !.vscode/settings.json 26 | !.vscode/tasks.json 27 | !.vscode/launch.json 28 | !.vscode/extensions.json 29 | .history/* 30 | 31 | # Miscellaneous 32 | /.angular/cache 33 | .sass-cache/ 34 | /connect.lock 35 | /coverage 36 | /libpeerconnection.log 37 | testem.log 38 | /typings 39 | 40 | # System files 41 | .DS_Store 42 | Thumbs.db 43 | 44 | .angular 45 | 46 | .nx/cache 47 | .nx/workspace-data -------------------------------------------------------------------------------- /ui/.prettierignore: -------------------------------------------------------------------------------- 1 | # Add files here to ignore them from prettier formatting 2 | 3 | /dist 4 | /coverage 5 | 6 | /.nx/cache 7 | /.nx/workspace-data -------------------------------------------------------------------------------- /ui/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true 3 | } 4 | -------------------------------------------------------------------------------- /ui/.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | semi: true, 3 | trailingComma: "all", 4 | singleQuote: true, 5 | printWidth: 120, 6 | tabWidth: 2 7 | }; 8 | -------------------------------------------------------------------------------- /ui/.storybook/main.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | framework: { 3 | name: "@storybook/angular", 4 | options: {} 5 | }, 6 | 7 | docs: { 8 | autodocs: true 9 | } 10 | }; 11 | -------------------------------------------------------------------------------- /ui/.storybook/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.base.json", 3 | "exclude": [ 4 | "../**/*.spec.js", 5 | "../**/*.test.js", 6 | "../**/*.spec.ts", 7 | "../**/*.test.ts", 8 | "../**/*.spec.tsx", 9 | "../**/*.test.tsx", 10 | "../**/*.spec.jsx", 11 | "../**/*.test.jsx", 12 | "jest.config.ts" 13 | ], 14 | "include": ["../**/*"] 15 | } 16 | -------------------------------------------------------------------------------- /ui/.travis.yml: -------------------------------------------------------------------------------- 1 | dist: trusty 2 | sudo: false 3 | 4 | language: node_js 5 | node_js: 6 | - "12" 7 | 8 | addons: 9 | apt: 10 | sources: 11 | - google-chrome 12 | packages: 13 | - google-chrome-stable 14 | 15 | cache: 16 | directories: 17 | - ./node_modules 18 | 19 | install: 20 | - npm install 21 | 22 | script: 23 | - npm run test -- --single-run --no-progress --browser=ChromeHeadlessCI 24 | - npm run e2e -- --no-progress --config=e2e/protractor-ci.conf.js 25 | -------------------------------------------------------------------------------- /ui/Dockerfile: -------------------------------------------------------------------------------- 1 | # Set nginx base image 2 | FROM nginx:alpine-perl 3 | 4 | LABEL maintainer="Hantsy Bai" 5 | 6 | EXPOSE 80 7 | ## Remove default Nginx website 8 | RUN rm -rf /usr/share/nginx/html/* 9 | 10 | RUN rm /etc/nginx/conf.d/default.conf 11 | 12 | ADD ./nginx/nginx.conf /etc/nginx/nginx.conf 13 | 14 | ADD ./dist /usr/share/nginx/html 15 | 16 | 17 | -------------------------------------------------------------------------------- /ui/README.md: -------------------------------------------------------------------------------- 1 | # Client 2 | 3 | This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 1.6.3. 4 | 5 | ## Development server 6 | 7 | Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The app will automatically reload if you change any of the source files. 8 | 9 | ## Code scaffolding 10 | 11 | Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`. 12 | 13 | ## Build 14 | 15 | Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. Use the `-prod` flag for a production build. 16 | 17 | ## Running unit tests 18 | 19 | Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io). 20 | 21 | ## Running end-to-end tests 22 | 23 | Run `ng e2e` to execute the end-to-end tests via [Protractor](http://www.protractortest.org/). 24 | 25 | ## Further help 26 | 27 | To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI README](https://github.com/angular/angular-cli/blob/master/README.md). 28 | -------------------------------------------------------------------------------- /ui/apps/shared-components-e2e/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["plugin:cypress/recommended", "../../.eslintrc.js"], 3 | "ignorePatterns": ["!**/*"], 4 | "overrides": [ 5 | { 6 | "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], 7 | "rules": {} 8 | }, 9 | { 10 | "files": ["src/plugins/index.js"], 11 | "rules": { 12 | "@typescript-eslint/no-var-requires": "off", 13 | "no-undef": "off" 14 | } 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /ui/apps/shared-components-e2e/cypress.json: -------------------------------------------------------------------------------- 1 | { 2 | "fileServerFolder": ".", 3 | "fixturesFolder": "./src/fixtures", 4 | "integrationFolder": "./src/integration", 5 | "modifyObstructiveCode": false, 6 | "supportFile": "./src/support/index.ts", 7 | "pluginsFile": false, 8 | "video": true, 9 | "videosFolder": "../../dist/cypress/apps/shared-components-e2e/videos", 10 | "screenshotsFolder": "../../dist/cypress/apps/shared-components-e2e/screenshots", 11 | "chromeWebSecurity": false, 12 | "baseUrl": "http://localhost:4400" 13 | } 14 | -------------------------------------------------------------------------------- /ui/apps/shared-components-e2e/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "shared-components-e2e", 3 | "$schema": "../../node_modules/nx/schemas/project-schema.json", 4 | "sourceRoot": "apps/shared-components-e2e/src", 5 | "projectType": "application", 6 | "tags": [], 7 | "implicitDependencies": ["shared-components"], 8 | "targets": { 9 | "e2e": { 10 | "executor": "@nx/cypress:cypress", 11 | "options": { 12 | "cypressConfig": "apps/shared-components-e2e/cypress.json", 13 | "devServerTarget": "shared-components:storybook" 14 | }, 15 | "configurations": { 16 | "ci": { 17 | "devServerTarget": "shared-components:storybook:ci" 18 | } 19 | } 20 | }, 21 | "lint": { 22 | "executor": "@nrwl/linter:eslint", 23 | "outputs": ["{options.outputFile}"], 24 | "options": { 25 | "lintFilePatterns": ["apps/shared-components-e2e/**/*.{js,ts}"] 26 | } 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /ui/apps/shared-components-e2e/src/fixtures/example.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Using fixtures to represent data", 3 | "email": "hello@cypress.io" 4 | } 5 | -------------------------------------------------------------------------------- /ui/apps/shared-components-e2e/src/support/commands.ts: -------------------------------------------------------------------------------- 1 | // *********************************************** 2 | // This example commands.js shows you how to 3 | // create various custom commands and overwrite 4 | // existing commands. 5 | // 6 | // For more comprehensive examples of custom 7 | // commands please read more here: 8 | // https://on.cypress.io/custom-commands 9 | // *********************************************** 10 | 11 | // eslint-disable-next-line @typescript-eslint/no-namespace 12 | declare namespace Cypress { 13 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 14 | interface Chainable { 15 | login(email: string, password: string): void; 16 | } 17 | } 18 | // 19 | // -- This is a parent command -- 20 | Cypress.Commands.add('login', (email, password) => { 21 | console.log('Custom command example: Login', email, password); 22 | }); 23 | // 24 | // -- This is a child command -- 25 | // Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... }) 26 | // 27 | // 28 | // -- This is a dual command -- 29 | // Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... }) 30 | // 31 | // 32 | // -- This will overwrite an existing command -- 33 | // Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... }) 34 | -------------------------------------------------------------------------------- /ui/apps/shared-components-e2e/src/support/index.ts: -------------------------------------------------------------------------------- 1 | // *********************************************************** 2 | // This example support/index.js is processed and 3 | // loaded automatically before your test files. 4 | // 5 | // This is a great place to put global configuration and 6 | // behavior that modifies Cypress. 7 | // 8 | // You can change the location of this file or turn off 9 | // automatically serving support files with the 10 | // 'supportFile' configuration option. 11 | // 12 | // You can read more here: 13 | // https://on.cypress.io/configuration 14 | // *********************************************************** 15 | 16 | // Import commands.js using ES2015 syntax: 17 | import './commands'; 18 | -------------------------------------------------------------------------------- /ui/apps/shared-components-e2e/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "sourceMap": false, 5 | "outDir": "../../dist/out-tsc", 6 | "allowJs": true, 7 | "types": ["cypress", "node"] 8 | }, 9 | "include": ["src/**/*.ts", "src/**/*.js"] 10 | } 11 | -------------------------------------------------------------------------------- /ui/apps/todolist-e2e/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["plugin:cypress/recommended", "../../.eslintrc.js"], 3 | "ignorePatterns": ["!**/*"], 4 | "overrides": [ 5 | { 6 | "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], 7 | "rules": {} 8 | } 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /ui/apps/todolist-e2e/cypress.json: -------------------------------------------------------------------------------- 1 | { 2 | "fileServerFolder": ".", 3 | "fixturesFolder": "./src/fixtures", 4 | "integrationFolder": "./src/integration", 5 | "modifyObstructiveCode": false, 6 | "supportFile": "./src/support/index.ts", 7 | "pluginsFile": false, 8 | "video": true, 9 | "videosFolder": "../../dist/cypress/apps/todolist-e2e/videos", 10 | "screenshotsFolder": "../../dist/cypress/apps/todolist-e2e/screenshots", 11 | "chromeWebSecurity": false 12 | } 13 | -------------------------------------------------------------------------------- /ui/apps/todolist-e2e/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "todolist-e2e", 3 | "$schema": "../../node_modules/nx/schemas/project-schema.json", 4 | "sourceRoot": "apps/todolist-e2e/src", 5 | "projectType": "application", 6 | "tags": [], 7 | "implicitDependencies": ["todolist"], 8 | "targets": { 9 | "e2e": { 10 | "executor": "@nx/cypress:cypress", 11 | "options": { 12 | "cypressConfig": "apps/todolist-e2e/cypress.json", 13 | "devServerTarget": "todolist:serve:development" 14 | }, 15 | "configurations": { 16 | "production": { 17 | "devServerTarget": "todolist:serve:production" 18 | } 19 | } 20 | }, 21 | "lint": { 22 | "executor": "@nrwl/linter:eslint", 23 | "outputs": ["{options.outputFile}"], 24 | "options": { 25 | "lintFilePatterns": ["apps/todolist-e2e/**/*.{js,ts}"] 26 | } 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /ui/apps/todolist-e2e/src/fixtures/example.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Using fixtures to represent data", 3 | "email": "hello@cypress.io" 4 | } 5 | -------------------------------------------------------------------------------- /ui/apps/todolist-e2e/src/integration/app.spec.ts: -------------------------------------------------------------------------------- 1 | import { getGreeting } from '../support/app.po'; 2 | 3 | describe('todolist', () => { 4 | beforeEach(() => cy.visit('/')); 5 | 6 | it('should display welcome message', () => { 7 | // Custom command example, see `../support/commands.ts` file 8 | cy.login('my-email@something.com', 'myPassword'); 9 | 10 | // Function helper example, see `../support/app.po.ts` file 11 | getGreeting().contains('Welcome todolist'); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /ui/apps/todolist-e2e/src/support/app.po.ts: -------------------------------------------------------------------------------- 1 | export const getGreeting = () => cy.get('h1'); 2 | -------------------------------------------------------------------------------- /ui/apps/todolist-e2e/src/support/commands.ts: -------------------------------------------------------------------------------- 1 | // *********************************************** 2 | // This example commands.js shows you how to 3 | // create various custom commands and overwrite 4 | // existing commands. 5 | // 6 | // For more comprehensive examples of custom 7 | // commands please read more here: 8 | // https://on.cypress.io/custom-commands 9 | // *********************************************** 10 | 11 | // eslint-disable-next-line @typescript-eslint/no-namespace 12 | declare namespace Cypress { 13 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 14 | interface Chainable { 15 | login(email: string, password: string): void; 16 | } 17 | } 18 | // 19 | // -- This is a parent command -- 20 | Cypress.Commands.add('login', (email, password) => { 21 | console.log('Custom command example: Login', email, password); 22 | }); 23 | // 24 | // -- This is a child command -- 25 | // Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... }) 26 | // 27 | // 28 | // -- This is a dual command -- 29 | // Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... }) 30 | // 31 | // 32 | // -- This will overwrite an existing command -- 33 | // Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... }) 34 | -------------------------------------------------------------------------------- /ui/apps/todolist-e2e/src/support/index.ts: -------------------------------------------------------------------------------- 1 | // *********************************************************** 2 | // This example support/index.js is processed and 3 | // loaded automatically before your test files. 4 | // 5 | // This is a great place to put global configuration and 6 | // behavior that modifies Cypress. 7 | // 8 | // You can change the location of this file or turn off 9 | // automatically serving support files with the 10 | // 'supportFile' configuration option. 11 | // 12 | // You can read more here: 13 | // https://on.cypress.io/configuration 14 | // *********************************************************** 15 | 16 | // Import commands.js using ES2015 syntax: 17 | import './commands'; 18 | -------------------------------------------------------------------------------- /ui/apps/todolist-e2e/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "sourceMap": false, 5 | "outDir": "../../dist/out-tsc", 6 | "allowJs": true, 7 | "types": ["cypress", "node"] 8 | }, 9 | "include": ["src/**/*.ts", "src/**/*.js"] 10 | } 11 | -------------------------------------------------------------------------------- /ui/apps/todolist/jest.config.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | export default { 3 | displayName: 'todolist', 4 | preset: '../../jest.preset.js', 5 | globals: {}, 6 | coverageDirectory: '../../coverage/apps/todolist', 7 | }; 8 | -------------------------------------------------------------------------------- /ui/apps/todolist/karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration file, see link for more information 2 | // https://karma-runner.github.io/1.0/config/configuration-file.html 3 | 4 | module.exports = function (config) { 5 | config.set({ 6 | basePath: '', 7 | frameworks: ['jasmine', '@angular-devkit/build-angular'], 8 | plugins: [ 9 | require('karma-jasmine'), 10 | require('karma-chrome-launcher'), 11 | require('karma-jasmine-html-reporter'), 12 | require('karma-coverage'), 13 | require('@angular-devkit/build-angular/plugins/karma') 14 | ], 15 | client: { 16 | jasmine: { 17 | // you can add configuration options for Jasmine here 18 | // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html 19 | // for example, you can disable the random execution with `random: false` 20 | // or set a specific seed with `seed: 4321` 21 | }, 22 | clearContext: false // leave Jasmine Spec Runner output visible in browser 23 | }, 24 | jasmineHtmlReporter: { 25 | suppressAll: true // removes the duplicated traces 26 | }, 27 | coverageReporter: { 28 | dir: require('path').join(__dirname, './coverage/todolist'), 29 | subdir: '.', 30 | reporters: [ 31 | { type: 'html' }, 32 | { type: 'text-summary' } 33 | ] 34 | }, 35 | reporters: ['progress', 'kjhtml'], 36 | port: 9876, 37 | colors: true, 38 | logLevel: config.LOG_INFO, 39 | autoWatch: true, 40 | browsers: ['Chrome'], 41 | singleRun: false, 42 | restartOnFileChange: true 43 | }); 44 | }; 45 | -------------------------------------------------------------------------------- /ui/apps/todolist/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "todolist", 3 | "$schema": "../../node_modules/nx/schemas/project-schema.json", 4 | "projectType": "application", 5 | "sourceRoot": "apps/todolist/src", 6 | "prefix": "app", 7 | "generators": { 8 | "@schematics/angular:application": { 9 | "strict": true 10 | } 11 | }, 12 | "targets": { 13 | "build": { 14 | "executor": "@angular-devkit/build-angular:browser", 15 | "options": { 16 | "outputPath": "dist/apps/todolist", 17 | "index": "apps/todolist/src/index.html", 18 | "main": "apps/todolist/src/main.ts", 19 | "polyfills": "apps/todolist/src/polyfills.ts", 20 | "tsConfig": "apps/todolist/tsconfig.json", 21 | "assets": ["apps/todolist/src/favicon.ico", "apps/todolist/src/assets"], 22 | "styles": ["apps/todolist/src/styles.css"], 23 | "scripts": [] 24 | }, 25 | "configurations": { 26 | "production": { 27 | "budgets": [ 28 | { 29 | "type": "initial", 30 | "maximumWarning": "500kb", 31 | "maximumError": "1mb" 32 | }, 33 | { 34 | "type": "anyComponentStyle", 35 | "maximumWarning": "2kb", 36 | "maximumError": "4kb" 37 | } 38 | ], 39 | "fileReplacements": [ 40 | { 41 | "replace": "apps/todolist/src/environments/environment.ts", 42 | "with": "apps/todolist/src/environments/environment.prod.ts" 43 | } 44 | ], 45 | "outputHashing": "all" 46 | }, 47 | "development": { 48 | "buildOptimizer": false, 49 | "optimization": false, 50 | "vendorChunk": true, 51 | "extractLicenses": false, 52 | "sourceMap": true, 53 | "namedChunks": true 54 | } 55 | }, 56 | "defaultConfiguration": "production" 57 | }, 58 | "serve": { 59 | "executor": "@angular-devkit/build-angular:dev-server", 60 | "configurations": { 61 | "production": { 62 | "buildTarget": "todolist:build:production" 63 | }, 64 | "development": { 65 | "buildTarget": "todolist:build:development" 66 | } 67 | }, 68 | "defaultConfiguration": "development" 69 | }, 70 | "extract-i18n": { 71 | "executor": "@angular-devkit/build-angular:extract-i18n", 72 | "options": { 73 | "buildTarget": "todolist:build" 74 | } 75 | }, 76 | "test": { 77 | "executor": "@nx/jest:jest", 78 | "outputs": ["{workspaceRoot}/coverage/apps/todolist"], 79 | "options": { 80 | "jestConfig": "apps/todolist/jest.config.ts" 81 | } 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /ui/apps/todolist/src/app/app-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { Routes, RouterModule } from '@angular/router'; 3 | import { LoadGuard } from './core/load-guard'; 4 | 5 | const routes: Routes = [ 6 | { path: '', redirectTo: '/home', pathMatch: 'full' }, 7 | { path: 'post', loadChildren: () => import('./post/post.module').then((post) => post.PostModule) }, 8 | { path: 'user', loadChildren: () => import('./user/user.module').then((user) => user.UserModule) }, 9 | { 10 | path: 'auth', 11 | // loadChildren: './auth/auth.module#AuthModule', 12 | loadChildren: () => import('./auth/auth.module').then((auth) => auth.AuthModule), 13 | canLoad: [LoadGuard], 14 | }, 15 | // { path: '**', component:PageNotFoundComponent} 16 | ]; 17 | 18 | @NgModule({ 19 | imports: [RouterModule.forRoot(routes)], 20 | exports: [RouterModule], 21 | }) 22 | export class AppRoutingModule {} 23 | -------------------------------------------------------------------------------- /ui/apps/todolist/src/app/app.component.css: -------------------------------------------------------------------------------- 1 | :host { 2 | display: flex; 3 | flex: 1; 4 | } 5 | 6 | mat-sidenav { 7 | width: 320px; 8 | } 9 | 10 | .content { 11 | padding: 12px; 12 | } 13 | /* removed 14 | /deep/ mat-icon.avatar 15 | */ 16 | mat-list-item mat-icon { 17 | overflow: hidden; 18 | width: 64px; 19 | height: 64px; 20 | /* border-radius: 50%; */ 21 | margin: 12px; 22 | } 23 | 24 | .name-container{ 25 | display: flex; 26 | margin: 25px; 27 | justify-content: center; 28 | } 29 | 30 | router-outlet{ 31 | margin: 0px 32 | } 33 | 34 | /* we don't need this as it makes the sidenav not fill the height by 100% and looks like floating 35 | /deep/ .mat-list-item-content { 36 | height: auto !important; 37 | } 38 | */ 39 | -------------------------------------------------------------------------------- /ui/apps/todolist/src/app/app.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 5 | Angular Material 6 | 7 | 8 | 9 | 12 | 13 | 14 | 15 | 16 | 17 |
18 |
19 | SIGN IN 20 |
21 |
22 |
Welcome, {{(currentUser|async)?.name}}
23 |
24 | 25 |
26 |
27 |
28 | 29 | 30 | 31 | {{link.icon}} 32 | {{link.label}} 33 | 34 | 35 | 48 |
49 |
56 | 57 | 67 | 68 | 69 | 70 |
71 |
72 | 73 | 74 | 75 | 76 | 77 |
78 | -------------------------------------------------------------------------------- /ui/apps/todolist/src/app/app.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | import { RouterTestingModule } from '@angular/router/testing'; 3 | import { AppComponent } from './app.component'; 4 | 5 | describe('AppComponent', () => { 6 | beforeEach(async () => { 7 | await TestBed.configureTestingModule({ 8 | imports: [ 9 | RouterTestingModule 10 | ], 11 | declarations: [ 12 | AppComponent 13 | ], 14 | }).compileComponents(); 15 | }); 16 | 17 | it('should create the app', () => { 18 | const fixture = TestBed.createComponent(AppComponent); 19 | const app = fixture.componentInstance; 20 | expect(app).toBeTruthy(); 21 | }); 22 | 23 | it(`should have as title 'todolist'`, () => { 24 | const fixture = TestBed.createComponent(AppComponent); 25 | const app = fixture.componentInstance; 26 | expect(app.title).toEqual('todolist'); 27 | }); 28 | 29 | it('should render title', () => { 30 | const fixture = TestBed.createComponent(AppComponent); 31 | fixture.detectChanges(); 32 | const compiled = fixture.nativeElement as HTMLElement; 33 | expect(compiled.querySelector('.content span')?.textContent).toContain('todolist app is running!'); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /ui/apps/todolist/src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { OnInit } from '@angular/core'; 3 | import { AuthService } from './core/auth.service'; 4 | import { MatDialog } from '@angular/material/dialog'; 5 | import { MatIconRegistry } from '@angular/material/icon'; 6 | import { DomSanitizer } from '@angular/platform-browser'; 7 | import { DialogComponent } from './dialog/dialog.component'; 8 | 9 | import { User } from './core/user.model'; 10 | import { Observable } from 'rxjs'; 11 | 12 | @Component({ 13 | selector: 'app-root', 14 | templateUrl: './app.component.html', 15 | styleUrls: ['./app.component.css'], 16 | standalone: false 17 | }) 18 | export class AppComponent implements OnInit { 19 | title = 'todolist'; 20 | links = [ 21 | { 22 | icon: 'home', 23 | path: '', 24 | label: 'HOME', 25 | }, 26 | 27 | { 28 | icon: 'list', 29 | path: '/post/list', 30 | label: 'POSTS', 31 | }, 32 | { 33 | icon: 'add', 34 | path: '/post/new', 35 | label: 'NEW POST', 36 | }, 37 | ]; 38 | 39 | isDarkTheme = false; 40 | currentUser: Observable; 41 | 42 | constructor( 43 | private auth: AuthService, 44 | private iconRegistry: MatIconRegistry, 45 | private sanitizer: DomSanitizer, 46 | private dialog: MatDialog, 47 | ) { 48 | // To avoid XSS attacks, the URL needs to be trusted from inside of your application. 49 | const avatarsSafeUrl = this.sanitizer.bypassSecurityTrustResourceUrl('./assets/avatars.svg'); 50 | this.iconRegistry.addSvgIconSetInNamespace('avatars', avatarsSafeUrl); 51 | this.currentUser = this.auth.currentUser(); 52 | } 53 | 54 | // openAdminDialog() { 55 | // this.dialog.open(DialogComponent).afterClosed() 56 | // .filter(result => !!result) 57 | // .subscribe(user => { 58 | // this.users.push(user); 59 | // this.selectedUser = user; 60 | // }); 61 | // } 62 | 63 | ngOnInit(): void { 64 | console.log('calling ngOnInit...'); 65 | this.auth.verifyAuth(); 66 | } 67 | 68 | signout() { 69 | this.auth.signout(); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /ui/apps/todolist/src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { BrowserModule } from '@angular/platform-browser'; 2 | import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; 3 | import { NgModule } from '@angular/core'; 4 | 5 | import { AppComponent } from './app.component'; 6 | import { CoreModule } from './core/core.module'; 7 | import { SharedModule } from './shared/shared.module'; 8 | import { HomeModule } from './home/home.module'; 9 | import { AppRoutingModule } from './app-routing.module'; 10 | import { DialogComponent } from './dialog/dialog.component'; 11 | import { LoadGuard } from './core/load-guard'; 12 | 13 | @NgModule({ 14 | declarations: [AppComponent, DialogComponent], 15 | imports: [ 16 | BrowserModule, 17 | BrowserAnimationsModule, 18 | CoreModule, 19 | SharedModule, 20 | AppRoutingModule, 21 | HomeModule 22 | ], 23 | providers: [], 24 | bootstrap: [AppComponent] 25 | }) 26 | export class AppModule { } 27 | -------------------------------------------------------------------------------- /ui/apps/todolist/src/app/auth/auth-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { Routes, RouterModule } from '@angular/router'; 3 | import { SigninComponent } from './signin/signin.component'; 4 | 5 | const routes: Routes = [ 6 | { 7 | path: 'signin', 8 | component: SigninComponent 9 | } 10 | ]; 11 | 12 | @NgModule({ 13 | imports: [RouterModule.forChild(routes)], 14 | exports: [RouterModule] 15 | }) 16 | export class AuthRoutingModule {} 17 | -------------------------------------------------------------------------------- /ui/apps/todolist/src/app/auth/auth.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | 4 | import { AuthRoutingModule } from './auth-routing.module'; 5 | import { SigninComponent } from './signin/signin.component'; 6 | import { SharedModule } from '../shared/shared.module'; 7 | 8 | @NgModule({ 9 | imports: [ 10 | SharedModule, 11 | AuthRoutingModule 12 | ], 13 | declarations: [SigninComponent] 14 | }) 15 | export class AuthModule { } 16 | -------------------------------------------------------------------------------- /ui/apps/todolist/src/app/auth/signin/signin.component.css: -------------------------------------------------------------------------------- 1 | :host { 2 | margin: 85px auto; 3 | } 4 | 5 | .mat-form-field { 6 | width: 100%; 7 | min-width: 300px; 8 | } 9 | 10 | mat-card-title, 11 | mat-card-content { 12 | display: flex; 13 | justify-content: center; 14 | } 15 | 16 | .loginError { 17 | padding: 16px; 18 | width: 300px; 19 | color: white; 20 | background-color: red; 21 | } 22 | 23 | .loginButtons { 24 | display: flex; 25 | flex-direction: row; 26 | justify-content: flex-end; 27 | } 28 | -------------------------------------------------------------------------------- /ui/apps/todolist/src/app/auth/signin/signin.component.html: -------------------------------------------------------------------------------- 1 | 2 | PLEASE SIGNIN 3 | 4 |
5 |

6 | 7 | 9 | 10 | 11 | username is required 12 | 13 |

14 | 15 |

16 | 17 | 19 | 20 | Password is Required. 21 | 22 | 23 | 24 |

25 | 26 |

27 | {{ errorMessage }} 28 |

29 | 30 |

31 | 32 |

33 | 34 |
35 |
36 |
37 | -------------------------------------------------------------------------------- /ui/apps/todolist/src/app/auth/signin/signin.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { AuthService } from '../../core/auth.service'; 3 | import { Router } from '@angular/router'; 4 | import { Subscription } from 'rxjs'; 5 | 6 | @Component({ 7 | selector: 'app-signin', 8 | templateUrl: './signin.component.html', 9 | styleUrls: ['./signin.component.css'], 10 | standalone: false 11 | }) 12 | export class SigninComponent implements OnInit { 13 | username = ''; 14 | password = ''; 15 | errorMessage = ''; 16 | 17 | sub: Subscription = null; 18 | 19 | constructor(private auth: AuthService, private router: Router) {} 20 | 21 | ngOnInit() { 22 | console.log('calling ngOnInit...'); 23 | } 24 | 25 | submit() { 26 | console.log('calling submit...'); 27 | this.auth.attempAuth({ username: this.username, password: this.password }).subscribe( 28 | (data) => { 29 | // console.log(data); 30 | this.router.navigate(['']); 31 | }, 32 | (err) => { 33 | // console.log(err); 34 | this.errorMessage = 'login failed'; 35 | return; 36 | }, 37 | ); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /ui/apps/todolist/src/app/core/auth-guard.ts: -------------------------------------------------------------------------------- 1 | import { Router, ActivatedRouteSnapshot, RouterStateSnapshot, UrlTree } from '@angular/router'; 2 | import { Injectable } from '@angular/core'; 3 | import { Observable } from 'rxjs'; 4 | import { AuthService } from './auth.service'; 5 | 6 | @Injectable() 7 | export class AuthGuard { 8 | 9 | constructor(private router: Router, private authService: AuthService) { } 10 | 11 | canActivate( 12 | route: ActivatedRouteSnapshot, 13 | state: RouterStateSnapshot 14 | ): Observable | Promise | boolean | UrlTree{ 15 | console.log('route::' + route.url); 16 | console.log('state::' + state.url); 17 | 18 | this.authService.isAuthenticated().subscribe(auth => { 19 | if (!auth) { 20 | this.router.navigate(['', 'auth', 'signin']); 21 | return false; 22 | } 23 | }); 24 | return true; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /ui/apps/todolist/src/app/core/auth-inteceptor.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { HttpInterceptor, HttpRequest, HttpHandler, HttpSentEvent, HttpHeaderResponse, HttpProgressEvent, HttpResponse, HttpUserEvent, HttpEvent, HttpErrorResponse } from '@angular/common/http'; 3 | import { Observable } from 'rxjs'; 4 | import { tap } from 'rxjs/operators'; 5 | import { TokenStorage } from './token-storage'; 6 | import { Router } from '@angular/router'; 7 | 8 | const TOKEN_HEADER_KEY = 'X-AUTH-TOKEN'; 9 | 10 | @Injectable() 11 | export class AuthInterceptor implements HttpInterceptor { 12 | constructor(private token: TokenStorage, private router: Router) {} 13 | 14 | intercept( 15 | req: HttpRequest, 16 | next: HttpHandler, 17 | ): Observable | HttpUserEvent> { 18 | return next.handle(req).pipe( 19 | tap({ 20 | next: (event: HttpEvent) => { 21 | if (event instanceof HttpResponse) { 22 | console.log(" http response headers:", event.headers) 23 | const token = event.headers.get(TOKEN_HEADER_KEY); 24 | if (token) { 25 | console.log('saving token ::' + token); 26 | this.token.save(token); 27 | } 28 | } 29 | }, 30 | error: (err: any) => { 31 | if (err instanceof HttpErrorResponse) { 32 | console.log(err); 33 | console.log('req url :: ' + req.url); 34 | if (!req.url.endsWith('/auth/user') && err.status === 401) { 35 | this.router.navigate(['', 'auth', 'signin']); 36 | } 37 | } 38 | }, 39 | }), 40 | ); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /ui/apps/todolist/src/app/core/auth.service.ts: -------------------------------------------------------------------------------- 1 | import { HttpClient, HttpHeaders } from '@angular/common/http'; 2 | import { Injectable } from '@angular/core'; 3 | import { Router } from '@angular/router'; 4 | import { BehaviorSubject, Observable } from 'rxjs'; 5 | import { distinctUntilChanged, map } from 'rxjs/operators'; 6 | 7 | import { environment } from '../../environments/environment'; 8 | import { Credentials } from './credentials.model'; 9 | import { TokenStorage } from './token-storage'; 10 | import { User } from './user.model'; 11 | 12 | const apiUrl = environment.baseApiUrl; 13 | 14 | interface State { 15 | user: User | null; 16 | authenticated: boolean; 17 | } 18 | 19 | const defaultState: State = { 20 | user: null, 21 | authenticated: false, 22 | }; 23 | 24 | const store = new BehaviorSubject(defaultState); 25 | 26 | class Store { 27 | private _store = store; 28 | changes = this._store.asObservable().pipe(distinctUntilChanged()); 29 | 30 | setState(state: State) { 31 | console.log('update user state:' + JSON.stringify(state)); 32 | this._store.next(state); 33 | } 34 | 35 | getState(): State { 36 | return this._store.value; 37 | } 38 | 39 | updateState(data: State) { 40 | this._store.next(Object.assign({}, this.getState(), data)); 41 | } 42 | 43 | purge() { 44 | this._store.next(defaultState); 45 | } 46 | } 47 | 48 | @Injectable() 49 | export class AuthService { 50 | private store: Store = new Store(); 51 | 52 | constructor(private http: HttpClient, private jwt: TokenStorage, private router: Router) {} 53 | 54 | signin(credentials: Credentials) { 55 | return this.http 56 | .post(`${apiUrl}/login`, credentials, { 57 | observe: 'response', 58 | responseType: 'json', 59 | }) 60 | .subscribe({ 61 | next: (data) => { 62 | if (data.status === 200) { 63 | const token = data.headers.get('X-AUTH-TOKEN'); 64 | console.log('login successfully, token: ' + token); 65 | if (token != null) this.jwt.save(token); 66 | this.store.setState({ 67 | user: data.body, 68 | authenticated: Boolean(data), 69 | }); 70 | } else { 71 | console.log('signin failed'); 72 | } 73 | }, 74 | error: (err) => { 75 | console.log(err); 76 | }, 77 | complete: () => { 78 | console.log('complete'); 79 | }, 80 | }); 81 | } 82 | 83 | restoreState() { 84 | const token = this.jwt.get(); 85 | if (token != null) { 86 | const headers = new HttpHeaders({ 87 | 'X-AUTH-TOKEN': token, 88 | }); 89 | this.http.get(`${apiUrl}/me`, { headers: headers, observe: 'response', responseType: 'json' }).subscribe({ 90 | next: (data) => { 91 | if (data.status === 200) { 92 | this.store.setState({ 93 | user: data.body, 94 | authenticated: true, 95 | }); 96 | } else { 97 | console.log('attemp restore state from token failed'); 98 | this.jwt.destroy(); 99 | } 100 | }, 101 | }); 102 | } 103 | } 104 | 105 | signout() { 106 | this.http.get(`${apiUrl}/logout`).subscribe({ 107 | next: (data) => { 108 | // reset the initial values 109 | this.jwt.destroy(); 110 | this.store.purge(); 111 | }, 112 | }); 113 | } 114 | 115 | currentUser(): Observable { 116 | return this.store.changes.pipe(map((data) => data.user)); 117 | } 118 | 119 | isAuthenticated(): Observable { 120 | return this.store.changes.pipe(map((data) => data.authenticated)); 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /ui/apps/todolist/src/app/core/core.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule, Optional, SkipSelf } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { HTTP_INTERCEPTORS, provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'; 4 | import { AuthGuard } from './auth-guard'; 5 | import { AuthService } from './auth.service'; 6 | import { TokenStorage } from './token-storage'; 7 | import { RouterModule } from '@angular/router'; 8 | import { AuthInterceptor } from './auth-inteceptor'; 9 | import { LoadGuard } from './load-guard'; 10 | import { TokenInterceptor } from './token-inteceptor'; 11 | 12 | @NgModule({ declarations: [], imports: [CommonModule, RouterModule], providers: [ 13 | AuthGuard, 14 | LoadGuard, 15 | AuthService, 16 | TokenStorage, 17 | { 18 | provide: HTTP_INTERCEPTORS, 19 | useClass: TokenInterceptor, 20 | multi: true, 21 | }, 22 | { 23 | provide: HTTP_INTERCEPTORS, 24 | useClass: AuthInterceptor, 25 | multi: true, 26 | }, 27 | provideHttpClient(withInterceptorsFromDi()), 28 | ] }) 29 | export class CoreModule { 30 | // Prevent reimport of the CoreModule 31 | constructor(@Optional() @SkipSelf() parentModule: CoreModule) { 32 | if (parentModule) { 33 | throw new Error('CoreModule is already loaded. Import it in the AppModule only'); 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /ui/apps/todolist/src/app/core/credentials.model.ts: -------------------------------------------------------------------------------- 1 | export interface Credentials { 2 | username: string; 3 | password: string; 4 | } 5 | -------------------------------------------------------------------------------- /ui/apps/todolist/src/app/core/load-guard.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Route, Router } from '@angular/router'; 3 | import { AuthService } from './auth.service'; 4 | import { tap } from 'rxjs/operators'; 5 | 6 | @Injectable() 7 | export class LoadGuard { 8 | constructor(private router: Router, private authService: AuthService) { } 9 | 10 | canLoad(route: Route) { 11 | console.log('call canLoad guard.'); 12 | this.authService.isAuthenticated().pipe( 13 | tap(auth => { 14 | if (!auth) { 15 | this.router.navigate(['', 'auth', 'signin']); 16 | return false; 17 | } 18 | }) 19 | ); 20 | return true; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /ui/apps/todolist/src/app/core/token-inteceptor.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { HttpInterceptor, HttpRequest, HttpHandler, HttpSentEvent, HttpHeaderResponse, HttpProgressEvent, HttpResponse, HttpUserEvent, HttpEvent, HttpErrorResponse } from '@angular/common/http'; 3 | import { Observable } from 'rxjs'; 4 | import { TokenStorage } from './token-storage'; 5 | import { Router } from '@angular/router'; 6 | 7 | const TOKEN_HEADER_KEY = 'X-AUTH-TOKEN'; 8 | 9 | @Injectable() 10 | export class TokenInterceptor implements HttpInterceptor { 11 | 12 | constructor(private token: TokenStorage, private router: Router) { } 13 | 14 | intercept(req: HttpRequest, next: HttpHandler): 15 | Observable | HttpUserEvent> { 16 | 17 | 18 | // set X-Requested-With = XMLHttpRequest 19 | req.headers.set('X-Requested-With', 'XMLHttpRequest'); 20 | 21 | // set X-AUTH-TOKEN 22 | if (this.token.get()) { 23 | console.log('set token in header ::' + this.token.get()); 24 | const authReq = req.clone({headers: req.headers.set(TOKEN_HEADER_KEY, this.token.get())}); 25 | return next.handle(authReq); 26 | } 27 | 28 | return next.handle(req); 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /ui/apps/todolist/src/app/core/token-storage.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, Inject } from '@angular/core'; 2 | 3 | const TOKEN_KEY = 'xAuthToken'; 4 | 5 | @Injectable() 6 | export class TokenStorage { 7 | constructor() {} 8 | 9 | save(token: string) { 10 | window.localStorage[TOKEN_KEY] = token; 11 | } 12 | 13 | get() { 14 | return window.localStorage[TOKEN_KEY]; 15 | } 16 | 17 | destroy() { 18 | window.localStorage.removeItem(TOKEN_KEY); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /ui/apps/todolist/src/app/core/user.model.ts: -------------------------------------------------------------------------------- 1 | export interface User { 2 | name: string; 3 | roles?: string[]; 4 | } 5 | -------------------------------------------------------------------------------- /ui/apps/todolist/src/app/dialog/dialog.component.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hantsy/angular-spring-reactive-sample/c931f218f69f576da27da2b2d48bdf0c43352b4b/ui/apps/todolist/src/app/dialog/dialog.component.css -------------------------------------------------------------------------------- /ui/apps/todolist/src/app/dialog/dialog.component.html: -------------------------------------------------------------------------------- 1 |

Add User Dialog

2 |
3 |
4 |
5 | 6 | 7 | 8 | Avatar - {{i + 1}} 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 |
21 | Is Admin? 22 | Is Cool? 23 |
24 |
25 | 26 | 27 | 28 | 29 |
30 | -------------------------------------------------------------------------------- /ui/apps/todolist/src/app/dialog/dialog.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { MatDialogRef } from '@angular/material/dialog'; 3 | 4 | @Component({ 5 | templateUrl: 'dialog.component.html', 6 | standalone: false 7 | }) 8 | export class DialogComponent { 9 | avatars = new Array(16).fill(0).map((_, i) => `svg-${i + 1}`); 10 | selectedAvatar = this.avatars[0]; 11 | 12 | constructor(public dialogRef: MatDialogRef) { } 13 | } 14 | -------------------------------------------------------------------------------- /ui/apps/todolist/src/app/home/home-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { Routes, RouterModule } from '@angular/router'; 3 | import { HomeComponent } from './home.component'; 4 | 5 | const routes: Routes = [ 6 | { path: 'home', component: HomeComponent } 7 | ]; 8 | 9 | @NgModule({ 10 | imports: [RouterModule.forChild(routes)], 11 | exports: [RouterModule], 12 | providers: [] 13 | }) 14 | export class HomeRoutingModule { } 15 | -------------------------------------------------------------------------------- /ui/apps/todolist/src/app/home/home.component.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hantsy/angular-spring-reactive-sample/c931f218f69f576da27da2b2d48bdf0c43352b4b/ui/apps/todolist/src/app/home/home.component.css -------------------------------------------------------------------------------- /ui/apps/todolist/src/app/home/home.component.html: -------------------------------------------------------------------------------- 1 |

{{ message }}

2 | -------------------------------------------------------------------------------- /ui/apps/todolist/src/app/home/home.component.spec.ts: -------------------------------------------------------------------------------- 1 | // Http testing module and mocking controller 2 | import { HttpTestingController } from '@angular/common/http/testing'; 3 | import { HttpClient, HttpErrorResponse } from '@angular/common/http'; 4 | import { TestBed, async, inject } from '@angular/core/testing'; 5 | import { 6 | BaseRequestOptions, 7 | Http, 8 | Response, 9 | ResponseOptions 10 | } from '@angular/http'; 11 | import { Observable, of, empty } from 'rxjs'; 12 | import { HomeComponent } from './home.component'; 13 | 14 | describe('Component: HomeComponent', () => { 15 | let comp: HomeComponent; 16 | 17 | it('message contains `Spring Boot 2`', () => { 18 | comp = new HomeComponent(); 19 | expect(comp.message).toContain('Spring Boot 2'); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /ui/apps/todolist/src/app/home/home.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-home', 5 | templateUrl: './home.component.html', 6 | styleUrls: ['./home.component.css'], 7 | standalone: false 8 | }) 9 | export class HomeComponent implements OnInit { 10 | constructor() {} 11 | 12 | ngOnInit() {} 13 | 14 | get message(): string { 15 | return 'Welcome to Angular and Spring Boot 2'; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /ui/apps/todolist/src/app/home/home.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { HomeComponent } from './home.component'; 4 | import { SharedModule } from '../shared/shared.module'; 5 | import { HomeRoutingModule } from './home-routing.module'; 6 | 7 | @NgModule({ 8 | imports: [ 9 | SharedModule, HomeRoutingModule 10 | ], 11 | declarations: [HomeComponent] 12 | }) 13 | export class HomeModule { } 14 | -------------------------------------------------------------------------------- /ui/apps/todolist/src/app/http-client.spec.ts: -------------------------------------------------------------------------------- 1 | // Http testing module and mocking controller 2 | import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing'; 3 | 4 | // Other imports 5 | import { TestBed } from '@angular/core/testing'; 6 | import { HttpClient, HttpErrorResponse, provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'; 7 | 8 | import { HttpHeaders } from '@angular/common/http'; 9 | 10 | interface Data { 11 | name: string; 12 | } 13 | 14 | const testUrl = '/data'; 15 | 16 | describe('HttpClient testing', () => { 17 | let httpClient: HttpClient; 18 | let httpTestingController: HttpTestingController; 19 | 20 | beforeEach(() => { 21 | TestBed.configureTestingModule({ 22 | imports: [], 23 | providers: [provideHttpClient(withInterceptorsFromDi()), provideHttpClientTesting()] 24 | }); 25 | 26 | // Inject the http service and test controller for each test 27 | httpClient = TestBed.get(HttpClient); 28 | httpTestingController = TestBed.get(HttpTestingController); 29 | }); 30 | afterEach(() => { 31 | // After every test, assert that there are no more pending requests. 32 | httpTestingController.verify(); 33 | }); 34 | /// Tests begin /// 35 | it('can test HttpClient.get', () => { 36 | const testData: Data = { name: 'Test Data' }; 37 | 38 | // Make an HTTP GET request 39 | httpClient.get(testUrl).subscribe(data => 40 | // When observable resolves, result should match test data 41 | expect(data).toEqual(testData) 42 | ); 43 | 44 | // The following `expectOne()` will match the request's URL. 45 | // If no requests or multiple requests matched that URL 46 | // `expectOne()` would throw. 47 | const req = httpTestingController.expectOne('/data'); 48 | 49 | // Assert that the request is a GET. 50 | expect(req.request.method).toEqual('GET'); 51 | 52 | // Respond with mock data, causing Observable to resolve. 53 | // Subscribe callback asserts that correct data was returned. 54 | req.flush(testData); 55 | 56 | // Finally, assert that there are no outstanding requests. 57 | httpTestingController.verify(); 58 | }); 59 | it('can test HttpClient.get with matching header', () => { 60 | const testData: Data = { name: 'Test Data' }; 61 | 62 | // Make an HTTP GET request with specific header 63 | httpClient 64 | .get(testUrl, { 65 | headers: new HttpHeaders({ Authorization: 'my-auth-token' }) 66 | }) 67 | .subscribe(data => expect(data).toEqual(testData)); 68 | 69 | // Find request with a predicate function. 70 | // Expect one request with an authorization header 71 | const req = httpTestingController.expectOne(req => 72 | req.headers.has('Authorization') 73 | ); 74 | req.flush(testData); 75 | }); 76 | 77 | it('can test multiple requests', () => { 78 | const testData: Data[] = [ 79 | { name: 'bob' }, 80 | { name: 'carol' }, 81 | { name: 'ted' }, 82 | { name: 'alice' } 83 | ]; 84 | 85 | // Make three requests in a row 86 | httpClient 87 | .get(testUrl) 88 | .subscribe(d => expect(d.length).toEqual(0, 'should have no data')); 89 | 90 | httpClient 91 | .get(testUrl) 92 | .subscribe(d => 93 | expect(d).toEqual([testData[0]], 'should be one element array') 94 | ); 95 | 96 | httpClient 97 | .get(testUrl) 98 | .subscribe(d => expect(d).toEqual(testData, 'should be expected data')); 99 | 100 | // get all pending requests that match the given URL 101 | const requests = httpTestingController.match(testUrl); 102 | expect(requests.length).toEqual(3); 103 | 104 | // Respond to each request with different results 105 | requests[0].flush([]); 106 | requests[1].flush([testData[0]]); 107 | requests[2].flush(testData); 108 | }); 109 | 110 | it('can test for 404 error', () => { 111 | const emsg = 'deliberate 404 error'; 112 | 113 | httpClient.get(testUrl).subscribe( 114 | data => fail('should have failed with the 404 error'), 115 | (error: HttpErrorResponse) => { 116 | expect(error.status).toEqual(404, 'status'); 117 | expect(error.error).toEqual(emsg, 'message'); 118 | } 119 | ); 120 | 121 | const req = httpTestingController.expectOne(testUrl); 122 | 123 | // Respond with mock error 124 | req.flush(emsg, { status: 404, statusText: 'Not Found' }); 125 | }); 126 | 127 | it('can test for network error', () => { 128 | const emsg = 'simulated network error'; 129 | 130 | httpClient.get(testUrl).subscribe( 131 | data => fail('should have failed with the network error'), 132 | (error: HttpErrorResponse) => { 133 | expect(error.error.message).toEqual(emsg, 'message'); 134 | } 135 | ); 136 | 137 | const req = httpTestingController.expectOne(testUrl); 138 | 139 | // Create mock ErrorEvent, raised when something goes wrong at the network level. 140 | // Connection timeout, DNS error, offline, etc 141 | const mockError = new ErrorEvent('Network error', { 142 | message: emsg, 143 | // The rest of this is optional and not used. 144 | // Just showing that you could provide this too. 145 | filename: 'HeroService.ts', 146 | lineno: 42, 147 | colno: 21 148 | }); 149 | 150 | // Respond with mock error 151 | req.error(mockError); 152 | }); 153 | 154 | it('httpTestingController.verify should fail if HTTP response not simulated', () => { 155 | // Sends request 156 | httpClient.get('some/api').subscribe(); 157 | 158 | // verify() should fail because haven't handled the pending request. 159 | expect(() => httpTestingController.verify()).toThrow(); 160 | 161 | // Now get and flush the request so that afterEach() doesn't fail 162 | const req = httpTestingController.expectOne('some/api'); 163 | req.flush(null); 164 | }); 165 | 166 | // Proves that verify in afterEach() really would catch error 167 | // if test doesn't simulate the HTTP response. 168 | // 169 | // Must disable this test because can't catch an error in an afterEach(). 170 | // Uncomment if you want to confirm that afterEach() does the job. 171 | // it('afterEach() should fail when HTTP response not simulated',() => { 172 | // // Sends request which is never handled by this test 173 | // httpClient.get('some/api').subscribe(); 174 | // }); 175 | }); 176 | 177 | /* 178 | Copyright 2017-2018 Google Inc. All Rights Reserved. 179 | Use of this source code is governed by an MIT-style license that 180 | can be found in the LICENSE file at http://angular.io/license 181 | */ 182 | -------------------------------------------------------------------------------- /ui/apps/todolist/src/app/http-error-handler.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { HttpErrorResponse } from '@angular/common/http'; 3 | 4 | import { Observable, of } from 'rxjs'; 5 | 6 | import { MessageService } from './message.service'; 7 | 8 | /** Type of the handleError function returned by HttpErrorHandler.createHandleError */ 9 | export type HandleError = ( 10 | operation?: string, 11 | result?: T 12 | ) => (error: HttpErrorResponse) => Observable; 13 | 14 | /** Handles HttpClient errors */ 15 | @Injectable() 16 | export class HttpErrorHandler { 17 | constructor(private messageService: MessageService) {} 18 | 19 | /** Create curried handleError function that already knows the service name */ 20 | createHandleError = (serviceName = '') => ( 21 | operation = 'operation', 22 | result = {} as T 23 | ) => this.handleError(serviceName, operation, result) 24 | 25 | /** 26 | * Returns a function that handles Http operation failures. 27 | * This error handler lets the app continue to run as if no error occurred. 28 | * @param serviceName = name of the data service that attempted the operation 29 | * @param operation - name of the operation that failed 30 | * @param result - optional value to return as the observable result 31 | */ 32 | handleError(serviceName = '', operation = 'operation', result = {} as T) { 33 | return (error: HttpErrorResponse): Observable => { 34 | // TODO: send the error to remote logging infrastructure 35 | console.error(error); // log to console instead 36 | 37 | const message = 38 | error.error instanceof ErrorEvent 39 | ? error.error.message 40 | : `server returned code ${error.status} with body "${error.error}"`; 41 | 42 | // TODO: better job of transforming error for user consumption 43 | this.messageService.add( 44 | `${serviceName}: ${operation} failed: ${message}` 45 | ); 46 | 47 | // Let the app keep running by returning a safe result. 48 | return of(result); 49 | }; 50 | } 51 | } 52 | 53 | /* 54 | Copyright 2017-2018 Google Inc. All Rights Reserved. 55 | Use of this source code is governed by an MIT-style license that 56 | can be found in the LICENSE file at http://angular.io/license 57 | */ 58 | -------------------------------------------------------------------------------- /ui/apps/todolist/src/app/message.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | 3 | @Injectable() 4 | export class MessageService { 5 | messages: string[] = []; 6 | 7 | add(message: string) { 8 | this.messages.push(message); 9 | } 10 | 11 | clear() { 12 | this.messages = []; 13 | } 14 | } 15 | 16 | /* 17 | Copyright 2017-2018 Google Inc. All Rights Reserved. 18 | Use of this source code is governed by an MIT-style license that 19 | can be found in the LICENSE file at http://angular.io/license 20 | */ 21 | -------------------------------------------------------------------------------- /ui/apps/todolist/src/app/post/edit-post/edit-post.component.css: -------------------------------------------------------------------------------- 1 | :host{ 2 | display: flex; 3 | flex: 1 auto; 4 | } 5 | -------------------------------------------------------------------------------- /ui/apps/todolist/src/app/post/edit-post/edit-post.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 |

{{'edit-post'}}

6 |
7 | 8 |

created at: {{post.createdDate|date:'short'}} • {{post.author?.username||'user'}}

9 |

10 | all fields marked with star are required. 11 |

12 |
13 |
14 | 15 | 16 | 17 | 18 | back to 19 | {{'post-list'}} 20 | 21 |
22 | -------------------------------------------------------------------------------- /ui/apps/todolist/src/app/post/edit-post/edit-post.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, OnDestroy } from '@angular/core'; 2 | import { Router, ActivatedRoute } from '@angular/router'; 3 | import { Subscription } from 'rxjs'; 4 | import { Post } from '../shared/post.model'; 5 | import { PostService } from '../shared/post.service'; 6 | 7 | @Component({ 8 | selector: 'app-edit-post', 9 | templateUrl: './edit-post.component.html', 10 | styleUrls: ['./edit-post.component.css'], 11 | standalone: false 12 | }) 13 | export class EditPostComponent implements OnInit, OnDestroy { 14 | post: Post = { title: '', content: '' }; 15 | slug: string; 16 | 17 | constructor(private router: Router, private route: ActivatedRoute) { } 18 | 19 | onPostUpdated(event) { 20 | console.log('post was updated!' + event); 21 | if (event) { 22 | this.router.navigate(['', 'post', 'list']); 23 | } 24 | } 25 | 26 | ngOnInit() { 27 | this.post = this.route.snapshot.data['post']; 28 | } 29 | 30 | ngOnDestroy() { 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /ui/apps/todolist/src/app/post/new-post/new-post.component.css: -------------------------------------------------------------------------------- 1 | :host{ 2 | display: flex; 3 | flex: 1 auto; 4 | } 5 | -------------------------------------------------------------------------------- /ui/apps/todolist/src/app/post/new-post/new-post.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |

{{'new-post'}}

5 | 6 | all fields marked with star are required. 7 | 8 |
9 | 10 | 11 | 12 | 13 | back to {{'post-list'}} 14 | 15 |
16 | -------------------------------------------------------------------------------- /ui/apps/todolist/src/app/post/new-post/new-post.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, OnDestroy } from '@angular/core'; 2 | import { Router } from '@angular/router'; 3 | import { Subscription } from 'rxjs'; 4 | import { Post } from '../shared/post.model'; 5 | import { PostService } from '../shared/post.service'; 6 | 7 | @Component({ 8 | selector: 'app-new-post', 9 | templateUrl: './new-post.component.html', 10 | styleUrls: ['./new-post.component.css'], 11 | standalone: false 12 | }) 13 | export class NewPostComponent implements OnInit, OnDestroy { 14 | post: Post = { title: '', content: '' }; 15 | sub: Subscription; 16 | 17 | constructor( private router: Router) { } 18 | 19 | onPostSaved(event) { 20 | console.log('post was saved::' + event); 21 | if (event) { 22 | this.router.navigate(['', 'post']); 23 | } 24 | } 25 | 26 | ngOnInit() { 27 | console.log('calling ngOnInit::NewPostComponent...'); 28 | } 29 | 30 | ngOnDestroy() { 31 | console.log('calling ngOnDestroy::NewPostComponent...'); 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /ui/apps/todolist/src/app/post/post-details/comment/comment-form.component.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hantsy/angular-spring-reactive-sample/c931f218f69f576da27da2b2d48bdf0c43352b4b/ui/apps/todolist/src/app/post/post-details/comment/comment-form.component.css -------------------------------------------------------------------------------- /ui/apps/todolist/src/app/post/post-details/comment/comment-form.component.html: -------------------------------------------------------------------------------- 1 |
3 |

4 | 5 | 7 | Comment is required 8 | At least 10 chars 9 | 10 |

11 |

12 | 14 |

15 |
16 | -------------------------------------------------------------------------------- /ui/apps/todolist/src/app/post/post-details/comment/comment-form.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; 2 | import { FormBuilder, FormControl, FormGroup, Validators } from '@angular/forms'; 3 | import { Post } from '../../shared/post.model'; 4 | import { PostService } from '../../shared/post.service'; 5 | 6 | @Component({ 7 | selector: 'app-comment-form', 8 | templateUrl: './comment-form.component.html', 9 | styleUrls: ['./comment-form.component.css'], 10 | standalone: false 11 | }) 12 | export class CommentFormComponent implements OnInit { 13 | @Input() post: Post; 14 | @Output() save = new EventEmitter(); 15 | 16 | commentForm: FormGroup; 17 | content: FormControl; 18 | 19 | constructor(private postService: PostService, private fb: FormBuilder) { 20 | this.content = new FormControl('', [Validators.required, Validators.minLength(10)]); 21 | this.commentForm = fb.group({ 22 | content: this.content, 23 | }); 24 | } 25 | 26 | ngOnInit() {} 27 | 28 | saveCommentForm() { 29 | console.log('submitting comment form @' + this.commentForm.value); 30 | 31 | this.postService.saveComment(this.post.id, this.commentForm.value).subscribe({ 32 | next: (data) => { 33 | //this.commentForm.controls['content'].setValue(null); 34 | this.content.reset(''); 35 | this.commentForm.markAsPristine(); 36 | this.commentForm.markAsUntouched(); 37 | this.save.emit(true); 38 | }, 39 | error: (error) => { 40 | this.save.emit(false); 41 | }, 42 | }); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /ui/apps/todolist/src/app/post/post-details/comment/comment-list-item.component.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hantsy/angular-spring-reactive-sample/c931f218f69f576da27da2b2d48bdf0c43352b4b/ui/apps/todolist/src/app/post/post-details/comment/comment-list-item.component.css -------------------------------------------------------------------------------- /ui/apps/todolist/src/app/post/post-details/comment/comment-list-item.component.html: -------------------------------------------------------------------------------- 1 | 2 | account_circle 3 |

{{comment.author?.username||'user'}} • {{comment.createdDate|date:'short'}}

4 |

{{comment.content}}

5 |
6 | -------------------------------------------------------------------------------- /ui/apps/todolist/src/app/post/post-details/comment/comment-list-item.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, Input } from '@angular/core'; 2 | 3 | import { Comment } from '../../shared/comment.model'; 4 | 5 | @Component({ 6 | selector: 'app-comment-list-item', 7 | templateUrl: './comment-list-item.component.html', 8 | styleUrls: ['./comment-list-item.component.css'], 9 | standalone: false 10 | }) 11 | export class CommentListItemComponent implements OnInit { 12 | 13 | @Input() comment: Comment; 14 | 15 | constructor() { } 16 | 17 | ngOnInit() { 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /ui/apps/todolist/src/app/post/post-details/comment/comment-list.component.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hantsy/angular-spring-reactive-sample/c931f218f69f576da27da2b2d48bdf0c43352b4b/ui/apps/todolist/src/app/post/post-details/comment/comment-list.component.css -------------------------------------------------------------------------------- /ui/apps/todolist/src/app/post/post-details/comment/comment-list.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /ui/apps/todolist/src/app/post/post-details/comment/comment-list.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, OnChanges, Input } from '@angular/core'; 2 | 3 | import { Post } from '../../shared/post.model'; 4 | import { Comment } from '../../shared/comment.model'; 5 | 6 | 7 | @Component({ 8 | selector: 'app-comment-list', 9 | templateUrl: './comment-list.component.html', 10 | styleUrls: ['./comment-list.component.css'], 11 | standalone: false 12 | }) 13 | export class CommentListComponent implements OnInit, OnChanges { 14 | 15 | @Input() post: Post; 16 | @Input() comments: Comment[]; 17 | 18 | constructor() { } 19 | 20 | ngOnInit() { 21 | console.log('calling ngOnInit::CommentListComponent...'); 22 | } 23 | 24 | ngOnChanges(changes: any) { 25 | console.log('calling ngChanges::CommentListComponent...' + JSON.stringify(changes)); 26 | //if (changes['comments'].previousValue != changes['comments'].currentValue) { 27 | // this.comments = changes['comments'].currentValue; 28 | //} 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /ui/apps/todolist/src/app/post/post-details/comment/comment-panel.component.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hantsy/angular-spring-reactive-sample/c931f218f69f576da27da2b2d48bdf0c43352b4b/ui/apps/todolist/src/app/post/post-details/comment/comment-panel.component.css -------------------------------------------------------------------------------- /ui/apps/todolist/src/app/post/post-details/comment/comment-panel.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {{'comments'}} 5 | 6 | You have to login before add your comments. 7 | 8 | 9 | 10 | 11 | 12 | 14 | 15 | 16 | 17 | 18 |
19 | 20 |
21 |
22 |

Please signin and add your comments.

23 |
24 | SIGN IN 25 |
26 |
27 |
28 | 29 | -------------------------------------------------------------------------------- /ui/apps/todolist/src/app/post/post-details/comment/comment-panel.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, Input } from '@angular/core'; 2 | import { Post } from '../../shared/post.model'; 3 | import { Comment } from '../../shared/comment.model'; 4 | import { PostService } from '../../shared/post.service'; 5 | 6 | @Component({ 7 | selector: 'app-comment-panel', 8 | templateUrl: './comment-panel.component.html', 9 | styleUrls: ['./comment-panel.component.css'], 10 | standalone: false 11 | }) 12 | export class CommentPanelComponent implements OnInit { 13 | 14 | @Input() post: Post; 15 | @Input() comments: Comment[]; 16 | 17 | constructor(private postService: PostService) { } 18 | 19 | ngOnInit() { 20 | } 21 | 22 | onCommentSaved(event) { 23 | console.log(event); 24 | if (event) { 25 | this.postService.getCommentsOfPost(this.post.id) 26 | .subscribe( 27 | (data) => this.comments = data, 28 | (err) => console.log(err) 29 | ); 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /ui/apps/todolist/src/app/post/post-details/post-details-panel/post-details-panel.component.css: -------------------------------------------------------------------------------- 1 | :host{ 2 | display: flex; 3 | flex: 1 auto; 4 | } 5 | -------------------------------------------------------------------------------- /ui/apps/todolist/src/app/post/post-details/post-details-panel/post-details-panel.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |

{{post.title}}

4 | {{post?.author?.username||'user'}} • {{post.createdDate|date:'short'}} 5 |
6 | 7 | 8 | {{post.content}} 9 | 10 | 11 |

12 | back to {{'post-list'}}. 13 |

14 |
15 | 18 |
19 | -------------------------------------------------------------------------------- /ui/apps/todolist/src/app/post/post-details/post-details-panel/post-details-panel.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, OnChanges, Input, SimpleChange } from '@angular/core'; 2 | 3 | import { Post } from '../../shared/post.model'; 4 | 5 | @Component({ 6 | selector: 'app-post-details-panel', 7 | templateUrl: './post-details-panel.component.html', 8 | styleUrls: ['./post-details-panel.component.css'], 9 | standalone: false 10 | }) 11 | export class PostDetailsPanelComponent implements OnInit, OnChanges { 12 | 13 | @Input() post: Post; 14 | 15 | constructor() { } 16 | 17 | ngOnInit() { 18 | } 19 | 20 | ngOnChanges(changes: { [propertyName: string]: SimpleChange }) { 21 | 22 | // if (changes && changes['post'] && changes['post'].currentValue) { 23 | // this.post = Object.assign({}, changes['post'].currentValue); 24 | // } 25 | // for (let propName in changes) { 26 | // let chng = changes[propName]; 27 | // let cur = JSON.stringify(chng.currentValue); 28 | // let prev = JSON.stringify(chng.previousValue); 29 | 30 | // console.log(`${propName}: currentValue = ${cur}, previousValue = ${prev}`); 31 | // } 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /ui/apps/todolist/src/app/post/post-details/post-details.component.css: -------------------------------------------------------------------------------- 1 | :host{ 2 | display: flex; 3 | flex: 1 auto; 4 | flex-direction: column; 5 | } 6 | -------------------------------------------------------------------------------- /ui/apps/todolist/src/app/post/post-details/post-details.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /ui/apps/todolist/src/app/post/post-details/post-details.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, OnDestroy, Input } from '@angular/core'; 2 | import { Router, ActivatedRoute } from '@angular/router'; 3 | 4 | import { Observable, Subscription, forkJoin } from 'rxjs'; 5 | import { flatMap } from 'rxjs/operators'; 6 | import { PostService } from '../shared/post.service'; 7 | import { Post } from '../shared/post.model'; 8 | import { Comment } from '../shared/comment.model'; 9 | 10 | @Component({ 11 | selector: 'app-post-details', 12 | templateUrl: './post-details.component.html', 13 | styleUrls: ['./post-details.component.css'], 14 | standalone: false 15 | }) 16 | export class PostDetailsComponent implements OnInit, OnDestroy { 17 | 18 | slug: string; 19 | post: Post = { title: '', content: '' }; 20 | comments: Comment[] = []; 21 | sub: Subscription; 22 | 23 | constructor( 24 | private postService: PostService, 25 | private router: Router, 26 | private route: ActivatedRoute 27 | ) { } 28 | 29 | ngOnInit() { 30 | console.log('calling ngOnInit::PostDetailsComponent... '); 31 | this.sub = this.route.params 32 | .pipe( 33 | flatMap(params => { 34 | this.slug = params['slug']; 35 | return forkJoin(this.postService.getPost(this.slug), this.postService.getCommentsOfPost(this.slug)); 36 | }) 37 | ) 38 | .subscribe((res: Array) => { 39 | console.log(res); 40 | this.post = res[0]; 41 | this.comments = res[1]; 42 | console.log(this.post); 43 | console.log(this.comments); 44 | }); 45 | 46 | // this.sub = this.route.params 47 | // .flatMap(params => this.postService.getPost(params['slug'])) 48 | // .subscribe( 49 | // (data) => this.post = data, 50 | // (err) => console.log(err) 51 | // ); 52 | } 53 | 54 | ngOnDestroy() { 55 | if (this.sub) { this.sub.unsubscribe(); } 56 | } 57 | 58 | } 59 | -------------------------------------------------------------------------------- /ui/apps/todolist/src/app/post/post-list/post-list.component.css: -------------------------------------------------------------------------------- 1 | :host{ 2 | display: flex; 3 | flex: 1 1 auto; 4 | flex-flow: row wrap; 5 | } 6 | 7 | .fab-bottom-right { 8 | position: fixed; 9 | /*add z-index*/ 10 | z-index: 10; 11 | right: 16px; 12 | bottom: 16px; 13 | } 14 | -------------------------------------------------------------------------------- /ui/apps/todolist/src/app/post/post-list/post-list.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |

{{post.title}}

5 |
6 | {{post.author?.username||'user'}} • {{post.createdDate|date:'short'}} 7 |
8 | 9 | 12 | 13 | 14 | edit 15 | 16 | view 17 | 18 | 21 |
22 | 25 | -------------------------------------------------------------------------------- /ui/apps/todolist/src/app/post/post-list/post-list.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnDestroy, OnInit } from '@angular/core'; 2 | import { Router } from '@angular/router'; 3 | import { Subscription } from 'rxjs'; 4 | 5 | import { Post } from '../shared/post.model'; 6 | import { PostService } from '../shared/post.service'; 7 | 8 | @Component({ 9 | selector: 'app-post-list', 10 | templateUrl: './post-list.component.html', 11 | styleUrls: ['./post-list.component.css'], 12 | standalone: false 13 | }) 14 | export class PostListComponent implements OnInit, OnDestroy { 15 | 16 | q = null; 17 | posts: Post[]; 18 | sub: Subscription; 19 | 20 | constructor(private router: Router, private postService: PostService) { 21 | } 22 | 23 | search() { 24 | this.sub = this.postService.getPosts({ q: this.q }) 25 | .subscribe({ 26 | next:data => this.posts = data, 27 | error: err => console.log(err) 28 | } 29 | ); 30 | } 31 | 32 | searchByTerm($event) { 33 | console.log('search by term:' + $event); 34 | this.updateTerm($event); 35 | this.search(); 36 | } 37 | 38 | updateTerm($event) { 39 | console.log('update term:' + $event); 40 | this.q = $event; 41 | } 42 | 43 | clearTerm($event) { 44 | console.log('clear term:' + $event); 45 | this.q = null; 46 | } 47 | 48 | addPost() { 49 | this.router.navigate(['', 'post', 'new']); 50 | } 51 | 52 | ngOnInit() { 53 | console.log('calling ngOnInit::PostListComponent'); 54 | this.search(); 55 | } 56 | 57 | ngOnDestroy() { 58 | console.log('calling ngOnDestroy::PostListComponent'); 59 | if (this.sub) { 60 | this.sub.unsubscribe(); 61 | } 62 | } 63 | 64 | } 65 | -------------------------------------------------------------------------------- /ui/apps/todolist/src/app/post/post-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { Routes, RouterModule } from '@angular/router'; 3 | import { PostDetailsComponent } from './post-details/post-details.component'; 4 | import { AuthGuard } from '../core/auth-guard'; 5 | import { NewPostComponent } from './new-post/new-post.component'; 6 | import { EditPostComponent } from './edit-post/edit-post.component'; 7 | import { PostListComponent } from './post-list/post-list.component'; 8 | import { PostDetailsResolve } from './shared/post-details-resolve'; 9 | 10 | const routes: Routes = [ 11 | { path: '', redirectTo: 'list' }, 12 | { path: 'list', component: PostListComponent }, 13 | { path: 'new', component: NewPostComponent, canActivate: [AuthGuard] }, 14 | { 15 | path: 'edit/:slug', 16 | component: EditPostComponent, 17 | canActivate: [AuthGuard], 18 | resolve: { 19 | post: PostDetailsResolve 20 | } 21 | }, 22 | { path: 'view/:slug', component: PostDetailsComponent } 23 | ]; 24 | 25 | @NgModule({ 26 | imports: [RouterModule.forChild(routes)], 27 | exports: [RouterModule] 28 | }) 29 | export class PostRoutingModule {} 30 | -------------------------------------------------------------------------------- /ui/apps/todolist/src/app/post/post.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { FormsModule, ReactiveFormsModule } from '@angular/forms'; 4 | 5 | import { SharedModule } from '../shared/shared.module'; 6 | import { PostRoutingModule } from './post-routing.module'; 7 | import { PostListComponent } from './post-list/post-list.component'; 8 | import { NewPostComponent } from './new-post/new-post.component'; 9 | import { EditPostComponent } from './edit-post/edit-post.component'; 10 | import { PostDetailsComponent } from './post-details/post-details.component'; 11 | import { PostDetailsPanelComponent } from './post-details/post-details-panel/post-details-panel.component'; 12 | import { CommentListComponent } from './post-details/comment/comment-list.component'; 13 | import { CommentListItemComponent } from './post-details/comment/comment-list-item.component'; 14 | import { CommentFormComponent } from './post-details/comment/comment-form.component'; 15 | import { CommentPanelComponent } from './post-details/comment/comment-panel.component'; 16 | import { PostFormComponent } from './shared/post-form/post-form.component'; 17 | import { PostService } from './shared/post.service'; 18 | import { PostDetailsResolve } from './shared/post-details-resolve'; 19 | 20 | @NgModule({ 21 | imports: [ 22 | SharedModule, 23 | PostRoutingModule 24 | ], 25 | declarations: [ 26 | PostListComponent, 27 | NewPostComponent, 28 | EditPostComponent, 29 | PostDetailsComponent, 30 | PostDetailsPanelComponent, 31 | CommentListComponent, 32 | CommentListItemComponent, 33 | CommentFormComponent, 34 | PostFormComponent, 35 | CommentPanelComponent 36 | ], 37 | exports: [ 38 | PostListComponent, 39 | NewPostComponent, 40 | EditPostComponent, 41 | PostDetailsComponent, 42 | PostDetailsPanelComponent, 43 | CommentListComponent, 44 | CommentListItemComponent, 45 | CommentFormComponent, 46 | PostFormComponent, 47 | CommentPanelComponent 48 | ], 49 | providers: [ 50 | PostService, 51 | PostDetailsResolve 52 | ] 53 | }) 54 | export class PostModule { } 55 | -------------------------------------------------------------------------------- /ui/apps/todolist/src/app/post/shared/comment.model.ts: -------------------------------------------------------------------------------- 1 | import { Username } from './username.model'; 2 | export interface Comment { 3 | id?: string; 4 | content: string; 5 | author?: Username; 6 | createdDate?: string; 7 | } 8 | -------------------------------------------------------------------------------- /ui/apps/todolist/src/app/post/shared/post-details-resolve.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { ActivatedRouteSnapshot } from '@angular/router'; 3 | import { PostService } from './post.service'; 4 | import { Post } from './post.model'; 5 | 6 | @Injectable() 7 | export class PostDetailsResolve { 8 | 9 | constructor(private postService: PostService) {} 10 | 11 | resolve(route: ActivatedRouteSnapshot) { 12 | return this.postService.getPost(route.paramMap.get('slug')); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /ui/apps/todolist/src/app/post/shared/post-form/post-form.component.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hantsy/angular-spring-reactive-sample/c931f218f69f576da27da2b2d48bdf0c43352b4b/ui/apps/todolist/src/app/post/shared/post-form/post-form.component.css -------------------------------------------------------------------------------- /ui/apps/todolist/src/app/post/shared/post-form/post-form.component.html: -------------------------------------------------------------------------------- 1 |
2 |

3 | 4 | 10 | 11 | Post Title is required 12 | 13 | 14 |

15 |

16 | 17 | 26 | 27 | Post Content is required 28 | 29 | 30 | At least 10 chars 31 | 32 | 33 |

34 |

35 | 36 |

37 | 38 |
39 | -------------------------------------------------------------------------------- /ui/apps/todolist/src/app/post/shared/post-form/post-form.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, OnDestroy, Input, Output, EventEmitter } from '@angular/core'; 2 | import { Router, ActivatedRoute } from '@angular/router'; 3 | import { Subscription } from 'rxjs'; 4 | import { Post } from '../post.model'; 5 | import { PostService } from '../post.service'; 6 | 7 | @Component({ 8 | selector: 'app-post-form', 9 | templateUrl: './post-form.component.html', 10 | styleUrls: ['./post-form.component.css'], 11 | standalone: false 12 | }) 13 | export class PostFormComponent implements OnInit, OnDestroy { 14 | @Input() post: Post = { title: '', content: '' }; 15 | @Output() saved: EventEmitter = new EventEmitter(); 16 | sub: Subscription; 17 | 18 | constructor(private postService: PostService) {} 19 | 20 | submit() { 21 | const _body = { 22 | title: this.post.title, 23 | content: this.post.content, 24 | } as Post; 25 | 26 | if (this.post.id) { 27 | this.postService.updatePost(this.post.id, _body).subscribe({ 28 | next: (data) => { 29 | this.saved.emit(true); 30 | }, 31 | error: (error) => { 32 | this.saved.emit(false); 33 | }, 34 | }); 35 | } else { 36 | this.postService.savePost(_body).subscribe({ 37 | next: (data) => { 38 | this.saved.emit(true); 39 | }, 40 | error: (error) => { 41 | this.saved.emit(false); 42 | }, 43 | }); 44 | } 45 | } 46 | 47 | ngOnInit() { 48 | console.log('calling ngOnInit::PostFormComponent...'); 49 | } 50 | 51 | ngOnDestroy() { 52 | console.log('calling ngOnDestroy::PostFormComponent...'); 53 | if (this.sub) { 54 | this.sub.unsubscribe(); 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /ui/apps/todolist/src/app/post/shared/post.model.ts: -------------------------------------------------------------------------------- 1 | import { Username } from './username.model'; 2 | export interface Post { 3 | id?: string; 4 | slug?: string; 5 | title: string; 6 | content: string; 7 | author?: Username; 8 | createdDate?: any; 9 | } 10 | -------------------------------------------------------------------------------- /ui/apps/todolist/src/app/post/shared/post.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, Inject } from '@angular/core'; 2 | import { Post } from './post.model'; 3 | import { Comment } from './comment.model'; 4 | import { Observable } from 'rxjs'; 5 | import { HttpClient } from '@angular/common/http'; 6 | import { environment } from '../../../environments/environment'; 7 | import { HttpParams } from '@angular/common/http'; 8 | 9 | @Injectable() 10 | export class PostService { 11 | apiUrl = environment.baseApiUrl + '/posts'; 12 | constructor(private http: HttpClient) {} 13 | 14 | getPosts(term?: any): Observable { 15 | const params: HttpParams = new HttpParams(); 16 | if (term) { 17 | Object.keys(term).map((key) => { 18 | if (term[key]) { 19 | params.set(key, term[key]); 20 | } 21 | }); 22 | } 23 | return this.http.get(`${this.apiUrl}`, { params }); 24 | } 25 | 26 | getPost(id: string): Observable { 27 | return this.http.get(`${this.apiUrl}/${id}`); 28 | } 29 | 30 | savePost(data: Post): Observable { 31 | console.log('saving post:' + data); 32 | return this.http.post(`${this.apiUrl}`, data); 33 | } 34 | 35 | updatePost(id: string, data: Post): Observable { 36 | console.log('updating post:' + data); 37 | return this.http.put(`${this.apiUrl}/${id}`, data); 38 | } 39 | 40 | deletePost(id: string): Observable { 41 | console.log('delete post by id:' + id); 42 | return this.http.delete(`${this.apiUrl}/${id}`); 43 | } 44 | 45 | saveComment(id: string, data: Comment): Observable { 46 | return this.http.post(`${this.apiUrl}/${id}/comments`, data); 47 | } 48 | 49 | getCommentsOfPost(id: string): Observable { 50 | return this.http.get(`${this.apiUrl}/${id}/comments`); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /ui/apps/todolist/src/app/post/shared/username.model.ts: -------------------------------------------------------------------------------- 1 | export interface Username { 2 | username: string; 3 | } 4 | -------------------------------------------------------------------------------- /ui/apps/todolist/src/app/shared/nl2br.pipe.spec.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable:no-unused-variable */ 2 | 3 | import { TestBed, async } from '@angular/core/testing'; 4 | import { Nl2brPipe } from './nl2br.pipe'; 5 | 6 | describe('Pipe: Nl2br', () => { 7 | it('create an instance', () => { 8 | const pipe = new Nl2brPipe(); 9 | expect(pipe).toBeTruthy(); 10 | 11 | expect(pipe.transform('hello\nworld')).toEqual('hello
world'); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /ui/apps/todolist/src/app/shared/nl2br.pipe.ts: -------------------------------------------------------------------------------- 1 | import { Pipe, PipeTransform } from '@angular/core'; 2 | 3 | @Pipe({ 4 | name: 'nl2br', 5 | standalone: false 6 | }) 7 | export class Nl2brPipe implements PipeTransform { 8 | 9 | transform(value: any, args?: any): any { 10 | 11 | if (value !== void 0) { 12 | return value.replace(/\n/g, '
'); 13 | } 14 | return value; 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /ui/apps/todolist/src/app/shared/shared.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { FormsModule, ReactiveFormsModule } from '@angular/forms'; 4 | import { MatButtonModule } from '@angular/material/button'; 5 | import { MatCardModule } from '@angular/material/card'; 6 | import { MatCheckboxModule } from '@angular/material/checkbox'; 7 | import { MatDialogModule } from '@angular/material/dialog'; 8 | import { MatGridListModule } from '@angular/material/grid-list'; 9 | import { MatIconModule } from '@angular/material/icon'; 10 | import { MatInputModule } from '@angular/material/input'; 11 | import { MatListModule } from '@angular/material/list'; 12 | import { MatMenuModule } from '@angular/material/menu'; 13 | import { MatSelectModule } from '@angular/material/select'; 14 | import { MatSidenavModule } from '@angular/material/sidenav'; 15 | import { MatSlideToggleModule } from '@angular/material/slide-toggle'; 16 | import { MatSnackBarModule } from '@angular/material/snack-bar'; 17 | import { MatTabsModule } from '@angular/material/tabs'; 18 | import { MatToolbarModule } from '@angular/material/toolbar'; 19 | import { MatTooltipModule } from '@angular/material/tooltip'; 20 | 21 | import { ShowAuthedDirective } from './show-authed.directive'; 22 | import { Nl2brPipe } from './nl2br.pipe'; 23 | 24 | const ANGULAR_MODULES: any[] = [FormsModule, ReactiveFormsModule]; 25 | 26 | const MATERIAL_MODULES: any[] = [ 27 | MatButtonModule, 28 | MatCardModule, 29 | MatDialogModule, 30 | MatIconModule, 31 | MatListModule, 32 | MatMenuModule, 33 | MatTooltipModule, 34 | MatSlideToggleModule, 35 | MatInputModule, 36 | MatCheckboxModule, 37 | MatToolbarModule, 38 | MatSnackBarModule, 39 | MatSidenavModule, 40 | MatTabsModule, 41 | MatSelectModule, 42 | MatGridListModule 43 | ]; 44 | 45 | 46 | @NgModule({ 47 | imports: [ 48 | CommonModule, 49 | ANGULAR_MODULES, 50 | MATERIAL_MODULES 51 | ], 52 | exports: [ 53 | CommonModule, 54 | ANGULAR_MODULES, 55 | MATERIAL_MODULES, 56 | ShowAuthedDirective, 57 | Nl2brPipe 58 | ], 59 | declarations: [ShowAuthedDirective, Nl2brPipe] 60 | }) 61 | export class SharedModule {} 62 | -------------------------------------------------------------------------------- /ui/apps/todolist/src/app/shared/show-authed.directive.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Directive, 3 | Input, 4 | OnInit, 5 | TemplateRef, 6 | ViewContainerRef 7 | } from '@angular/core'; 8 | import { AuthService } from '../core/auth.service'; 9 | 10 | @Directive({ 11 | selector: '[showAuthed]', 12 | standalone: false 13 | }) 14 | export class ShowAuthedDirective implements OnInit { 15 | constructor( 16 | private templateRef: TemplateRef, 17 | private authService: AuthService, 18 | private viewContainer: ViewContainerRef 19 | ) {} 20 | 21 | condition: boolean; 22 | 23 | ngOnInit() { 24 | this.authService.isAuthenticated().subscribe( 25 | (isAuthenticated) => { 26 | if (isAuthenticated && this.condition || !isAuthenticated && !this.condition) { 27 | this.viewContainer.createEmbeddedView(this.templateRef); 28 | } else { 29 | this.viewContainer.clear(); 30 | } 31 | } 32 | ) 33 | } 34 | 35 | @Input() set showAuthed(condition: boolean) { 36 | this.condition = condition; 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /ui/apps/todolist/src/app/user/profile.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | 3 | @Injectable() 4 | export class ProfileService { 5 | 6 | constructor() { } 7 | 8 | } 9 | -------------------------------------------------------------------------------- /ui/apps/todolist/src/app/user/profile/profile.component.css: -------------------------------------------------------------------------------- 1 | :host{ 2 | display: flex; 3 | flex: 1 auto; 4 | } 5 | -------------------------------------------------------------------------------- /ui/apps/todolist/src/app/user/profile/profile.component.html: -------------------------------------------------------------------------------- 1 |

2 | profile works! 3 |

4 | -------------------------------------------------------------------------------- /ui/apps/todolist/src/app/user/profile/profile.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-profile', 5 | templateUrl: './profile.component.html', 6 | styleUrls: ['./profile.component.css'], 7 | standalone: false 8 | }) 9 | export class ProfileComponent implements OnInit { 10 | 11 | constructor() { } 12 | 13 | ngOnInit() { 14 | } 15 | 16 | } 17 | -------------------------------------------------------------------------------- /ui/apps/todolist/src/app/user/user-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { Routes, RouterModule } from '@angular/router'; 3 | import { ProfileComponent } from './profile/profile.component'; 4 | 5 | const routes: Routes = [ 6 | { path: '', redirectTo: 'profile' }, 7 | { path: 'profile', component: ProfileComponent } 8 | ]; 9 | 10 | @NgModule({ 11 | imports: [RouterModule.forChild(routes)], 12 | exports: [RouterModule] 13 | }) 14 | export class UserRoutingModule { } 15 | -------------------------------------------------------------------------------- /ui/apps/todolist/src/app/user/user.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | 4 | import { UserRoutingModule } from './user-routing.module'; 5 | import { ProfileComponent } from './profile/profile.component'; 6 | import { SharedModule } from '../shared/shared.module'; 7 | import { ProfileService } from './profile.service'; 8 | 9 | @NgModule({ 10 | imports: [ 11 | SharedModule, 12 | UserRoutingModule 13 | ], 14 | declarations: [ProfileComponent], 15 | providers: [ProfileService] 16 | }) 17 | export class UserModule { } 18 | -------------------------------------------------------------------------------- /ui/apps/todolist/src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hantsy/angular-spring-reactive-sample/c931f218f69f576da27da2b2d48bdf0c43352b4b/ui/apps/todolist/src/assets/.gitkeep -------------------------------------------------------------------------------- /ui/apps/todolist/src/environments/environment.cors.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: false, 3 | baseApiUrl: 'http://localhost:8080' 4 | }; 5 | -------------------------------------------------------------------------------- /ui/apps/todolist/src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true, 3 | baseApiUrl: '/api' 4 | }; 5 | -------------------------------------------------------------------------------- /ui/apps/todolist/src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | // The file contents for the current environment will overwrite these during build. 2 | // The build system defaults to the dev environment which uses `environment.ts`, but if you do 3 | // `ng build --env=prod` then `environment.prod.ts` will be used instead. 4 | // The list of which env maps to which file can be found in `.angular-cli.json`. 5 | 6 | export const environment = { 7 | production: false, 8 | baseApiUrl: '/api' 9 | }; 10 | -------------------------------------------------------------------------------- /ui/apps/todolist/src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hantsy/angular-spring-reactive-sample/c931f218f69f576da27da2b2d48bdf0c43352b4b/ui/apps/todolist/src/favicon.ico -------------------------------------------------------------------------------- /ui/apps/todolist/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Client 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /ui/apps/todolist/src/main.ts: -------------------------------------------------------------------------------- 1 | import { enableProdMode } from '@angular/core'; 2 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 3 | 4 | import { AppModule } from './app/app.module'; 5 | import { environment } from './environments/environment'; 6 | 7 | if (environment.production) { 8 | enableProdMode(); 9 | } 10 | 11 | platformBrowserDynamic().bootstrapModule(AppModule) 12 | .catch(err => console.log(err)); 13 | -------------------------------------------------------------------------------- /ui/apps/todolist/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 | /*************************************************************************************************** 52 | * APPLICATION IMPORTS 53 | */ 54 | -------------------------------------------------------------------------------- /ui/apps/todolist/src/styles.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | html, 6 | body { 7 | display: flex; 8 | flex-direction: column; 9 | 10 | font-family: Roboto, Arial, sans-serif; 11 | margin: 0; 12 | height: 100%; 13 | } 14 | 15 | mat-list-item mat-icon > svg { 16 | border-radius: 50%; 17 | } 18 | 19 | html, 20 | body { 21 | height: 100%; 22 | } 23 | body { 24 | margin: 0; 25 | font-family: Roboto, 'Helvetica Neue', sans-serif; 26 | } 27 | -------------------------------------------------------------------------------- /ui/apps/todolist/src/test.ts: -------------------------------------------------------------------------------- 1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files 2 | 3 | import 'zone.js/testing'; 4 | import { getTestBed } from '@angular/core/testing'; 5 | import { 6 | BrowserDynamicTestingModule, 7 | platformBrowserDynamicTesting 8 | } from '@angular/platform-browser-dynamic/testing'; 9 | 10 | declare const require: { 11 | context(path: string, deep?: boolean, filter?: RegExp): { 12 | (id: string): T; 13 | keys(): string[]; 14 | }; 15 | }; 16 | 17 | // First, initialize the Angular testing environment. 18 | getTestBed().initTestEnvironment( 19 | BrowserDynamicTestingModule, 20 | platformBrowserDynamicTesting(), 21 | ); 22 | 23 | // Then we find all the tests. 24 | const context = require.context('./', true, /\.spec\.ts$/); 25 | // And load the modules. 26 | context.keys().forEach(context); 27 | -------------------------------------------------------------------------------- /ui/apps/todolist/src/theme.scss: -------------------------------------------------------------------------------- 1 | @import '~@angular/material/_theming'; 2 | 3 | @include mat-core(); 4 | 5 | $primary: mat-palette($mat-red); 6 | $accent: mat-palette($mat-blue); 7 | 8 | $theme: mat-light-theme($primary, $accent); 9 | 10 | @include angular-material-theme($theme); 11 | 12 | .dark-theme { 13 | 14 | $dark-primary: mat-palette($mat-light-blue); 15 | $dark-accent: mat-palette($mat-green); 16 | 17 | $dark-theme: mat-dark-theme($dark-primary, $dark-accent); 18 | 19 | @include angular-material-theme($dark-theme); 20 | } 21 | -------------------------------------------------------------------------------- /ui/apps/todolist/src/typings.d.ts: -------------------------------------------------------------------------------- 1 | /* SystemJS module definition */ 2 | declare var module: NodeModule; 3 | interface NodeModule { 4 | id: string; 5 | } 6 | -------------------------------------------------------------------------------- /ui/apps/todolist/tailwind.config.js: -------------------------------------------------------------------------------- 1 | const { createGlobPatternsForDependencies } = require('@nx/angular/tailwind'); 2 | const { join } = require('path'); 3 | 4 | module.exports = { 5 | content: [ 6 | join(__dirname, 'src/**/!(*.stories|*.spec).{ts,html}'), 7 | ...createGlobPatternsForDependencies(__dirname), 8 | ], 9 | theme: { 10 | extend: {}, 11 | }, 12 | plugins: [], 13 | }; 14 | -------------------------------------------------------------------------------- /ui/apps/todolist/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "types": [] 6 | }, 7 | "files": ["src/main.ts", "src/polyfills.ts"], 8 | "include": ["src/**/*.d.ts"], 9 | "exclude": ["jest.config.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /ui/apps/todolist/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "references": [ 4 | { 5 | "path": "./tsconfig.app.json" 6 | }, 7 | { 8 | "path": "./tsconfig.spec.json" 9 | }, 10 | { 11 | "path": "../../.storybook/tsconfig.json" 12 | } 13 | ], 14 | "compilerOptions": { 15 | "target": "ES2022", 16 | "forceConsistentCasingInFileNames": true, 17 | "strict": true, 18 | "noImplicitOverride": true, 19 | "noPropertyAccessFromIndexSignature": true, 20 | "noImplicitReturns": true, 21 | "noFallthroughCasesInSwitch": true, 22 | "useDefineForClassFields": false 23 | }, 24 | "angularCompilerOptions": { 25 | "enableI18nLegacyMessageIdFormat": false, 26 | "strictInjectionParameters": true, 27 | "strictInputAccessModifiers": true, 28 | "strictTemplates": true 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /ui/apps/todolist/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "module": "commonjs", 6 | "types": ["jest", "node"] 7 | }, 8 | "include": ["jest.config.ts", "**/*.test.ts", "**/*.spec.ts", "**/*.d.ts"], 9 | "composite": true 10 | } 11 | -------------------------------------------------------------------------------- /ui/decorate-angular-cli.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This file decorates the Angular CLI with the Nx CLI to enable features such as computation caching 3 | * and faster execution of tasks. 4 | * 5 | * It does this by: 6 | * 7 | * - Patching the Angular CLI to warn you in case you accidentally use the undecorated ng command. 8 | * - Symlinking the ng to nx command, so all commands run through the Nx CLI 9 | * - Updating the package.json postinstall script to give you control over this script 10 | * 11 | * The Nx CLI decorates the Angular CLI, so the Nx CLI is fully compatible with it. 12 | * Every command you run should work the same when using the Nx CLI, except faster. 13 | * 14 | * Because of symlinking you can still type `ng build/test/lint` in the terminal. The ng command, in this case, 15 | * will point to nx, which will perform optimizations before invoking ng. So the Angular CLI is always invoked. 16 | * The Nx CLI simply does some optimizations before invoking the Angular CLI. 17 | * 18 | * To opt out of this patch: 19 | * - Replace occurrences of nx with ng in your package.json 20 | * - Remove the script from your postinstall script in your package.json 21 | * - Delete and reinstall your node_modules 22 | */ 23 | 24 | const fs = require('fs'); 25 | const os = require('os'); 26 | const cp = require('child_process'); 27 | const isWindows = os.platform() === 'win32'; 28 | let output; 29 | try { 30 | output = require('@nx/workspace').output; 31 | } catch (e) { 32 | console.warn( 33 | 'Angular CLI could not be decorated to enable computation caching. Please ensure @nx/workspace is installed.' 34 | ); 35 | process.exit(0); 36 | } 37 | 38 | /** 39 | * Symlink of ng to nx, so you can keep using `ng build/test/lint` and still 40 | * invoke the Nx CLI and get the benefits of computation caching. 41 | */ 42 | function symlinkNgCLItoNxCLI() { 43 | try { 44 | const ngPath = './node_modules/.bin/ng'; 45 | const nxPath = './node_modules/.bin/nx'; 46 | if (isWindows) { 47 | /** 48 | * This is the most reliable way to create symlink-like behavior on Windows. 49 | * Such that it works in all shells and works with npx. 50 | */ 51 | ['', '.cmd', '.ps1'].forEach((ext) => { 52 | if (fs.existsSync(nxPath + ext)) 53 | fs.writeFileSync(ngPath + ext, fs.readFileSync(nxPath + ext)); 54 | }); 55 | } else { 56 | // If unix-based, symlink 57 | cp.execSync(`ln -sf ./nx ${ngPath}`); 58 | } 59 | } catch (e) { 60 | output.error({ 61 | title: 62 | 'Unable to create a symlink from the Angular CLI to the Nx CLI:' + 63 | e.message, 64 | }); 65 | throw e; 66 | } 67 | } 68 | 69 | try { 70 | symlinkNgCLItoNxCLI(); 71 | require('nx/src/adapter/decorate-cli').decorateCli(); 72 | output.log({ 73 | title: 'Angular CLI has been decorated to enable computation caching.', 74 | }); 75 | } catch (e) { 76 | output.error({ 77 | title: 'Decoration of the Angular CLI did not complete successfully', 78 | }); 79 | } 80 | -------------------------------------------------------------------------------- /ui/jest.config.ts: -------------------------------------------------------------------------------- 1 | import { getJestProjectsAsync } from '@nx/jest'; 2 | 3 | export default async () => ({ 4 | projects: await getJestProjectsAsync(), 5 | }); 6 | -------------------------------------------------------------------------------- /ui/jest.preset.js: -------------------------------------------------------------------------------- 1 | const nxPreset = require('@nx/jest/preset').default; 2 | 3 | module.exports = { 4 | ...nxPreset, 5 | /* TODO: Update to latest Jest snapshotFormat 6 | * By default Nx has kept the older style of Jest Snapshot formats 7 | * to prevent breaking of any existing tests with snapshots. 8 | * It's recommend you update to the latest format. 9 | * You can do this by removing snapshotFormat property 10 | * and running tests with --update-snapshot flag. 11 | * Example: "nx affected --targets=test --update-snapshot" 12 | * More info: https://jestjs.io/docs/upgrading-to-jest29#snapshot-format 13 | */ 14 | snapshotFormat: { escapeString: true, printBasicPrototype: true }, 15 | }; 16 | -------------------------------------------------------------------------------- /ui/karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration file, see link for more information 2 | // https://karma-runner.github.io/1.0/config/configuration-file.html 3 | 4 | const { join } = require('path'); 5 | const { constants } = require('karma'); 6 | 7 | module.exports = () => { 8 | return { 9 | basePath: '', 10 | frameworks: ['jasmine', '@angular-devkit/build-angular'], 11 | plugins: [ 12 | require('karma-jasmine'), 13 | require('karma-chrome-launcher'), 14 | require('karma-jasmine-html-reporter'), 15 | require('karma-coverage'), 16 | require('@angular-devkit/build-angular/plugins/karma'), 17 | ], 18 | client: { 19 | jasmine: { 20 | // you can add configuration options for Jasmine here 21 | // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html 22 | // for example, you can disable the random execution with `random: false` 23 | // or set a specific seed with `seed: 4321` 24 | }, 25 | clearContext: false, // leave Jasmine Spec Runner output visible in browser 26 | }, 27 | jasmineHtmlReporter: { 28 | suppressAll: true, // removes the duplicated traces 29 | }, 30 | coverageReporter: { 31 | dir: join(__dirname, './coverage'), 32 | subdir: '.', 33 | reporters: [{ type: 'html' }, { type: 'text-summary' }], 34 | }, 35 | reporters: ['progress', 'kjhtml'], 36 | port: 9876, 37 | colors: true, 38 | logLevel: constants.LOG_INFO, 39 | autoWatch: true, 40 | browsers: ['Chrome'], 41 | singleRun: true, 42 | }; 43 | }; 44 | -------------------------------------------------------------------------------- /ui/libs/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hantsy/angular-spring-reactive-sample/c931f218f69f576da27da2b2d48bdf0c43352b4b/ui/libs/.gitkeep -------------------------------------------------------------------------------- /ui/libs/shared/components/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["../../../.eslintrc.json"], 3 | "ignorePatterns": ["!**/*"], 4 | "overrides": [ 5 | { 6 | "files": ["*.ts"], 7 | "extends": [ 8 | "plugin:@nrwl/nx/angular", 9 | "plugin:@angular-eslint/template/process-inline-templates" 10 | ], 11 | "rules": { 12 | "@angular-eslint/directive-selector": [ 13 | "error", 14 | { 15 | "type": "attribute", 16 | "prefix": "app", 17 | "style": "camelCase" 18 | } 19 | ], 20 | "@angular-eslint/component-selector": [ 21 | "error", 22 | { 23 | "type": "element", 24 | "prefix": "app", 25 | "style": "kebab-case" 26 | } 27 | ] 28 | } 29 | }, 30 | { 31 | "files": ["*.html"], 32 | "extends": ["plugin:@nrwl/nx/angular-template"], 33 | "rules": {} 34 | } 35 | ] 36 | } 37 | -------------------------------------------------------------------------------- /ui/libs/shared/components/.storybook/main.js: -------------------------------------------------------------------------------- 1 | const rootMain = require('../../../../.storybook/main'); 2 | 3 | module.exports = { 4 | ...rootMain, 5 | 6 | core: { ...rootMain.core, builder: 'webpack5' }, 7 | 8 | stories: [ 9 | ...rootMain.stories, 10 | '../src/lib/**/*.stories.mdx', 11 | '../src/lib/**/*.stories.@(js|jsx|ts|tsx)', 12 | ], 13 | addons: ['@storybook/addon-essentials', ...rootMain.addons], 14 | webpackFinal: async (config, { configType }) => { 15 | // apply any global webpack configs that might have been specified in .storybook/main.js 16 | if (rootMain.webpackFinal) { 17 | config = await rootMain.webpackFinal(config, { configType }); 18 | } 19 | 20 | // add your own webpack tweaks if needed 21 | 22 | return config; 23 | }, 24 | }; 25 | -------------------------------------------------------------------------------- /ui/libs/shared/components/.storybook/preview.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hantsy/angular-spring-reactive-sample/c931f218f69f576da27da2b2d48bdf0c43352b4b/ui/libs/shared/components/.storybook/preview.js -------------------------------------------------------------------------------- /ui/libs/shared/components/.storybook/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "emitDecoratorMetadata": true 5 | }, 6 | "exclude": ["../**/*.spec.ts", "jest.config.ts"], 7 | "include": [ 8 | "../src/**/*.stories.ts", 9 | "../src/**/*.stories.js", 10 | "../src/**/*.stories.jsx", 11 | "../src/**/*.stories.tsx", 12 | "../src/**/*.stories.mdx", 13 | "*.js" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /ui/libs/shared/components/README.md: -------------------------------------------------------------------------------- 1 | # shared-components 2 | 3 | This library was generated with [Nx](https://nx.dev). 4 | 5 | ## Running unit tests 6 | 7 | Run `nx test shared-components` to execute the unit tests. 8 | -------------------------------------------------------------------------------- /ui/libs/shared/components/jest.config.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | export default { 3 | displayName: 'shared-components', 4 | preset: '../../../jest.preset.js', 5 | setupFilesAfterEnv: ['/src/test-setup.ts'], 6 | globals: {}, 7 | coverageDirectory: '../../../coverage/libs/shared/components', 8 | transform: { 9 | '^.+\\.(ts|mjs|js|html)$': [ 10 | 'jest-preset-angular', 11 | { 12 | tsconfig: '/tsconfig.spec.json', 13 | stringifyContentPathRegex: '\\.(html|svg)$', 14 | }, 15 | ], 16 | }, 17 | transformIgnorePatterns: ['node_modules/(?!.*\\.mjs$)'], 18 | snapshotSerializers: [ 19 | 'jest-preset-angular/build/serializers/no-ng-attributes', 20 | 'jest-preset-angular/build/serializers/ng-snapshot', 21 | 'jest-preset-angular/build/serializers/html-comment', 22 | ], 23 | }; 24 | -------------------------------------------------------------------------------- /ui/libs/shared/components/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "shared-components", 3 | "$schema": "../../../node_modules/nx/schemas/project-schema.json", 4 | "projectType": "library", 5 | "sourceRoot": "libs/shared/components/src", 6 | "prefix": "app", 7 | "targets": { 8 | "test": { 9 | "executor": "@nx/jest:jest", 10 | "outputs": ["{workspaceRoot}/coverage/libs/shared/components"], 11 | "options": { 12 | "jestConfig": "libs/shared/components/jest.config.ts" 13 | } 14 | }, 15 | "lint": { 16 | "executor": "@nrwl/linter:eslint", 17 | "options": { 18 | "lintFilePatterns": [ 19 | "libs/shared/components/**/*.ts", 20 | "libs/shared/components/**/*.html" 21 | ] 22 | } 23 | }, 24 | "storybook": { 25 | "executor": "@storybook/angular:start-storybook", 26 | "options": { 27 | "port": 4400, 28 | "configDir": "libs/shared/components/.storybook", 29 | "browserTarget": "shared-components:build-storybook", 30 | "compodoc": false 31 | }, 32 | "configurations": { 33 | "ci": { 34 | "quiet": true 35 | } 36 | } 37 | }, 38 | "build-storybook": { 39 | "executor": "@storybook/angular:build-storybook", 40 | "outputs": ["{options.outputPath}"], 41 | "options": { 42 | "outputDir": "dist/storybook/shared-components", 43 | "configDir": "libs/shared/components/.storybook", 44 | "browserTarget": "shared-components:build-storybook", 45 | "compodoc": false 46 | }, 47 | "configurations": { 48 | "ci": { 49 | "quiet": true 50 | } 51 | } 52 | } 53 | }, 54 | "tags": [] 55 | } 56 | -------------------------------------------------------------------------------- /ui/libs/shared/components/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './lib/shared-components.module'; 2 | -------------------------------------------------------------------------------- /ui/libs/shared/components/src/lib/shared-components.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | 4 | @NgModule({ 5 | imports: [CommonModule], 6 | }) 7 | export class SharedComponentsModule {} 8 | -------------------------------------------------------------------------------- /ui/libs/shared/components/src/test-setup.ts: -------------------------------------------------------------------------------- 1 | import 'jest-preset-angular/setup-jest'; 2 | -------------------------------------------------------------------------------- /ui/libs/shared/components/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../tsconfig.base.json", 3 | "files": [], 4 | "include": [], 5 | "references": [ 6 | { 7 | "path": "./tsconfig.lib.json" 8 | }, 9 | { 10 | "path": "./tsconfig.spec.json" 11 | }, 12 | { 13 | "path": "./.storybook/tsconfig.json" 14 | } 15 | ], 16 | "compilerOptions": { 17 | "target": "es2020", 18 | "forceConsistentCasingInFileNames": true, 19 | "strict": true, 20 | "noImplicitOverride": true, 21 | "noPropertyAccessFromIndexSignature": true, 22 | "noImplicitReturns": true, 23 | "noFallthroughCasesInSwitch": true 24 | }, 25 | "angularCompilerOptions": { 26 | "enableI18nLegacyMessageIdFormat": false, 27 | "strictInjectionParameters": true, 28 | "strictInputAccessModifiers": true, 29 | "strictTemplates": true 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /ui/libs/shared/components/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../../dist/out-tsc", 5 | "declaration": true, 6 | "declarationMap": true, 7 | "inlineSources": true, 8 | "types": [] 9 | }, 10 | "exclude": [ 11 | "src/test-setup.ts", 12 | "**/*.spec.ts", 13 | "**/*.test.ts", 14 | "**/*.stories.ts", 15 | "**/*.stories.js", 16 | "jest.config.ts" 17 | ], 18 | "include": ["**/*.ts"] 19 | } 20 | -------------------------------------------------------------------------------- /ui/libs/shared/components/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../../dist/out-tsc", 5 | "module": "commonjs", 6 | "types": ["jest", "node"] 7 | }, 8 | "files": ["src/test-setup.ts"], 9 | "include": ["jest.config.ts", "**/*.test.ts", "**/*.spec.ts", "**/*.d.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /ui/migrations.json: -------------------------------------------------------------------------------- 1 | { 2 | "migrations": [ 3 | { 4 | "version": "21.0.0-beta.8", 5 | "description": "Removes the legacy cache configuration from nx.json", 6 | "implementation": "./src/migrations/update-21-0-0/remove-legacy-cache", 7 | "package": "nx", 8 | "name": "remove-legacy-cache" 9 | }, 10 | { 11 | "version": "21.0.0-beta.8", 12 | "description": "Removes the legacy cache configuration from nx.json", 13 | "implementation": "./src/migrations/update-21-0-0/remove-custom-tasks-runner", 14 | "package": "nx", 15 | "name": "remove-custom-tasks-runner" 16 | }, 17 | { 18 | "version": "21.0.0-beta.11", 19 | "description": "Updates release version config based on the breaking changes in Nx v21", 20 | "implementation": "./src/migrations/update-21-0-0/release-version-config-changes", 21 | "package": "nx", 22 | "name": "release-version-config-changes" 23 | }, 24 | { 25 | "version": "21.0.0-beta.11", 26 | "description": "Updates release changelog config based on the breaking changes in Nx v21", 27 | "implementation": "./src/migrations/update-21-0-0/release-changelog-config-changes", 28 | "package": "nx", 29 | "name": "release-changelog-config-changes" 30 | }, 31 | { 32 | "version": "21.1.0-beta.2", 33 | "description": "Adds **/nx-rules.mdc and **/nx.instructions.md to .gitignore if not present", 34 | "implementation": "./src/migrations/update-21-1-0/add-gitignore-entry", 35 | "package": "nx", 36 | "name": "21-1-0-add-ignore-entries-for-nx-rule-files" 37 | }, 38 | { 39 | "version": "21.0.0-beta.10", 40 | "description": "Removes the `tsConfig` and `copyFiles` options from the `@nx/cypress:cypress` executor.", 41 | "implementation": "./src/migrations/update-21-0-0/remove-tsconfig-and-copy-files-options-from-cypress-executor", 42 | "package": "@nx/cypress", 43 | "name": "remove-tsconfig-and-copy-files-options-from-cypress-executor" 44 | }, 45 | { 46 | "cli": "nx", 47 | "version": "21.0.0-beta.9", 48 | "description": "Replace usage of `getJestProjects` with `getJestProjectsAsync`.", 49 | "implementation": "./src/migrations/update-21-0-0/replace-getJestProjects-with-getJestProjectsAsync", 50 | "package": "@nx/jest", 51 | "name": "replace-getJestProjects-with-getJestProjectsAsync-v21" 52 | }, 53 | { 54 | "version": "21.0.0-beta.10", 55 | "description": "Remove the previously deprecated and unused `tsConfig` option from the `@nx/jest:jest` executor.", 56 | "implementation": "./src/migrations/update-21-0-0/remove-tsconfig-option-from-jest-executor", 57 | "package": "@nx/jest", 58 | "name": "remove-tsconfig-option-from-jest-executor" 59 | }, 60 | { 61 | "cli": "nx", 62 | "version": "20.4.0-beta.1", 63 | "requires": { "@angular/core": ">=19.1.0" }, 64 | "description": "Update the @angular/cli package version to ~19.1.0.", 65 | "factory": "./src/migrations/update-20-4-0/update-angular-cli", 66 | "package": "@nx/angular", 67 | "name": "update-angular-cli-version-19-1-0" 68 | }, 69 | { 70 | "cli": "nx", 71 | "version": "20.5.0-beta.5", 72 | "requires": { "@angular/core": ">=19.2.0" }, 73 | "description": "Update the @angular/cli package version to ~19.2.0.", 74 | "factory": "./src/migrations/update-20-5-0/update-angular-cli", 75 | "package": "@nx/angular", 76 | "name": "update-angular-cli-version-19-2-0" 77 | }, 78 | { 79 | "cli": "nx", 80 | "version": "21.0.0-beta.3", 81 | "description": "Set the `continuous` option to `true` for continuous tasks.", 82 | "factory": "./src/migrations/update-21-0-0/set-continuous-option", 83 | "package": "@nx/angular", 84 | "name": "set-continuous-option" 85 | } 86 | ] 87 | } 88 | -------------------------------------------------------------------------------- /ui/nginx/nginx.conf: -------------------------------------------------------------------------------- 1 | # see: https://hackerbox.io/articles/dockerised-nginx-env-vars/ to set env in nginx.conf 2 | 3 | user nginx; 4 | worker_processes 1; 5 | error_log /var/log/nginx/error.log warn; 6 | pid /var/run/nginx.pid; 7 | load_module modules/ngx_http_perl_module.so; 8 | 9 | env BACKEND_API_URL; 10 | 11 | events { 12 | worker_connections 1024; 13 | } 14 | 15 | http { 16 | include /etc/nginx/mime.types; 17 | sendfile on; 18 | 19 | perl_set $baseApiUrl 'sub { return $ENV{"BACKEND_API_URL"}; }'; 20 | 21 | server { 22 | listen 80; 23 | server_name localhost; 24 | resolver 127.0.0.11 ipv6=off; 25 | resolver_timeout 1s; 26 | 27 | root /usr/share/nginx/html; 28 | index index.html index.htm; 29 | include /etc/nginx/mime.types; 30 | 31 | location /api { 32 | proxy_http_version 1.1; 33 | proxy_set_header Upgrade $http_upgrade; 34 | proxy_set_header Connection "upgrade"; 35 | proxy_set_header Host $host; 36 | proxy_set_header X-Real-IP $remote_addr; 37 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 38 | proxy_set_header X-Forwarded-Proto $scheme; 39 | 40 | rewrite ^/api/(.*) /$1 break; 41 | proxy_pass ${baseApiUrl}; 42 | } 43 | 44 | location /test { 45 | return 200 'the environment variable contains: ${baseApiUrl}'; 46 | } 47 | 48 | location / { 49 | try_files $uri $uri/ /index.html; 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /ui/nx.json: -------------------------------------------------------------------------------- 1 | { 2 | "targetDefaults": { 3 | "build": { 4 | "dependsOn": ["^build"], 5 | "inputs": ["production", "^production"], 6 | "cache": true 7 | }, 8 | "build-storybook": { 9 | "inputs": [ 10 | "default", 11 | "^production", 12 | "{workspaceRoot}/.storybook/**/*", 13 | "{projectRoot}/.storybook/**/*", 14 | "{projectRoot}/tsconfig.storybook.json" 15 | ], 16 | "cache": true 17 | }, 18 | "e2e": { 19 | "inputs": ["default", "^production"], 20 | "cache": true 21 | }, 22 | "lint": { 23 | "cache": true 24 | }, 25 | "@nx/jest:jest": { 26 | "inputs": ["default", "^production", "{workspaceRoot}/jest.preset.js"], 27 | "cache": true, 28 | "options": { 29 | "passWithNoTests": true 30 | }, 31 | "configurations": { 32 | "ci": { 33 | "ci": true, 34 | "codeCoverage": true 35 | } 36 | } 37 | } 38 | }, 39 | "cli": { 40 | "analytics": false 41 | }, 42 | "generators": { 43 | "@nx/angular:application": { 44 | "style": "css", 45 | "linter": "eslint", 46 | "unitTestRunner": "jest", 47 | "e2eTestRunner": "none" 48 | }, 49 | "@nx/angular:library": { 50 | "linter": "eslint", 51 | "unitTestRunner": "jest" 52 | }, 53 | "@nx/angular:component": { 54 | "style": "css" 55 | } 56 | }, 57 | "namedInputs": { 58 | "default": ["{projectRoot}/**/*", "sharedGlobals"], 59 | "sharedGlobals": [], 60 | "production": [ 61 | "default", 62 | "!{projectRoot}/.storybook/**/*", 63 | "!{projectRoot}/**/*.stories.@(js|jsx|ts|tsx|mdx)", 64 | "!{projectRoot}/tsconfig.storybook.json", 65 | "!{projectRoot}/**/?(*.)+(spec|test).[jt]s?(x)?(.snap)", 66 | "!{projectRoot}/tsconfig.spec.json", 67 | "!{projectRoot}/jest.config.[jt]s", 68 | "!{projectRoot}/src/test-setup.[jt]s" 69 | ] 70 | }, 71 | "nxCloudAccessToken": "MDlhOGI4MTYtODdlMy00ZDkyLWJkYWMtNzY0MzFhZDUyNDFlfHJlYWQtd3JpdGU=", 72 | "useInferencePlugins": false, 73 | "defaultBase": "master", 74 | "useLegacyCache": true 75 | } 76 | -------------------------------------------------------------------------------- /ui/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "client", 3 | "version": "0.0.0", 4 | "license": "MIT", 5 | "scripts": { 6 | "ng": "nx", 7 | "start": "nx serve --proxy-config proxy.conf.json", 8 | "start:cors": "nx serve -c cors", 9 | "build": "nx build -c production", 10 | "test": "nx test", 11 | "lint": "nx lint", 12 | "e2e": "nx e2e", 13 | "postinstall": "node ./decorate-angular-cli.js" 14 | }, 15 | "private": true, 16 | "dependencies": { 17 | "@angular/animations": "19.2.14", 18 | "@angular/cdk": "19.2.18", 19 | "@angular/common": "19.2.14", 20 | "@angular/compiler": "19.2.14", 21 | "@angular/core": "19.2.14", 22 | "@angular/forms": "19.2.14", 23 | "@angular/material": "19.2.18", 24 | "@angular/platform-browser": "19.2.14", 25 | "@angular/platform-browser-dynamic": "19.2.14", 26 | "@angular/router": "19.2.14", 27 | "@nx/angular": "^20.5.1", 28 | "@storybook/addon-interactions": "8.6.11", 29 | "rxjs": "7.8.1", 30 | "storybook": "^8.2.8", 31 | "tslib": "^2.3.0", 32 | "web-animations-js": "^2.3.2", 33 | "zone.js": "0.15.0" 34 | }, 35 | "devDependencies": { 36 | "@angular-devkit/build-angular": "^19.2.14", 37 | "@angular-devkit/core": "19.2.9", 38 | "@angular-devkit/schematics": "19.2.9", 39 | "@angular-eslint/eslint-plugin": "19.2.0", 40 | "@angular-eslint/eslint-plugin-template": "19.2.0", 41 | "@angular-eslint/template-parser": "19.2.0", 42 | "@angular/cli": "~19.2.0", 43 | "@angular/compiler-cli": "19.2.14", 44 | "@angular/language-service": "19.2.14", 45 | "@nx/cypress": "21.1.2", 46 | "@nx/jest": "21.1.2", 47 | "@nx/storybook": "21.1.2", 48 | "@nx/workspace": "21.1.2", 49 | "@schematics/angular": "19.2.9", 50 | "@storybook/addon-essentials": "8.6.11", 51 | "@storybook/core-server": "8.6.11", 52 | "@types/jasmine": "~4.0.0", 53 | "@types/jest": "29.5.14", 54 | "@types/node": "18.16.9", 55 | "@typescript-eslint/utils": "^7.16.0", 56 | "autoprefixer": "^10.4.7", 57 | "codelyzer": "^0.0.28", 58 | "cypress": "^14.4.0", 59 | "eslint": "^8.50.0", 60 | "eslint-plugin-cypress": "2.13.4", 61 | "eslint-plugin-storybook": "^0.6.14", 62 | "jasmine-core": "4.2.0", 63 | "jasmine-spec-reporter": "~5.0.0", 64 | "jest": "29.7.0", 65 | "jest-environment-jsdom": "29.7.0", 66 | "jest-preset-angular": "14.4.2", 67 | "karma": "6.4.2", 68 | "karma-chrome-launcher": "~3.1.0", 69 | "karma-coverage": "~2.2.0", 70 | "karma-coverage-istanbul-reporter": "~3.0.2", 71 | "karma-jasmine": "5.1.0", 72 | "karma-jasmine-html-reporter": "2.0.0", 73 | "nx": "21.1.2", 74 | "postcss": "^8.4.14", 75 | "prettier": "^2.6.2", 76 | "react": "^18.2.0", 77 | "react-dom": "^18.2.0", 78 | "tailwindcss": "^3.1.3", 79 | "ts-jest": "29.1.4", 80 | "ts-node": "10.9.1", 81 | "tslint": "^6.1.0", 82 | "typescript": "5.7.3", 83 | "webpack": "^5.64.0" 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /ui/proxy.conf.json: -------------------------------------------------------------------------------- 1 | { 2 | "/api": { 3 | "target": "http://localhost:8080", 4 | "changeOrigin": false, 5 | "secure": false, 6 | "logLevel": "debug", 7 | "pathRewrite": { "^/api": "" } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /ui/storybook-migration-summary.md: -------------------------------------------------------------------------------- 1 | # Storybook 7 Migration Summary 2 | 3 | ## Upgrade Storybook packages 4 | 5 | The following command was ran to upgrade the Storybook packages: 6 | 7 | ```bash 8 | npx storybook@latest upgrade 9 | ``` 10 | 11 | ## Your `.storybook/main.js|ts` files were prepared for Storybook's automigration scripts 12 | 13 | Some adjustments were made to your `.storybook/main.js|ts` files so that 14 | the Storybook automigration scripts could run successfully. The changes that were made are as follows: 15 | 16 | - Remove the `as StorybookConfig` typecast from the main.ts files, if any, 17 | since it is not needed any more. 18 | - Remove the `path.resolve` calls from the Next.js Storybook configuration, if any, since it breaks the Storybook automigration scripts. 19 | 20 | ## The Storybook automigration scripts were ran 21 | 22 | The following commands ran successfully and your Storybook configuration was successfully migrated to the latest version 7: 23 | 24 | - `npx storybook@latest automigrate --config-dir libs/shared/components/.storybook --renderer @storybook/angular` 25 | 26 | Please make sure to check the results yourself and make sure that everything is working as expected. 27 | 28 | Also, we may have missed something. Please make sure to check the logs of the Storybook CLI commands that were run, and look for 29 | the `❌ Failed trying to evaluate` message or `❌ The migration failed to update` message. This will indicate if a command was 30 | unsuccessful, and will help you run the migration again, manually. 31 | 32 | ## Final adjustments 33 | 34 | After the Storybook automigration scripts have run, some additional adjustments were made to your 35 | workspace, to make sure that everything is working as expected. These adjustments are as follows: 36 | 37 | - The `vite-tsconfig-paths` plugin was removed from the Storybook configuration files since it's no longer needed. 38 | - The `viteConfigPath` option was added to the Storybook builder, where needed. 39 | - The import package for the `StorybookConfig` type was changed to be framework specific. 40 | - The `uiFramework` option was removed from your project's Storybook targets. 41 | - The `lit` package was added to your workspace, if you are using the 42 | Web Components `@storybook/web-components` package. Please note that the `lit-html` package is 43 | no longer needed by Storybook v7. So, if you are not using it anywhere else, you can safely remove it. 44 | 45 | ## Next steps 46 | 47 | You can make sure everything is working as expected by trying 48 | to build or serve your Storybook as you normally would. 49 | 50 | ```bash 51 | npx nx build-storybook project-name 52 | ``` 53 | 54 | ```bash 55 | npx nx storybook project-name 56 | ``` 57 | 58 | Please read the [Storybook 7.0.0 release article](https://storybook.js.org/blog/storybook-7-0/) and the 59 | official [Storybook 7.0.0 migration guide](https://storybook.js.org/docs/react/migration-guide) 60 | for more information. 61 | 62 | You can also read the docs for the [@nx/storybook:migrate-7 generator](https://nx.dev/packages/storybook/generators/migrate-7) and our [Storybook 7 setup guide](https://nx.dev/packages/storybook/documents/storybook-7-setup). 63 | -------------------------------------------------------------------------------- /ui/tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: [ 4 | "./src/**/*.{html,ts}", 5 | ], 6 | theme: { 7 | extend: {}, 8 | }, 9 | plugins: [], 10 | } 11 | -------------------------------------------------------------------------------- /ui/testing/activated-route-stub.ts: -------------------------------------------------------------------------------- 1 | // export for convenience. 2 | export { ActivatedRoute } from '@angular/router'; 3 | 4 | import { convertToParamMap, ParamMap, Params } from '@angular/router'; 5 | import { ReplaySubject } from 'rxjs'; 6 | 7 | /** 8 | * An ActivateRoute test double with a `paramMap` observable. 9 | * Use the `setParamMap()` method to add the next `paramMap` value. 10 | */ 11 | export class ActivatedRouteStub { 12 | // Use a ReplaySubject to share previous values with subscribers 13 | // and pump new values into the `paramMap` observable 14 | private subject = new ReplaySubject(); 15 | 16 | constructor(initialParams?: Params) { 17 | this.setParamMap(initialParams); 18 | } 19 | 20 | /** The mock paramMap observable */ 21 | readonly paramMap = this.subject.asObservable(); 22 | 23 | /** Set the paramMap observables's next value */ 24 | setParamMap(params?: Params) { 25 | this.subject.next(convertToParamMap(params)); 26 | }; 27 | } 28 | 29 | 30 | /* 31 | Copyright 2017-2018 Google Inc. All Rights Reserved. 32 | Use of this source code is governed by an MIT-style license that 33 | can be found in the LICENSE file at http://angular.io/license 34 | */ -------------------------------------------------------------------------------- /ui/testing/async-observable-helpers.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Mock async observables that return asynchronously. 3 | * The observable either emits once and completes or errors. 4 | * 5 | * Must call `tick()` when test with `fakeAsync()`. 6 | * 7 | * THE FOLLOWING DON'T WORK 8 | * Using `of().delay()` triggers TestBed errors; 9 | * see https://github.com/angular/angular/issues/10127 . 10 | * 11 | * Using `asap` scheduler - as in `of(value, asap)` - doesn't work either. 12 | */ 13 | import { defer } from 'rxjs'; 14 | 15 | /** Create async observable that emits-once and completes 16 | * after a JS engine turn */ 17 | export function asyncData(data: T) { 18 | return defer(() => Promise.resolve(data)); 19 | } 20 | 21 | /** Create async observable error that errors 22 | * after a JS engine turn */ 23 | export function asyncError(errorObject: any) { 24 | return defer(() => Promise.reject(errorObject)); 25 | } 26 | 27 | 28 | /* 29 | Copyright 2017-2018 Google Inc. All Rights Reserved. 30 | Use of this source code is governed by an MIT-style license that 31 | can be found in the LICENSE file at http://angular.io/license 32 | */ 33 | -------------------------------------------------------------------------------- /ui/testing/global-jasmine.ts: -------------------------------------------------------------------------------- 1 | import jasmineRequire from 'jasmine-core/lib/jasmine-core/jasmine.js'; 2 | 3 | window['jasmineRequire'] = jasmineRequire; 4 | 5 | /* 6 | Copyright 2017-2018 Google Inc. All Rights Reserved. 7 | Use of this source code is governed by an MIT-style license that 8 | can be found in the LICENSE file at http://angular.io/license 9 | */ 10 | -------------------------------------------------------------------------------- /ui/testing/index.ts: -------------------------------------------------------------------------------- 1 | import { DebugElement } from '@angular/core'; 2 | import { tick, ComponentFixture } from '@angular/core/testing'; 3 | 4 | export * from './async-observable-helpers'; 5 | export * from './activated-route-stub'; 6 | export * from './jasmine-matchers'; 7 | export * from './router-link-directive-stub'; 8 | 9 | ///// Short utilities ///// 10 | 11 | /** Wait a tick, then detect changes */ 12 | export function advance(f: ComponentFixture): void { 13 | tick(); 14 | f.detectChanges(); 15 | } 16 | 17 | /** 18 | * Create custom DOM event the old fashioned way 19 | * 20 | * https://developer.mozilla.org/en-US/docs/Web/API/Event/initEvent 21 | * Although officially deprecated, some browsers (phantom) don't accept the preferred "new Event(eventName)" 22 | */ 23 | export function newEvent( 24 | eventName: string, 25 | bubbles = false, 26 | cancelable = false 27 | ) { 28 | const evt = document.createEvent('CustomEvent'); // MUST be 'CustomEvent' 29 | evt.initCustomEvent(eventName, bubbles, cancelable, null); 30 | return evt; 31 | } 32 | 33 | // See https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/button 34 | /** Button events to pass to `DebugElement.triggerEventHandler` for RouterLink event handler */ 35 | export const ButtonClickEvents = { 36 | left: { button: 0 }, 37 | right: { button: 2 } 38 | }; 39 | 40 | /** Simulate element click. Defaults to mouse left-button click event. */ 41 | export function click( 42 | el: DebugElement | HTMLElement, 43 | eventObj: any = ButtonClickEvents.left 44 | ): void { 45 | if (el instanceof HTMLElement) { 46 | el.click(); 47 | } else { 48 | el.triggerEventHandler('click', eventObj); 49 | } 50 | } 51 | 52 | /* 53 | Copyright 2017-2018 Google Inc. All Rights Reserved. 54 | Use of this source code is governed by an MIT-style license that 55 | can be found in the LICENSE file at http://angular.io/license 56 | */ 57 | -------------------------------------------------------------------------------- /ui/testing/jasmine-matchers.d.ts: -------------------------------------------------------------------------------- 1 | declare namespace jasmine { 2 | interface Matchers { 3 | toHaveText(actual: any, expectationFailOutput?: any): jasmine.CustomMatcher; 4 | } 5 | } 6 | 7 | 8 | /* 9 | Copyright 2017-2018 Google Inc. All Rights Reserved. 10 | Use of this source code is governed by an MIT-style license that 11 | can be found in the LICENSE file at http://angular.io/license 12 | */ 13 | -------------------------------------------------------------------------------- /ui/testing/jasmine-matchers.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | //// Jasmine Custom Matchers //// 4 | // Be sure to extend jasmine-matchers.d.ts when adding matchers 5 | 6 | export function addMatchers(): void { 7 | jasmine.addMatchers({ 8 | toHaveText: toHaveText 9 | }); 10 | } 11 | 12 | function toHaveText(): jasmine.CustomMatcher { 13 | return { 14 | compare: function (actual: any, expectedText: string, expectationFailOutput?: any): jasmine.CustomMatcherResult { 15 | const actualText = elementText(actual); 16 | const pass = actualText.indexOf(expectedText) > -1; 17 | const message = pass ? '' : composeMessage(); 18 | return { pass, message }; 19 | 20 | function composeMessage () { 21 | const a = (actualText.length < 100 ? actualText : actualText.substr(0, 100) + '...'); 22 | const efo = expectationFailOutput ? ` '${expectationFailOutput}'` : ''; 23 | return `Expected element to have text content '${expectedText}' instead of '${a}'${efo}`; 24 | } 25 | } 26 | }; 27 | } 28 | 29 | function elementText(n: any): string { 30 | if (n instanceof Array) { 31 | return n.map(elementText).join(''); 32 | } 33 | 34 | if (n.nodeType === Node.COMMENT_NODE) { 35 | return ''; 36 | } 37 | 38 | if (n.nodeType === Node.ELEMENT_NODE && n.hasChildNodes()) { 39 | return elementText(Array.prototype.slice.call(n.childNodes)); 40 | } 41 | 42 | if (n.nativeElement) { n = n.nativeElement; } 43 | 44 | return n.textContent; 45 | } 46 | 47 | 48 | /* 49 | Copyright 2017-2018 Google Inc. All Rights Reserved. 50 | Use of this source code is governed by an MIT-style license that 51 | can be found in the LICENSE file at http://angular.io/license 52 | */ -------------------------------------------------------------------------------- /ui/testing/router-link-directive-stub.ts: -------------------------------------------------------------------------------- 1 | import { Directive, Input } from '@angular/core'; 2 | 3 | // export for convenience. 4 | export { RouterLink } from '@angular/router'; 5 | 6 | /* tslint:disable:directive-class-suffix */ 7 | @Directive({ 8 | selector: '[routerLink]', 9 | host: { '(click)': 'onClick()' } 10 | }) 11 | export class RouterLinkDirectiveStub { 12 | @Input('routerLink') linkParams: any; 13 | navigatedTo: any = null; 14 | 15 | onClick() { 16 | this.navigatedTo = this.linkParams; 17 | } 18 | } 19 | 20 | /// Dummy module to satisfy Angular Language service. Never used. 21 | import { NgModule } from '@angular/core'; 22 | 23 | @NgModule({ 24 | declarations: [RouterLinkDirectiveStub] 25 | }) 26 | export class RouterStubsModule {} 27 | 28 | /* 29 | Copyright 2017-2018 Google Inc. All Rights Reserved. 30 | Use of this source code is governed by an MIT-style license that 31 | can be found in the LICENSE file at http://angular.io/license 32 | */ 33 | -------------------------------------------------------------------------------- /ui/tools/tsconfig.tools.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.base.json", 3 | "compilerOptions": { 4 | "outDir": "../dist/out-tsc/tools", 5 | "rootDir": ".", 6 | "module": "commonjs", 7 | "target": "es5", 8 | "types": ["node"], 9 | "importHelpers": false 10 | }, 11 | "include": ["**/*.ts"] 12 | } 13 | -------------------------------------------------------------------------------- /ui/tsconfig.base.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "baseUrl": ".", 5 | "outDir": "./dist/out-tsc", 6 | "forceConsistentCasingInFileNames": true, 7 | "strict": true, 8 | "noImplicitOverride": true, 9 | "noPropertyAccessFromIndexSignature": true, 10 | "noImplicitReturns": true, 11 | "noFallthroughCasesInSwitch": true, 12 | "sourceMap": true, 13 | "declaration": false, 14 | "downlevelIteration": true, 15 | "experimentalDecorators": true, 16 | "moduleResolution": "node", 17 | "importHelpers": true, 18 | "target": "es2020", 19 | "module": "es2020", 20 | "lib": ["es2020", "dom"], 21 | "paths": { 22 | "shared/components": ["libs/shared/components/src/index.ts"] 23 | }, 24 | "rootDir": "." 25 | }, 26 | "angularCompilerOptions": { 27 | "enableI18nLegacyMessageIdFormat": false, 28 | "strictInjectionParameters": true, 29 | "strictInputAccessModifiers": true, 30 | "strictTemplates": true 31 | }, 32 | "exclude": ["node_modules", "tmp"] 33 | } 34 | -------------------------------------------------------------------------------- /ui/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "tslint:recommended", 3 | "rulesDirectory": [ 4 | "codelyzer" 5 | ], 6 | "rules": { 7 | "align": { 8 | "options": [ 9 | "parameters", 10 | "statements" 11 | ] 12 | }, 13 | "array-type": false, 14 | "arrow-return-shorthand": true, 15 | "curly": true, 16 | "deprecation": { 17 | "severity": "warning" 18 | }, 19 | "eofline": true, 20 | "import-blacklist": [ 21 | true, 22 | "rxjs/Rx" 23 | ], 24 | "import-spacing": true, 25 | "indent": { 26 | "options": [ 27 | "spaces" 28 | ] 29 | }, 30 | "max-classes-per-file": false, 31 | "max-line-length": [ 32 | true, 33 | 140 34 | ], 35 | "member-ordering": [ 36 | true, 37 | { 38 | "order": [ 39 | "static-field", 40 | "instance-field", 41 | "static-method", 42 | "instance-method" 43 | ] 44 | } 45 | ], 46 | "no-console": [ 47 | true, 48 | "debug", 49 | "info", 50 | "time", 51 | "timeEnd", 52 | "trace" 53 | ], 54 | "no-empty": false, 55 | "no-inferrable-types": [ 56 | true, 57 | "ignore-params" 58 | ], 59 | "no-non-null-assertion": true, 60 | "no-redundant-jsdoc": true, 61 | "no-switch-case-fall-through": true, 62 | "no-var-requires": false, 63 | "object-literal-key-quotes": [ 64 | true, 65 | "as-needed" 66 | ], 67 | "quotemark": [ 68 | true, 69 | "single" 70 | ], 71 | "semicolon": { 72 | "options": [ 73 | "always" 74 | ] 75 | }, 76 | "space-before-function-paren": { 77 | "options": { 78 | "anonymous": "never", 79 | "asyncArrow": "always", 80 | "constructor": "never", 81 | "method": "never", 82 | "named": "never" 83 | } 84 | }, 85 | "typedef": [ 86 | true, 87 | "call-signature" 88 | ], 89 | "typedef-whitespace": { 90 | "options": [ 91 | { 92 | "call-signature": "nospace", 93 | "index-signature": "nospace", 94 | "parameter": "nospace", 95 | "property-declaration": "nospace", 96 | "variable-declaration": "nospace" 97 | }, 98 | { 99 | "call-signature": "onespace", 100 | "index-signature": "onespace", 101 | "parameter": "onespace", 102 | "property-declaration": "onespace", 103 | "variable-declaration": "onespace" 104 | } 105 | ] 106 | }, 107 | "variable-name": { 108 | "options": [ 109 | "ban-keywords", 110 | "check-format", 111 | "allow-pascal-case" 112 | ] 113 | }, 114 | "whitespace": { 115 | "options": [ 116 | "check-branch", 117 | "check-decl", 118 | "check-operator", 119 | "check-separator", 120 | "check-type", 121 | "check-typecast" 122 | ] 123 | }, 124 | "component-class-suffix": true, 125 | "contextual-lifecycle": true, 126 | "directive-class-suffix": true, 127 | "no-conflicting-lifecycle": true, 128 | "no-host-metadata-property": true, 129 | "no-input-rename": true, 130 | "no-inputs-metadata-property": true, 131 | "no-output-native": true, 132 | "no-output-on-prefix": true, 133 | "no-output-rename": true, 134 | "no-outputs-metadata-property": true, 135 | "template-banana-in-box": true, 136 | "template-no-negated-async": true, 137 | "use-lifecycle-interface": true, 138 | "use-pipe-transform-interface": true, 139 | "directive-selector": [ 140 | true, 141 | "attribute", 142 | "app", 143 | "camelCase" 144 | ], 145 | "component-selector": [ 146 | true, 147 | "element", 148 | "app", 149 | "kebab-case" 150 | ] 151 | } 152 | } 153 | --------------------------------------------------------------------------------