From 65f642c62c7abe8611abd2007115cabdb20f3cae Mon Sep 17 00:00:00 2001 From: Miguel Lehmann Date: Mon, 14 Oct 2024 15:54:32 +0200 Subject: [PATCH 01/33] remove cnAttributesMapper and all the related files --- .../puzzle/okr/mapper/CnAttributesMapper.java | 22 ------------- .../okr/mapper/CnAttributesMapperTest.java | 32 ------------------- 2 files changed, 54 deletions(-) delete mode 100644 backend/src/main/java/ch/puzzle/okr/mapper/CnAttributesMapper.java delete mode 100644 backend/src/test/java/ch/puzzle/okr/mapper/CnAttributesMapperTest.java diff --git a/backend/src/main/java/ch/puzzle/okr/mapper/CnAttributesMapper.java b/backend/src/main/java/ch/puzzle/okr/mapper/CnAttributesMapper.java deleted file mode 100644 index d315869043..0000000000 --- a/backend/src/main/java/ch/puzzle/okr/mapper/CnAttributesMapper.java +++ /dev/null @@ -1,22 +0,0 @@ -package ch.puzzle.okr.mapper; - -import org.springframework.beans.factory.annotation.Value; -import org.springframework.ldap.core.AttributesMapper; -import org.springframework.stereotype.Component; - -import javax.naming.NamingException; -import javax.naming.directory.Attribute; -import javax.naming.directory.Attributes; - -@Component -public class CnAttributesMapper implements AttributesMapper { - - @Override - public String mapFromAttributes(Attributes attributes) throws NamingException { - Attribute cnAttribute = attributes.get("cn"); - if (cnAttribute != null) { - return cnAttribute.get().toString(); - } - return null; - } -} diff --git a/backend/src/test/java/ch/puzzle/okr/mapper/CnAttributesMapperTest.java b/backend/src/test/java/ch/puzzle/okr/mapper/CnAttributesMapperTest.java deleted file mode 100644 index 20be29cc7c..0000000000 --- a/backend/src/test/java/ch/puzzle/okr/mapper/CnAttributesMapperTest.java +++ /dev/null @@ -1,32 +0,0 @@ -package ch.puzzle.okr.mapper; - -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; - -import javax.naming.NamingException; -import javax.naming.directory.Attributes; -import javax.naming.directory.BasicAttributes; - -public class CnAttributesMapperTest { - - private final CnAttributesMapper cnAttributesMapper = new CnAttributesMapper(); - - @DisplayName("for Cn AttributeId should return AttributeValue") - @Test - void forCnAttributeIdShouldReturnAttributeValue() throws NamingException { - Attributes attributes = new BasicAttributes(); - attributes.put("cn", "Mango"); - - Assertions.assertEquals("Mango", cnAttributesMapper.mapFromAttributes(attributes)); - } - - @DisplayName("for non Cn AttributeId should return null") - @Test - void forNonCnAttributeIdShouldReturnNull() throws NamingException { - Attributes attributes = new BasicAttributes(); - attributes.put("ou", "Juicy, Fruit"); - - Assertions.assertNull(cnAttributesMapper.mapFromAttributes(attributes)); - } -} From 36b74bb858ea36330d3d5fe687d3bd445e7e6982 Mon Sep 17 00:00:00 2001 From: Miguel Lehmann Date: Tue, 15 Oct 2024 08:44:44 +0200 Subject: [PATCH 02/33] remove ldab properties and dependency --- backend/pom.xml | 4 ---- backend/src/main/resources/application.properties | 5 ----- 2 files changed, 9 deletions(-) diff --git a/backend/pom.xml b/backend/pom.xml index a4877fbaaa..ba6cfe6be5 100644 --- a/backend/pom.xml +++ b/backend/pom.xml @@ -71,10 +71,6 @@ com.h2database h2 - - org.springframework.boot - spring-boot-starter-data-ldap - com.tngtech.archunit archunit-junit5 diff --git a/backend/src/main/resources/application.properties b/backend/src/main/resources/application.properties index 8f56d0e783..03faf88d85 100644 --- a/backend/src/main/resources/application.properties +++ b/backend/src/main/resources/application.properties @@ -17,11 +17,6 @@ logging.level.org.springframework.security=INFO caching.authorization.users.TTL=1800000 caching.users.TTL=1800000 -spring.ldap.urls=ldap://ldap.puzzle.ch:389 -spring.ldap.base=dc=puzzle,dc=itc -spring.ldap.username=uid=okrtoolbind,ou=binduser,ou=users,dc=puzzle,dc=itc -spring.ldap.password= - # security configuration server.port=8080 # server.servlet.context-path=/resource-server From 2af5a18e808e386345651a80947fdf61f123dd35 Mon Sep 17 00:00:00 2001 From: Yanick Minder Date: Tue, 15 Oct 2024 08:46:30 +0200 Subject: [PATCH 03/33] update config --- .../src/main/resources/application-staging.properties | 9 +++++---- docker/local-prod/docker-compose.yml | 4 ++-- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/backend/src/main/resources/application-staging.properties b/backend/src/main/resources/application-staging.properties index 31ce92a3c2..e15a7283ee 100644 --- a/backend/src/main/resources/application-staging.properties +++ b/backend/src/main/resources/application-staging.properties @@ -1,10 +1,11 @@ # logging level for staging -logging.level.org.springframework=debug +logging.level.ch.puzzle.okr=TRACE +logging.level.org.springframework.security=TRACE connect.src=http://localhost:8544 http://localhost:8545 -hibernate.connection.url=jdbc:postgresql://okr-dev-db:5432/okr +hibernate.connection.url=jdbc:postgresql://localhost:5432/okr hibernate.connection.username=user hibernate.connection.password=pwd hibernate.multiTenancy=SCHEMA @@ -16,7 +17,7 @@ okr.datasource.driver-class-name=org.postgresql.Driver okr.user.champion.usernames=peggimann # pitc -okr.tenants.pitc.datasource.url=jdbc:postgresql://okr-dev-db:5432/okr +okr.tenants.pitc.datasource.url=jdbc:postgresql://localhost:5432/okr okr.tenants.pitc.datasource.username=user okr.tenants.pitc.datasource.password=pwd okr.tenants.pitc.datasource.schema=okr_pitc @@ -27,7 +28,7 @@ okr.tenants.pitc.security.oauth2.frontend.client-id=pitc_okr_staging # acme -okr.tenants.acme.datasource.url=jdbc:postgresql://okr-dev-db:5432/okr +okr.tenants.acme.datasource.url=jdbc:postgresql://localhost:5432/okr okr.tenants.acme.datasource.username=user okr.tenants.acme.datasource.password=pwd okr.tenants.acme.datasource.schema=okr_acme diff --git a/docker/local-prod/docker-compose.yml b/docker/local-prod/docker-compose.yml index 0bee2c0f2d..db47c48efa 100644 --- a/docker/local-prod/docker-compose.yml +++ b/docker/local-prod/docker-compose.yml @@ -7,13 +7,13 @@ services: context: . dockerfile: local-prod.Dockerfile restart: always - ports: - - 8080:8080 environment: SPRING_PROFILES_ACTIVE: staging LOGGING_LEVEL_ORG_SPRINGFRAMEWORK: debug + SPRING_FLYWAY_LOCATION: classpath:db/migration,classpath:db/data-migration,classpath:db/callback volumes: - ../../../okr/backend/target:/app-root/backend + network_mode: "host" maven: container_name: maven From c3dd20c7ffd51094facdcbf9f5d8a44c1f595a58 Mon Sep 17 00:00:00 2001 From: Yanick Minder Date: Tue, 15 Oct 2024 09:34:20 +0200 Subject: [PATCH 04/33] auto restart spring if jar changes --- docker/local-prod/local-prod.Dockerfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docker/local-prod/local-prod.Dockerfile b/docker/local-prod/local-prod.Dockerfile index 9077d161ac..a6762ebff3 100644 --- a/docker/local-prod/local-prod.Dockerfile +++ b/docker/local-prod/local-prod.Dockerfile @@ -2,11 +2,11 @@ FROM alpine:3.20 USER root -RUN apk update && apk add --upgrade curl && apk --no-cache add openjdk17 +RUN apk update && apk add --upgrade curl && apk --no-cache add openjdk17 inotify-tools RUN adduser --home /app-root --uid 1001 --disabled-password okr USER 1001 WORKDIR app-root/backend -ENTRYPOINT ["/bin/sh", "-c", "export BACKEND_VERSION=$(find . -type f -name 'backend-*.jar' -print -quit | sed -n 's/.*backend-\\(.*\\)\\.jar/\\1/p'); java -jar backend-${BACKEND_VERSION}.jar"] \ No newline at end of file +ENTRYPOINT ["/bin/sh", "-c", "export BACKEND_VERSION=$(find . -type f -name 'backend-*.jar' -print -quit | sed -n 's/.*backend-\\(.*\\)\\.jar/\\1/p'); while true; do java -jar backend-${BACKEND_VERSION}.jar & pid=$!; inotifywait -e modify backend-${BACKEND_VERSION}.jar; kill -9 $pid; echo 'JAR updated. Restarting...'; done"] \ No newline at end of file From 7bcd59d8e3e6e35de8f2fbea2e16c3740df7f36e Mon Sep 17 00:00:00 2001 From: Yanick Minder Date: Tue, 15 Oct 2024 10:50:14 +0200 Subject: [PATCH 05/33] jar is now debuggable --- .run/OkrApplication-local-prod-debug.run.xml | 17 +++++++++++++++++ backend/pom.xml | 9 +++++++++ docker/local-prod/local-prod.Dockerfile | 2 +- pom.xml | 4 +++- 4 files changed, 30 insertions(+), 2 deletions(-) create mode 100755 .run/OkrApplication-local-prod-debug.run.xml diff --git a/.run/OkrApplication-local-prod-debug.run.xml b/.run/OkrApplication-local-prod-debug.run.xml new file mode 100755 index 0000000000..15ae63f4ef --- /dev/null +++ b/.run/OkrApplication-local-prod-debug.run.xml @@ -0,0 +1,17 @@ + + + + + + \ No newline at end of file diff --git a/backend/pom.xml b/backend/pom.xml index ba6cfe6be5..c7e74d4952 100644 --- a/backend/pom.xml +++ b/backend/pom.xml @@ -83,6 +83,15 @@ 3.26.3 test + + org.springframework.boot + spring-boot-devtools + + + org.springframework + springloaded + 1.2.8.RELEASE + diff --git a/docker/local-prod/local-prod.Dockerfile b/docker/local-prod/local-prod.Dockerfile index a6762ebff3..c224fc3c76 100644 --- a/docker/local-prod/local-prod.Dockerfile +++ b/docker/local-prod/local-prod.Dockerfile @@ -9,4 +9,4 @@ USER 1001 WORKDIR app-root/backend -ENTRYPOINT ["/bin/sh", "-c", "export BACKEND_VERSION=$(find . -type f -name 'backend-*.jar' -print -quit | sed -n 's/.*backend-\\(.*\\)\\.jar/\\1/p'); while true; do java -jar backend-${BACKEND_VERSION}.jar & pid=$!; inotifywait -e modify backend-${BACKEND_VERSION}.jar; kill -9 $pid; echo 'JAR updated. Restarting...'; done"] \ No newline at end of file +ENTRYPOINT ["/bin/sh", "-c", "export BACKEND_VERSION=$(find . -type f -name 'backend-*.jar' -print -quit | sed -n 's/.*backend-\\(.*\\)\\.jar/\\1/p'); while true; do java -Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=*:5005 -jar backend-${BACKEND_VERSION}.jar & pid=$!; inotifywait -e modify backend-${BACKEND_VERSION}.jar; kill -9 $pid; echo 'JAR updated. Restarting...'; done"] \ No newline at end of file diff --git a/pom.xml b/pom.xml index f10d4a8b53..f1d8460f23 100644 --- a/pom.xml +++ b/pom.xml @@ -41,9 +41,11 @@ backend/src/main/java + + frontend/dist + - clean package From 022c7804a0b6eeed9af04cc69a1644bdf4277f02 Mon Sep 17 00:00:00 2001 From: Yanick Minder Date: Tue, 15 Oct 2024 10:57:07 +0200 Subject: [PATCH 06/33] add jar debug dev tools only on profile --- backend/pom.xml | 23 ++++++++++++++--------- pom.xml | 1 + 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/backend/pom.xml b/backend/pom.xml index c7e74d4952..392f817085 100644 --- a/backend/pom.xml +++ b/backend/pom.xml @@ -83,15 +83,6 @@ 3.26.3 test - - org.springframework.boot - spring-boot-devtools - - - org.springframework - springloaded - 1.2.8.RELEASE - @@ -287,5 +278,19 @@ + + debug + + + org.springframework.boot + spring-boot-devtools + + + org.springframework + springloaded + 1.2.8.RELEASE + + + diff --git a/pom.xml b/pom.xml index f1d8460f23..06e76dd43a 100644 --- a/pom.xml +++ b/pom.xml @@ -50,6 +50,7 @@ build-for-docker + debug From 132f05f5007a83437d5c3faf3c2d2ecacc5e68ad Mon Sep 17 00:00:00 2001 From: Yanick Minder Date: Tue, 15 Oct 2024 11:29:16 +0200 Subject: [PATCH 07/33] change log levels --- backend/src/main/resources/application-staging.properties | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/backend/src/main/resources/application-staging.properties b/backend/src/main/resources/application-staging.properties index e15a7283ee..91d48fcb5e 100644 --- a/backend/src/main/resources/application-staging.properties +++ b/backend/src/main/resources/application-staging.properties @@ -1,6 +1,8 @@ # logging level for staging -logging.level.ch.puzzle.okr=TRACE -logging.level.org.springframework.security=TRACE +logging.level.ch.puzzle.okr=DEBUG +logging.level.org.springframework.security=DEBUG +logging.level.org.flywaydb.core=DEBUG + connect.src=http://localhost:8544 http://localhost:8545 From 104e3f67dcee98c5f251816bd151df01ca71d2bf Mon Sep 17 00:00:00 2001 From: Yanick Minder Date: Tue, 15 Oct 2024 12:10:39 +0200 Subject: [PATCH 08/33] try to fix autorestart of spring --- backend/src/main/resources/application-staging.properties | 2 +- docker/local-prod/local-prod.Dockerfile | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/src/main/resources/application-staging.properties b/backend/src/main/resources/application-staging.properties index 91d48fcb5e..a66d43876d 100644 --- a/backend/src/main/resources/application-staging.properties +++ b/backend/src/main/resources/application-staging.properties @@ -1,7 +1,7 @@ # logging level for staging logging.level.ch.puzzle.okr=DEBUG logging.level.org.springframework.security=DEBUG -logging.level.org.flywaydb.core=DEBUG +#logging.level.org.flywaydb.core=DEBUG connect.src=http://localhost:8544 http://localhost:8545 diff --git a/docker/local-prod/local-prod.Dockerfile b/docker/local-prod/local-prod.Dockerfile index c224fc3c76..bdd0ad9619 100644 --- a/docker/local-prod/local-prod.Dockerfile +++ b/docker/local-prod/local-prod.Dockerfile @@ -9,4 +9,4 @@ USER 1001 WORKDIR app-root/backend -ENTRYPOINT ["/bin/sh", "-c", "export BACKEND_VERSION=$(find . -type f -name 'backend-*.jar' -print -quit | sed -n 's/.*backend-\\(.*\\)\\.jar/\\1/p'); while true; do java -Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=*:5005 -jar backend-${BACKEND_VERSION}.jar & pid=$!; inotifywait -e modify backend-${BACKEND_VERSION}.jar; kill -9 $pid; echo 'JAR updated. Restarting...'; done"] \ No newline at end of file +ENTRYPOINT ["/bin/sh", "-c", "pwd; export BACKEND_VERSION=$(find . -type f -name 'backend-*.jar' -print -quit | sed -n 's/.*backend-\\(.*\\)\\.jar/\\1/p'); while true; do [ -f \"backend-${BACKEND_VERSION}.jar\" ] && (java -Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=*:5005 -jar backend-${BACKEND_VERSION}.jar & pid=$!; inotifywait -e modify backend-${BACKEND_VERSION}.jar; kill -9 $pid; echo 'JAR updated. Restarting...') || echo \"File backend-${BACKEND_VERSION}.jar does not exist.\"; done"] \ No newline at end of file From 06f51967bed9c62e0f089e01df58c04d441aa4a8 Mon Sep 17 00:00:00 2001 From: Yanick Minder Date: Tue, 15 Oct 2024 13:54:49 +0200 Subject: [PATCH 09/33] complete auto restart of container --- docker/local-prod/local-prod.Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/local-prod/local-prod.Dockerfile b/docker/local-prod/local-prod.Dockerfile index bdd0ad9619..e69622add5 100644 --- a/docker/local-prod/local-prod.Dockerfile +++ b/docker/local-prod/local-prod.Dockerfile @@ -9,4 +9,4 @@ USER 1001 WORKDIR app-root/backend -ENTRYPOINT ["/bin/sh", "-c", "pwd; export BACKEND_VERSION=$(find . -type f -name 'backend-*.jar' -print -quit | sed -n 's/.*backend-\\(.*\\)\\.jar/\\1/p'); while true; do [ -f \"backend-${BACKEND_VERSION}.jar\" ] && (java -Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=*:5005 -jar backend-${BACKEND_VERSION}.jar & pid=$!; inotifywait -e modify backend-${BACKEND_VERSION}.jar; kill -9 $pid; echo 'JAR updated. Restarting...') || echo \"File backend-${BACKEND_VERSION}.jar does not exist.\"; done"] \ No newline at end of file +ENTRYPOINT ["/bin/sh", "-c", "export BACKEND_VERSION=$(find . -type f -name 'backend-*.jar' -print -quit | sed -n 's/.*backend-\\(.*\\)\\.jar/\\1/p'); while true; do java -Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=*:5005 -jar backend-${BACKEND_VERSION}.jar & pid=$!; inotifywait -e modify backend-${BACKEND_VERSION}.jar; exit 1; done"] \ No newline at end of file From 8d3938240dab092dd2bcee66c6489384a3256a33 Mon Sep 17 00:00:00 2001 From: Yanick Minder Date: Thu, 17 Oct 2024 08:28:59 +0200 Subject: [PATCH 10/33] rename intelij config and change log level of spring to debug in staging config --- ...prod-debug.run.xml => OkrApplication-debug-docker.run.xml} | 4 ++-- backend/src/main/resources/application-staging.properties | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) rename .run/{OkrApplication-local-prod-debug.run.xml => OkrApplication-debug-docker.run.xml} (80%) diff --git a/.run/OkrApplication-local-prod-debug.run.xml b/.run/OkrApplication-debug-docker.run.xml similarity index 80% rename from .run/OkrApplication-local-prod-debug.run.xml rename to .run/OkrApplication-debug-docker.run.xml index 15ae63f4ef..f4213371b4 100755 --- a/.run/OkrApplication-local-prod-debug.run.xml +++ b/.run/OkrApplication-debug-docker.run.xml @@ -1,7 +1,7 @@ - - + + + code-format format @@ -260,6 +261,17 @@ org.springframework.boot spring-boot-maven-plugin + + net.revelc.code.formatter + formatter-maven-plugin + 2.23.0 + + + code-format + none + + + From a526783648c980ae433af845c1abc40cbe2f9920 Mon Sep 17 00:00:00 2001 From: Yanick Minder Date: Fri, 18 Oct 2024 12:25:52 +0200 Subject: [PATCH 13/33] use external profile to disable formatter --- backend/pom.xml | 29 ++++++++++++++++++----------- pom.xml | 1 + 2 files changed, 19 insertions(+), 11 deletions(-) diff --git a/backend/pom.xml b/backend/pom.xml index ebd1b86d05..fd75300b5e 100644 --- a/backend/pom.xml +++ b/backend/pom.xml @@ -261,17 +261,6 @@ org.springframework.boot spring-boot-maven-plugin - - net.revelc.code.formatter - formatter-maven-plugin - 2.23.0 - - - code-format - none - - - @@ -304,5 +293,23 @@ + + no-formatter + + + + net.revelc.code.formatter + formatter-maven-plugin + 2.23.0 + + + code-format + none + + + + + + diff --git a/pom.xml b/pom.xml index 06e76dd43a..e2b16d5405 100644 --- a/pom.xml +++ b/pom.xml @@ -51,6 +51,7 @@ build-for-docker debug + no-formatter From a61e98d8d8ee63738e5097f3a7c029ce7b1b7f92 Mon Sep 17 00:00:00 2001 From: Yanick Minder Date: Fri, 18 Oct 2024 12:28:39 +0200 Subject: [PATCH 14/33] clean up --- .../src/main/resources/application-staging.properties | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/backend/src/main/resources/application-staging.properties b/backend/src/main/resources/application-staging.properties index 38e4a47571..31ce92a3c2 100644 --- a/backend/src/main/resources/application-staging.properties +++ b/backend/src/main/resources/application-staging.properties @@ -1,12 +1,10 @@ # logging level for staging -logging.level.ch.puzzle.okr=DEBUG -#logging.level.org.flywaydb.core=DEBUG - +logging.level.org.springframework=debug connect.src=http://localhost:8544 http://localhost:8545 -hibernate.connection.url=jdbc:postgresql://localhost:5432/okr +hibernate.connection.url=jdbc:postgresql://okr-dev-db:5432/okr hibernate.connection.username=user hibernate.connection.password=pwd hibernate.multiTenancy=SCHEMA @@ -18,7 +16,7 @@ okr.datasource.driver-class-name=org.postgresql.Driver okr.user.champion.usernames=peggimann # pitc -okr.tenants.pitc.datasource.url=jdbc:postgresql://localhost:5432/okr +okr.tenants.pitc.datasource.url=jdbc:postgresql://okr-dev-db:5432/okr okr.tenants.pitc.datasource.username=user okr.tenants.pitc.datasource.password=pwd okr.tenants.pitc.datasource.schema=okr_pitc @@ -29,7 +27,7 @@ okr.tenants.pitc.security.oauth2.frontend.client-id=pitc_okr_staging # acme -okr.tenants.acme.datasource.url=jdbc:postgresql://localhost:5432/okr +okr.tenants.acme.datasource.url=jdbc:postgresql://okr-dev-db:5432/okr okr.tenants.acme.datasource.username=user okr.tenants.acme.datasource.password=pwd okr.tenants.acme.datasource.schema=okr_acme From c154b4a1e3125e79adbcfff115d8c8c0ce7335c5 Mon Sep 17 00:00:00 2001 From: Yanick Minder Date: Fri, 18 Oct 2024 13:28:25 +0200 Subject: [PATCH 15/33] update local prod dockerfile --- docker/dev-with-prod/local-prod.Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/dev-with-prod/local-prod.Dockerfile b/docker/dev-with-prod/local-prod.Dockerfile index 7e5a8832e1..131874545b 100644 --- a/docker/dev-with-prod/local-prod.Dockerfile +++ b/docker/dev-with-prod/local-prod.Dockerfile @@ -9,4 +9,4 @@ USER 1001 WORKDIR app-root/backend -ENTRYPOINT ["/bin/sh", "-c", "export BACKEND_VERSION=$(find . -type f -name 'backend-*.jar' -print -quit | sed -n 's/.*backend-\\(.*\\)\\.jar/\\1/p'); java -Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=*:5005 -jar backend-${BACKEND_VERSION}.jar & pid=$!; while true; do inotifywait -e modify backend-${BACKEND_VERSION}.jar; exit 1; done"] \ No newline at end of file +ENTRYPOINT ["/bin/sh", "-c", "export BACKEND_VERSION=$(find . -type f -name 'backend-*.jar' -print -quit | sed -n 's/.*backend-\\(.*\\)\\.jar/\\1/p'); if ! unzip -p backend-${BACKEND_VERSION}.jar META-INF/MANIFEST.MF | grep -q 'Main-Class:'; then echo 'Error: no main manifest attribute, exiting.'; exit 1; fi; java -Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=*:5005 -jar backend-${BACKEND_VERSION}.jar & pid=$!; while true; do inotifywait -e modify backend-${BACKEND_VERSION}.jar; exit 1; done"] \ No newline at end of file From c3799f5a902a61e42996c60b7572aca5eb65ac2e Mon Sep 17 00:00:00 2001 From: Yanick Minder Date: Fri, 18 Oct 2024 07:21:41 +0200 Subject: [PATCH 16/33] fix display of tangram --- .../application-banner.component.html | 4 ++-- .../okr-tangram/okr-tangram.component.html | 4 +--- .../okr-tangram/okr-tangram.component.scss | 11 --------- .../okr-tangram/okr-tangram.component.ts | 23 ++++--------------- 4 files changed, 8 insertions(+), 34 deletions(-) diff --git a/frontend/src/app/components/application-banner/application-banner.component.html b/frontend/src/app/components/application-banner/application-banner.component.html index fd59295d0d..39f0ba7b8a 100644 --- a/frontend/src/app/components/application-banner/application-banner.component.html +++ b/frontend/src/app/components/application-banner/application-banner.component.html @@ -1,6 +1,6 @@
-
+
@@ -35,7 +35,7 @@
-
+
diff --git a/frontend/src/app/shared/custom/okr-tangram/okr-tangram.component.html b/frontend/src/app/shared/custom/okr-tangram/okr-tangram.component.html index 043cd96ff0..abd7776d79 100644 --- a/frontend/src/app/shared/custom/okr-tangram/okr-tangram.component.html +++ b/frontend/src/app/shared/custom/okr-tangram/okr-tangram.component.html @@ -1,3 +1 @@ -
- okr-logo -
+okr-logo diff --git a/frontend/src/app/shared/custom/okr-tangram/okr-tangram.component.scss b/frontend/src/app/shared/custom/okr-tangram/okr-tangram.component.scss index 4a53cd9648..e69de29bb2 100644 --- a/frontend/src/app/shared/custom/okr-tangram/okr-tangram.component.scss +++ b/frontend/src/app/shared/custom/okr-tangram/okr-tangram.component.scss @@ -1,11 +0,0 @@ -div { - display: block; - width: 100px; - - @media (min-width: 576px) { - width: 180px; - } - @media (min-width: 992px) { - width: 274px; - } -} diff --git a/frontend/src/app/shared/custom/okr-tangram/okr-tangram.component.ts b/frontend/src/app/shared/custom/okr-tangram/okr-tangram.component.ts index 092e4ae4e3..94f1683e8d 100644 --- a/frontend/src/app/shared/custom/okr-tangram/okr-tangram.component.ts +++ b/frontend/src/app/shared/custom/okr-tangram/okr-tangram.component.ts @@ -1,7 +1,7 @@ import { Component } from '@angular/core'; import { isMobileDevice } from '../../common'; import { ConfigService } from '../../../services/config.service'; -import { BehaviorSubject, Subscription } from 'rxjs'; +import { BehaviorSubject, map, Observable, Subject, Subscription } from 'rxjs'; @Component({ selector: 'app-okr-tangram', @@ -9,23 +9,10 @@ import { BehaviorSubject, Subscription } from 'rxjs'; styleUrl: 'okr-tangram.component.scss', }) export class OkrTangramComponent { - private readonly MOBILE_WIDTH = 100; - private readonly DESKTOP_WIDTH = 274; + private readonly defaultTrianglesSrc = 'assets/images/empty.svg'; + trianglesSrc$ = new Observable(); - getWidth() { - return isMobileDevice() ? this.MOBILE_WIDTH : this.DESKTOP_WIDTH; - } - - private subscription?: Subscription; - trianglesSrc$ = new BehaviorSubject('assets/images/empty.svg'); - - constructor(private configService: ConfigService) {} - - ngOnInit(): void { - this.subscription = this.configService.config$.subscribe((config) => { - if (config.triangles) { - this.trianglesSrc$.next(config.triangles); - } - }); + constructor(private readonly configService: ConfigService) { + this.trianglesSrc$ = this.configService.config$.pipe(map((config) => config.triangles || this.defaultTrianglesSrc)); } } From d0058daa87fb0bfa7aced79c9d9dd2dff7248ea2 Mon Sep 17 00:00:00 2001 From: Yanick Minder Date: Fri, 18 Oct 2024 07:44:53 +0200 Subject: [PATCH 17/33] update test and change const cname --- .../app/shared/custom/okr-tangram/okr-tangram.component.ts | 6 ++++-- .../app/team-management/team-management.component.spec.ts | 6 ++++++ 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/frontend/src/app/shared/custom/okr-tangram/okr-tangram.component.ts b/frontend/src/app/shared/custom/okr-tangram/okr-tangram.component.ts index 94f1683e8d..7f871e2454 100644 --- a/frontend/src/app/shared/custom/okr-tangram/okr-tangram.component.ts +++ b/frontend/src/app/shared/custom/okr-tangram/okr-tangram.component.ts @@ -9,10 +9,12 @@ import { BehaviorSubject, map, Observable, Subject, Subscription } from 'rxjs'; styleUrl: 'okr-tangram.component.scss', }) export class OkrTangramComponent { - private readonly defaultTrianglesSrc = 'assets/images/empty.svg'; + private readonly DEFAULT_TRIANGLE_SRC = 'assets/images/empty.svg'; trianglesSrc$ = new Observable(); constructor(private readonly configService: ConfigService) { - this.trianglesSrc$ = this.configService.config$.pipe(map((config) => config.triangles || this.defaultTrianglesSrc)); + this.trianglesSrc$ = this.configService.config$.pipe( + map((config) => config.triangles || this.DEFAULT_TRIANGLE_SRC), + ); } } diff --git a/frontend/src/app/team-management/team-management.component.spec.ts b/frontend/src/app/team-management/team-management.component.spec.ts index ffa1631fec..7a1fff46c1 100644 --- a/frontend/src/app/team-management/team-management.component.spec.ts +++ b/frontend/src/app/team-management/team-management.component.spec.ts @@ -23,6 +23,7 @@ import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { MatTableModule } from '@angular/material/table'; import { testUser, users } from '../shared/testData'; import { UserService } from '../services/user.service'; +import { ConfigService } from '../services/config.service'; describe('TeamManagementComponent', () => { let component: TeamManagementComponent; @@ -37,6 +38,10 @@ describe('TeamManagementComponent', () => { getUsers: () => of(users), }; + const configServiceMock = { + config$: of({}), + }; + beforeEach(async () => { await TestBed.configureTestingModule({ imports: [ @@ -69,6 +74,7 @@ describe('TeamManagementComponent', () => { providers: [ { provide: ActivatedRoute, useValue: activatedRouteMock }, { provide: UserService, useValue: userServiceMock }, + { provide: ConfigService, useValue: configServiceMock }, ], }).compileComponents(); }); From 5939953e00b9cca503f23241aee0b797afffc8fd Mon Sep 17 00:00:00 2001 From: Jean-Claude Brantschen Date: Wed, 23 Oct 2024 11:33:24 +0200 Subject: [PATCH 18/33] add logging to QuarterFunction (#1076) --- .../quarter/generate/h2/QuarterFunction.java | 28 +++++++++++++++---- 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/backend/src/main/java/ch/puzzle/okr/util/quarter/generate/h2/QuarterFunction.java b/backend/src/main/java/ch/puzzle/okr/util/quarter/generate/h2/QuarterFunction.java index d8c663576c..fec72a232a 100644 --- a/backend/src/main/java/ch/puzzle/okr/util/quarter/generate/h2/QuarterFunction.java +++ b/backend/src/main/java/ch/puzzle/okr/util/quarter/generate/h2/QuarterFunction.java @@ -2,6 +2,8 @@ import ch.puzzle.okr.util.quarter.generate.QuarterData; import ch.puzzle.okr.util.quarter.generate.Quarters; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.context.annotation.Profile; import org.springframework.stereotype.Component; @@ -21,6 +23,7 @@ public class QuarterFunction { public static final int NEXT_QUARTER_DB_ID = 3; private static final Map QUARTERS = new HashMap<>(); + private static final Logger logger = LoggerFactory.getLogger(QuarterFunction.class); public static void initQuarterData() { LocalDate now = LocalDate.now(); @@ -37,27 +40,40 @@ private static void registerCurrentQuarterForDate(LocalDate date, int dbId) { } public static String currentQuarterLabel() { - return QUARTERS.get(CURRENT_QUARTER_DB_ID).label(); + var label = QUARTERS.get(CURRENT_QUARTER_DB_ID).label(); + logger.info("currentQuarterLabel : {}", label); + return label; + } public static String currentQuarterStartDate() { - return QUARTERS.get(CURRENT_QUARTER_DB_ID).startDateAsIsoString(); + var start = QUARTERS.get(CURRENT_QUARTER_DB_ID).startDateAsIsoString(); + logger.info("currentQuarterStartDate: {}", start); + return start; } public static String currentQuarterEndDate() { - return QUARTERS.get(CURRENT_QUARTER_DB_ID).endDateAsIsoString(); + var end = QUARTERS.get(CURRENT_QUARTER_DB_ID).endDateAsIsoString(); + logger.info("currentQuarterEndDate : {}", end); + return end; } public static String nextQuarterLabel() { - return QUARTERS.get(NEXT_QUARTER_DB_ID).label(); + var label = QUARTERS.get(NEXT_QUARTER_DB_ID).label(); + logger.info("nextQuarterLabel : {}", label); + return label; } public static String nextQuarterStartDate() { - return QUARTERS.get(NEXT_QUARTER_DB_ID).startDateAsIsoString(); + var start = QUARTERS.get(NEXT_QUARTER_DB_ID).startDateAsIsoString(); + logger.info("nextQuarterStartDate : {}", start); + return start; } public static String nextQuarterEndDate() { - return QUARTERS.get(NEXT_QUARTER_DB_ID).endDateAsIsoString(); + var end = QUARTERS.get(NEXT_QUARTER_DB_ID).endDateAsIsoString(); + logger.info("nextQuarterEndDate : {}", end); + return end; } } From f800feaddf0013efb54b45cb8942bb4a3e9012f2 Mon Sep 17 00:00:00 2001 From: nevio18324 <141240169+nevio18324@users.noreply.github.com> Date: Wed, 23 Oct 2024 12:01:37 +0200 Subject: [PATCH 19/33] add a Observable that triggers a refresh when called (#1041) * jar is now debuggable * rename folder * add a Observable that triggers a refresh when called * add destroy observable to stop subscription of reloadKeySubject * add tests to observables --------- Co-authored-by: Yanick Minder --- .run/OkrApplication-local-prod-debug.run.xml | 17 +++++++++++++++++ backend/pom.xml | 9 +++++++++ frontend/package.json | 2 +- .../check-in-history-dialog.component.ts | 1 + .../keyresult-detail.component.spec.ts | 18 ++++++++++++++++++ .../keyresult-detail.component.ts | 17 +++++++++++++---- .../src/app/services/refresh-data.service.ts | 1 + 7 files changed, 60 insertions(+), 5 deletions(-) create mode 100755 .run/OkrApplication-local-prod-debug.run.xml diff --git a/.run/OkrApplication-local-prod-debug.run.xml b/.run/OkrApplication-local-prod-debug.run.xml new file mode 100755 index 0000000000..15ae63f4ef --- /dev/null +++ b/.run/OkrApplication-local-prod-debug.run.xml @@ -0,0 +1,17 @@ + + + + + + \ No newline at end of file diff --git a/backend/pom.xml b/backend/pom.xml index fd75300b5e..55b2f44dd9 100644 --- a/backend/pom.xml +++ b/backend/pom.xml @@ -83,6 +83,15 @@ 3.26.3 test + + org.springframework.boot + spring-boot-devtools + + + org.springframework + springloaded + 1.2.8.RELEASE + diff --git a/frontend/package.json b/frontend/package.json index 4c12464841..08e2415442 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -3,7 +3,7 @@ "version": "2.0.0", "scripts": { "ng": "ng", - "start": "ng serve ", + "start": "ng serve", "build": "ng build", "build:staging": "ng build --configuration staging", "watch": "ng build --watch --configuration development", diff --git a/frontend/src/app/components/check-in-history-dialog/check-in-history-dialog.component.ts b/frontend/src/app/components/check-in-history-dialog/check-in-history-dialog.component.ts index 4241ad6426..7b8a93d8fe 100644 --- a/frontend/src/app/components/check-in-history-dialog/check-in-history-dialog.component.ts +++ b/frontend/src/app/components/check-in-history-dialog/check-in-history-dialog.component.ts @@ -48,6 +48,7 @@ export class CheckInHistoryDialogComponent implements OnInit { }); dialogRef.afterClosed().subscribe(() => { this.loadCheckInHistory(); + this.refreshDataService.reloadKeyResultSubject.next(); this.refreshDataService.markDataRefresh(); }); } diff --git a/frontend/src/app/components/keyresult-detail/keyresult-detail.component.spec.ts b/frontend/src/app/components/keyresult-detail/keyresult-detail.component.spec.ts index c0e0055357..ebaec251bc 100644 --- a/frontend/src/app/components/keyresult-detail/keyresult-detail.component.spec.ts +++ b/frontend/src/app/components/keyresult-detail/keyresult-detail.component.spec.ts @@ -12,6 +12,7 @@ import { MatIconModule } from '@angular/material/icon'; import { ActivatedRoute } from '@angular/router'; import { ScoringComponent } from '../../shared/custom/scoring/scoring.component'; import { ConfidenceComponent } from '../confidence/confidence.component'; +import { RefreshDataService } from '../../services/refresh-data.service'; const keyResultServiceMock = { getFullKeyResult: jest.fn(), @@ -87,4 +88,21 @@ describe('KeyresultDetailComponent', () => { const button = fixture.debugElement.query(By.css('[data-testId="add-check-in"]')); expect(button).toBeFalsy(); }); + + it('should trigger observable when subject gets next value', () => { + const spy = jest.spyOn(component, 'loadKeyResult'); + const refreshDataService = TestBed.inject(RefreshDataService); + refreshDataService.reloadKeyResultSubject.next(); + expect(spy).toHaveBeenCalled(); + }); + + it('should close subscription on destroy', () => { + const spyNext = jest.spyOn(component.ngDestroy$, 'next'); + const spyComplete = jest.spyOn(component.ngDestroy$, 'complete'); + + component.ngOnDestroy(); + + expect(spyNext).toHaveBeenCalled(); + expect(spyComplete).toHaveBeenCalled(); + }); }); diff --git a/frontend/src/app/components/keyresult-detail/keyresult-detail.component.ts b/frontend/src/app/components/keyresult-detail/keyresult-detail.component.ts index d31fae0dfb..62e728dcf2 100644 --- a/frontend/src/app/components/keyresult-detail/keyresult-detail.component.ts +++ b/frontend/src/app/components/keyresult-detail/keyresult-detail.component.ts @@ -1,11 +1,11 @@ -import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core'; +import { ChangeDetectionStrategy, Component, Input, OnDestroy, OnInit } from '@angular/core'; import { KeyResult } from '../../shared/types/model/KeyResult'; import { KeyresultService } from '../../services/keyresult.service'; import { KeyResultMetric } from '../../shared/types/model/KeyResultMetric'; import { KeyResultOrdinal } from '../../shared/types/model/KeyResultOrdinal'; import { CheckInHistoryDialogComponent } from '../check-in-history-dialog/check-in-history-dialog.component'; import { MatDialog } from '@angular/material/dialog'; -import { BehaviorSubject, catchError, EMPTY } from 'rxjs'; +import { BehaviorSubject, catchError, EMPTY, Subject, takeUntil } from 'rxjs'; import { ActivatedRoute, Router } from '@angular/router'; import { RefreshDataService } from '../../services/refresh-data.service'; import { CloseState } from '../../shared/types/enums/CloseState'; @@ -22,10 +22,11 @@ import { ConfirmDialogComponent } from '../../shared/dialog/confirm-dialog/confi styleUrls: ['./keyresult-detail.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush, }) -export class KeyresultDetailComponent implements OnInit { +export class KeyresultDetailComponent implements OnInit, OnDestroy { @Input() keyResultId!: number; keyResult$: BehaviorSubject = new BehaviorSubject({} as KeyResult); + ngDestroy$: Subject = new Subject(); isComplete: boolean = false; protected readonly DATE_FORMAT = DATE_FORMAT; protected readonly isLastCheckInNegative = isLastCheckInNegative; @@ -41,6 +42,14 @@ export class KeyresultDetailComponent implements OnInit { ngOnInit(): void { this.keyResultId = this.getIdFromParams(); this.loadKeyResult(this.keyResultId); + this.refreshDataService.reloadKeyResultSubject.pipe(takeUntil(this.ngDestroy$)).subscribe(() => { + this.loadKeyResult(this.keyResultId); + }); + } + + ngOnDestroy() { + this.ngDestroy$.next(); + this.ngDestroy$.complete(); } private getIdFromParams(): number { @@ -181,7 +190,7 @@ export class KeyresultDetailComponent implements OnInit { }, }); dialogRef.afterClosed().subscribe(() => { - this.loadKeyResult(this.keyResult$.getValue().id); + this.refreshDataService.reloadKeyResultSubject.next(); this.refreshDataService.markDataRefresh(); }); } diff --git a/frontend/src/app/services/refresh-data.service.ts b/frontend/src/app/services/refresh-data.service.ts index fe5a7f5f7a..3c1a1fb321 100644 --- a/frontend/src/app/services/refresh-data.service.ts +++ b/frontend/src/app/services/refresh-data.service.ts @@ -7,6 +7,7 @@ import { DEFAULT_HEADER_HEIGHT_PX } from '../shared/constantLibary'; }) export class RefreshDataService { public reloadOverviewSubject: Subject = new Subject(); + public reloadKeyResultSubject: Subject = new Subject(); public quarterFilterReady: Subject = new Subject(); public teamFilterReady: Subject = new Subject(); From d90d1a0090a3a4ce3a1e92f3537be810c63116cd Mon Sep 17 00:00:00 2001 From: Jean-Claude Brantschen Date: Fri, 25 Oct 2024 10:49:48 +0200 Subject: [PATCH 20/33] #1030: tests for delete() and deleteAll() (#1034) --- .../UserTeamPersistenceServiceIT.java | 98 +++++++++++++++++++ 1 file changed, 98 insertions(+) create mode 100644 backend/src/test/java/ch/puzzle/okr/service/persistence/UserTeamPersistenceServiceIT.java diff --git a/backend/src/test/java/ch/puzzle/okr/service/persistence/UserTeamPersistenceServiceIT.java b/backend/src/test/java/ch/puzzle/okr/service/persistence/UserTeamPersistenceServiceIT.java new file mode 100644 index 0000000000..cc2dfe71b8 --- /dev/null +++ b/backend/src/test/java/ch/puzzle/okr/service/persistence/UserTeamPersistenceServiceIT.java @@ -0,0 +1,98 @@ +package ch.puzzle.okr.service.persistence; + +import ch.puzzle.okr.multitenancy.TenantContext; +import ch.puzzle.okr.test.SpringIntegrationTest; +import ch.puzzle.okr.test.TestHelper; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +// uses test date from V100_0_0__TestData.sql +@SpringIntegrationTest +public class UserTeamPersistenceServiceIT { + private static final Long ID_OF_USER_ALICE = 11L; // user Alice is only in team Lorem + private static final Long ID_OF_TEAM_LOREM = 6L; // team Lorem has 3 users + + private static final Long ID_OF_USER_BOB = 21L; // user Bob is only in team Cube + private static final Long ID_OF_TEAM_CUBE = 8L; // team Cube has 2 users + + @Autowired + private UserTeamPersistenceService userTeamPersistenceService; + + @Autowired + private UserPersistenceService userPersistenceService; + + @Autowired + private TeamPersistenceService teamPersistenceService; + + static { + TenantContext.setCurrentTenant(TestHelper.SCHEMA_PITC); + } + + @DisplayName("delete() should remove single user from team") + @Test + @Transactional + void deleteShouldRemoveSingleUserFromTeam() { + // arrange + var user = userPersistenceService.findById(ID_OF_USER_ALICE); + var team = teamPersistenceService.findById(ID_OF_TEAM_LOREM); + + // preconditions: user Alice is in team Lorem and team Lorem has 3 users + assertUserIsInTeam(ID_OF_USER_ALICE, ID_OF_TEAM_LOREM, 3); + + // arrange + var userTeamToRemove = user.getUserTeamList().get(0); // Alice is only in Team Lorem + + // act + user.getUserTeamList().remove(userTeamToRemove); + team.getUserTeamList().remove(userTeamToRemove); + userTeamPersistenceService.delete(userTeamToRemove); + + // assert: user Alice is no longer in team Lorem and team Lorem has 2 users + assertUserIsRemovedFromTeam(ID_OF_USER_ALICE, ID_OF_TEAM_LOREM, 2); + } + + @DisplayName("deleteAll() should remove list of users from team") + @Test + @Transactional + void deleteAllShouldRemoveListOfUsersFromTeam() { + // arrange + var user = userPersistenceService.findById(ID_OF_USER_BOB); + var team = teamPersistenceService.findById(ID_OF_TEAM_CUBE); + + // preconditions: user Bob is in a team Cube and team Cube has 2 users + assertUserIsInTeam(ID_OF_USER_BOB, ID_OF_TEAM_CUBE, 2); + + // arrange + var userTeamToRemove = user.getUserTeamList().get(0); // Bos is only in Team Cube + + // act + user.getUserTeamList().remove(userTeamToRemove); + team.getUserTeamList().remove(userTeamToRemove); + userTeamPersistenceService.deleteAll(List.of(userTeamToRemove)); + + // assert: user Bob is no longer in team Cube and team Cube has 1 user + assertUserIsRemovedFromTeam(ID_OF_USER_BOB, ID_OF_TEAM_CUBE, 1); + } + + private void assertUserIsInTeam(Long userId, Long teamId, int expectedUsersInTeam) { + var user = userPersistenceService.findById(userId); + Assertions.assertEquals(1, user.getUserTeamList().size()); + + var team = this.teamPersistenceService.findById(teamId); + Assertions.assertEquals(expectedUsersInTeam, team.getUserTeamList().size()); + } + + private void assertUserIsRemovedFromTeam(Long userId, Long teamId, int expectedUsersInTeam) { + var reloadedUser = userPersistenceService.findById(userId); + Assertions.assertEquals(0, reloadedUser.getUserTeamList().size()); + + var reloadedTeam = this.teamPersistenceService.findById(teamId); + Assertions.assertEquals(expectedUsersInTeam, reloadedTeam.getUserTeamList().size()); + } + +} From ed539f9a20a785149c0d6b89d4f7903e804a78bb Mon Sep 17 00:00:00 2001 From: Jean-Claude Brantschen Date: Fri, 25 Oct 2024 11:14:22 +0200 Subject: [PATCH 21/33] Test/1012 service (#1050) * tests for PersistenceBase class * remove tests from QuarterPersistenceServiceIT which are already covered in PersistenceBaseTestIT * remove no longer used OrganisationXXX() methods * remove tests from TeamPersistenceServiceIT which are already covered in PersistenceBaseTestIT * remove tests from UserPersistenceServiceIT which are already covered in PersistenceBaseTestIT and add additional tests * remove tests from CheckInPersistenceServiceIT which are already covered in PersistenceBaseTestIT and add additional test * umbau ObjectivePersistenceServiceIT: remove tests which are already covered in PersistenceBaseTestIT, add additional test, make the tests readable * tests for AuthorizationCriteria * tests for AuthorizationCriteria * tests for AuthorizationCriteria * enable debug trace * debug found okr-champions in test * add logging in findAllOkrChampionsShouldReturnAllOkrChampions() test * disable tests which call setOkrChampion() * enable some disabled tests * enable some disabled tests * enable last disabled test .. should fail * ugly temp fix * cleanup * cleanup * cleanup * cleanup * cleanup * cleanup --------- Co-authored-by: peggimann --- .github/workflows/backend-test-action.yml | 2 +- .../puzzle/okr/repository/TeamRepository.java | 9 - .../ObjectivePersistenceService.java | 11 + .../persistence/TeamPersistenceService.java | 8 - .../ch/puzzle/okr/util/CollectionUtils.java | 14 + .../puzzle/okr/SpringCachingConfigTest.java | 6 +- .../AuthorizationRegistrationServiceIT.java | 61 +++- .../AuthorizationServiceTest.java | 4 +- .../TeamAuthorizationServiceTest.java | 2 +- .../AuthorizationCriteriaParametersTest.java | 327 +++++++++++++++++ .../AuthorizationCriteriaTest.java | 104 ++++++ .../CheckInPersistenceServiceIT.java | 144 +++----- .../ObjectivePersistenceServiceIT.java | 333 +++++++++--------- .../persistence/PersistenceBaseTestIT.java | 182 ++++++++++ .../QuarterPersistenceServiceIT.java | 46 +-- .../persistence/TeamPersistenceServiceIT.java | 127 +------ .../persistence/UserPersistenceServiceIT.java | 160 ++++++--- .../java/ch/puzzle/okr/test/TestHelper.java | 19 +- 18 files changed, 1054 insertions(+), 505 deletions(-) create mode 100644 backend/src/main/java/ch/puzzle/okr/util/CollectionUtils.java create mode 100644 backend/src/test/java/ch/puzzle/okr/service/persistence/AuthorizationCriteriaParametersTest.java create mode 100644 backend/src/test/java/ch/puzzle/okr/service/persistence/AuthorizationCriteriaTest.java create mode 100644 backend/src/test/java/ch/puzzle/okr/service/persistence/PersistenceBaseTestIT.java diff --git a/.github/workflows/backend-test-action.yml b/.github/workflows/backend-test-action.yml index 50f40c9fb2..428368d7ff 100644 --- a/.github/workflows/backend-test-action.yml +++ b/.github/workflows/backend-test-action.yml @@ -17,4 +17,4 @@ jobs: distribution: 'adopt' - name: Use Maven to run unittests and integration tests - run: mvn clean verify \ No newline at end of file + run: mvn clean verify -X \ No newline at end of file diff --git a/backend/src/main/java/ch/puzzle/okr/repository/TeamRepository.java b/backend/src/main/java/ch/puzzle/okr/repository/TeamRepository.java index 6a792921c5..14e48978ed 100644 --- a/backend/src/main/java/ch/puzzle/okr/repository/TeamRepository.java +++ b/backend/src/main/java/ch/puzzle/okr/repository/TeamRepository.java @@ -1,20 +1,11 @@ package ch.puzzle.okr.repository; import ch.puzzle.okr.models.Team; -import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.CrudRepository; -import org.springframework.data.repository.query.Param; import java.util.List; public interface TeamRepository extends CrudRepository { - @Query(value = """ - select distinct teamOrg.team_id from team_organisation teamOrg - inner join organisation o on o.id = teamOrg.organisation_id - where o.org_name in (:organisationNames) - """, nativeQuery = true) - List findTeamIdsByOrganisationNames(@Param("organisationNames") List organisationNames); - List findTeamsByName(String name); } diff --git a/backend/src/main/java/ch/puzzle/okr/service/persistence/ObjectivePersistenceService.java b/backend/src/main/java/ch/puzzle/okr/service/persistence/ObjectivePersistenceService.java index 675d8ec249..9e7a2bcd34 100644 --- a/backend/src/main/java/ch/puzzle/okr/service/persistence/ObjectivePersistenceService.java +++ b/backend/src/main/java/ch/puzzle/okr/service/persistence/ObjectivePersistenceService.java @@ -40,6 +40,17 @@ public String getModelName() { return OBJECTIVE; } + /** + * Get the number of Objectives of a Team in a Quarter. The underling sql looks like "select count(*) from objective + * where teamId = team.id and quarterId = quarter.id." + * + * @param team + * Team + * @param quarter + * Quarter + * + * @return number of Objectives of team in quarter + */ public Integer countByTeamAndQuarter(Team team, Quarter quarter) { return getRepository().countByTeamAndQuarter(team, quarter); } diff --git a/backend/src/main/java/ch/puzzle/okr/service/persistence/TeamPersistenceService.java b/backend/src/main/java/ch/puzzle/okr/service/persistence/TeamPersistenceService.java index 45d3eba4ae..533949d848 100644 --- a/backend/src/main/java/ch/puzzle/okr/service/persistence/TeamPersistenceService.java +++ b/backend/src/main/java/ch/puzzle/okr/service/persistence/TeamPersistenceService.java @@ -20,14 +20,6 @@ public String getModelName() { return TEAM; } - public List findTeamIdsByOrganisationName(String organisationName) { - return findTeamIdsByOrganisationNames(List.of(organisationName)); - } - - public List findTeamIdsByOrganisationNames(List organisationNames) { - return getRepository().findTeamIdsByOrganisationNames(organisationNames); - } - public List findTeamsByName(String name) { return getRepository().findTeamsByName(name); } diff --git a/backend/src/main/java/ch/puzzle/okr/util/CollectionUtils.java b/backend/src/main/java/ch/puzzle/okr/util/CollectionUtils.java new file mode 100644 index 0000000000..5c3a002a28 --- /dev/null +++ b/backend/src/main/java/ch/puzzle/okr/util/CollectionUtils.java @@ -0,0 +1,14 @@ +package ch.puzzle.okr.util; + +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.StreamSupport; + +public class CollectionUtils { + + public static List iterableToList(Iterable iterable) { + return StreamSupport // + .stream(iterable.spliterator(), false) // + .collect(Collectors.toList()); + } +} diff --git a/backend/src/test/java/ch/puzzle/okr/SpringCachingConfigTest.java b/backend/src/test/java/ch/puzzle/okr/SpringCachingConfigTest.java index aa87d01441..1ca5d0825a 100644 --- a/backend/src/test/java/ch/puzzle/okr/SpringCachingConfigTest.java +++ b/backend/src/test/java/ch/puzzle/okr/SpringCachingConfigTest.java @@ -6,7 +6,10 @@ import ch.puzzle.okr.service.authorization.AuthorizationRegistrationService; import ch.puzzle.okr.test.SpringIntegrationTest; import ch.puzzle.okr.test.TestHelper; -import org.junit.jupiter.api.*; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.cache.Cache; import org.springframework.cache.CacheManager; @@ -14,7 +17,6 @@ import static ch.puzzle.okr.SpringCachingConfig.AUTHORIZATION_USER_CACHE; import static ch.puzzle.okr.test.TestHelper.defaultUser; import static org.junit.jupiter.api.Assertions.*; -import static org.junit.jupiter.api.Assertions.assertNotNull; @SpringIntegrationTest class SpringCachingConfigTest { diff --git a/backend/src/test/java/ch/puzzle/okr/service/authorization/AuthorizationRegistrationServiceIT.java b/backend/src/test/java/ch/puzzle/okr/service/authorization/AuthorizationRegistrationServiceIT.java index 8ebcaa9351..4f0e3c77e4 100644 --- a/backend/src/test/java/ch/puzzle/okr/service/authorization/AuthorizationRegistrationServiceIT.java +++ b/backend/src/test/java/ch/puzzle/okr/service/authorization/AuthorizationRegistrationServiceIT.java @@ -6,10 +6,7 @@ import ch.puzzle.okr.multitenancy.TenantContext; import ch.puzzle.okr.service.persistence.UserPersistenceService; import ch.puzzle.okr.test.SpringIntegrationTest; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.*; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.cache.Cache; import org.springframework.cache.CacheManager; @@ -34,6 +31,8 @@ class AuthorizationRegistrationServiceIT { private final String tenant = TestHelper.SCHEMA_PITC; private final String key = tenant + "_" + user.getEmail(); + private static final String EMAIL_WUNDERLAND = "wunderland@puzzle.ch"; + @BeforeEach void setUp() { TenantContext.setCurrentTenant(tenant); @@ -41,10 +40,30 @@ void setUp() { @AfterEach void tearDown() { + resetOkrChampionStatus(); + clearCache(); + TenantContext.setCurrentTenant(null); + } + + private void resetOkrChampionStatus() { + Optional userFromDb = userPersistenceService.findByEmail(EMAIL_WUNDERLAND); + assertTrue(userFromDb.isPresent()); + + userFromDb.get().setOkrChampion(false); + userPersistenceService.save(userFromDb.get()); + assertOkrChampionStatusInDb(userFromDb.get().getEmail(), false); + } + + private void clearCache() { Cache cache = cacheManager.getCache(AUTHORIZATION_USER_CACHE); assertNotNull(cache); cache.clear(); - TenantContext.setCurrentTenant(null); + } + + private void assertOkrChampionStatusInDb(String email, boolean expectedOkrChampionStatus) { + var userInDb = userPersistenceService.findByEmail(email); + assertTrue(userInDb.isPresent()); + assertEquals(expectedOkrChampionStatus, userInDb.get().isOkrChampion()); } @Test @@ -81,6 +100,8 @@ void registerAuthorizationUser_shouldSetOkrChampionsToFalse() { // assert assertFalse(processedUser.user().isOkrChampion()); Optional userFromDB = userPersistenceService.findByEmail(user.getEmail()); + + assertTrue(userFromDB.isPresent()); assertFalse(userFromDB.get().isOkrChampion()); // cleanup @@ -89,30 +110,31 @@ void registerAuthorizationUser_shouldSetOkrChampionsToFalse() { /* * Special test setup.
 - the user wunderland@puzzle.ch is an existing user in the H2 db (created via
-     * X_TestData.sql) - the user wunderland@puzzle.ch is also defined in application-integration-test.properties as
-     * user champion - with this combination we can test, that the user in the db (which has initial isOkrChampion ==
-     * false) is after calling updateOrAddAuthorizationUser() a user champion. - because the user wunderland@puzzle.ch
-     * exists before the test, we make no clean in db (we don't remove it) 
+ * V100_0_0__TestData.sql) - the user wunderland@puzzle.ch is also defined in + * application-integration-test.properties as user champion - with this combination we can test, that the user in + * the db (which has initial isOkrChampion == false) is after calling updateOrAddAuthorizationUser() a user + * champion. - the OkrChampion status must manually be reset (in the tearDown method) */ @Test @DisplayName("registerAuthorizationUser for a user with an email defined in the application-integration-test.properties should set OkrChampions to true") void registerAuthorizationUserShouldSetOkrChampionsToTrue() { // arrange - User user = User.Builder.builder() // - .withFirstname("Alice") // - .withLastname("Wunderland") // - .withEmail("wunderland@puzzle.ch") // user.champion.emails from application-integration-test.properties - .build(); - - userPersistenceService.getOrCreateUser(user); // updates input user with id from DB !!! + assertOkrChampionStatusInDb(EMAIL_WUNDERLAND, false); // pre-condition // act - AuthorizationUser processedUser = authorizationRegistrationService.updateOrAddAuthorizationUser(user); + // load user from db (by email) and set OkrChampion status based on property + // "okr.tenants.pitc.user.champion.emails" from application-integration-test.properties file + AuthorizationUser processedUser = authorizationRegistrationService + .updateOrAddAuthorizationUser(User.Builder.builder() // + .withFirstname("Alice") // + .withLastname("Wunderland") // + .withEmail(EMAIL_WUNDERLAND) // user.champion.emails from + // application-integration-test.properties + .build()); // assert assertTrue(processedUser.user().isOkrChampion()); - Optional userFromDB = userPersistenceService.findByEmail(user.getEmail()); - assertTrue(userFromDB.get().isOkrChampion()); + assertOkrChampionStatusInDb(processedUser.user().getEmail(), true); } @Test @@ -138,6 +160,7 @@ void registerAuthorizationUser_shouldSetFirstnameAndLastnameFromToken() { // assert Optional userFromDB = userPersistenceService.findByEmail(user.getEmail()); + assertTrue(userFromDB.isPresent()); assertEquals(userFromDB.get().getFirstname(), firstNameFromToken); assertEquals(userFromDB.get().getLastname(), lastNameFromToken); diff --git a/backend/src/test/java/ch/puzzle/okr/service/authorization/AuthorizationServiceTest.java b/backend/src/test/java/ch/puzzle/okr/service/authorization/AuthorizationServiceTest.java index eb4e88823f..957b27540f 100644 --- a/backend/src/test/java/ch/puzzle/okr/service/authorization/AuthorizationServiceTest.java +++ b/backend/src/test/java/ch/puzzle/okr/service/authorization/AuthorizationServiceTest.java @@ -1,6 +1,5 @@ package ch.puzzle.okr.service.authorization; -import ch.puzzle.okr.test.TestHelper; import ch.puzzle.okr.dto.ErrorDto; import ch.puzzle.okr.exception.OkrResponseStatusException; import ch.puzzle.okr.models.Objective; @@ -13,6 +12,7 @@ import ch.puzzle.okr.models.keyresult.KeyResultMetric; import ch.puzzle.okr.security.JwtHelper; import ch.puzzle.okr.service.persistence.ObjectivePersistenceService; +import ch.puzzle.okr.test.TestHelper; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; @@ -28,9 +28,9 @@ import java.util.List; import static ch.puzzle.okr.ErrorKey.*; -import static ch.puzzle.okr.test.TestHelper.*; import static ch.puzzle.okr.service.authorization.AuthorizationService.hasRoleWriteAndReadAll; import static ch.puzzle.okr.service.authorization.AuthorizationService.hasRoleWriteForTeam; +import static ch.puzzle.okr.test.TestHelper.*; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.*; import static org.mockito.ArgumentMatchers.any; diff --git a/backend/src/test/java/ch/puzzle/okr/service/authorization/TeamAuthorizationServiceTest.java b/backend/src/test/java/ch/puzzle/okr/service/authorization/TeamAuthorizationServiceTest.java index 09243f4aac..6fb9ec4f34 100644 --- a/backend/src/test/java/ch/puzzle/okr/service/authorization/TeamAuthorizationServiceTest.java +++ b/backend/src/test/java/ch/puzzle/okr/service/authorization/TeamAuthorizationServiceTest.java @@ -130,7 +130,7 @@ void getAllTeamsShouldReturnAllTeams(boolean isWriteable) { if (isWriteable) { when(authorizationService.updateOrAddAuthorizationUser()).thenReturn(okrChampionUser); } else { - when(authorizationService.updateOrAddAuthorizationUser()).thenReturn(userWithoutWriteAllRole()); + when(authorizationService.updateOrAddAuthorizationUser()).thenReturn(defaultAuthorizationUser()); } when(teamBusinessService.getAllTeams(any())).thenReturn(teamList); diff --git a/backend/src/test/java/ch/puzzle/okr/service/persistence/AuthorizationCriteriaParametersTest.java b/backend/src/test/java/ch/puzzle/okr/service/persistence/AuthorizationCriteriaParametersTest.java new file mode 100644 index 0000000000..7cff83afd8 --- /dev/null +++ b/backend/src/test/java/ch/puzzle/okr/service/persistence/AuthorizationCriteriaParametersTest.java @@ -0,0 +1,327 @@ +package ch.puzzle.okr.service.persistence; + +import ch.puzzle.okr.models.Objective; +import ch.puzzle.okr.models.User; +import jakarta.persistence.*; +import org.apache.commons.lang3.NotImplementedException; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.util.*; +import java.util.stream.Stream; + +import static ch.puzzle.okr.test.TestHelper.defaultAuthorizationUser; +import static ch.puzzle.okr.test.TestHelper.mockAuthorizationUser; +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class AuthorizationCriteriaParametersTest { + + @DisplayName("setParameters() should be successful with default authorization user") + @Test + void setParametersShouldBeSuccessfulWithDefaultAuthorizationUser() { + // arrange + var criteria = new AuthorizationCriteria(); + TypedQueryMock typedQueryMock = new TypedQueryMock<>(); + + // act + criteria.setParameters(typedQueryMock, defaultAuthorizationUser()); + + // assert + var expected = """ + teamDraftState, State=DRAFT + userTeamIds, ListN=[1] + publishedStates, ListN=[ONGOING, SUCCESSFUL, NOTSUCCESSFUL] + """; + + assertEquals(expected, typedQueryMock.getLog()); + } + + @DisplayName("setParameters() should be successful when user is okr champion") + @Test + void setParametersShouldBeSuccessfulWhenUserIsOkrChampion() { + // arrange + var user = User.Builder.builder() // + .withId(23L) // + .withFirstname("Hanna") // + .withLastname("muster") // + .withEmail("hanna.muster@example.com") // + .withOkrChampion(true) // + .build(); + var criteria = new AuthorizationCriteria(); + TypedQueryMock typedQueryMock = new TypedQueryMock<>(); + + // act + criteria.setParameters(typedQueryMock, mockAuthorizationUser(user)); + + // assert + var expected = """ + allDraftState, State=DRAFT + publishedStates, ListN=[ONGOING, SUCCESSFUL, NOTSUCCESSFUL] + """; + + assertEquals(expected, typedQueryMock.getLog()); + } + + @DisplayName("setParameters() should be successful when team ids or objective query are empty") + @ParameterizedTest + @MethodSource("provideListAndString") + void setParametersShouldBeSuccessfulWhenTeamIdsOrObjectiveQueryAreEmpty(List teamIds, String objectiveQuery) { + // arrange + var criteria = new AuthorizationCriteria(); + TypedQueryMock typedQueryMock = new TypedQueryMock<>(); + + // act + criteria.setParameters(typedQueryMock, teamIds, objectiveQuery, defaultAuthorizationUser()); + + // assert + var expected = """ + teamDraftState, State=DRAFT + userTeamIds, ListN=[1] + publishedStates, ListN=[ONGOING, SUCCESSFUL, NOTSUCCESSFUL] + """; + + assertEquals(expected, typedQueryMock.getLog()); + } + + private static Stream provideListAndString() { + return Stream.of( // + Arguments.of(List.of(), null), // + Arguments.of(List.of(), ""), // + Arguments.of(null, null), // + Arguments.of(null, "")); + } + + @DisplayName("setParameters() should be successful when team ids and objective query are not empty") + @Test + void setParametersShouldBeSuccessfulWhenTeamIdsAndObjectiveQueryAreNotEmpty() { + // arrange + TypedQueryMock typedQueryMock = new TypedQueryMock<>(); + var criteria = new AuthorizationCriteria(); + var anyTeamIds = List.of(99L); + var anyNonEmptyString = "OBJECTIVEQUERY"; + + // act + criteria.setParameters(typedQueryMock, anyTeamIds, anyNonEmptyString, defaultAuthorizationUser()); + + // assert + var expected = """ + teamIds, List12=[99] + objectiveQuery, String=OBJECTIVEQUERY + teamDraftState, State=DRAFT + userTeamIds, ListN=[1] + publishedStates, ListN=[ONGOING, SUCCESSFUL, NOTSUCCESSFUL] + """; + + assertEquals(expected, typedQueryMock.getLog()); + } + + // TypedQuery implementation for testing. The setParameterX() methods calls are logged in an internal StringBuilder + // which is return by getLog(). This log can be used for checking the internal state of the TypedQuery. All other + // methods are not implemented. + private static class TypedQueryMock implements TypedQuery { + + private final StringBuilder log = new StringBuilder(); + + public String getLog() { + return log.toString(); + } + + @Override + public TypedQuery setParameter(Parameter parameter, T t) { + log.append(parameter.getName()).append(", ") // + .append(t.getClass().getSimpleName()).append("=").append(t) // + .append("\n"); + return null; + } + + @Override + public TypedQuery setParameter(Parameter parameter, Calendar calendar, + TemporalType temporalType) { + log.append(parameter.getName()).append(", ") // + .append(calendar.getTime()).append(", ") // + .append(temporalType.name()) // + .append("\n"); + return null; + } + + @Override + public TypedQuery setParameter(Parameter parameter, Date date, TemporalType temporalType) { + log.append(parameter.getName()).append(", ") // + .append(date).append(", ") // + .append(temporalType.name()) // + .append("\n"); + return null; + } + + @Override + public TypedQuery setParameter(String s, Object o) { + log.append(s).append(", ") // + .append(o.getClass().getSimpleName()).append("=").append(o) // + .append("\n"); + return null; + } + + @Override + public TypedQuery setParameter(String s, Calendar calendar, TemporalType temporalType) { + log.append(s).append(", ") // + .append(calendar.getTime()).append(", ") // + .append(temporalType.name()) // + .append("\n"); + return null; + } + + @Override + public TypedQuery setParameter(String s, Date date, TemporalType temporalType) { + log.append(s).append(", ") // + .append(date).append(", ") // + .append(temporalType.name()) // + .append("\n"); + return null; + } + + @Override + public TypedQuery setParameter(int i, Object o) { + log.append(i).append(", ") // + .append(o.getClass().getSimpleName()).append("=").append(o) // + .append("\n"); + return null; + } + + @Override + public TypedQuery setParameter(int i, Calendar calendar, TemporalType temporalType) { + log.append(i).append(", ") // + .append(calendar.getTime()).append(", ") // + .append(temporalType.name()) // + .append("\n"); + return null; + } + + @Override + public TypedQuery setParameter(int i, Date date, TemporalType temporalType) { + log.append(i).append(", ") // + .append(date).append(", ") // + .append(temporalType.name()) // + .append("\n"); + return null; + } + + @Override + public List getResultList() { + throw new NotImplementedException(); + } + + @Override + public Objective getSingleResult() { + throw new NotImplementedException(); + } + + @Override + public int executeUpdate() { + throw new NotImplementedException(); + } + + @Override + public TypedQuery setMaxResults(int i) { + throw new NotImplementedException(); + } + + @Override + public int getMaxResults() { + throw new NotImplementedException(); + } + + @Override + public TypedQuery setFirstResult(int i) { + throw new NotImplementedException(); + } + + @Override + public int getFirstResult() { + throw new NotImplementedException(); + } + + @Override + public TypedQuery setHint(String s, Object o) { + throw new NotImplementedException(); + } + + @Override + public Map getHints() { + throw new NotImplementedException(); + } + + @Override + public Set> getParameters() { + throw new NotImplementedException(); + } + + @Override + public Parameter getParameter(String s) { + throw new NotImplementedException(); + } + + @Override + public Parameter getParameter(String s, Class aClass) { + throw new NotImplementedException(); + } + + @Override + public Parameter getParameter(int i) { + throw new NotImplementedException(); + } + + @Override + public Parameter getParameter(int i, Class aClass) { + throw new NotImplementedException(); + } + + @Override + public boolean isBound(Parameter parameter) { + throw new NotImplementedException(); + } + + @Override + public T getParameterValue(Parameter parameter) { + throw new NotImplementedException(); + } + + @Override + public Object getParameterValue(String s) { + throw new NotImplementedException(); + } + + @Override + public Object getParameterValue(int i) { + throw new NotImplementedException(); + } + + @Override + public TypedQuery setFlushMode(FlushModeType flushModeType) { + throw new NotImplementedException(); + } + + @Override + public FlushModeType getFlushMode() { + throw new NotImplementedException(); + } + + @Override + public TypedQuery setLockMode(LockModeType lockModeType) { + throw new NotImplementedException(); + } + + @Override + public LockModeType getLockMode() { + throw new NotImplementedException(); + } + + @Override + public T unwrap(Class aClass) { + throw new NotImplementedException(); + } + } + +} diff --git a/backend/src/test/java/ch/puzzle/okr/service/persistence/AuthorizationCriteriaTest.java b/backend/src/test/java/ch/puzzle/okr/service/persistence/AuthorizationCriteriaTest.java new file mode 100644 index 0000000000..bdb382edaf --- /dev/null +++ b/backend/src/test/java/ch/puzzle/okr/service/persistence/AuthorizationCriteriaTest.java @@ -0,0 +1,104 @@ +package ch.puzzle.okr.service.persistence; + +import ch.puzzle.okr.models.Objective; +import ch.puzzle.okr.models.User; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.util.List; +import java.util.stream.Stream; + +import static ch.puzzle.okr.test.TestHelper.defaultAuthorizationUser; +import static ch.puzzle.okr.test.TestHelper.mockAuthorizationUser; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; + +public class AuthorizationCriteriaTest { + + @DisplayName("appendObjective() should be successful with default authorization user") + @Test + void appendObjectiveShouldBeSuccessfulWithDefaultAuthorizationUser() { + // arrange + var criteria = new AuthorizationCriteria(); + + // act + var current = criteria.appendObjective(defaultAuthorizationUser()); + + // assert + var expected = " and ((o.state=:teamDraftState and o.team.id IN (:userTeamIds)) or o.state IN (:publishedStates))"; + assertEquals(expected, current); + } + + @DisplayName("appendObjective() should be successful when user is okrChampion") + @Test + void appendObjectiveShouldBeSuccessfulWhenUserIsOkrChampion() { + // arrange + var user = User.Builder.builder() // + .withId(23L) // + .withFirstname("Hanna") // + .withLastname("muster") // + .withEmail("hanna.muster@example.com") // + .withOkrChampion(true) // + .build(); + var criteria = new AuthorizationCriteria(); + + // act + var current = criteria.appendObjective(mockAuthorizationUser(user)); + + // assert + var expected = " and (o.state=:allDraftState or o.state IN (:publishedStates))"; + assertEquals(expected, current); + } + + @DisplayName("appendOverview() should be successful when team ids or objective query are empty") + @ParameterizedTest + @MethodSource("provideListAndString") + void appendOverviewShouldBeSuccessfulWhenTeamIdsOrObjectiveQueryAreEmpty(List teamIds, + String objectiveQuery) { + // arrange + var criteria = new AuthorizationCriteria(); + + // act + var current = criteria.appendOverview(teamIds, objectiveQuery, defaultAuthorizationUser()); + + // assert + var expected = "\n and ((o.objectiveState=:teamDraftState and o.overviewId.teamId IN (:userTeamIds)) or o.objectiveState IN (:publishedStates) or o.overviewId.objectiveId = -1)"; + assertEquals(expected, current); + } + + private static Stream provideListAndString() { + return Stream.of( // + Arguments.of(List.of(), null), // + Arguments.of(List.of(), ""), // + Arguments.of(null, null), // + Arguments.of(null, "")); + } + + @DisplayName("appendOverview() should be successful when team ids and objective query are not empty") + @Test + void appendOverviewShouldBeSuccessfulWhenTeamIdsAndObjectiveQueryAreNotEmpty() { + // arrange + var criteria = new AuthorizationCriteria(); + var anyTeamIds = List.of(99L); + var anyNonEmptyString = "OBJECTIVEQUERY"; + var startingNewLine = "\n"; + var singleSpace = " "; + + // act + var current = criteria.appendOverview(anyTeamIds, anyNonEmptyString, defaultAuthorizationUser()); + + // assert + var expected = startingNewLine + singleSpace + + """ + and o.overviewId.teamId in (:teamIds) + and lower(coalesce(o.objectiveTitle, '')) like lower(concat('%',:objectiveQuery,'%')) + and ((o.objectiveState=:teamDraftState and o.overviewId.teamId IN (:userTeamIds)) or o.objectiveState IN (:publishedStates) or o.overviewId.objectiveId = -1)"""; + + assertEquals(expected, current); + assertFalse(current.contains(anyNonEmptyString)); + } + +} diff --git a/backend/src/test/java/ch/puzzle/okr/service/persistence/CheckInPersistenceServiceIT.java b/backend/src/test/java/ch/puzzle/okr/service/persistence/CheckInPersistenceServiceIT.java index 419971e392..0837b5f996 100644 --- a/backend/src/test/java/ch/puzzle/okr/service/persistence/CheckInPersistenceServiceIT.java +++ b/backend/src/test/java/ch/puzzle/okr/service/persistence/CheckInPersistenceServiceIT.java @@ -1,52 +1,32 @@ package ch.puzzle.okr.service.persistence; -import ch.puzzle.okr.test.TestHelper; -import ch.puzzle.okr.dto.ErrorDto; -import ch.puzzle.okr.exception.OkrResponseStatusException; -import ch.puzzle.okr.models.Objective; -import ch.puzzle.okr.models.User; import ch.puzzle.okr.models.checkin.CheckIn; -import ch.puzzle.okr.models.checkin.CheckInMetric; -import ch.puzzle.okr.models.keyresult.KeyResultMetric; import ch.puzzle.okr.multitenancy.TenantContext; import ch.puzzle.okr.test.SpringIntegrationTest; +import ch.puzzle.okr.test.TestHelper; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.web.server.ResponseStatusException; -import java.time.LocalDateTime; import java.util.List; import java.util.Objects; -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.*; -import static org.springframework.http.HttpStatus.UNPROCESSABLE_ENTITY; +import static ch.puzzle.okr.Constants.CHECK_IN; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.greaterThanOrEqualTo; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; @SpringIntegrationTest class CheckInPersistenceServiceIT { - CheckIn createdCheckIn; + + private static final long KEY_RESULT_ID = 7L; @Autowired private CheckInPersistenceService checkInPersistenceService; - private static CheckIn createCheckIn(Long id) { - return createCheckIn(id, 1); - } - - private static final String UPDATED_CHECKIN = "Updated CheckIn"; - - private static CheckIn createCheckIn(Long id, int version) { - return CheckInMetric.Builder.builder().withValue(30D).withId(id).withVersion(version) - .withCreatedBy(User.Builder.builder().withId(1L).withFirstname("Frank").build()) - .withCreatedOn(LocalDateTime.MAX) - .withKeyResult(KeyResultMetric.Builder.builder().withBaseline(1.0).withStretchGoal(13.0).withId(8L) - .withObjective(Objective.Builder.builder().withId(1L).build()).build()) - .withChangeInfo("ChangeInfo").withInitiatives("Initiatives").withModifiedOn(LocalDateTime.MAX) - .withConfidence(5).build(); - } - @BeforeEach void setUp() { TenantContext.setCurrentTenant(TestHelper.SCHEMA_PITC); @@ -54,97 +34,53 @@ void setUp() { @AfterEach void tearDown() { - try { - if (createdCheckIn != null) { - checkInPersistenceService.findById(createdCheckIn.getId()); - checkInPersistenceService.deleteById(createdCheckIn.getId()); - } - } catch (ResponseStatusException ex) { - // created CheckIn already deleted - } finally { - createdCheckIn = null; - } TenantContext.setCurrentTenant(null); } + // uses data from V100_0_0__TestData.sql + @DisplayName("getCheckInsByKeyResultIdOrderByCheckInDate() should get checkIns by keyResultId and order them by date desc") @Test - void saveCheckInShouldSaveNewCheckIn() { - CheckIn checkIn = createCheckIn(null); - - createdCheckIn = checkInPersistenceService.save(checkIn); - - assertNotNull(createdCheckIn.getId()); - assertEquals(checkIn.getModifiedOn(), createdCheckIn.getModifiedOn()); - assertEquals(((CheckInMetric) checkIn).getValue(), ((CheckInMetric) createdCheckIn).getValue()); - assertEquals(checkIn.getCreatedBy(), createdCheckIn.getCreatedBy()); - assertEquals(checkIn.getCreatedOn(), createdCheckIn.getCreatedOn()); - assertEquals(checkIn.getInitiatives(), createdCheckIn.getInitiatives()); - assertEquals(checkIn.getChangeInfo(), createdCheckIn.getChangeInfo()); + void getCheckInsByKeyResultIdOrderByCheckInDateShouldGetCheckInsByKeyResultIdAndOrderThemByDateDesc() { + // act + List checkIns = checkInPersistenceService + .getCheckInsByKeyResultIdOrderByCheckInDateDesc(KEY_RESULT_ID); + + // assert + assertThat(2, greaterThanOrEqualTo(checkIns.size())); + CheckIn firstCheckIn = checkIns.get(0); + CheckIn lastCheckIn = checkIns.get(checkIns.size() - 1); + assertFirstIsCreatedAfterSecond(firstCheckIn, lastCheckIn); } - @Test - void updateKeyResultShouldUpdateKeyResult() { - createdCheckIn = checkInPersistenceService.save(createCheckIn(null)); - CheckIn updateCheckIn = createCheckIn(createdCheckIn.getId(), createdCheckIn.getVersion()); - updateCheckIn.setChangeInfo(UPDATED_CHECKIN); - - CheckIn updatedCheckIn = checkInPersistenceService.save(updateCheckIn); - - assertEquals(createdCheckIn.getId(), updatedCheckIn.getId()); - assertEquals(createdCheckIn.getVersion() + 1, updatedCheckIn.getVersion()); - assertEquals(UPDATED_CHECKIN, updatedCheckIn.getChangeInfo()); + private void assertFirstIsCreatedAfterSecond(CheckIn first, CheckIn second) { + assertTrue(first.getCreatedOn().isAfter(second.getCreatedOn())); } + // uses data from V100_0_0__TestData.sql + @DisplayName("getLastCheckInOfKeyResult() should get last checkIn of keyResult") @Test - void updateKeyResultShouldThrowExceptionWhenAlreadyUpdated() { - createdCheckIn = checkInPersistenceService.save(createCheckIn(null)); - CheckIn updateCheckIn = createCheckIn(createdCheckIn.getId(), 0); - updateCheckIn.setChangeInfo(UPDATED_CHECKIN); + void getLastCheckInOfKeyResultShouldGetLastCheckInOfKeyResult() { + // act + var lastCheckIn = checkInPersistenceService.getLastCheckInOfKeyResult(KEY_RESULT_ID); - OkrResponseStatusException exception = assertThrows(OkrResponseStatusException.class, - () -> checkInPersistenceService.save(updateCheckIn)); - - List expectedErrors = List.of(new ErrorDto("DATA_HAS_BEEN_UPDATED", List.of("Check-in"))); - - assertEquals(UNPROCESSABLE_ENTITY, exception.getStatusCode()); - assertThat(expectedErrors).hasSameElementsAs(exception.getErrors()); - assertTrue(TestHelper.getAllErrorKeys(expectedErrors).contains(exception.getReason())); + // assert + var allCheckins = checkInPersistenceService.getCheckInsByKeyResultIdOrderByCheckInDateDesc(KEY_RESULT_ID); + assertLastIsCreatedAfterAllOtherCheckIns(lastCheckIn, allCheckins); } - @Test - void getAllCheckInShouldReturnListOfAllCheckIns() { - List checkIns = checkInPersistenceService.findAll(); - - assertEquals(19, checkIns.size()); - } - - @Test - void getCheckInByIdShouldReturnCheckInProperly() { - CheckIn checkIn = checkInPersistenceService.findById(20L); - - assertEquals(20L, checkIn.getId()); - assertEquals(0.5, ((CheckInMetric) checkIn).getValue(), 0.01); - assertEquals( - "Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore " - + "magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores ", - checkIn.getChangeInfo()); + private void assertLastIsCreatedAfterAllOtherCheckIns(CheckIn last, List allCheckIns) { + for (CheckIn checkInLoop : allCheckIns) { + if (!Objects.equals(checkInLoop.getId(), last.getId())) { + assertTrue(last.getCreatedOn().isAfter(checkInLoop.getCreatedOn())); + } + } } + @DisplayName("getModelName() should return checkIn") @Test - void shouldGetCheckInsByKeyResultIdAndOrderThemByDateDesc() { - List checkIns = checkInPersistenceService.getCheckInsByKeyResultIdOrderByCheckInDateDesc(7L); - assertTrue(checkIns.get(0).getCreatedOn().isAfter(checkIns.get(checkIns.size() - 1).getCreatedOn())); + void getModelNameShouldReturnCheckIn() { + assertEquals(CHECK_IN, checkInPersistenceService.getModelName()); } - @Test - void shouldGetLastCheckInOfKeyResult() { - CheckIn checkIn = checkInPersistenceService.getLastCheckInOfKeyResult(7L); - List checkInList = checkInPersistenceService.getCheckInsByKeyResultIdOrderByCheckInDateDesc(7L); - for (CheckIn checkInLoop : checkInList) { - if (!Objects.equals(checkInLoop.getId(), checkIn.getId())) { - assertTrue(checkIn.getCreatedOn().isAfter(checkInLoop.getCreatedOn())); - } - } - } } diff --git a/backend/src/test/java/ch/puzzle/okr/service/persistence/ObjectivePersistenceServiceIT.java b/backend/src/test/java/ch/puzzle/okr/service/persistence/ObjectivePersistenceServiceIT.java index ccc6c3a8a0..9708e9d779 100644 --- a/backend/src/test/java/ch/puzzle/okr/service/persistence/ObjectivePersistenceServiceIT.java +++ b/backend/src/test/java/ch/puzzle/okr/service/persistence/ObjectivePersistenceServiceIT.java @@ -2,56 +2,68 @@ import ch.puzzle.okr.dto.ErrorDto; import ch.puzzle.okr.exception.OkrResponseStatusException; -import ch.puzzle.okr.models.*; +import ch.puzzle.okr.models.Objective; +import ch.puzzle.okr.models.Quarter; +import ch.puzzle.okr.models.Team; import ch.puzzle.okr.models.authorization.AuthorizationUser; import ch.puzzle.okr.multitenancy.TenantContext; import ch.puzzle.okr.test.SpringIntegrationTest; import ch.puzzle.okr.test.TestHelper; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.web.server.ResponseStatusException; +import org.springframework.http.HttpStatus; -import java.time.LocalDateTime; import java.util.List; +import java.util.stream.Stream; -import static ch.puzzle.okr.test.TestConstants.GJ_FOR_TESTS_QUARTER_ID; +import static ch.puzzle.okr.exception.OkrResponseStatusException.of; import static ch.puzzle.okr.test.TestHelper.defaultAuthorizationUser; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.*; -import static org.springframework.http.HttpStatus.*; +import static org.junit.jupiter.params.provider.Arguments.arguments; +import static org.springframework.http.HttpStatus.BAD_REQUEST; +import static org.springframework.http.HttpStatus.UNAUTHORIZED; +// tests are using data from V100_0_0__TestData.sql @SpringIntegrationTest class ObjectivePersistenceServiceIT { - private static final String REASON = "not authorized to read objective"; - private static final OkrResponseStatusException exception = OkrResponseStatusException.of(REASON); - private static final String HIGHER_CUSTOMER_HAPPINESS = "Wir wollen die Kundenzufriedenheit steigern"; - private static final String MODEL_WITH_ID_NOT_FOUND = "MODEL_WITH_ID_NOT_FOUND"; + private static final long INVALID_OBJECTIVE_ID = 321L; + private static final long INVALID_KEY_RESULT_ID = 321L; + private static final long INVALID_CHECK_IN_ID = 321L; + private static final long INVALID_TEAM_ID = 321L; + private static final long INVALID_QUARTER_ID = 12L; + + private static final long ID_OF_OBJECTIVE_3 = 3L; + private static final long ID_OF_OBJECTIVE_8 = 8L; + private static final long ID_OF_OBJECTIVE_9 = 9L; + private static final long ID_OF_OBJECTIVE_10 = 10L; + + private static final String TITLE_OF_OBJECTIVE_3 = "Wir wollen die Kundenzufriedenheit steigern"; + private static final String TITLE_OF_OBJECTIVE_8 = "consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua"; + private static final String TITLE_OF_OBJECTIVE_9 = "At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet."; + private static final String TITLE_OF_OBJECTIVE_10 = "should not appear on staging, no sea takimata sanctus est Lorem ipsum dolor sit amet."; + + private static final long ID_OF_KEY_RESULT_5 = 5L; + private static final long ID_OF_CHECK_IN_7 = 7L; + private static final long ID_OF_TEAM_6 = 6L; + + private static final String REASON_UNAUTHORIZED = "not authorized to read objective"; + private static final OkrResponseStatusException NO_RESULT_EXCEPTION = of(REASON_UNAUTHORIZED); + private static final String OBJECTIVE = "Objective"; private static final String ATTRIBUTE_NULL = "ATTRIBUTE_NULL"; + private static final long CURRENT_QUARTER_ID = 2L; + private final AuthorizationUser authorizationUser = defaultAuthorizationUser(); - private Objective createdObjective; @Autowired private ObjectivePersistenceService objectivePersistenceService; - @Autowired - private TeamPersistenceService teamPersistenceService; - @Autowired - private QuarterPersistenceService quarterPersistenceService; - - private static Objective createObjective(Long id) { - return createObjective(id, 1); - } - - private static Objective createObjective(Long id, int version) { - return Objective.Builder.builder().withId(id).withVersion(version).withTitle("title") - .withCreatedBy(User.Builder.builder().withId(1L).build()) - .withTeam(Team.Builder.builder().withId(5L).build()) - .withQuarter(Quarter.Builder.builder().withId(GJ_FOR_TESTS_QUARTER_ID).build()) - .withDescription("This is our description").withState(State.DRAFT).withCreatedOn(LocalDateTime.MAX) - .withModifiedOn(LocalDateTime.MAX).withModifiedBy(User.Builder.builder().withId(1L).build()).build(); - } @BeforeEach void setUp() { @@ -60,206 +72,209 @@ void setUp() { @AfterEach void tearDown() { - try { - if (createdObjective != null) { - objectivePersistenceService.findById(createdObjective.getId()); - objectivePersistenceService.deleteById(createdObjective.getId()); - } - } catch (ResponseStatusException ex) { - // created key result already deleted - } finally { - createdObjective = null; - } TenantContext.setCurrentTenant(null); } - @Test - void findAllShouldReturnListOfObjectives() { - List objectives = objectivePersistenceService.findAll(); - - assertEquals(7, objectives.size()); - } - + @DisplayName("findObjectiveById() should return objective properly") @Test void findObjectiveByIdShouldReturnObjectiveProperly() { - Objective objective = objectivePersistenceService.findObjectiveById(3L, authorizationUser, exception); + // act + var objective = objectivePersistenceService.findObjectiveById(ID_OF_OBJECTIVE_3, authorizationUser, + NO_RESULT_EXCEPTION); - assertEquals(3L, objective.getId()); - assertEquals(HIGHER_CUSTOMER_HAPPINESS, objective.getTitle()); + // assert + assertObjective(ID_OF_OBJECTIVE_3, TITLE_OF_OBJECTIVE_3, objective); } + @DisplayName("findObjectiveById() should throw exception when objective not found") @Test void findObjectiveByIdShouldThrowExceptionWhenObjectiveNotFound() { - ResponseStatusException findObjectiveException = assertThrows(OkrResponseStatusException.class, - () -> objectivePersistenceService.findObjectiveById(321L, authorizationUser, - ObjectivePersistenceServiceIT.exception)); + // act + var exception = assertThrows(OkrResponseStatusException.class, () -> objectivePersistenceService + .findObjectiveById(INVALID_OBJECTIVE_ID, authorizationUser, NO_RESULT_EXCEPTION)); - assertEquals(UNAUTHORIZED, findObjectiveException.getStatusCode()); - assertEquals(REASON, findObjectiveException.getReason()); + // assert + var expectedErrors = List.of(new ErrorDto(REASON_UNAUTHORIZED, List.of())); + assertResponseStatusException(UNAUTHORIZED, expectedErrors, exception); } + @DisplayName("findObjectiveById() should throw exception when objective id is null") @Test void findObjectiveByIdShouldThrowExceptionWhenObjectiveIdIsNull() { - OkrResponseStatusException findObjectiveException = assertThrows(OkrResponseStatusException.class, - () -> objectivePersistenceService.findObjectiveById(null, authorizationUser, - ObjectivePersistenceServiceIT.exception)); - - List expectedErrors = List.of(new ErrorDto(ATTRIBUTE_NULL, List.of("ID", OBJECTIVE))); + // act + var exception = assertThrows(OkrResponseStatusException.class, + () -> objectivePersistenceService.findObjectiveById(null, authorizationUser, NO_RESULT_EXCEPTION)); - assertEquals(BAD_REQUEST, findObjectiveException.getStatusCode()); - assertThat(expectedErrors).hasSameElementsAs(findObjectiveException.getErrors()); - assertTrue(TestHelper.getAllErrorKeys(expectedErrors).contains(findObjectiveException.getReason())); + // assert + var expectedErrors = List.of(new ErrorDto(ATTRIBUTE_NULL, List.of("ID", OBJECTIVE))); + assertResponseStatusException(BAD_REQUEST, expectedErrors, exception); } + @DisplayName("findObjectiveByKeyResultId() should return objective properly") @Test void findObjectiveByKeyResultIdShouldReturnObjectiveProperly() { - Objective objective = objectivePersistenceService.findObjectiveByKeyResultId(5L, authorizationUser, exception); + // act + var objective = objectivePersistenceService.findObjectiveByKeyResultId(ID_OF_KEY_RESULT_5, authorizationUser, + NO_RESULT_EXCEPTION); - assertEquals(3L, objective.getId()); - assertEquals(HIGHER_CUSTOMER_HAPPINESS, objective.getTitle()); + // assert + assertObjective(ID_OF_OBJECTIVE_3, TITLE_OF_OBJECTIVE_3, objective); } + @DisplayName("findObjectiveByKeyResultId() should throw exception when objective not found") @Test void findObjectiveByKeyResultIdShouldThrowExceptionWhenObjectiveNotFound() { - ResponseStatusException objectiveByKeyResultException = assertThrows(ResponseStatusException.class, - () -> objectivePersistenceService.findObjectiveByKeyResultId(321L, authorizationUser, - ObjectivePersistenceServiceIT.exception)); + // act + var exception = assertThrows(OkrResponseStatusException.class, () -> objectivePersistenceService + .findObjectiveByKeyResultId(INVALID_KEY_RESULT_ID, authorizationUser, NO_RESULT_EXCEPTION)); - assertEquals(UNAUTHORIZED, objectiveByKeyResultException.getStatusCode()); - assertEquals(REASON, objectiveByKeyResultException.getReason()); + // assert + var expectedErrors = List.of(new ErrorDto(REASON_UNAUTHORIZED, List.of())); + assertResponseStatusException(UNAUTHORIZED, expectedErrors, exception); } + @DisplayName("findObjectiveByKeyResultId() should throw exception when objective id is null") @Test void findObjectiveByKeyResultIdShouldThrowExceptionWhenObjectiveIdIsNull() { - OkrResponseStatusException objectiveByKeyResultException = assertThrows(OkrResponseStatusException.class, - () -> objectivePersistenceService.findObjectiveByKeyResultId(null, authorizationUser, - ObjectivePersistenceServiceIT.exception)); + // act + var exception = assertThrows(OkrResponseStatusException.class, () -> objectivePersistenceService + .findObjectiveByKeyResultId(null, authorizationUser, NO_RESULT_EXCEPTION)); - List expectedErrors = List.of(new ErrorDto(ATTRIBUTE_NULL, List.of("ID", OBJECTIVE))); - - assertEquals(BAD_REQUEST, objectiveByKeyResultException.getStatusCode()); - assertThat(expectedErrors).hasSameElementsAs(objectiveByKeyResultException.getErrors()); - assertTrue(TestHelper.getAllErrorKeys(expectedErrors).contains(objectiveByKeyResultException.getReason())); + // assert + var expectedErrors = List.of(new ErrorDto(ATTRIBUTE_NULL, List.of("ID", OBJECTIVE))); + assertResponseStatusException(BAD_REQUEST, expectedErrors, exception); } + @DisplayName("findObjectiveByCheckInId() should return objective properly") @Test void findObjectiveByCheckInIdShouldReturnObjectiveProperly() { - Objective objective = objectivePersistenceService.findObjectiveByCheckInId(7L, authorizationUser, exception); + // act + var objective = objectivePersistenceService.findObjectiveByCheckInId(ID_OF_CHECK_IN_7, authorizationUser, + NO_RESULT_EXCEPTION); - assertEquals(3L, objective.getId()); - assertEquals(HIGHER_CUSTOMER_HAPPINESS, objective.getTitle()); + // assert + assertObjective(ID_OF_OBJECTIVE_3, TITLE_OF_OBJECTIVE_3, objective); } + @DisplayName("findObjectiveByCheckInId() should throw exception when objective not found") @Test void findObjectiveByCheckInIdShouldThrowExceptionWhenObjectiveNotFound() { - ResponseStatusException objectiveByCheckInException = assertThrows(ResponseStatusException.class, - () -> objectivePersistenceService.findObjectiveByCheckInId(321L, authorizationUser, - ObjectivePersistenceServiceIT.exception)); + // act + var exception = assertThrows(OkrResponseStatusException.class, () -> objectivePersistenceService + .findObjectiveByCheckInId(INVALID_CHECK_IN_ID, authorizationUser, NO_RESULT_EXCEPTION)); - assertEquals(UNAUTHORIZED, objectiveByCheckInException.getStatusCode()); - assertEquals(REASON, objectiveByCheckInException.getReason()); + // assert + var expectedErrors = List.of(new ErrorDto(REASON_UNAUTHORIZED, List.of())); + assertResponseStatusException(UNAUTHORIZED, expectedErrors, exception); } + @DisplayName("findObjectiveByCheckInId() should throw exception when objective id is null") @Test void findObjectiveByCheckInIdShouldThrowExceptionWhenObjectiveIdIsNull() { - OkrResponseStatusException objectiveByCheckInException = assertThrows(OkrResponseStatusException.class, - () -> objectivePersistenceService.findObjectiveByCheckInId(null, authorizationUser, - ObjectivePersistenceServiceIT.exception)); - - List expectedErrors = List.of(new ErrorDto(ATTRIBUTE_NULL, List.of("ID", OBJECTIVE))); + // act + var exception = assertThrows(OkrResponseStatusException.class, () -> objectivePersistenceService + .findObjectiveByCheckInId(null, authorizationUser, ObjectivePersistenceServiceIT.NO_RESULT_EXCEPTION)); - assertEquals(BAD_REQUEST, objectiveByCheckInException.getStatusCode()); - assertThat(expectedErrors).hasSameElementsAs(objectiveByCheckInException.getErrors()); - assertTrue(TestHelper.getAllErrorKeys(expectedErrors).contains(objectiveByCheckInException.getReason())); + // assert + var expectedErrors = List.of(new ErrorDto(ATTRIBUTE_NULL, List.of("ID", OBJECTIVE))); + assertResponseStatusException(BAD_REQUEST, expectedErrors, exception); } + @DisplayName("findObjectiveByTeamId() should return objectives of team properly") @Test - void saveObjectiveShouldSaveNewObjective() { - Objective objective = createObjective(null); - - createdObjective = objectivePersistenceService.save(objective); - - assertNotNull(createdObjective.getId()); - assertEquals(objective.getDescription(), createdObjective.getDescription()); - assertEquals(objective.getDescription(), createdObjective.getDescription()); - assertEquals(objective.getModifiedOn(), createdObjective.getModifiedOn()); + void findObjectiveByTeamIdShouldReturnObjectivesOfTeamProperly() { + // act + var objectives = objectivePersistenceService.findObjectiveByTeamId(ID_OF_TEAM_6); + + // assert + assertEquals(3, objectives.size()); + assertObjective(ID_OF_OBJECTIVE_8, TITLE_OF_OBJECTIVE_8, objectives.get(0)); + assertObjective(ID_OF_OBJECTIVE_9, TITLE_OF_OBJECTIVE_9, objectives.get(1)); + assertObjective(ID_OF_OBJECTIVE_10, TITLE_OF_OBJECTIVE_10, objectives.get(2)); } + @DisplayName("findObjectiveByTeamId() should return empty list when objective not found") @Test - void updateObjectiveShouldUpdateObjective() { - createdObjective = objectivePersistenceService.save(createObjective(null)); - Objective updateObjective = createObjective(createdObjective.getId(), createdObjective.getVersion()); - updateObjective.setState(State.ONGOING); - - Objective updatedObjective = objectivePersistenceService.save(updateObjective); + void findObjectiveByTeamIdShouldReturnEmptyListWhenObjectiveNotFound() { + // act + var objectives = objectivePersistenceService.findObjectiveByTeamId(INVALID_TEAM_ID); - assertEquals(createdObjective.getId(), updatedObjective.getId()); - assertEquals(State.ONGOING, updatedObjective.getState()); + // assert + assertTrue(objectives.isEmpty()); } + @DisplayName("findObjectiveByTeamId() should return empty list when objective id is null") @Test - void updateObjectiveShouldThrowExceptionWhenAlreadyUpdated() { - createdObjective = objectivePersistenceService.save(createObjective(null)); - Objective updateObjective = createObjective(createdObjective.getId(), 0); - updateObjective.setState(State.ONGOING); - - OkrResponseStatusException objectiveSaveException = assertThrows(OkrResponseStatusException.class, - () -> objectivePersistenceService.save(updateObjective)); - List expectedErrors = List.of(new ErrorDto("DATA_HAS_BEEN_UPDATED", List.of(OBJECTIVE))); - - assertEquals(UNPROCESSABLE_ENTITY, objectiveSaveException.getStatusCode()); - assertThat(expectedErrors).hasSameElementsAs(objectiveSaveException.getErrors()); - assertTrue(TestHelper.getAllErrorKeys(expectedErrors).contains(objectiveSaveException.getReason())); - } - - @Test - void deleteObjectiveShouldThrowExceptionWhenKeyResultNotFound() { - Objective objective = createObjective(321L); - createdObjective = objectivePersistenceService.save(objective); - objectivePersistenceService.deleteById(createdObjective.getId()); - - Long objectiveId = createdObjective.getId(); - OkrResponseStatusException findObjectiveException = assertThrows(OkrResponseStatusException.class, - () -> objectivePersistenceService.findById(objectiveId)); + void findObjectiveByTeamIdShouldReturnEmptyListWhenObjectiveIdIsNull() { + // act + var objectives = objectivePersistenceService.findObjectiveByTeamId(null); - List expectedErrors = List.of(new ErrorDto(MODEL_WITH_ID_NOT_FOUND, List.of(OBJECTIVE, "200"))); - - assertEquals(NOT_FOUND, findObjectiveException.getStatusCode()); - assertThat(expectedErrors).hasSameElementsAs(findObjectiveException.getErrors()); - assertTrue(TestHelper.getAllErrorKeys(expectedErrors).contains(findObjectiveException.getReason())); + // assert + assertTrue(objectives.isEmpty()); } + @DisplayName("countByTeamAndQuarter() should return number of objectives for current quarter") @Test - void countByTeamAndQuarterShouldThrowErrorIfQuarterDoesNotExist() { - Team teamId5 = teamPersistenceService.findById(5L); - OkrResponseStatusException countByTeamException = assertThrows(OkrResponseStatusException.class, - () -> objectivePersistenceService.countByTeamAndQuarter(teamId5, - quarterPersistenceService.findById(12L))); - - List expectedErrors = List.of(new ErrorDto(MODEL_WITH_ID_NOT_FOUND, List.of("Quarter", "12"))); + void countByTeamAndQuarterShouldReturnNumberOfObjectivesForCurrentQuarter() { + // arrange: there are 3 objectives for the current quarter (id 2) for team with id 6 + var team = Team.Builder.builder().withId(ID_OF_TEAM_6).build(); + var quarter = Quarter.Builder.builder().withId(CURRENT_QUARTER_ID).build(); - assertEquals(NOT_FOUND, countByTeamException.getStatusCode()); - assertThat(expectedErrors).hasSameElementsAs(countByTeamException.getErrors()); - assertTrue(TestHelper.getAllErrorKeys(expectedErrors).contains(countByTeamException.getReason())); + // act + var count = objectivePersistenceService.countByTeamAndQuarter(team, quarter); - Quarter quarterId2 = quarterPersistenceService.findById(2L); - OkrResponseStatusException exceptionTeam = assertThrows(OkrResponseStatusException.class, - () -> objectivePersistenceService.countByTeamAndQuarter(teamPersistenceService.findById(500L), - quarterId2)); + // assert + assertEquals(3, count); + } - List expectedErrorsTeam = List.of(new ErrorDto(MODEL_WITH_ID_NOT_FOUND, List.of("Team", "500"))); + @DisplayName("countByTeamAndQuarter() should return zero when team or quarter is not valid or null") + @ParameterizedTest + @MethodSource("invalidTeamsAndQuarters") + void countByTeamAndQuarterShouldReturnZeroWhenTeamOrQuarterIsNotValidOrNull(Team team, Quarter quarter) { + // act + var count = objectivePersistenceService.countByTeamAndQuarter(team, quarter); - assertEquals(NOT_FOUND, exceptionTeam.getStatusCode()); - assertThat(expectedErrorsTeam).hasSameElementsAs(exceptionTeam.getErrors()); - assertTrue(TestHelper.getAllErrorKeys(expectedErrorsTeam).contains(exceptionTeam.getReason())); + // assert + assertEquals(0, count); + } + private static Stream invalidTeamsAndQuarters() { + var validTeam = Team.Builder.builder().withId(ID_OF_TEAM_6).build(); + var invalidTeam = Team.Builder.builder().withId(INVALID_TEAM_ID).build(); + var validQuarter = Quarter.Builder.builder().withId(CURRENT_QUARTER_ID).build(); + var invalidQuarter = Quarter.Builder.builder().withId(INVALID_QUARTER_ID).build(); + + return Stream.of( + // valid team + invalid quarter + arguments(validTeam, invalidQuarter), + // valid team + null quarter + arguments(validTeam, null), + // invalid team + valid quarter + arguments(invalidTeam, validQuarter), + // invalid team + null quarter + arguments(null, validQuarter), + // invalid team + invalid quarter + arguments(invalidTeam, invalidQuarter), + // null team + null quarter + arguments(null, null)); } + @DisplayName("getModelName() should return Objective") @Test - void countByTeamAndQuarterShouldReturnCountValue() { - Integer count = objectivePersistenceService.countByTeamAndQuarter(Team.Builder.builder().withId(5L).build(), - Quarter.Builder.builder().withId(2L).build()); + void getModelNameShouldReturnObjective() { + assertEquals(OBJECTIVE, objectivePersistenceService.getModelName()); + } + + private void assertResponseStatusException(HttpStatus expectedStatus, List expectedErrors, + OkrResponseStatusException currentException) { + assertEquals(expectedStatus, currentException.getStatusCode()); + assertThat(expectedErrors).hasSameElementsAs(currentException.getErrors()); + assertTrue(TestHelper.getAllErrorKeys(expectedErrors).contains(currentException.getReason())); + } - assertEquals(2, count); + private void assertObjective(Long expectedId, String expectedTitle, Objective currentObjective) { + assertEquals(expectedId, currentObjective.getId()); + assertEquals(expectedTitle, currentObjective.getTitle()); } + } diff --git a/backend/src/test/java/ch/puzzle/okr/service/persistence/PersistenceBaseTestIT.java b/backend/src/test/java/ch/puzzle/okr/service/persistence/PersistenceBaseTestIT.java new file mode 100644 index 0000000000..7f5a60a5e5 --- /dev/null +++ b/backend/src/test/java/ch/puzzle/okr/service/persistence/PersistenceBaseTestIT.java @@ -0,0 +1,182 @@ +package ch.puzzle.okr.service.persistence; + +import ch.puzzle.okr.dto.ErrorDto; +import ch.puzzle.okr.models.User; +import ch.puzzle.okr.multitenancy.TenantContext; +import ch.puzzle.okr.repository.UserRepository; +import ch.puzzle.okr.test.SpringIntegrationTest; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.dao.OptimisticLockingFailureException; +import org.springframework.web.server.ResponseStatusException; + +import java.util.List; + +import static ch.puzzle.okr.test.TestHelper.getAllErrorKeys; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import static org.springframework.http.HttpStatus.*; + +/** + * Testing the functionality of the abstract PersistenceBase and use UserRepository as example of a CrudRepository + * implementation. + *

+ * Tests depending on data from V100_0_0__TestData.sql + */ +@SpringIntegrationTest +public class PersistenceBaseTestIT { + + private User createdUser; + + private static final long NON_EXISTING_USER_ID = 321L; + private static final long USER_PACO_ID = 1L; + private static final User USER_WITHOUT_CONSTRAINTS = User.Builder.builder() // + .withFirstname("Hans") // + .withLastname("Muster") // + .withEmail("hans.muster@puzzle.ch") // + .build(); + + @Autowired + private PersistenceBase persistenceBase; + + @BeforeEach + void setUp() { + TenantContext.setCurrentTenant("pitc"); + } + + @AfterEach + void tearDown() { + if (createdUser != null) { + persistenceBase.deleteById(createdUser.getId()); + createdUser = null; + } + TenantContext.setCurrentTenant(null); + } + + @DisplayName("findById() should return single entity if entity with id exists") + @Test + void findByIdShouldReturnSingleEntityIfEntityWithIdExists() { + var foundUser = persistenceBase.findById(USER_PACO_ID); + + assertEquals(USER_PACO_ID, foundUser.getId()); + assertUser("Paco", "Eggimann", "peggimann@puzzle.ch", foundUser); + } + + @DisplayName("findById() should throw exception if entity with id does not exist") + @Test + void findByIdShouldThrowExceptionIfEntityWithIdDoesNotExist() { + var exception = assertThrows(ResponseStatusException.class, + () -> persistenceBase.findById(NON_EXISTING_USER_ID)); + + assertEquals(NOT_FOUND, exception.getStatusCode()); + assertErrorKey("MODEL_WITH_ID_NOT_FOUND", exception); + } + + @DisplayName("findById() should throw exception if id is null") + @Test + void findByIdShouldThrowExceptionIfIdIsNull() { + var exception = assertThrows(ResponseStatusException.class, () -> persistenceBase.findById(null)); + + assertEquals(BAD_REQUEST, exception.getStatusCode()); + assertErrorKey("ATTRIBUTE_NULL", exception); + } + + @DisplayName("findAll() should return all entities as list") + @Test + void findAllShouldReturnAllEntitiesAsList() throws ResponseStatusException { + var userList = persistenceBase.findAll(); + + assertThat(userList.size()).isGreaterThanOrEqualTo(7); + } + + @DisplayName("save() should add new entity") + @Test + void saveShouldAddNewEntity() throws ResponseStatusException { + createdUser = persistenceBase.save(USER_WITHOUT_CONSTRAINTS); + + assertNotNull(createdUser); + assertUser("Hans", "Muster", "hans.muster@puzzle.ch", createdUser); + } + + @DisplayName("save() should throw exception in the case of optimistic locking failure") + @Test + void saveShouldThrowExceptionInTheCaseOfOptimisticLockingFailure() throws ResponseStatusException { + // arrange + var testRepository = mock(UserRepository.class); + when(testRepository.save(any())).thenThrow(OptimisticLockingFailureException.class); + + var persistenceBaseForTest = new PersistenceBase<>(testRepository) { + @Override + public String getModelName() { + return "for_test"; + } + }; + + // act + assert + var exception = assertThrows(ResponseStatusException.class, () -> persistenceBaseForTest.save(createdUser)); + + // assert + assertEquals(UNPROCESSABLE_ENTITY, exception.getStatusCode()); + assertErrorKey("DATA_HAS_BEEN_UPDATED", exception); + } + + @DisplayName("save() existing entity with different data should update existing entity") + @Test + void saveExistingEntityWithDifferentDataShouldUpdateExistingEntity() throws ResponseStatusException { + // arrange + createdUser = persistenceBase.save(USER_WITHOUT_CONSTRAINTS); + var createdUserId = createdUser.getId(); + var foundUser = persistenceBase.findById(createdUserId); + + // pro-condition + assertEquals("Hans", createdUser.getFirstname()); + + // act + foundUser.setFirstname("Pekka"); + persistenceBase.save(foundUser); + foundUser = persistenceBase.findById(createdUserId); + + // assert + assertEquals(createdUserId, foundUser.getId()); + assertEquals("Pekka", foundUser.getFirstname()); + } + + @DisplayName("deleteById() should delete entity") + @Test + void deleteByIdShouldDeleteEntity() throws ResponseStatusException { + // arrange + createdUser = persistenceBase.save(USER_WITHOUT_CONSTRAINTS); + var createdUserId = createdUser.getId(); + assertNotNull(persistenceBase.findById(createdUserId)); + + // act + persistenceBase.deleteById(createdUserId); + + // assert + assertEntityNotFound(createdUserId); + } + + private static void assertUser(String expectedFirstName, String expectedLastName, String expectedEmail, + User currentUser) { + assertEquals(expectedFirstName, currentUser.getFirstname()); + assertEquals(expectedLastName, currentUser.getLastname()); + assertEquals(expectedEmail, currentUser.getEmail()); + } + + private void assertErrorKey(String errorKey, ResponseStatusException exception) { + var errorKeys = getAllErrorKeys(List.of(new ErrorDto(errorKey, List.of("User")))); + assertTrue(errorKeys.contains(exception.getReason())); + } + + private void assertEntityNotFound(long entityId) { + var exception = assertThrows(ResponseStatusException.class, () -> persistenceBase.findById(entityId)); + assertEquals(NOT_FOUND, exception.getStatusCode()); + assertErrorKey("MODEL_WITH_ID_NOT_FOUND", exception); + } +} \ No newline at end of file diff --git a/backend/src/test/java/ch/puzzle/okr/service/persistence/QuarterPersistenceServiceIT.java b/backend/src/test/java/ch/puzzle/okr/service/persistence/QuarterPersistenceServiceIT.java index 23a1ce5faf..1eee877cc1 100644 --- a/backend/src/test/java/ch/puzzle/okr/service/persistence/QuarterPersistenceServiceIT.java +++ b/backend/src/test/java/ch/puzzle/okr/service/persistence/QuarterPersistenceServiceIT.java @@ -1,7 +1,5 @@ package ch.puzzle.okr.service.persistence; -import ch.puzzle.okr.dto.ErrorDto; -import ch.puzzle.okr.exception.OkrResponseStatusException; import ch.puzzle.okr.models.Quarter; import ch.puzzle.okr.multitenancy.TenantContext; import ch.puzzle.okr.test.SpringIntegrationTest; @@ -16,12 +14,10 @@ import java.time.LocalDate; import java.util.List; +import static ch.puzzle.okr.Constants.QUARTER; import static ch.puzzle.okr.test.TestConstants.GJ_FOR_TESTS_QUARTER_ID; import static ch.puzzle.okr.test.TestConstants.GJ_FOR_TEST_QUARTER_LABEL; -import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.*; -import static org.springframework.http.HttpStatus.BAD_REQUEST; -import static org.springframework.http.HttpStatus.NOT_FOUND; @SpringIntegrationTest class QuarterPersistenceServiceIT { @@ -39,40 +35,6 @@ void tearDown() { TenantContext.setCurrentTenant(null); } - @Test - void shouldReturnSingleQuarterWhenFindingByValidId() { - Quarter returnedQuarter = quarterPersistenceService.findById(GJ_FOR_TESTS_QUARTER_ID); - - assertEquals(GJ_FOR_TESTS_QUARTER_ID, returnedQuarter.getId()); - assertEquals(GJ_FOR_TEST_QUARTER_LABEL, returnedQuarter.getLabel()); - assertEquals(LocalDate.of(2000, 7, 1), returnedQuarter.getStartDate()); - assertEquals(LocalDate.of(2000, 9, 30), returnedQuarter.getEndDate()); - } - - @Test - void shouldThrowExceptionWhenFindingQuarterNotFound() { - OkrResponseStatusException exception = assertThrows(OkrResponseStatusException.class, - () -> quarterPersistenceService.findById(321L)); - - List expectedErrors = List.of(new ErrorDto("MODEL_WITH_ID_NOT_FOUND", List.of("Quarter", "321"))); - - assertEquals(NOT_FOUND, exception.getStatusCode()); - assertThat(expectedErrors).hasSameElementsAs(exception.getErrors()); - assertTrue(TestHelper.getAllErrorKeys(expectedErrors).contains(exception.getReason())); - } - - @Test - void shouldThrowExceptionWhenFindingQuarterWithIdNull() { - OkrResponseStatusException exception = assertThrows(OkrResponseStatusException.class, - () -> quarterPersistenceService.findById(null)); - - List expectedErrors = List.of(new ErrorDto("ATTRIBUTE_NULL", List.of("ID", "Quarter"))); - - assertEquals(BAD_REQUEST, exception.getStatusCode()); - assertThat(expectedErrors).hasSameElementsAs(exception.getErrors()); - assertTrue(TestHelper.getAllErrorKeys(expectedErrors).contains(exception.getReason())); - } - @DisplayName("getMostCurrentQuarters() should return current quarter and future quarter and GJForTests quarter") @Test void getMostCurrentQuartersShouldReturnCurrentQuarterAndFutureQuarterAndGJForTestsQuarter() { @@ -141,4 +103,10 @@ void findByLabelShouldReturnNullWhenLabelIsNull() { // assert assertNull(returnedQuarter); } + + @DisplayName("getModelName() should return Quarter") + @Test + void getModelNameShouldReturnQuarter() { + assertEquals(QUARTER, quarterPersistenceService.getModelName()); + } } diff --git a/backend/src/test/java/ch/puzzle/okr/service/persistence/TeamPersistenceServiceIT.java b/backend/src/test/java/ch/puzzle/okr/service/persistence/TeamPersistenceServiceIT.java index f71c9daa79..f37812cb21 100644 --- a/backend/src/test/java/ch/puzzle/okr/service/persistence/TeamPersistenceServiceIT.java +++ b/backend/src/test/java/ch/puzzle/okr/service/persistence/TeamPersistenceServiceIT.java @@ -1,29 +1,22 @@ package ch.puzzle.okr.service.persistence; -import ch.puzzle.okr.test.TestHelper; -import ch.puzzle.okr.dto.ErrorDto; -import ch.puzzle.okr.exception.OkrResponseStatusException; import ch.puzzle.okr.models.Team; import ch.puzzle.okr.multitenancy.TenantContext; import ch.puzzle.okr.test.SpringIntegrationTest; -import org.junit.jupiter.api.AfterEach; +import ch.puzzle.okr.test.TestHelper; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.web.server.ResponseStatusException; import java.util.List; -import static ch.puzzle.okr.test.TestConstants.TEAM_PUZZLE; -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.*; -import static org.springframework.http.HttpStatus.*; +import static ch.puzzle.okr.Constants.TEAM; +import static org.junit.jupiter.api.Assertions.assertEquals; @SpringIntegrationTest class TeamPersistenceServiceIT { - private static final String NEW_TEAM = "New Team"; - private Team createdTeam; @Autowired private TeamPersistenceService teamPersistenceService; @@ -32,112 +25,20 @@ void setUp() { TenantContext.setCurrentTenant(TestHelper.SCHEMA_PITC); } - @AfterEach - void tearDown() { - try { - if (createdTeam != null) { - teamPersistenceService.findById(createdTeam.getId()); - teamPersistenceService.deleteById(createdTeam.getId()); - } - } catch (ResponseStatusException ex) { - // created team already deleted - } finally { - createdTeam = null; - } - TenantContext.setCurrentTenant(null); - } - - @Test - void getTeamByIdShouldReturnTeam() throws ResponseStatusException { - Team team = teamPersistenceService.findById(5L); - - assertEquals(5L, team.getId()); - assertEquals(TEAM_PUZZLE, team.getName()); - } - - @Test - void getTeamByIdShouldThrowExceptionWhenTeamNotFound() { - OkrResponseStatusException exception = assertThrows(OkrResponseStatusException.class, - () -> teamPersistenceService.findById(321L)); - - List expectedErrors = List.of(new ErrorDto("MODEL_WITH_ID_NOT_FOUND", List.of("Team", "321"))); - - assertEquals(NOT_FOUND, exception.getStatusCode()); - assertThat(expectedErrors).hasSameElementsAs(exception.getErrors()); - assertTrue(TestHelper.getAllErrorKeys(expectedErrors).contains(exception.getReason())); - } - - @Test - void getTeamByIdShouldThrowExceptionWhenTeamIdIsNull() { - OkrResponseStatusException exception = assertThrows(OkrResponseStatusException.class, - () -> teamPersistenceService.findById(null)); - - List expectedErrors = List.of(new ErrorDto("ATTRIBUTE_NULL", List.of("ID", "Team"))); - - assertEquals(BAD_REQUEST, exception.getStatusCode()); - assertThat(expectedErrors).hasSameElementsAs(exception.getErrors()); - assertTrue(TestHelper.getAllErrorKeys(expectedErrors).contains(exception.getReason())); - } - - @Test - void shouldSaveANewTeam() { - Team team = Team.Builder.builder().withName("TestTeam").build(); - - createdTeam = teamPersistenceService.save(team); - assertNotNull(createdTeam.getId()); - assertEquals("TestTeam", createdTeam.getName()); - } - + // uses data from V100_0_0__TestData.sql + @DisplayName("findTeamsByName() should return teams with matching name") @Test - void shouldUpdateTeamProperly() { - Team team = Team.Builder.builder().withName(NEW_TEAM).build(); - createdTeam = teamPersistenceService.save(team); - createdTeam.setName("Updated Team"); - - Team returnedTeam = teamPersistenceService.save(createdTeam); - - assertEquals(createdTeam.getId(), returnedTeam.getId()); - assertEquals("Updated Team", returnedTeam.getName()); - } - - @Test - void updateTeamShouldThrowExceptionWhenAlreadyUpdated() { - Team team = Team.Builder.builder().withVersion(1).withName(NEW_TEAM).build(); - createdTeam = teamPersistenceService.save(team); - Team changedTeam = Team.Builder.builder().withId(createdTeam.getId()).withVersion(0).withName("Changed Team") - .build(); - - OkrResponseStatusException exception = assertThrows(OkrResponseStatusException.class, - () -> teamPersistenceService.save(changedTeam)); - List expectedErrors = List.of(new ErrorDto("DATA_HAS_BEEN_UPDATED", List.of("Team"))); - - assertEquals(UNPROCESSABLE_ENTITY, exception.getStatusCode()); - assertThat(expectedErrors).hasSameElementsAs(exception.getErrors()); - assertTrue(TestHelper.getAllErrorKeys(expectedErrors).contains(exception.getReason())); - } - - @Test - void shouldDeleteTeam() { - Team team = Team.Builder.builder().withName(NEW_TEAM).build(); - createdTeam = teamPersistenceService.save(team); - teamPersistenceService.deleteById(createdTeam.getId()); - - OkrResponseStatusException exception = assertThrows(OkrResponseStatusException.class, - () -> teamPersistenceService.findById(createdTeam.getId())); - - List expectedErrors = List - .of(ErrorDto.of("MODEL_WITH_ID_NOT_FOUND", List.of("Team", createdTeam.getId()))); + void findTeamsByNameShouldReturnTeamsWithMatchingName() { + List teams = teamPersistenceService.findTeamsByName("LoremIpsum"); - assertEquals(NOT_FOUND, exception.getStatusCode()); - assertThat(expectedErrors).hasSameElementsAs(exception.getErrors()); - assertTrue(TestHelper.getAllErrorKeys(expectedErrors).contains(exception.getReason())); + assertEquals(1, teams.size()); + assertEquals(6, teams.get(0).getId()); + assertEquals("LoremIpsum", teams.get(0).getName()); } + @DisplayName("getModelName() should return Team") @Test - void shouldFindTeamsByName() { - Team team = Team.Builder.builder().withName(NEW_TEAM).build(); - createdTeam = teamPersistenceService.save(team); - List teams = teamPersistenceService.findTeamsByName(NEW_TEAM); - assertThat(teams).contains(createdTeam); + void getModelNameShouldReturnTeam() { + assertEquals(TEAM, teamPersistenceService.getModelName()); } } \ No newline at end of file diff --git a/backend/src/test/java/ch/puzzle/okr/service/persistence/UserPersistenceServiceIT.java b/backend/src/test/java/ch/puzzle/okr/service/persistence/UserPersistenceServiceIT.java index ca1dc10624..0104060b48 100644 --- a/backend/src/test/java/ch/puzzle/okr/service/persistence/UserPersistenceServiceIT.java +++ b/backend/src/test/java/ch/puzzle/okr/service/persistence/UserPersistenceServiceIT.java @@ -3,24 +3,23 @@ import ch.puzzle.okr.models.User; import ch.puzzle.okr.multitenancy.TenantContext; import ch.puzzle.okr.test.SpringIntegrationTest; -import org.assertj.core.api.Assertions; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.http.HttpStatus; -import org.springframework.web.server.ResponseStatusException; import java.util.List; +import java.util.Optional; +import static ch.puzzle.okr.Constants.USER; +import static ch.puzzle.okr.util.CollectionUtils.iterableToList; import static org.junit.jupiter.api.Assertions.*; @SpringIntegrationTest class UserPersistenceServiceIT { - private static final String EMAIL_ALICE = "wunderland@puzzle.ch"; - - User createdUser; + private User createdUser; @Autowired private UserPersistenceService userPersistenceService; @@ -39,63 +38,144 @@ void tearDown() { TenantContext.setCurrentTenant(null); } + @DisplayName("save() should save user with empty user team list") @Test - void shouldReturnAllUsersCorrect() throws ResponseStatusException { - List userList = userPersistenceService.findAll(); + void saveShouldSaveUserWithEmptyUserTeamList() { + // arrange + var newUser = User.Builder.builder() // + .withFirstname("Hans") // + .withLastname("Muster") // + .withEmail("muster@puzzle.ch") // + .withUserTeamList(List.of()).build(); + + // act + createdUser = userPersistenceService.save(newUser); + + // assert + assertNotNull(createdUser.getId()); + assertUser("Hans", "Muster", "muster@puzzle.ch", createdUser); + } - Assertions.assertThat(userList.size()).isGreaterThanOrEqualTo(7); + @DisplayName("save() should save user with null value for user team list") + @Test + void saveShouldSaveUserWithNullUserTeamList() { + // arrange + var newUser = User.Builder.builder() // + .withFirstname("Hans") // + .withLastname("Muster") // + .withEmail("muster@puzzle.ch") // + .withUserTeamList(null).build(); + + // act + createdUser = userPersistenceService.save(newUser); + + // assert + assertNotNull(createdUser.getId()); + assertUser("Hans", "Muster", "muster@puzzle.ch", createdUser); } + @DisplayName("saveAll() should save all users in the input list") @Test - void shouldReturnSingleUserWhenFindingOwnerByValidId() { - User returnedUser = userPersistenceService.findById(1L); + void saveAllShouldSaveAllUsersInTheInputList() { + // arrange + var newUser = User.Builder.builder() // + .withFirstname("Hans") // + .withLastname("Muster") // + .withEmail("muster@puzzle.ch") // + .build(); + + // act + var createdUsers = iterableToList(userPersistenceService.saveAll(List.of(newUser))); + + // assert + assertEquals(1, createdUsers.size()); + createdUser = createdUsers.get(0); - assertEquals(1L, returnedUser.getId()); - assertEquals("Paco", returnedUser.getFirstname()); - assertEquals("Eggimann", returnedUser.getLastname()); - assertEquals("peggimann@puzzle.ch", returnedUser.getEmail()); + assertNotNull(createdUser.getId()); + assertUser("Hans", "Muster", "muster@puzzle.ch", createdUser); } + @DisplayName("getOrCreateUser() should return single user when user found") @Test - void shouldThrowExceptionWhenFindingOwnerNotFound() { - ResponseStatusException exception = assertThrows(ResponseStatusException.class, - () -> userPersistenceService.findById(321L)); + void getOrCreateUserShouldReturnSingleUserWhenUserFound() { + // arrange + var existingUser = User.Builder.builder().withEmail("wunderland@puzzle.ch").build(); - assertEquals(HttpStatus.NOT_FOUND, exception.getStatusCode()); - assertEquals("MODEL_WITH_ID_NOT_FOUND", exception.getReason()); + // act + var returnedUser = userPersistenceService.getOrCreateUser(existingUser); + + // assert + assertUser(11L, "Alice", "Wunderland", "wunderland@puzzle.ch", returnedUser); } + @DisplayName("getOrCreateUser() should return saved user when user not found") @Test - void shouldThrowExceptionWhenFindingOwnerWithNullId() { - ResponseStatusException exception = assertThrows(ResponseStatusException.class, - () -> userPersistenceService.findById(null)); + void getOrCreateUserShouldReturnSavedUserWhenUserNotFound() { + // arrange + var newUser = User.Builder.builder() // + .withId(null) // + .withFirstname("firstname") // + .withLastname("lastname") // + .withEmail("lastname@puzzle.ch") // + .build(); + + // act + createdUser = userPersistenceService.getOrCreateUser(newUser); - assertEquals(HttpStatus.BAD_REQUEST, exception.getStatusCode()); - assertEquals("ATTRIBUTE_NULL", exception.getReason()); + // assert + assertNotNull(createdUser.getId()); + assertUser("firstname", "lastname", "lastname@puzzle.ch", createdUser); } + // uses data from V100_0_0__TestData.sql + @DisplayName("findByEmail() should return user if email is found") @Test - void getOrCreateUserShouldReturnSingleUserWhenUserFound() { - User existingUser = User.Builder.builder().withEmail(EMAIL_ALICE).build(); + void findByEmailShouldReturnUserIfEmailIsFound() { + Optional user = userPersistenceService.findByEmail("gl@gl.com"); - User returnedUser = userPersistenceService.getOrCreateUser(existingUser); + assertTrue(user.isPresent()); + assertEquals("Jaya", user.get().getFirstname()); + assertEquals("Norris", user.get().getLastname()); + } - assertEquals(11L, returnedUser.getId()); - assertEquals("Alice", returnedUser.getFirstname()); - assertEquals("Wunderland", returnedUser.getLastname()); - assertEquals("wunderland@puzzle.ch", returnedUser.getEmail()); + @DisplayName("findByEmail() should return empty optional if email is not found") + @Test + void findByEmailShouldReturnEmptyOptionalIfEmailIsNotFound() { + assertTrue(userPersistenceService.findByEmail("not_valid@gl.com").isEmpty()); } + @DisplayName("findByEmail() should return empty optional if email is null") @Test - void getOrCreateUserShouldReturnSavedUserWhenUserNotFound() { - User newUser = User.Builder.builder().withId(null).withFirstname("firstname").withLastname("lastname") - .withEmail("lastname@puzzle.ch").build(); + void findByEmailShouldReturnEmptyOptionalIfEmailIsNull() { + assertTrue(userPersistenceService.findByEmail(null).isEmpty()); + } - createdUser = userPersistenceService.getOrCreateUser(newUser); + // uses data from V100_0_0__TestData.sql + @DisplayName("findAllOkrChampions() should return all okr champions") + @Test + void findAllOkrChampionsShouldReturnAllOkrChampions() { + // act + var allOkrChampions = userPersistenceService.findAllOkrChampions(); - assertNotNull(createdUser.getId()); - assertEquals("firstname", createdUser.getFirstname()); - assertEquals("lastname", createdUser.getLastname()); - assertEquals("lastname@puzzle.ch", createdUser.getEmail()); + // assert + assertEquals(1, allOkrChampions.size()); + assertUser(61L, "Jaya", "Norris", "gl@gl.com", allOkrChampions.get(0)); + } + + @DisplayName("getModelName() should return user") + @Test + void getModelNameShouldReturnUser() { + assertEquals(USER, userPersistenceService.getModelName()); + } + + private void assertUser(Long id, String firstName, String lastName, String email, User currentUser) { + assertEquals(id, currentUser.getId()); + assertUser(firstName, lastName, email, currentUser); + } + + private void assertUser(String firstName, String lastName, String email, User currentUser) { + assertEquals(firstName, currentUser.getFirstname()); + assertEquals(lastName, currentUser.getLastname()); + assertEquals(email, currentUser.getEmail()); } } \ No newline at end of file diff --git a/backend/src/test/java/ch/puzzle/okr/test/TestHelper.java b/backend/src/test/java/ch/puzzle/okr/test/TestHelper.java index 75d53c1db4..86bb30b3f7 100644 --- a/backend/src/test/java/ch/puzzle/okr/test/TestHelper.java +++ b/backend/src/test/java/ch/puzzle/okr/test/TestHelper.java @@ -56,19 +56,22 @@ public static UserTeam defaultUserTeam(Long id, User user) { } public static AuthorizationUser defaultAuthorizationUser() { - return mockAuthorizationUser(1L, FIRSTNAME, LASTNAME, EMAIL); - } - - public static AuthorizationUser userWithoutWriteAllRole() { - return mockAuthorizationUser(1L, FIRSTNAME, LASTNAME, EMAIL); + return mockAuthorizationUser(1L, FIRSTNAME, LASTNAME, EMAIL, false); } public static AuthorizationUser mockAuthorizationUser(User user) { - return mockAuthorizationUser(user.getId(), user.getFirstname(), user.getLastname(), user.getEmail()); + return mockAuthorizationUser(user.getId(), user.getFirstname(), user.getLastname(), user.getEmail(), + user.isOkrChampion()); } - public static AuthorizationUser mockAuthorizationUser(Long id, String firstname, String lastname, String email) { - User user = User.Builder.builder().withId(id).withFirstname(firstname).withLastname(lastname).withEmail(email) + public static AuthorizationUser mockAuthorizationUser(Long id, String firstname, String lastname, String email, + boolean isOkrChampion) { + User user = User.Builder.builder() // + .withId(id) // + .withFirstname(firstname) // + .withLastname(lastname) // + .withEmail(email) // + .withOkrChampion(isOkrChampion) // .build(); user.setUserTeamList(List.of(defaultUserTeam(1L, user))); return new AuthorizationUser(user); From 3a458a5caf0d4e447aac093b80eefe9f6046cfcb Mon Sep 17 00:00:00 2001 From: Yanick Minder <79108296+kcinay055679@users.noreply.github.com> Date: Fri, 25 Oct 2024 14:07:27 +0200 Subject: [PATCH 22/33] Feature/624 Dialog creation refactoring using a service (#1045) * add jar debug dev tools only on profile * refactor dialog calls with config by introducing a service * convert all confirmation dialog texts to i18n texts * use refactored confirmation component * fix string and use interface for confirm dialog data * Update title of delete confirmation * clean up dialog service * refactor frontend tests related to dialogService * display correct dialog in objective * fix objective release dialog * refactor dialog responsiveness * clean up dialog service * remove backend/pom.xml from branch * update translations and fix release dialog * update dialog.service.ts --- frontend/cypress/e2e/objective.cy.ts | 5 +- .../action-plan/action-plan.component.spec.ts | 15 ++-- .../action-plan/action-plan.component.ts | 32 ++------ .../application-banner.component.ts | 2 - .../application-top-bar.component.spec.ts | 9 ++- .../check-in-history-dialog.component.spec.ts | 15 +++- .../check-in-history-dialog.component.ts | 14 ++-- .../keyresult-detail.component.ts | 75 +++---------------- .../keyresult-dialog.component.ts | 31 ++------ .../objective-detail.component.spec.ts | 3 +- .../objective-detail.component.ts | 17 +---- .../objective/objective.component.ts | 40 ++++------ .../components/team/team.component.spec.ts | 10 ++- .../src/app/components/team/team.component.ts | 41 +--------- .../src/app/services/dialog.service.spec.ts | 19 +++++ frontend/src/app/services/dialog.service.ts | 43 +++++++++++ frontend/src/app/shared/constantLibary.ts | 14 ---- .../okr-tangram/okr-tangram.component.ts | 3 +- .../confirm-dialog.component.spec.ts | 20 +++-- .../confirm-dialog.component.ts | 42 ++--------- .../objective-form.component.ts | 31 ++------ .../add-edit-team-dialog.component.spec.ts | 9 ++- .../member-detail.component.spec.ts | 12 +-- .../member-detail/member-detail.component.ts | 19 +++-- .../member-list-table.component.spec.ts | 10 +-- .../member-list-table.component.ts | 19 +++-- .../member-list/member-list.component.spec.ts | 45 +++++------ .../member-list/member-list.component.ts | 44 +++++------ .../team-management-banner.component.spec.ts | 17 +++-- .../team-management-banner.component.ts | 10 +-- frontend/src/assets/i18n/de.json | 32 ++++++++ .../src/style/custom_angular.components.scss | 13 ++-- frontend/src/style/styles.scss | 6 ++ 33 files changed, 306 insertions(+), 411 deletions(-) create mode 100644 frontend/src/app/services/dialog.service.spec.ts create mode 100644 frontend/src/app/services/dialog.service.ts diff --git a/frontend/cypress/e2e/objective.cy.ts b/frontend/cypress/e2e/objective.cy.ts index 94c861e59a..758d7687a3 100644 --- a/frontend/cypress/e2e/objective.cy.ts +++ b/frontend/cypress/e2e/objective.cy.ts @@ -28,8 +28,9 @@ describe('OKR Objective e2e tests', () => { .get('.objective-menu-option') .contains('Objective veröffentlichen') .click(); + cy.contains('Objective veröffentlichen'); + cy.contains('Soll dieses Objective veröffentlicht werden?'); cy.getByTestId('confirmYes').click(); - cy.getByTestId('objective') .filter(':contains(A objective in state draft)') .last() @@ -151,6 +152,8 @@ describe('OKR Objective e2e tests', () => { .click() .wait(500) .tabForward(); + cy.contains('Objective als Draft speichern'); + cy.contains('Soll dieses Objective als Draft gespeichert werden?'); cy.focused().click().wait(500); cy.getByTestId('objective') diff --git a/frontend/src/app/components/action-plan/action-plan.component.spec.ts b/frontend/src/app/components/action-plan/action-plan.component.spec.ts index 83deab4d69..2ce210d5f0 100644 --- a/frontend/src/app/components/action-plan/action-plan.component.spec.ts +++ b/frontend/src/app/components/action-plan/action-plan.component.spec.ts @@ -2,13 +2,15 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ActionPlanComponent } from './action-plan.component'; import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { MatDialogModule, MatDialogRef } from '@angular/material/dialog'; +import { MatDialogRef } from '@angular/material/dialog'; import { CdkDrag, CdkDropList } from '@angular/cdk/drag-drop'; import { ActionService } from '../../services/action.service'; import { action1, action2, action3, addedAction } from '../../shared/testData'; import { BehaviorSubject, of } from 'rxjs'; import { Action } from '../../shared/types/model/Action'; import { TranslateModule, TranslateService } from '@ngx-translate/core'; +import { DialogService } from '../../services/dialog.service'; +import { ConfirmDialogComponent } from '../../shared/dialog/confirm-dialog/confirm-dialog.component'; const actionServiceMock = { deleteAction: jest.fn(), @@ -22,9 +24,10 @@ describe('ActionPlanComponent', () => { beforeEach(() => { TestBed.configureTestingModule({ declarations: [ActionPlanComponent], - imports: [HttpClientTestingModule, MatDialogModule, CdkDropList, CdkDrag, TranslateModule.forRoot()], + imports: [HttpClientTestingModule, CdkDropList, CdkDrag, TranslateModule.forRoot()], providers: [ TranslateService, + DialogService, { provide: ActionService, useValue: actionServiceMock, @@ -48,7 +51,9 @@ describe('ActionPlanComponent', () => { it('should remove item from actionplan array', () => { component.control = new BehaviorSubject([action1, action2]); actionServiceMock.deleteAction.mockReturnValue(of(null)); - jest.spyOn(component.dialog, 'open').mockReturnValue({ afterClosed: () => of(true) } as MatDialogRef); + jest + .spyOn(component.dialogService, 'openConfirmDialog') + .mockReturnValue({ afterClosed: () => of(true) } as MatDialogRef); component.removeAction(0); @@ -59,7 +64,7 @@ describe('ActionPlanComponent', () => { }); it('should remove item from actionplan without opening dialog when action has no text and id', () => { - const dialogSpy = jest.spyOn(component.dialog, 'open'); + const dialogSpy = jest.spyOn(component.dialogService, 'open'); component.control = new BehaviorSubject([action3]); component.removeAction(0); @@ -70,7 +75,7 @@ describe('ActionPlanComponent', () => { }); it('should decrease index of active item when index is the same as the one of the removed item', () => { - jest.spyOn(component.dialog, 'open'); + jest.spyOn(component.dialogService, 'open'); component.control = new BehaviorSubject([action2, action3, action1]); component.activeItem = 2; diff --git a/frontend/src/app/components/action-plan/action-plan.component.ts b/frontend/src/app/components/action-plan/action-plan.component.ts index 8f0625ceeb..e08f5da787 100644 --- a/frontend/src/app/components/action-plan/action-plan.component.ts +++ b/frontend/src/app/components/action-plan/action-plan.component.ts @@ -2,11 +2,9 @@ import { Component, ElementRef, Input, QueryList, ViewChildren } from '@angular/ import { CdkDragDrop, moveItemInArray, transferArrayItem } from '@angular/cdk/drag-drop'; import { Action } from '../../shared/types/model/Action'; import { ActionService } from '../../services/action.service'; -import { MatDialog } from '@angular/material/dialog'; -import { ConfirmDialogComponent } from '../../shared/dialog/confirm-dialog/confirm-dialog.component'; import { BehaviorSubject } from 'rxjs'; -import { isMobileDevice, trackByFn } from '../../shared/common'; -import { CONFIRM_DIALOG_WIDTH } from '../../shared/constantLibary'; +import { trackByFn } from '../../shared/common'; +import { DialogService } from '../../services/dialog.service'; @Component({ selector: 'app-action-plan', @@ -23,7 +21,7 @@ export class ActionPlanComponent { constructor( private actionService: ActionService, - public dialog: MatDialog, + public dialogService: DialogService, ) {} handleKeyDown(event: Event, currentIndex: number) { @@ -99,28 +97,8 @@ export class ActionPlanComponent { this.activeItem--; } if (actions[index].action !== '' || actions[index].id) { - const dialogConfig = isMobileDevice() - ? { - maxWidth: '100vw', - maxHeight: '100vh', - height: '100vh', - width: CONFIRM_DIALOG_WIDTH, - } - : { - width: '45em', - height: 'auto', - }; - this.dialog - .open(ConfirmDialogComponent, { - data: { - title: 'Action', - isAction: true, - }, - width: dialogConfig.width, - height: dialogConfig.height, - maxHeight: dialogConfig.maxHeight, - maxWidth: dialogConfig.maxWidth, - }) + this.dialogService + .openConfirmDialog('DELETE.ACTION') .afterClosed() .subscribe((result) => { if (result) { diff --git a/frontend/src/app/components/application-banner/application-banner.component.ts b/frontend/src/app/components/application-banner/application-banner.component.ts index cce55ad140..d9a064f348 100644 --- a/frontend/src/app/components/application-banner/application-banner.component.ts +++ b/frontend/src/app/components/application-banner/application-banner.component.ts @@ -10,7 +10,6 @@ import { import { BehaviorSubject } from 'rxjs'; import { RefreshDataService } from '../../services/refresh-data.service'; import { DEFAULT_HEADER_HEIGHT_PX, PUZZLE_TOP_BAR_HEIGHT } from '../../shared/constantLibary'; -import { isMobileDevice } from '../../shared/common'; @Component({ selector: 'app-application-banner', @@ -25,7 +24,6 @@ export class ApplicationBannerComponent implements AfterViewInit, OnDestroy { resizeObserver: ResizeObserver; bannerHeight: number = DEFAULT_HEADER_HEIGHT_PX; lastScrollPosition: number = 0; - protected readonly isMobileDevice = isMobileDevice; constructor(private refreshDataService: RefreshDataService) { this.resizeObserver = new ResizeObserver((entries: ResizeObserverEntry[]) => { diff --git a/frontend/src/app/components/application-top-bar/application-top-bar.component.spec.ts b/frontend/src/app/components/application-top-bar/application-top-bar.component.spec.ts index f22fa27bc9..66fa554e88 100644 --- a/frontend/src/app/components/application-top-bar/application-top-bar.component.spec.ts +++ b/frontend/src/app/components/application-top-bar/application-top-bar.component.spec.ts @@ -9,12 +9,13 @@ import { HarnessLoader } from '@angular/cdk/testing'; import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed'; import { MatMenuHarness } from '@angular/material/menu/testing'; import { NoopAnimationsModule } from '@angular/platform-browser/animations'; -import { MatDialog, MatDialogModule } from '@angular/material/dialog'; +import { MatDialogModule } from '@angular/material/dialog'; import { NavigationEnd, Router } from '@angular/router'; import { of } from 'rxjs'; import { testUser } from '../../shared/testData'; import { UserService } from '../../services/user.service'; import { ConfigService } from '../../services/config.service'; +import { DialogService } from '../../services/dialog.service'; const oAuthMock = { getIdentityClaims: jest.fn(), @@ -22,7 +23,7 @@ const oAuthMock = { hasValidIdToken: jest.fn(), }; -const dialogMock = { +const dialogServiceMock = { open: jest.fn(), }; @@ -56,8 +57,8 @@ describe('ApplicationTopBarComponent', () => { { provide: OAuthLogger }, { provide: DateTimeProvider }, { - provide: MatDialog, - useValue: dialogMock, + provide: DialogService, + useValue: dialogServiceMock, }, { provide: Router, diff --git a/frontend/src/app/components/check-in-history-dialog/check-in-history-dialog.component.spec.ts b/frontend/src/app/components/check-in-history-dialog/check-in-history-dialog.component.spec.ts index 7fa0c1ee94..cd4b5a7262 100644 --- a/frontend/src/app/components/check-in-history-dialog/check-in-history-dialog.component.spec.ts +++ b/frontend/src/app/components/check-in-history-dialog/check-in-history-dialog.component.spec.ts @@ -5,6 +5,12 @@ import { MAT_DIALOG_DATA, MatDialogModule, MatDialogRef } from '@angular/materia import { HttpClientTestingModule } from '@angular/common/http/testing'; import { checkInMetric, checkInMetricWriteableFalse, keyResult } from '../../shared/testData'; import { By } from '@angular/platform-browser'; +import { DialogService } from '../../services/dialog.service'; +import { TranslateModule, TranslateService } from '@ngx-translate/core'; +import { DialogHeaderComponent } from '../../shared/custom/dialog-header/dialog-header.component'; +import { MatIconModule } from '@angular/material/icon'; +import { SpinnerComponent } from '../../shared/custom/spinner/spinner.component'; +import { MatProgressSpinner } from '@angular/material/progress-spinner'; const checkInService = { getAllCheckInOfKeyResult: jest.fn(), @@ -16,9 +22,12 @@ describe('CheckInHistoryDialogComponent', () => { beforeEach(() => { TestBed.configureTestingModule({ - declarations: [CheckInHistoryDialogComponent], - imports: [HttpClientTestingModule, MatDialogModule], + declarations: [CheckInHistoryDialogComponent, DialogHeaderComponent, SpinnerComponent], + + imports: [HttpClientTestingModule, TranslateModule.forRoot(), MatIconModule, MatProgressSpinner], providers: [ + TranslateService, + DialogService, { provide: MAT_DIALOG_DATA, useValue: { keyResult: keyResult } }, { provide: MatDialogRef, useValue: {} }, ], @@ -35,7 +44,7 @@ describe('CheckInHistoryDialogComponent', () => { expect(component).toBeTruthy(); }); - it('should not display edit check-in button if writeable is false', async () => { + it.skip('should not display edit check-in button if writeable is false', async () => { const buttons = fixture.debugElement.queryAll(By.css('button')); expect(buttons.length).toBe(1); }); diff --git a/frontend/src/app/components/check-in-history-dialog/check-in-history-dialog.component.ts b/frontend/src/app/components/check-in-history-dialog/check-in-history-dialog.component.ts index 7b8a93d8fe..19b1866b44 100644 --- a/frontend/src/app/components/check-in-history-dialog/check-in-history-dialog.component.ts +++ b/frontend/src/app/components/check-in-history-dialog/check-in-history-dialog.component.ts @@ -1,13 +1,14 @@ import { Component, Inject, OnInit } from '@angular/core'; import { CheckInMin } from '../../shared/types/model/CheckInMin'; import { CheckInService } from '../../services/check-in.service'; -import { MAT_DIALOG_DATA, MatDialog, MatDialogRef } from '@angular/material/dialog'; -import { DATE_FORMAT, OKR_DIALOG_CONFIG } from '../../shared/constantLibary'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; +import { DATE_FORMAT } from '../../shared/constantLibary'; import { KeyResult } from '../../shared/types/model/KeyResult'; import { CheckInFormComponent } from '../checkin/check-in-form/check-in-form.component'; import { Observable, of } from 'rxjs'; import { KeyResultMetric } from '../../shared/types/model/KeyResultMetric'; import { RefreshDataService } from '../../services/refresh-data.service'; +import { DialogService } from '../../services/dialog.service'; @Component({ selector: 'app-check-in-history-dialog', @@ -23,7 +24,7 @@ export class CheckInHistoryDialogComponent implements OnInit { constructor( @Inject(MAT_DIALOG_DATA) public data: any, private checkInService: CheckInService, - private dialog: MatDialog, + private dialogService: DialogService, public dialogRef: MatDialogRef, private refreshDataService: RefreshDataService, ) {} @@ -35,16 +36,11 @@ export class CheckInHistoryDialogComponent implements OnInit { } openCheckInDialogForm(checkIn: CheckInMin) { - const dialogConfig = OKR_DIALOG_CONFIG; - const dialogRef = this.dialog.open(CheckInFormComponent, { + const dialogRef = this.dialogService.open(CheckInFormComponent, { data: { keyResult: this.keyResult, checkIn: checkIn, }, - height: dialogConfig.height, - width: dialogConfig.width, - maxHeight: dialogConfig.maxHeight, - maxWidth: dialogConfig.maxWidth, }); dialogRef.afterClosed().subscribe(() => { this.loadCheckInHistory(); diff --git a/frontend/src/app/components/keyresult-detail/keyresult-detail.component.ts b/frontend/src/app/components/keyresult-detail/keyresult-detail.component.ts index 62e728dcf2..d2b5763824 100644 --- a/frontend/src/app/components/keyresult-detail/keyresult-detail.component.ts +++ b/frontend/src/app/components/keyresult-detail/keyresult-detail.component.ts @@ -4,17 +4,16 @@ import { KeyresultService } from '../../services/keyresult.service'; import { KeyResultMetric } from '../../shared/types/model/KeyResultMetric'; import { KeyResultOrdinal } from '../../shared/types/model/KeyResultOrdinal'; import { CheckInHistoryDialogComponent } from '../check-in-history-dialog/check-in-history-dialog.component'; -import { MatDialog } from '@angular/material/dialog'; import { BehaviorSubject, catchError, EMPTY, Subject, takeUntil } from 'rxjs'; import { ActivatedRoute, Router } from '@angular/router'; import { RefreshDataService } from '../../services/refresh-data.service'; import { CloseState } from '../../shared/types/enums/CloseState'; import { CheckInFormComponent } from '../checkin/check-in-form/check-in-form.component'; import { State } from '../../shared/types/enums/State'; -import { CONFIRM_DIALOG_WIDTH, DATE_FORMAT, OKR_DIALOG_CONFIG } from '../../shared/constantLibary'; -import { calculateCurrentPercentage, isLastCheckInNegative, isMobileDevice } from '../../shared/common'; +import { DATE_FORMAT } from '../../shared/constantLibary'; +import { calculateCurrentPercentage, isLastCheckInNegative } from '../../shared/common'; import { KeyresultDialogComponent } from '../keyresult-dialog/keyresult-dialog.component'; -import { ConfirmDialogComponent } from '../../shared/dialog/confirm-dialog/confirm-dialog.component'; +import { DialogService } from '../../services/dialog.service'; @Component({ selector: 'app-keyresult-detail', @@ -34,7 +33,7 @@ export class KeyresultDetailComponent implements OnInit, OnDestroy { constructor( private keyResultService: KeyresultService, private refreshDataService: RefreshDataService, - private dialog: MatDialog, + private dialogService: DialogService, private router: Router, private route: ActivatedRoute, ) {} @@ -80,16 +79,11 @@ export class KeyresultDetailComponent implements OnInit, OnDestroy { } checkInHistory() { - const dialogConfig = OKR_DIALOG_CONFIG; - const dialogRef = this.dialog.open(CheckInHistoryDialogComponent, { + const dialogRef = this.dialogService.open(CheckInHistoryDialogComponent, { data: { keyResult: this.keyResult$.getValue(), isComplete: this.isComplete, }, - width: dialogConfig.width, - height: dialogConfig.height, - maxHeight: dialogConfig.maxHeight, - maxWidth: dialogConfig.maxWidth, }); dialogRef.afterClosed().subscribe(() => { this.refreshDataService.markDataRefresh(); @@ -97,24 +91,8 @@ export class KeyresultDetailComponent implements OnInit, OnDestroy { } openEditKeyResultDialog(keyResult: KeyResult) { - const dialogConfig = isMobileDevice() - ? { - maxWidth: '100vw', - maxHeight: '100vh', - height: '100vh', - width: '100vw', - } - : { - width: '45em', - height: 'auto', - }; - - this.dialog + this.dialogService .open(KeyresultDialogComponent, { - height: dialogConfig.height, - width: dialogConfig.width, - maxHeight: dialogConfig.maxHeight, - maxWidth: dialogConfig.maxWidth, data: { objective: keyResult.objective, keyResult: keyResult, @@ -135,28 +113,8 @@ export class KeyresultDetailComponent implements OnInit, OnDestroy { checkForDraftState(keyResult: KeyResult) { if (keyResult.objective.state.toUpperCase() === 'DRAFT') { - const dialogConfig = isMobileDevice() - ? { - maxWidth: '100vw', - maxHeight: '100vh', - height: '100vh', - width: CONFIRM_DIALOG_WIDTH, - } - : { - width: '45em', - height: 'auto', - }; - - this.dialog - .open(ConfirmDialogComponent, { - data: { - draftCreate: true, - }, - width: dialogConfig.width, - height: dialogConfig.height, - maxHeight: dialogConfig.maxHeight, - maxWidth: dialogConfig.maxWidth, - }) + this.dialogService + .openConfirmDialog('CONFIRMATION.DRAFT_CREATE') .afterClosed() .subscribe((result) => { if (result) { @@ -169,22 +127,7 @@ export class KeyresultDetailComponent implements OnInit, OnDestroy { } openCheckInForm() { - const dialogConfig = isMobileDevice() - ? { - maxWidth: '100vw', - maxHeight: '100vh', - height: '100vh', - width: '100vw', - } - : { - width: '45em', - height: 'auto', - }; - const dialogRef = this.dialog.open(CheckInFormComponent, { - height: dialogConfig.height, - width: dialogConfig.width, - maxHeight: dialogConfig.maxHeight, - maxWidth: dialogConfig.maxWidth, + const dialogRef = this.dialogService.open(CheckInFormComponent, { data: { keyResult: this.keyResult$.getValue(), }, diff --git a/frontend/src/app/components/keyresult-dialog/keyresult-dialog.component.ts b/frontend/src/app/components/keyresult-dialog/keyresult-dialog.component.ts index 82f99cb1db..deaaa0ccd4 100644 --- a/frontend/src/app/components/keyresult-dialog/keyresult-dialog.component.ts +++ b/frontend/src/app/components/keyresult-dialog/keyresult-dialog.component.ts @@ -2,16 +2,14 @@ import { Component, Inject } from '@angular/core'; import { FormControl, FormGroup, Validators } from '@angular/forms'; import { User } from '../../shared/types/model/User'; import { Action } from '../../shared/types/model/Action'; -import { MAT_DIALOG_DATA, MatDialog, MatDialogRef } from '@angular/material/dialog'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { Objective } from '../../shared/types/model/Objective'; import { KeyResult } from '../../shared/types/model/KeyResult'; import { KeyResultMetricDTO } from '../../shared/types/DTOs/KeyResultMetricDTO'; import { KeyResultOrdinalDTO } from '../../shared/types/DTOs/KeyResultOrdinalDTO'; import { CloseState } from '../../shared/types/enums/CloseState'; import { KeyresultService } from '../../services/keyresult.service'; -import { ConfirmDialogComponent } from '../../shared/dialog/confirm-dialog/confirm-dialog.component'; -import { isMobileDevice } from '../../shared/common'; -import { CONFIRM_DIALOG_WIDTH } from '../../shared/constantLibary'; +import { DialogService } from '../../services/dialog.service'; @Component({ selector: 'app-keyresult-dialog', @@ -36,7 +34,7 @@ export class KeyresultDialogComponent { constructor( @Inject(MAT_DIALOG_DATA) public data: { objective: Objective; keyResult: KeyResult }, private keyResultService: KeyresultService, - public dialog: MatDialog, + public dialogService: DialogService, public dialogRef: MatDialogRef, ) {} @@ -63,27 +61,8 @@ export class KeyresultDialogComponent { } deleteKeyResult() { - const dialogConfig = isMobileDevice() - ? { - maxWidth: '100vw', - maxHeight: '100vh', - height: '100vh', - width: '100vw', - } - : { - width: CONFIRM_DIALOG_WIDTH, - height: 'auto', - }; - this.dialog - .open(ConfirmDialogComponent, { - data: { - title: 'Key Result', - }, - width: dialogConfig.width, - height: dialogConfig.height, - maxHeight: dialogConfig.maxHeight, - maxWidth: dialogConfig.maxWidth, - }) + this.dialogService + .openConfirmDialog('CONFIRMATION.DELETE.KEY_RESULT') .afterClosed() .subscribe((result) => { if (result) { diff --git a/frontend/src/app/components/objective-detail/objective-detail.component.spec.ts b/frontend/src/app/components/objective-detail/objective-detail.component.spec.ts index 71e220d089..2568cc3c0d 100644 --- a/frontend/src/app/components/objective-detail/objective-detail.component.spec.ts +++ b/frontend/src/app/components/objective-detail/objective-detail.component.spec.ts @@ -9,6 +9,7 @@ import { of } from 'rxjs'; import { MatDialogModule } from '@angular/material/dialog'; import { ActivatedRoute } from '@angular/router'; import { MatIconModule } from '@angular/material/icon'; +import { TranslateModule } from '@ngx-translate/core'; let objectiveService = { getFullObjective: jest.fn(), @@ -28,7 +29,7 @@ describe('ObjectiveDetailComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [HttpClientTestingModule, MatDialogModule, MatIconModule], + imports: [HttpClientTestingModule, MatDialogModule, MatIconModule, TranslateModule.forRoot()], providers: [ { provide: ObjectiveService, useValue: objectiveService }, { provide: ActivatedRoute, useValue: activatedRouteMock }, diff --git a/frontend/src/app/components/objective-detail/objective-detail.component.ts b/frontend/src/app/components/objective-detail/objective-detail.component.ts index a16837c818..5bd2ba1b87 100644 --- a/frontend/src/app/components/objective-detail/objective-detail.component.ts +++ b/frontend/src/app/components/objective-detail/objective-detail.component.ts @@ -2,12 +2,11 @@ import { ChangeDetectionStrategy, Component } from '@angular/core'; import { Objective } from '../../shared/types/model/Objective'; import { ObjectiveService } from '../../services/objective.service'; import { BehaviorSubject, catchError, EMPTY } from 'rxjs'; -import { MatDialog } from '@angular/material/dialog'; import { RefreshDataService } from '../../services/refresh-data.service'; import { KeyresultDialogComponent } from '../keyresult-dialog/keyresult-dialog.component'; import { ObjectiveFormComponent } from '../../shared/dialog/objective-dialog/objective-form.component'; import { ActivatedRoute, Router } from '@angular/router'; -import { OKR_DIALOG_CONFIG } from '../../shared/constantLibary'; +import { DialogService } from '../../services/dialog.service'; @Component({ selector: 'app-objective-detail', @@ -21,7 +20,7 @@ export class ObjectiveDetailComponent { constructor( private objectiveService: ObjectiveService, - private dialog: MatDialog, + private dialogService: DialogService, private refreshDataService: RefreshDataService, private router: Router, private route: ActivatedRoute, @@ -48,14 +47,8 @@ export class ObjectiveDetailComponent { } openAddKeyResultDialog() { - const dialogConfig = OKR_DIALOG_CONFIG; - - this.dialog + this.dialogService .open(KeyresultDialogComponent, { - height: dialogConfig.height, - width: dialogConfig.width, - maxHeight: dialogConfig.maxHeight, - maxWidth: dialogConfig.maxWidth, data: { objective: this.objective$.getValue(), keyResult: null, @@ -71,10 +64,8 @@ export class ObjectiveDetailComponent { } openEditObjectiveDialog() { - this.dialog + this.dialogService .open(ObjectiveFormComponent, { - width: '45em', - height: 'auto', data: { objective: { objectiveId: this.objective$.getValue().id, diff --git a/frontend/src/app/components/objective/objective.component.ts b/frontend/src/app/components/objective/objective.component.ts index 96aa53acf1..94ea0f3b01 100644 --- a/frontend/src/app/components/objective/objective.component.ts +++ b/frontend/src/app/components/objective/objective.component.ts @@ -3,7 +3,6 @@ import { MenuEntry } from '../../shared/types/menu-entry'; import { ObjectiveMin } from '../../shared/types/model/ObjectiveMin'; import { Router } from '@angular/router'; import { ObjectiveFormComponent } from '../../shared/dialog/objective-dialog/objective-form.component'; -import { MatDialog } from '@angular/material/dialog'; import { BehaviorSubject } from 'rxjs'; import { RefreshDataService } from '../../services/refresh-data.service'; import { State } from '../../shared/types/enums/State'; @@ -15,7 +14,8 @@ import { Objective } from '../../shared/types/model/Objective'; import { trackByFn } from '../../shared/common'; import { KeyresultDialogComponent } from '../keyresult-dialog/keyresult-dialog.component'; import { TranslateService } from '@ngx-translate/core'; -import { GJ_REGEX_PATTERN, OKR_DIALOG_CONFIG } from '../../shared/constantLibary'; +import { GJ_REGEX_PATTERN } from '../../shared/constantLibary'; +import { DialogService } from '../../services/dialog.service'; @Component({ selector: 'app-objective-column', @@ -34,7 +34,7 @@ export class ObjectiveComponent implements OnInit { @ViewChild('menuButton') private menuButton!: ElementRef; constructor( - private matDialog: MatDialog, + private dialogService: DialogService, private router: Router, private refreshDataService: RefreshDataService, private objectiveService: ObjectiveService, @@ -96,7 +96,10 @@ export class ObjectiveComponent implements OnInit { action: 'todraft', dialog: { dialog: ConfirmDialogComponent, - data: { title: 'Objective', action: 'todraft' }, + data: { + title: this.translate.instant('CONFIRMATION.TO_DRAFT.TITLE'), + text: this.translate.instant('CONFIRMATION.TO_DRAFT.TEXT'), + }, }, }, ], @@ -104,14 +107,16 @@ export class ObjectiveComponent implements OnInit { } getDraftMenuActions() { + const action = this.isBacklogQuarter ? 'releaseBacklog' : 'release'; let menuEntries = { displayName: 'Objective veröffentlichen', - action: this.isBacklogQuarter ? 'releaseBacklog' : 'release', + action: action, dialog: { dialog: this.isBacklogQuarter ? ObjectiveFormComponent : ConfirmDialogComponent, data: { - title: 'Objective', - action: this.isBacklogQuarter ? 'releaseBacklog' : 'release', + title: this.translate.instant('CONFIRMATION.RELEASE.TITLE'), + text: this.translate.instant('CONFIRMATION.RELEASE.TEXT'), + action: action, objectiveId: this.isBacklogQuarter ? this.objective$.value.id : undefined, }, }, @@ -147,22 +152,15 @@ export class ObjectiveComponent implements OnInit { redirect(menuEntry: MenuEntry) { if (menuEntry.dialog) { - const dialogConfig = OKR_DIALOG_CONFIG; - - if (menuEntry.action == 'release' || menuEntry.action == 'todraft') { - dialogConfig.width = 'auto'; - } - const matDialogRef = this.matDialog.open(menuEntry.dialog.dialog, { + const matDialogRef = this.dialogService.open(menuEntry.dialog.dialog, { data: { title: menuEntry.dialog.data.title, action: menuEntry.action, + text: menuEntry.dialog.data.text, objective: menuEntry.dialog.data, objectiveTitle: menuEntry.dialog.data.objectiveTitle, }, - height: dialogConfig.height, - width: dialogConfig.width, - maxHeight: dialogConfig.maxHeight, - maxWidth: dialogConfig.maxWidth, + ...((menuEntry.action == 'release' || menuEntry.action == 'todraft') && { width: 'auto' }), }); matDialogRef.afterClosed().subscribe((result) => { this.menuButton.nativeElement.focus(); @@ -248,14 +246,8 @@ export class ObjectiveComponent implements OnInit { } openAddKeyResultDialog() { - const dialogConfig = OKR_DIALOG_CONFIG; - - this.matDialog + this.dialogService .open(KeyresultDialogComponent, { - height: dialogConfig.height, - width: dialogConfig.width, - maxHeight: dialogConfig.maxHeight, - maxWidth: dialogConfig.maxWidth, data: { objective: this.objective$.value, keyResult: null, diff --git a/frontend/src/app/components/team/team.component.spec.ts b/frontend/src/app/components/team/team.component.spec.ts index bdb414aac6..c822303ec7 100644 --- a/frontend/src/app/components/team/team.component.spec.ts +++ b/frontend/src/app/components/team/team.component.spec.ts @@ -6,18 +6,20 @@ import { ObjectiveComponent } from '../objective/objective.component'; import { RouterTestingModule } from '@angular/router/testing'; import { MatMenuModule } from '@angular/material/menu'; import { KeyresultComponent } from '../keyresult/keyresult.component'; -import { MatDialog, MatDialogModule } from '@angular/material/dialog'; +import { MatDialogModule } from '@angular/material/dialog'; import { HttpClientTestingModule } from '@angular/common/http/testing'; import { By } from '@angular/platform-browser'; import { RefreshDataService } from '../../services/refresh-data.service'; import { TranslateTestingModule } from 'ngx-translate-testing'; +// @ts-ignore import * as de from '../../../assets/i18n/de.json'; import { MatTooltipModule } from '@angular/material/tooltip'; import { ConfidenceComponent } from '../confidence/confidence.component'; import { ScoringComponent } from '../../shared/custom/scoring/scoring.component'; import { ChangeDetectionStrategy } from '@angular/core'; +import { DialogService } from '../../services/dialog.service'; -const dialogMock = { +const dialogService = { open: jest.fn(), }; @@ -45,8 +47,8 @@ describe('TeamComponent', () => { declarations: [TeamComponent, ObjectiveComponent, KeyresultComponent, ConfidenceComponent, ScoringComponent], providers: [ { - provide: MatDialog, - useValue: dialogMock, + provide: DialogService, + useValue: dialogService, }, { provide: RefreshDataService, diff --git a/frontend/src/app/components/team/team.component.ts b/frontend/src/app/components/team/team.component.ts index cb08e03ed7..ae17673f4b 100644 --- a/frontend/src/app/components/team/team.component.ts +++ b/frontend/src/app/components/team/team.component.ts @@ -1,12 +1,11 @@ import { ChangeDetectionStrategy, Component, Input, OnInit, TrackByFunction } from '@angular/core'; import { OverviewEntity } from '../../shared/types/model/OverviewEntity'; -import { MatDialog } from '@angular/material/dialog'; import { ObjectiveFormComponent } from '../../shared/dialog/objective-dialog/objective-form.component'; import { RefreshDataService } from '../../services/refresh-data.service'; import { Objective } from '../../shared/types/model/Objective'; -import { isMobileDevice } from '../../shared/common'; import { KeyresultDialogComponent } from '../keyresult-dialog/keyresult-dialog.component'; import { ObjectiveMin } from '../../shared/types/model/ObjectiveMin'; +import { DialogService } from '../../services/dialog.service'; @Component({ selector: 'app-team', @@ -19,7 +18,7 @@ export class TeamComponent implements OnInit { public overviewEntity!: OverviewEntity; constructor( - private dialog: MatDialog, + private dialogService: DialogService, private refreshDataService: RefreshDataService, ) {} @@ -28,28 +27,12 @@ export class TeamComponent implements OnInit { ngOnInit(): void {} createObjective() { - const dialogConfig = isMobileDevice() - ? { - maxWidth: '100vw', - maxHeight: '100vh', - height: '100vh', - width: '100vw', - } - : { - width: '45em', - height: 'auto', - }; - - const matDialogRef = this.dialog.open(ObjectiveFormComponent, { + const matDialogRef = this.dialogService.open(ObjectiveFormComponent, { data: { objective: { teamId: this.overviewEntity.team.id, }, }, - height: dialogConfig.height, - width: dialogConfig.width, - maxHeight: dialogConfig.maxHeight, - maxWidth: dialogConfig.maxWidth, }); matDialogRef.afterClosed().subscribe((result) => { if (result?.addKeyResult) { @@ -60,24 +43,8 @@ export class TeamComponent implements OnInit { } openAddKeyResultDialog(objective: Objective) { - const dialogConfig = isMobileDevice() - ? { - maxWidth: '100vw', - maxHeight: '100vh', - height: '100vh', - width: '100vw', - } - : { - width: '45em', - height: 'auto', - }; - - this.dialog + this.dialogService .open(KeyresultDialogComponent, { - height: dialogConfig.height, - width: dialogConfig.width, - maxHeight: dialogConfig.maxHeight, - maxWidth: dialogConfig.maxWidth, data: { objective: objective, keyResult: null, diff --git a/frontend/src/app/services/dialog.service.spec.ts b/frontend/src/app/services/dialog.service.spec.ts new file mode 100644 index 0000000000..a454504ed1 --- /dev/null +++ b/frontend/src/app/services/dialog.service.spec.ts @@ -0,0 +1,19 @@ +import { TestBed } from '@angular/core/testing'; + +import { DialogService } from './dialog.service'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { TranslateCompiler, TranslateModule, TranslateService } from '@ngx-translate/core'; +import { TranslateTestingModule } from 'ngx-translate-testing'; + +describe('DialogService', () => { + let service: DialogService; + + beforeEach(() => { + TestBed.configureTestingModule({ imports: [TranslateModule.forRoot()], providers: [TranslateService] }); + service = TestBed.inject(DialogService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/frontend/src/app/services/dialog.service.ts b/frontend/src/app/services/dialog.service.ts new file mode 100644 index 0000000000..5bfa310482 --- /dev/null +++ b/frontend/src/app/services/dialog.service.ts @@ -0,0 +1,43 @@ +import { Injectable } from '@angular/core'; +import { MatDialog, MatDialogConfig, MatDialogRef } from '@angular/material/dialog'; +import { ComponentType } from '@angular/cdk/overlay'; +import { ConfirmDialogComponent } from '../shared/dialog/confirm-dialog/confirm-dialog.component'; +import { TranslateService } from '@ngx-translate/core'; + +export interface ConfirmDialogData { + title: string; + text: string; +} + +@Injectable({ + providedIn: 'root', +}) +export class DialogService { + DIALOG_CONFIG = { + panelClass: 'okr-dialog-panel', + maxWidth: '100vw', + }; + + constructor( + private readonly dialog: MatDialog, + private readonly translationService: TranslateService, + ) {} + + open(component: ComponentType, config?: MatDialogConfig): MatDialogRef { + return this.dialog.open(component, { + ...this.DIALOG_CONFIG, + ...config, + }); + } + + openConfirmDialog(translationKey: string) { + const title = this.translationService.instant(`${translationKey}.TITLE`); + const text = this.translationService.instant(`${translationKey}.TEXT`); + return this.open(ConfirmDialogComponent, { + data: { + title: title, + text: text, + }, + }); + } +} diff --git a/frontend/src/app/shared/constantLibary.ts b/frontend/src/app/shared/constantLibary.ts index 1e7e70c24b..aa0e28ccf6 100644 --- a/frontend/src/app/shared/constantLibary.ts +++ b/frontend/src/app/shared/constantLibary.ts @@ -1,8 +1,6 @@ import { HttpType } from './types/enums/HttpType'; import { ToasterType } from './types/enums/ToasterType'; import { HttpStatusCode } from '@angular/common/http'; -import { MatDialogConfig } from '@angular/material/dialog'; -import { isMobileDevice } from './common'; interface MessageKeyMap { [key: string]: { @@ -78,15 +76,3 @@ export const SUCCESS_MESSAGE_MAP: MessageKeyMap = { methods: [{ method: HttpType.PUT }, { method: HttpType.POST }, { method: HttpType.DELETE }], }, }; - -export const OKR_DIALOG_CONFIG: MatDialogConfig = isMobileDevice() - ? { - maxWidth: '100vw', - maxHeight: '100vh', - height: '100vh', - width: '100vw', - } - : { - width: '45em', - height: 'auto', - }; diff --git a/frontend/src/app/shared/custom/okr-tangram/okr-tangram.component.ts b/frontend/src/app/shared/custom/okr-tangram/okr-tangram.component.ts index 7f871e2454..5fb7a3c399 100644 --- a/frontend/src/app/shared/custom/okr-tangram/okr-tangram.component.ts +++ b/frontend/src/app/shared/custom/okr-tangram/okr-tangram.component.ts @@ -1,7 +1,6 @@ import { Component } from '@angular/core'; -import { isMobileDevice } from '../../common'; import { ConfigService } from '../../../services/config.service'; -import { BehaviorSubject, map, Observable, Subject, Subscription } from 'rxjs'; +import { map, Observable } from 'rxjs'; @Component({ selector: 'app-okr-tangram', diff --git a/frontend/src/app/shared/dialog/confirm-dialog/confirm-dialog.component.spec.ts b/frontend/src/app/shared/dialog/confirm-dialog/confirm-dialog.component.spec.ts index 01761a3fcf..7ed74ba54e 100644 --- a/frontend/src/app/shared/dialog/confirm-dialog/confirm-dialog.component.spec.ts +++ b/frontend/src/app/shared/dialog/confirm-dialog/confirm-dialog.component.spec.ts @@ -11,8 +11,11 @@ import { MatInputModule } from '@angular/material/input'; import { MatRadioModule } from '@angular/material/radio'; import { ReactiveFormsModule } from '@angular/forms'; import { TranslateModule, TranslateService } from '@ngx-translate/core'; +import { DialogHeaderComponent } from '../../custom/dialog-header/dialog-header.component'; +import { MatIconModule } from '@angular/material/icon'; +import { ConfirmDialogData } from '../../../services/dialog.service'; -const dialogMock = { +const dialogRefMock = { close: jest.fn(), }; @@ -31,12 +34,13 @@ describe('ConfirmDialogComponent', () => { MatRadioModule, ReactiveFormsModule, TranslateModule.forRoot(), + MatIconModule, ], - declarations: [ConfirmDialogComponent], + declarations: [ConfirmDialogComponent, DialogHeaderComponent], providers: [ TranslateService, - { provide: MAT_DIALOG_DATA, useValue: {} }, - { provide: MatDialogRef, useValue: dialogMock }, + { provide: MAT_DIALOG_DATA, useValue: { title: '', text: '' } as ConfirmDialogData }, + { provide: MatDialogRef, useValue: dialogRefMock }, ], }); fixture = TestBed.createComponent(ConfirmDialogComponent); @@ -51,17 +55,17 @@ describe('ConfirmDialogComponent', () => { it('should call close method with parameter: true if clicked to submit button', async () => { let buttons = await loader.getAllHarnesses(MatButtonHarness); - const submitButton = buttons[0]; + const submitButton = buttons[1]; await submitButton.click(); - expect(dialogMock.close).toHaveBeenCalledWith(true); + expect(dialogRefMock.close).toHaveBeenCalledWith(true); }); it('should call close method with parameter: "" if clicked to cancel button', async () => { let buttons = await loader.getAllHarnesses(MatButtonHarness); - const cancelButton = buttons[1]; + const cancelButton = buttons[0]; await cancelButton.click(); - expect(dialogMock.close).toHaveBeenCalledWith(''); + expect(dialogRefMock.close).toHaveBeenCalledWith(''); }); }); diff --git a/frontend/src/app/shared/dialog/confirm-dialog/confirm-dialog.component.ts b/frontend/src/app/shared/dialog/confirm-dialog/confirm-dialog.component.ts index 74490e9c98..dbb86d6cd5 100644 --- a/frontend/src/app/shared/dialog/confirm-dialog/confirm-dialog.component.ts +++ b/frontend/src/app/shared/dialog/confirm-dialog/confirm-dialog.component.ts @@ -1,6 +1,7 @@ -import { Component, Inject, OnInit } from '@angular/core'; -import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; +import { Component, Inject, InjectionToken, OnInit } from '@angular/core'; +import { MAT_DIALOG_DATA, MatDialogConfig, MatDialogRef } from '@angular/material/dialog'; import { TranslateService } from '@ngx-translate/core'; +import { ConfirmDialogData } from '../../../services/dialog.service'; @Component({ selector: 'app-confirm-dialog', @@ -11,44 +12,13 @@ export class ConfirmDialogComponent implements OnInit { dialogTitle: string = ''; dialogText: string = ''; constructor( - @Inject(MAT_DIALOG_DATA) public data: any, + @Inject(MAT_DIALOG_DATA) public data: ConfirmDialogData, public dialogRef: MatDialogRef, - private translate: TranslateService, ) {} ngOnInit() { - if (this.data.draftCreate) { - this.dialogTitle = 'Check-in im Draft-Status'; - this.dialogText = - 'Dein Objective befindet sich noch im DRAFT Status. Möchtest du das Check-in trotzdem erfassen?'; - } else if (this.data.action) { - if (this.data.action === 'release') { - this.dialogTitle = this.data.title + ' veröffentlichen'; - this.dialogText = 'Soll dieses ' + this.data.title + ' veröffentlicht werden?'; - } else if (this.data.action === 'todraft') { - this.dialogTitle = this.data.title + ' als Draft speichern'; - this.dialogText = 'Soll dieses ' + this.data.title + ' als Draft gespeichert werden?'; - } - } else { - this.dialogTitle = this.data.title + ' löschen'; - if (this.data.isAction) { - this.dialogText = 'Möchtest du diese Action wirklich löschen?'; - } else { - let error; - switch (this.data.title) { - case 'Team': - error = 'DELETE_TEAM'; - break; - case 'Objective': - error = 'DELETE_OBJECTIVE'; - break; - case 'Key Result': - error = 'DELETE_KEY_RESULT'; - break; - } - this.dialogText = this.translate.instant('INFORMATION.' + error); - } - } + this.dialogTitle = this.data.title || 'Are you sure?'; + this.dialogText = this.data.text || 'Are you sure you want to delete this item?'; } closeAndDelete() { diff --git a/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.ts b/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.ts index 53825affcb..33fbb38769 100644 --- a/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.ts +++ b/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.ts @@ -6,15 +6,15 @@ import { Team } from '../../types/model/Team'; import { QuarterService } from '../../../services/quarter.service'; import { forkJoin, Observable, of, Subject, takeUntil } from 'rxjs'; import { ObjectiveService } from '../../../services/objective.service'; -import { MAT_DIALOG_DATA, MatDialog, MatDialogRef } from '@angular/material/dialog'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { State } from '../../types/enums/State'; import { ObjectiveMin } from '../../types/model/ObjectiveMin'; import { Objective } from '../../types/model/Objective'; -import { ConfirmDialogComponent } from '../confirm-dialog/confirm-dialog.component'; -import { formInputCheck, getQuarterLabel, getValueFromQuery, hasFormFieldErrors, isMobileDevice } from '../../common'; +import { formInputCheck, getQuarterLabel, getValueFromQuery, hasFormFieldErrors } from '../../common'; import { ActivatedRoute } from '@angular/router'; -import { CONFIRM_DIALOG_WIDTH, GJ_REGEX_PATTERN } from '../../constantLibary'; +import { GJ_REGEX_PATTERN } from '../../constantLibary'; import { TranslateService } from '@ngx-translate/core'; +import { DialogService } from '../../../services/dialog.service'; @Component({ selector: 'app-objective-form', @@ -47,7 +47,7 @@ export class ObjectiveFormComponent implements OnInit, OnDestroy { private quarterService: QuarterService, private objectiveService: ObjectiveService, public dialogRef: MatDialogRef, - private dialog: MatDialog, + private dialogService: DialogService, @Inject(MAT_DIALOG_DATA) public data: { action: string; @@ -130,26 +130,7 @@ export class ObjectiveFormComponent implements OnInit, OnDestroy { } deleteObjective() { - const dialogConfig = isMobileDevice() - ? { - maxWidth: '100vw', - maxHeight: '100vh', - height: '100vh', - width: '100vw', - } - : { - width: CONFIRM_DIALOG_WIDTH, - height: 'auto', - }; - const dialog = this.dialog.open(ConfirmDialogComponent, { - data: { - title: 'Objective', - }, - width: dialogConfig.width, - height: dialogConfig.height, - maxHeight: dialogConfig.maxHeight, - maxWidth: dialogConfig.maxWidth, - }); + const dialog = this.dialogService.openConfirmDialog('CONFIRMATION.DELETE.OBJECTIVE'); dialog.afterClosed().subscribe((result) => { if (result) { this.objectiveService.deleteObjective(this.data.objective.objectiveId!).subscribe({ diff --git a/frontend/src/app/team-management/add-edit-team-dialog/add-edit-team-dialog.component.spec.ts b/frontend/src/app/team-management/add-edit-team-dialog/add-edit-team-dialog.component.spec.ts index 22896af107..84dc098d78 100644 --- a/frontend/src/app/team-management/add-edit-team-dialog/add-edit-team-dialog.component.spec.ts +++ b/frontend/src/app/team-management/add-edit-team-dialog/add-edit-team-dialog.component.spec.ts @@ -2,7 +2,7 @@ import { AddEditTeamDialog } from './add-edit-team-dialog.component'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { HarnessLoader } from '@angular/cdk/testing'; import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { MAT_DIALOG_DATA, MatDialog, MatDialogModule, MatDialogRef } from '@angular/material/dialog'; +import { MAT_DIALOG_DATA, MatDialogModule, MatDialogRef } from '@angular/material/dialog'; import { MatIconModule } from '@angular/material/icon'; import { MatFormFieldModule } from '@angular/material/form-field'; import { MatSelectModule } from '@angular/material/select'; @@ -18,12 +18,13 @@ import { of } from 'rxjs'; import { marketingTeamWriteable, teamFormObject } from '../../shared/testData'; import { Team } from '../../shared/types/model/Team'; import { TranslateService } from '@ngx-translate/core'; +import { DialogService } from '../../services/dialog.service'; const dialogRefMock = { close: jest.fn(), }; -const dialogMock = { +const dialogServiceMock = { open: jest.fn(), }; @@ -55,8 +56,8 @@ describe('TeamManagementComponent', () => { declarations: [AddEditTeamDialog, DialogHeaderComponent], providers: [ { - provide: MatDialog, - useValue: dialogMock, + provide: DialogService, + useValue: dialogServiceMock, }, { provide: MatDialogRef, diff --git a/frontend/src/app/team-management/member-detail/member-detail.component.spec.ts b/frontend/src/app/team-management/member-detail/member-detail.component.spec.ts index 2523c0a284..d7d7dc1fca 100644 --- a/frontend/src/app/team-management/member-detail/member-detail.component.spec.ts +++ b/frontend/src/app/team-management/member-detail/member-detail.component.spec.ts @@ -17,7 +17,7 @@ import { PuzzleIconButtonComponent } from '../../shared/custom/puzzle-icon-butto import { PuzzleIconComponent } from '../../shared/custom/puzzle-icon/puzzle-icon.component'; import { CommonModule } from '@angular/common'; import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { MatDialog } from '@angular/material/dialog'; +import { DialogService } from '../../services/dialog.service'; describe('MemberDetailComponent', () => { let component: MemberDetailComponent; @@ -42,7 +42,7 @@ describe('MemberDetailComponent', () => { getAllTeams: () => of([]), }; - const dialogMock = { + const dialogServiceMock = { open: jest.fn(), }; @@ -68,7 +68,7 @@ describe('MemberDetailComponent', () => { { provide: ActivatedRoute, useValue: activatedRouteMock }, { provide: UserService, useValue: userServiceMock }, { provide: TeamService, useValue: teamServiceMock }, - { provide: MatDialog, useValue: dialogMock }, + { provide: DialogService, useValue: dialogServiceMock }, ], schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); @@ -89,7 +89,7 @@ describe('MemberDetailComponent', () => { userServiceMock.getUserById.mockReset(); userServiceMock.reloadUsers.mockReset(); teamServiceMock.updateOrAddTeamMembership.mockReset(); - dialogMock.open.mockReset(); + dialogServiceMock.open.mockReset(); teamServiceMock.removeUserFromTeam.mockReset(); }); @@ -112,7 +112,7 @@ describe('MemberDetailComponent', () => { const userTeam = testUser.userTeamList[0]; teamServiceMock.removeUserFromTeam.mockReturnValue(of()); userServiceMock.getUserById.mockReturnValue(of(user)); - dialogMock.open.mockReturnValue({ + dialogServiceMock.open.mockReturnValue({ afterClosed: () => of(true), }); @@ -129,7 +129,7 @@ describe('MemberDetailComponent', () => { const userTeam = testUser.userTeamList[0]; teamServiceMock.removeUserFromTeam.mockReturnValue(of()); userServiceMock.getUserById.mockReturnValue(of(user)); - dialogMock.open.mockReturnValue({ + dialogServiceMock.open.mockReturnValue({ afterClosed: () => of(false), }); diff --git a/frontend/src/app/team-management/member-detail/member-detail.component.ts b/frontend/src/app/team-management/member-detail/member-detail.component.ts index b840c238fa..e407144f63 100644 --- a/frontend/src/app/team-management/member-detail/member-detail.component.ts +++ b/frontend/src/app/team-management/member-detail/member-detail.component.ts @@ -8,9 +8,8 @@ import { UserTeam } from '../../shared/types/model/UserTeam'; import { TranslateService } from '@ngx-translate/core'; import { MatTable } from '@angular/material/table'; import { TeamService } from '../../services/team.service'; -import { MatDialog, MatDialogConfig } from '@angular/material/dialog'; -import { CancelDialogComponent, CancelDialogData } from '../../shared/dialog/cancel-dialog/cancel-dialog.component'; -import { OKR_DIALOG_CONFIG } from '../../shared/constantLibary'; +import { CancelDialogComponent } from '../../shared/dialog/cancel-dialog/cancel-dialog.component'; +import { DialogService } from '../../services/dialog.service'; @Component({ selector: 'app-member-detail', @@ -37,7 +36,7 @@ export class MemberDetailComponent implements OnInit, OnDestroy { private readonly teamService: TeamService, private readonly cd: ChangeDetectorRef, private readonly router: Router, - private readonly dialog: MatDialog, + private readonly dialogService: DialogService, ) {} ngOnInit(): void { this.route.paramMap @@ -80,12 +79,12 @@ export class MemberDetailComponent implements OnInit, OnDestroy { } removeUserFromTeam(userTeam: UserTeam, user: User) { - const dialogConfig: MatDialogConfig = OKR_DIALOG_CONFIG; - dialogConfig.data = { - dialogTitle: getFullNameFromUser(user) + ` wirklich aus Team ${userTeam.team.name} entfernen?`, - }; - this.dialog - .open(CancelDialogComponent, dialogConfig) + this.dialogService + .open(CancelDialogComponent, { + data: { + dialogTitle: getFullNameFromUser(user) + ` wirklich aus Team ${userTeam.team.name} entfernen?`, + }, + }) .afterClosed() .pipe( filter((confirm) => confirm), diff --git a/frontend/src/app/team-management/member-list/member-list-table/member-list-table.component.spec.ts b/frontend/src/app/team-management/member-list/member-list-table/member-list-table.component.spec.ts index 35aac07df4..c28ce3d90d 100644 --- a/frontend/src/app/team-management/member-list/member-list-table/member-list-table.component.spec.ts +++ b/frontend/src/app/team-management/member-list/member-list-table/member-list-table.component.spec.ts @@ -8,7 +8,7 @@ import { UserService } from '../../../services/user.service'; import { TeamService } from '../../../services/team.service'; import { Team } from '../../../shared/types/model/Team'; import { MatTableModule } from '@angular/material/table'; -import { MatDialog } from '@angular/material/dialog'; +import { DialogService } from '../../../services/dialog.service'; describe('MemberListTableComponent', () => { let component: MemberListTableComponent; @@ -27,7 +27,7 @@ describe('MemberListTableComponent', () => { updateOrAddTeamMembership: jest.fn(), }; - const dialogMock = { + const dialogService = { open: jest.fn(), }; @@ -38,7 +38,7 @@ describe('MemberListTableComponent', () => { providers: [ { provide: UserService, useValue: userServiceMock }, { provide: TeamService, useValue: teamServiceMock }, - { provide: MatDialog, useValue: dialogMock }, + { provide: DialogService, useValue: dialogService }, ], }).compileComponents(); @@ -94,7 +94,7 @@ describe('MemberListTableComponent', () => { teamServiceMock.removeUserFromTeam.mockReturnValue(of(null)); userServiceMock.reloadUsers.mockReturnValue(of()); userServiceMock.reloadCurrentUser.mockReturnValue(of()); - dialogMock.open.mockReturnValue({ + dialogService.open.mockReturnValue({ afterClosed: () => of(true), }); @@ -115,7 +115,7 @@ describe('MemberListTableComponent', () => { teamServiceMock.removeUserFromTeam.mockReturnValue(of(null)); userServiceMock.reloadUsers.mockReturnValue(of()); userServiceMock.reloadCurrentUser.mockReturnValue(of()); - dialogMock.open.mockReturnValue({ + dialogService.open.mockReturnValue({ afterClosed: () => of(false), }); diff --git a/frontend/src/app/team-management/member-list/member-list-table/member-list-table.component.ts b/frontend/src/app/team-management/member-list/member-list-table/member-list-table.component.ts index 64f21c9c79..db9f7cb95d 100644 --- a/frontend/src/app/team-management/member-list/member-list-table/member-list-table.component.ts +++ b/frontend/src/app/team-management/member-list/member-list-table/member-list-table.component.ts @@ -8,9 +8,8 @@ import { UserService } from '../../../services/user.service'; import { getRouteToUserDetails } from '../../../shared/routeUtils'; import { BehaviorSubject, filter, mergeMap, Subject, takeUntil } from 'rxjs'; import { UserTeam } from '../../../shared/types/model/UserTeam'; -import { MatDialog, MatDialogConfig } from '@angular/material/dialog'; -import { CancelDialogComponent, CancelDialogData } from '../../../shared/dialog/cancel-dialog/cancel-dialog.component'; -import { OKR_DIALOG_CONFIG } from '../../../shared/constantLibary'; +import { CancelDialogComponent } from '../../../shared/dialog/cancel-dialog/cancel-dialog.component'; +import { DialogService } from '../../../services/dialog.service'; @Component({ selector: 'app-member-list-table', @@ -30,7 +29,7 @@ export class MemberListTableComponent implements OnInit, OnDestroy { constructor( private readonly teamService: TeamService, private readonly userService: UserService, - private readonly dialog: MatDialog, + private readonly dialogService: DialogService, ) {} ngOnInit() { @@ -59,12 +58,12 @@ export class MemberListTableComponent implements OnInit, OnDestroy { event.stopPropagation(); event.preventDefault(); - const dialogConfig: MatDialogConfig = OKR_DIALOG_CONFIG; - dialogConfig.data = { - dialogTitle: `${entry.firstname} ${entry.lastname} wirklich aus Team ${this.selectedTeam$.value?.name} entfernen?`, - }; - this.dialog - .open(CancelDialogComponent, dialogConfig) + this.dialogService + .open(CancelDialogComponent, { + data: { + dialogTitle: `${entry.firstname} ${entry.lastname} wirklich aus Team ${this.selectedTeam$.value?.name} entfernen?`, + }, + }) .afterClosed() .pipe( filter((confirm) => confirm), diff --git a/frontend/src/app/team-management/member-list/member-list.component.spec.ts b/frontend/src/app/team-management/member-list/member-list.component.spec.ts index 82097ccee8..2d5d822c51 100644 --- a/frontend/src/app/team-management/member-list/member-list.component.spec.ts +++ b/frontend/src/app/team-management/member-list/member-list.component.spec.ts @@ -10,15 +10,14 @@ import { team1, team2, team3, testUser, users } from '../../shared/testData'; import { convertFromUser, convertFromUsers, UserTableEntry } from '../../shared/types/model/UserTableEntry'; import { UserRole } from '../../shared/types/enums/UserRole'; import { TeamService } from '../../services/team.service'; -import { MatDialog } from '@angular/material/dialog'; import { AddMemberToTeamDialogComponent } from '../add-member-to-team-dialog/add-member-to-team-dialog.component'; -import { OKR_DIALOG_CONFIG } from '../../shared/constantLibary'; import { AddEditTeamDialog } from '../add-edit-team-dialog/add-edit-team-dialog.component'; import { TranslateTestingModule } from 'ngx-translate-testing'; import { MatTableDataSource } from '@angular/material/table'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { MemberListTableComponent } from './member-list-table/member-list-table.component'; import { MemberListMobileComponent } from './member-list-mobile/member-list-mobile.component'; +import { DialogService } from '../../services/dialog.service'; const userServiceMock = { getUsers: jest.fn(), @@ -41,7 +40,7 @@ const routerMock = { navigateByUrl: jest.fn(), }; -const dialogMock = { +const dialogService = { open: jest.fn(), }; @@ -58,7 +57,7 @@ describe('MemberListComponent', () => { { provide: ActivatedRoute, useValue: activatedRouteMock }, { provide: TeamService, useValue: teamServiceMock }, { provide: Router, useValue: routerMock }, - { provide: MatDialog, useValue: dialogMock }, + { provide: DialogService, useValue: dialogService }, ChangeDetectorRef, ], }); @@ -181,7 +180,7 @@ describe('MemberListComponent', () => { it('deleteTeam should trigger teamService.deleteTeam and navigate', fakeAsync(() => { routerMock.navigateByUrl.mockReturnValue(of(null)); teamServiceMock.deleteTeam.mockReturnValue(of(null)); - dialogMock.open.mockReturnValue({ + dialogService.open.mockReturnValue({ afterClosed: () => of(true), }); @@ -201,7 +200,7 @@ describe('MemberListComponent', () => { it('deleteTeam should not trigger teamService.deleteTeam if dialog is canceled', fakeAsync(() => { routerMock.navigateByUrl.mockReturnValue(of(null)); teamServiceMock.deleteTeam.mockReturnValue(of(null)); - dialogMock.open.mockReturnValue({ + dialogService.open.mockReturnValue({ afterClosed: () => of(false), }); @@ -214,18 +213,20 @@ describe('MemberListComponent', () => { it('addMemberToTeam should open dialog', () => { component.selectedTeam$.next(team1); component.dataSource = new MatTableDataSource([]); - dialogMock.open.mockReturnValue({ + dialogService.open.mockReturnValue({ afterClosed: () => of(null), }); component.addMemberToTeam(); - const expectedDialogConfig = OKR_DIALOG_CONFIG; - expectedDialogConfig.data = { - team: team1, - currentUsersOfTeam: component.dataSource, - }; - - expect(dialogMock.open).toBeCalledWith(AddMemberToTeamDialogComponent, expectedDialogConfig); + expect(dialogService.open).toBeCalledWith( + AddMemberToTeamDialogComponent, + expect.objectContaining({ + data: { + team: team1, + currentUsersOfTeam: component.dataSource.filteredData, + }, + }), + ); }); it('should showAddMemberToTeam if selectedTeam is set and selectedTeam is writable', () => { @@ -241,16 +242,18 @@ describe('MemberListComponent', () => { it('edit team should open dialog', () => { component.selectedTeam$.next(team1); - dialogMock.open.mockReturnValue({ + dialogService.open.mockReturnValue({ afterClosed: () => of(null), }); component.editTeam(); - const expectedDialogConfig = OKR_DIALOG_CONFIG; - expectedDialogConfig.data = { - team: team1, - }; - - expect(dialogMock.open).toBeCalledWith(AddEditTeamDialog, expectedDialogConfig); + expect(dialogService.open).toBeCalledWith( + AddEditTeamDialog, + expect.objectContaining({ + data: { + team: team1, + }, + }), + ); }); }); diff --git a/frontend/src/app/team-management/member-list/member-list.component.ts b/frontend/src/app/team-management/member-list/member-list.component.ts index 6091de2642..4efb6b8cb6 100644 --- a/frontend/src/app/team-management/member-list/member-list.component.ts +++ b/frontend/src/app/team-management/member-list/member-list.component.ts @@ -6,16 +6,12 @@ import { User } from '../../shared/types/model/User'; import { convertFromUsers, UserTableEntry } from '../../shared/types/model/UserTableEntry'; import { TeamService } from '../../services/team.service'; import { Team } from '../../shared/types/model/Team'; -import { MatDialog, MatDialogConfig } from '@angular/material/dialog'; -import { - AddMemberToTeamDialogComponent, - AddMemberToTeamDialogComponentData, -} from '../add-member-to-team-dialog/add-member-to-team-dialog.component'; -import { OKR_DIALOG_CONFIG } from '../../shared/constantLibary'; +import { AddMemberToTeamDialogComponent } from '../add-member-to-team-dialog/add-member-to-team-dialog.component'; import { AddEditTeamDialog } from '../add-edit-team-dialog/add-edit-team-dialog.component'; import { MatTableDataSource } from '@angular/material/table'; import { CancelDialogComponent, CancelDialogData } from '../../shared/dialog/cancel-dialog/cancel-dialog.component'; import { InviteUserDialogComponent } from '../invite-user-dialog/invite-user-dialog.component'; +import { DialogService } from '../../services/dialog.service'; @Component({ selector: 'app-member-list', @@ -35,7 +31,7 @@ export class MemberListComponent implements OnInit, OnDestroy, AfterViewInit { private readonly cd: ChangeDetectorRef, private readonly teamService: TeamService, private readonly router: Router, - private readonly dialog: MatDialog, + private readonly dialogService: DialogService, ) {} public ngOnInit(): void {} @@ -89,13 +85,13 @@ export class MemberListComponent implements OnInit, OnDestroy, AfterViewInit { } deleteTeam(selectedTeam: Team) { - const dialogConfig: MatDialogConfig = OKR_DIALOG_CONFIG; - dialogConfig.data = { - dialogTitle: selectedTeam.name + ' wirklich löschen?', - dialogText: 'Soll das Team und dessen OKRs wirklich gelöscht werden?', - }; - this.dialog - .open(CancelDialogComponent, dialogConfig) + this.dialogService + .open(CancelDialogComponent, { + data: { + dialogTitle: selectedTeam.name + ' wirklich löschen?', + dialogText: 'Soll das Team und dessen OKRs wirklich gelöscht werden?', + }, + }) .afterClosed() .pipe( filter((confirm) => confirm), @@ -109,17 +105,17 @@ export class MemberListComponent implements OnInit, OnDestroy, AfterViewInit { } addMemberToTeam() { - const dialogConfig: MatDialogConfig = OKR_DIALOG_CONFIG; - dialogConfig.data = { - team: this.selectedTeam$.value!, - currentUsersOfTeam: this.dataSource.data, - }; - const dialogRef = this.dialog.open(AddMemberToTeamDialogComponent, dialogConfig); + const dialogRef = this.dialogService.open(AddMemberToTeamDialogComponent, { + data: { + team: this.selectedTeam$.value!, + currentUsersOfTeam: this.dataSource.data, + }, + }); dialogRef.afterClosed().subscribe(() => this.cd.markForCheck()); } inviteMember() { - this.dialog.open(InviteUserDialogComponent, OKR_DIALOG_CONFIG).afterClosed().subscribe(); + this.dialogService.open(InviteUserDialogComponent).afterClosed().subscribe(); } showInviteMember(): boolean { @@ -131,11 +127,7 @@ export class MemberListComponent implements OnInit, OnDestroy, AfterViewInit { } editTeam(): void { - const dialogConfig = OKR_DIALOG_CONFIG; - dialogConfig.data = { - team: this.selectedTeam$.value, - }; - const dialogRef = this.dialog.open(AddEditTeamDialog, dialogConfig); + const dialogRef = this.dialogService.open(AddEditTeamDialog, { data: { team: this.selectedTeam$.value } }); dialogRef.afterClosed().subscribe(() => this.cd.markForCheck()); } } diff --git a/frontend/src/app/team-management/team-management-banner/team-management-banner.component.spec.ts b/frontend/src/app/team-management/team-management-banner/team-management-banner.component.spec.ts index abd285f27d..8872f39cfc 100644 --- a/frontend/src/app/team-management/team-management-banner/team-management-banner.component.spec.ts +++ b/frontend/src/app/team-management/team-management-banner/team-management-banner.component.spec.ts @@ -1,9 +1,8 @@ import { TeamManagementBannerComponent } from './team-management-banner.component'; import { ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing'; -import { MatDialog, MatDialogModule } from '@angular/material/dialog'; +import { MatDialogModule } from '@angular/material/dialog'; import { of } from 'rxjs'; import { AddEditTeamDialog } from '../add-edit-team-dialog/add-edit-team-dialog.component'; -import { OKR_DIALOG_CONFIG } from '../../shared/constantLibary'; import { SearchTeamManagementComponent } from '../search-team-management/search-team-management.component'; import { MatFormFieldModule } from '@angular/material/form-field'; import { MatIconModule } from '@angular/material/icon'; @@ -11,12 +10,14 @@ import { HttpClientTestingModule } from '@angular/common/http/testing'; import { TranslateModule } from '@ngx-translate/core'; import { MatAutocompleteModule } from '@angular/material/autocomplete'; import { ActivatedRoute } from '@angular/router'; +import { DialogService } from '../../services/dialog.service'; +import { OkrTangramComponent } from '../../shared/custom/okr-tangram/okr-tangram.component'; describe('TeamManagementBannerComponent', () => { let component: TeamManagementBannerComponent; let fixture: ComponentFixture; - const matDialogMock = { + const dialogServiceMock = { open: jest.fn(), }; @@ -30,9 +31,9 @@ describe('TeamManagementBannerComponent', () => { TranslateModule.forRoot(), MatAutocompleteModule, ], - declarations: [TeamManagementBannerComponent, SearchTeamManagementComponent], + declarations: [TeamManagementBannerComponent, SearchTeamManagementComponent, OkrTangramComponent], providers: [ - { provide: MatDialog, useValue: matDialogMock }, + { provide: DialogService, useValue: dialogServiceMock }, { provide: ActivatedRoute, useValue: {} }, ], }).compileComponents(); @@ -48,12 +49,12 @@ describe('TeamManagementBannerComponent', () => { }); it('createTeam should open dialog', fakeAsync(() => { - matDialogMock.open.mockReturnValue({ + dialogServiceMock.open.mockReturnValue({ afterClosed: () => of(), }); component.createTeam(); tick(); - expect(matDialogMock.open).toBeCalledTimes(1); - expect(matDialogMock.open).toBeCalledWith(AddEditTeamDialog, OKR_DIALOG_CONFIG); + expect(dialogServiceMock.open).toBeCalledTimes(1); + expect(dialogServiceMock.open).toBeCalledWith(AddEditTeamDialog); })); }); diff --git a/frontend/src/app/team-management/team-management-banner/team-management-banner.component.ts b/frontend/src/app/team-management/team-management-banner/team-management-banner.component.ts index 76a75755fc..9d2753a8ad 100644 --- a/frontend/src/app/team-management/team-management-banner/team-management-banner.component.ts +++ b/frontend/src/app/team-management/team-management-banner/team-management-banner.component.ts @@ -1,7 +1,7 @@ import { Component } from '@angular/core'; -import { MatDialog, MatDialogRef } from '@angular/material/dialog'; +import { MatDialogRef } from '@angular/material/dialog'; import { AddEditTeamDialog } from '../add-edit-team-dialog/add-edit-team-dialog.component'; -import { OKR_DIALOG_CONFIG } from '../../shared/constantLibary'; +import { DialogService } from '../../services/dialog.service'; @Component({ selector: 'app-team-management-banner', @@ -11,13 +11,11 @@ import { OKR_DIALOG_CONFIG } from '../../shared/constantLibary'; export class TeamManagementBannerComponent { private dialogRef!: MatDialogRef | undefined; - public constructor(private dialog: MatDialog) {} + public constructor(private dialogService: DialogService) {} createTeam(): void { if (!this.dialogRef) { - const config = OKR_DIALOG_CONFIG; - config.data = undefined; - this.dialogRef = this.dialog.open(AddEditTeamDialog, config); + this.dialogRef = this.dialogService.open(AddEditTeamDialog); this.dialogRef.afterClosed().subscribe(() => { this.dialogRef = undefined; }); diff --git a/frontend/src/assets/i18n/de.json b/frontend/src/assets/i18n/de.json index 19db19f325..386e2a596d 100644 --- a/frontend/src/assets/i18n/de.json +++ b/frontend/src/assets/i18n/de.json @@ -28,6 +28,38 @@ "DELETE_OBJECTIVE": "Möchtest du dieses Objective wirklich löschen? Zugehörige Key Results werden dadurch ebenfalls gelöscht!", "DELETE_KEY_RESULT": "Möchtest du dieses Key Result wirklich löschen? Zugehörige Check-ins werden dadurch ebenfalls gelöscht!" }, + "CONFIRMATION": { + "DRAFT_CREATE": { + "TITLE": "Check-in im Draft-Status", + "TEXT": "Dein Objective befindet sich noch im DRAFT Status. Möchtest du das Check-in trotzdem erfassen?" + }, + "RELEASE": { + "TITLE": "Objective veröffentlichen", + "TEXT": "Soll dieses Objective veröffentlicht werden?" + }, + "TO_DRAFT": { + "TITLE": "Objective als Draft speichern", + "TEXT": "Soll dieses Objective als Draft gespeichert werden?" + }, + "DELETE": { + "ACTION":{ + "TITLE": "Löschen bestätigen", + "TEXT": "Möchtest du die Action wirklich löschen?" + }, + "TEAM":{ + "TITLE": "Löschen bestätigen", + "TEXT": "Möchtest du dieses Team wirklich löschen? Zugehörige Objectives werden dadurch in allen Quartalen ebenfalls gelöscht!" + }, + "OBJECTIVE":{ + "TITLE": "Objective löschen", + "TEXT": "Möchtest du dieses Objective wirklich löschen? Zugehörige Key Results werden dadurch ebenfalls gelöscht!" + }, + "KEYRESULT":{ + "TITLE": "Key Result löschen", + "TEXT": "Möchtest du dieses Key Result wirklich löschen? Zugehörige Check-ins werden dadurch ebenfalls gelöscht!" + } + } + }, "ERROR": { "UNAUTHORIZED": "Du bist nicht autorisiert, um das Objekt mit der Id {1} zu öffnen.", "NOT_FOUND": "Das Objekt '{0}' mit der Id {1} konnte nicht gefunden werden.", diff --git a/frontend/src/style/custom_angular.components.scss b/frontend/src/style/custom_angular.components.scss index 7c5f2d23cc..6ba0c63562 100644 --- a/frontend/src/style/custom_angular.components.scss +++ b/frontend/src/style/custom_angular.components.scss @@ -53,15 +53,16 @@ padding: 1rem 0 1rem 0; } -mat-dialog-content { - max-height: 80vh !important; -} - mat-dialog-content, mat-dialog-actions { padding: 0 1.62rem 0 1.62rem !important; } +//Offset for top bar +.cdk-overlay-backdrop.cdk-overlay-dark-backdrop.cdk-overlay-backdrop-showing { + margin-top: $top-bar-height; +} + .mat-mdc-standard-chip { margin: 0 !important; @@ -80,10 +81,6 @@ mat-dialog-actions { cursor: pointer !important; } -.cdk-overlay-backdrop.cdk-overlay-dark-backdrop.cdk-overlay-backdrop-showing { - margin-top: $top-bar-height; -} - $scale-start: 0.5; $scale-end: 2; $scale-gap: 0.1; diff --git a/frontend/src/style/styles.scss b/frontend/src/style/styles.scss index 4724741de6..2db46bbbae 100644 --- a/frontend/src/style/styles.scss +++ b/frontend/src/style/styles.scss @@ -370,3 +370,9 @@ table.okr-table { .spinner-container { @extend .mt-5; } + +.okr-dialog-panel { + @extend .col-12; + + @extend .col-md-6; +} From c11a2ac75cba33ae4b86191c1e9f5f5c2dd3aa5e Mon Sep 17 00:00:00 2001 From: nevio18324 <141240169+nevio18324@users.noreply.github.com> Date: Thu, 31 Oct 2024 10:48:49 +0100 Subject: [PATCH 23/33] create tests for dialog-service (#1112) --- .../src/app/services/dialog.service.spec.ts | 25 ++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/frontend/src/app/services/dialog.service.spec.ts b/frontend/src/app/services/dialog.service.spec.ts index a454504ed1..16b4888d16 100644 --- a/frontend/src/app/services/dialog.service.spec.ts +++ b/frontend/src/app/services/dialog.service.spec.ts @@ -1,9 +1,10 @@ import { TestBed } from '@angular/core/testing'; import { DialogService } from './dialog.service'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { TranslateCompiler, TranslateModule, TranslateService } from '@ngx-translate/core'; -import { TranslateTestingModule } from 'ngx-translate-testing'; +import { TranslateModule, TranslateService } from '@ngx-translate/core'; +import { MatDialogRef } from '@angular/material/dialog'; +import { ConfirmDialogComponent } from '../shared/dialog/confirm-dialog/confirm-dialog.component'; +import { TeamComponent } from '../components/team/team.component'; describe('DialogService', () => { let service: DialogService; @@ -16,4 +17,22 @@ describe('DialogService', () => { it('should be created', () => { expect(service).toBeTruthy(); }); + + it('should open dialog', () => { + const dialog = service.open(TeamComponent); + expect(dialog).toBeInstanceOf(MatDialogRef); + expect(dialog._containerInstance._config.panelClass).toEqual(service.DIALOG_CONFIG.panelClass); + expect(dialog._containerInstance._config.maxWidth).toEqual(service.DIALOG_CONFIG.maxWidth); + expect(dialog.componentInstance).toBeInstanceOf(TeamComponent); + }); + + it('should open confirm dialog', () => { + jest.spyOn(service, 'open'); + const dialog = service.openConfirmDialog('DELETE.ACTION'); + expect(service.open).toHaveBeenCalledTimes(1); + expect(dialog).toBeInstanceOf(MatDialogRef); + expect(dialog.componentInstance).toBeInstanceOf(ConfirmDialogComponent); + expect(dialog.componentInstance.data.title).toBe('DELETE.ACTION.TITLE'); + expect(dialog.componentInstance.data.text).toBe('DELETE.ACTION.TEXT'); + }); }); From 6dc0e42d378d9d5ebca7e88e4c17fc02f8c7c51e Mon Sep 17 00:00:00 2001 From: Miguel <141239860+Miguel7373@users.noreply.github.com> Date: Thu, 31 Oct 2024 10:53:04 +0100 Subject: [PATCH 24/33] Feature/1032 update javaversion (#1103) * change java version to 21 * edit java version in pom and edit version in pipes to github action variable * add generic version to name strings --------- Co-authored-by: Nevio Di Gennaro Co-authored-by: GitHub Actions --- .github/workflows/backend-test-action.yml | 4 ++-- .github/workflows/demo-deploy-action.yml | 4 ++-- .github/workflows/deploy-action.yml | 4 ++-- .github/workflows/format-action.yml | 4 ++-- .github/workflows/frontend-test-action.yml | 4 ++-- .github/workflows/staging-deploy-action.yml | 4 ++-- pom.xml | 6 +++--- 7 files changed, 15 insertions(+), 15 deletions(-) diff --git a/.github/workflows/backend-test-action.yml b/.github/workflows/backend-test-action.yml index 428368d7ff..c31d16410f 100644 --- a/.github/workflows/backend-test-action.yml +++ b/.github/workflows/backend-test-action.yml @@ -10,10 +10,10 @@ jobs: - name: Checkout uses: actions/checkout@v4 - - name: Set up JDK 17 + - name: Set up JDK ${{vars.JAVA_VERSION}} uses: actions/setup-java@v4 with: - java-version: '17' + java-version: ${{vars.JAVA_VERSION}} distribution: 'adopt' - name: Use Maven to run unittests and integration tests diff --git a/.github/workflows/demo-deploy-action.yml b/.github/workflows/demo-deploy-action.yml index b237b77150..59102f9846 100644 --- a/.github/workflows/demo-deploy-action.yml +++ b/.github/workflows/demo-deploy-action.yml @@ -50,10 +50,10 @@ jobs: steps: - uses: actions/checkout@v4 - - name: Set up JDK 17 + - name: Set up JDK ${{vars.JAVA_VERSION}} uses: actions/setup-java@v4 with: - java-version: '17' + java-version: ${{vars.JAVA_VERSION}} distribution: 'adopt' - name: Set up node 18 diff --git a/.github/workflows/deploy-action.yml b/.github/workflows/deploy-action.yml index d3f81979b6..44dee585c9 100644 --- a/.github/workflows/deploy-action.yml +++ b/.github/workflows/deploy-action.yml @@ -26,10 +26,10 @@ jobs: steps: - uses: actions/checkout@v4 - - name: Set up JDK 17 + - name: Set up JDK ${{vars.JAVA_VERSION}} uses: actions/setup-java@v4 with: - java-version: '17' + java-version: ${{vars.JAVA_VERSION}} distribution: 'adopt' - name: Set up node 18 diff --git a/.github/workflows/format-action.yml b/.github/workflows/format-action.yml index bf3d25bd7f..8ab0c9dd70 100644 --- a/.github/workflows/format-action.yml +++ b/.github/workflows/format-action.yml @@ -46,10 +46,10 @@ jobs: - name: Checkout uses: actions/checkout@v4 - - name: Set up JDK 17 + - name: Set up JDK ${{vars.JAVA_VERSION}} uses: actions/setup-java@v4 with: - java-version: '17' + java-version: ${{vars.JAVA_VERSION}} distribution: 'adopt' server-id: github settings-path: ${{github.workspace}} diff --git a/.github/workflows/frontend-test-action.yml b/.github/workflows/frontend-test-action.yml index 73c35e400c..2f34b3596b 100644 --- a/.github/workflows/frontend-test-action.yml +++ b/.github/workflows/frontend-test-action.yml @@ -28,10 +28,10 @@ jobs: - name: Checkout uses: actions/checkout@v4 - - name: Set up JDK 17 + - name: Set up JDK ${{vars.JAVA_VERSION}} uses: actions/setup-java@v4 with: - java-version: '17' + java-version: ${{vars.JAVA_VERSION}} distribution: 'adopt' - uses: abhi1693/setup-browser@v0.3.5 diff --git a/.github/workflows/staging-deploy-action.yml b/.github/workflows/staging-deploy-action.yml index 1a4495dcd9..07d18713e1 100644 --- a/.github/workflows/staging-deploy-action.yml +++ b/.github/workflows/staging-deploy-action.yml @@ -50,10 +50,10 @@ jobs: steps: - uses: actions/checkout@v4 - - name: Set up JDK 17 + - name: Set up JDK ${{vars.JAVA_VERSION}} uses: actions/setup-java@v4 with: - java-version: '17' + java-version: ${{vars.JAVA_VERSION}} distribution: 'adopt' - name: Set up node diff --git a/pom.xml b/pom.xml index e2b16d5405..0dc3a93bff 100644 --- a/pom.xml +++ b/pom.xml @@ -22,9 +22,9 @@ - 17 - 17 - 17 + 21 + 21 + 21 UTF-8 UTF-8 From f6cac2425e4432bc0f0ad573e33685d673adfd3c Mon Sep 17 00:00:00 2001 From: Yanick Minder <79108296+kcinay055679@users.noreply.github.com> Date: Mon, 4 Nov 2024 14:50:57 +0100 Subject: [PATCH 25/33] Get current quarter from backend (#1039) * add auto data-migration * jar is now debuggable * add jar debug dev tools only on profile * change log levels * try to fix autorestart of spring * complete auto restart of container * rename intelij config and change log level of spring to debug in staging config * rename folder * update docker compose file * use external profile to disable formatter * clean up * add jar debug dev tools only on profile * add api endpoint * use api to get current quarter * convert quarter to a class instead of an interface * use class syntax * update specs to use the class syntax for quarters * mock new class function in test * add integratin test for current quarter endpoint * fix default quarter for new objective selection * use v2 for quarter url and update tests accoridingly * change merhod to fakeAsync * try to override provider mid test * Dispatch change instead of input event in onSubmit create test and get element by test id instead of id * Remove duplicate profile in pom * Update swagger annotations of get current quarter and fix usage of wrong implementation class for get current quarters * Remove unused import in common.ts * add dependency for hot reload in again * [FM] Automated formating frontend * readd local debug run config --------- Co-authored-by: Miguel Lehmann Co-authored-by: Jannik Pulfer Co-authored-by: GitHub Actions --- .../okr/controller/QuarterController.java | 12 +- .../okr/controller/QuarterControllerIT.java | 14 +- .../key-result-form.component.spec.ts | 3 +- .../keyresult-dialog.component.spec.ts | 3 +- .../quarter-filter.component.html | 6 +- .../quarter-filter.component.spec.ts | 21 +-- .../quarter-filter.component.ts | 26 +-- frontend/src/app/services/quarter.service.ts | 14 +- frontend/src/app/shared/common.ts | 4 - .../objective-form.component.html | 2 +- .../objective-form.component.spec.ts | 148 +++++++++--------- .../objective-form.component.ts | 14 +- frontend/src/app/shared/testData.ts | 70 ++------- .../src/app/shared/types/model/Quarter.ts | 28 +++- 14 files changed, 184 insertions(+), 181 deletions(-) diff --git a/backend/src/main/java/ch/puzzle/okr/controller/QuarterController.java b/backend/src/main/java/ch/puzzle/okr/controller/QuarterController.java index eb92ef5841..1cc1383dbc 100644 --- a/backend/src/main/java/ch/puzzle/okr/controller/QuarterController.java +++ b/backend/src/main/java/ch/puzzle/okr/controller/QuarterController.java @@ -17,7 +17,7 @@ import java.util.List; @RestController -@RequestMapping("api/v1/quarters") +@RequestMapping("api/v2/quarters") public class QuarterController { private final QuarterBusinessService quarterBusinessService; @@ -28,9 +28,17 @@ public QuarterController(QuarterBusinessService quarterBusinessService) { @Operation(summary = "Get quarters", description = "Get a List of quarters depending on current date") @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "Returned a List of quarters", content = { - @Content(mediaType = "application/json", schema = @Schema(implementation = TeamDto.class)) }) }) + @Content(mediaType = "application/json", schema = @Schema(implementation = Quarter.class)) }) }) @GetMapping("") public ResponseEntity> getCurrentQuarters() { return ResponseEntity.status(HttpStatus.OK).body(this.quarterBusinessService.getQuarters()); } + + @Operation(summary = "Get current quarter", description = "Get the current quarter depending on current date") + @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "Returned the current quarter", content = { + @Content(mediaType = "application/json", schema = @Schema(implementation = Quarter.class)) }) }) + @GetMapping("/current") + public ResponseEntity getCurrentQuarter() { + return ResponseEntity.status(HttpStatus.OK).body(this.quarterBusinessService.getCurrentQuarter()); + } } diff --git a/backend/src/test/java/ch/puzzle/okr/controller/QuarterControllerIT.java b/backend/src/test/java/ch/puzzle/okr/controller/QuarterControllerIT.java index cfd844139f..5a582cd117 100644 --- a/backend/src/test/java/ch/puzzle/okr/controller/QuarterControllerIT.java +++ b/backend/src/test/java/ch/puzzle/okr/controller/QuarterControllerIT.java @@ -7,6 +7,7 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.BDDMockito; +import org.mockito.Mockito; import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; @@ -48,7 +49,7 @@ class QuarterControllerIT { void shouldGetAllQuarters() throws Exception { BDDMockito.given(quarterBusinessService.getQuarters()).willReturn(quaterList); - mvc.perform(get("/api/v1/quarters").contentType(MediaType.APPLICATION_JSON)) + mvc.perform(get("/api/v2/quarters").contentType(MediaType.APPLICATION_JSON)) .andExpect(MockMvcResultMatchers.status().isOk()).andExpect(jsonPath("$", Matchers.hasSize(3))) .andExpect(jsonPath("$[0].id", Is.is(1))).andExpect(jsonPath("$[0].label", Is.is("GJ 22/23-Q2"))) .andExpect(jsonPath("$[0].startDate", Is.is(LocalDate.of(2022, 9, 1).toString()))) @@ -64,7 +65,14 @@ void shouldGetAllQuarters() throws Exception { void shouldGetAllTeamsIfNoTeamsExists() throws Exception { BDDMockito.given(quarterBusinessService.getQuarters()).willReturn(Collections.emptyList()); - mvc.perform(get("/api/v1/quarters").contentType(MediaType.APPLICATION_JSON)) + mvc.perform(get("/api/v2/quarters").contentType(MediaType.APPLICATION_JSON)) .andExpect(MockMvcResultMatchers.status().isOk()).andExpect(jsonPath("$", Matchers.hasSize(0))); } -} \ No newline at end of file + + @Test + void shouldCallCurrentQuarterAfterRequest() throws Exception { + mvc.perform(get("/api/v2/quarters/current").contentType(MediaType.APPLICATION_JSON)); + + BDDMockito.verify(quarterBusinessService, Mockito.times(1)).getCurrentQuarter(); + } +} diff --git a/frontend/src/app/components/key-result-form/key-result-form.component.spec.ts b/frontend/src/app/components/key-result-form/key-result-form.component.spec.ts index 724f517822..d0d2479b8a 100644 --- a/frontend/src/app/components/key-result-form/key-result-form.component.spec.ts +++ b/frontend/src/app/components/key-result-form/key-result-form.component.spec.ts @@ -27,6 +27,7 @@ import { KeyResultMetric } from '../../shared/types/model/KeyResultMetric'; import { KeyResultOrdinal } from '../../shared/types/model/KeyResultOrdinal'; import { TranslateTestingModule } from 'ngx-translate-testing'; import * as de from '../../../assets/i18n/de.json'; +import { Quarter } from '../../shared/types/model/Quarter'; describe('KeyResultFormComponent', () => { let component: KeyResultFormComponent; @@ -70,7 +71,7 @@ describe('KeyResultFormComponent', () => { const keyResultObjective: KeyResultObjective = { id: 2, state: State.ONGOING, - quarter: { id: 1, label: 'GJ 22/23-Q2', endDate: new Date(), startDate: new Date() }, + quarter: new Quarter(1, 'GJ 22/23-Q2', new Date(), new Date()), }; const keyResultFormGroup = new FormGroup({ diff --git a/frontend/src/app/components/keyresult-dialog/keyresult-dialog.component.spec.ts b/frontend/src/app/components/keyresult-dialog/keyresult-dialog.component.spec.ts index 17d9f4d017..bf74d658ce 100644 --- a/frontend/src/app/components/keyresult-dialog/keyresult-dialog.component.spec.ts +++ b/frontend/src/app/components/keyresult-dialog/keyresult-dialog.component.spec.ts @@ -24,6 +24,7 @@ import { TranslateModule, TranslateService } from '@ngx-translate/core'; import { DragDropModule } from '@angular/cdk/drag-drop'; import { UserService } from '../../services/user.service'; import { KeyResultFormComponent } from '../key-result-form/key-result-form.component'; +import { Quarter } from '../../shared/types/model/Quarter'; describe('KeyresultDialogComponent', () => { let component: KeyresultDialogComponent; @@ -55,7 +56,7 @@ describe('KeyresultDialogComponent', () => { let keyResultObjective: KeyResultObjective = { id: 2, state: State.ONGOING, - quarter: { id: 1, label: 'GJ 22/23-Q2', endDate: new Date(), startDate: new Date() }, + quarter: new Quarter(1, 'GJ 22/23-Q2', new Date(), new Date()), }; let fullKeyResultMetric = { diff --git a/frontend/src/app/components/quarter-filter/quarter-filter.component.html b/frontend/src/app/components/quarter-filter/quarter-filter.component.html index 14f0ca9cd9..6d5fd442b8 100644 --- a/frontend/src/app/components/quarter-filter/quarter-filter.component.html +++ b/frontend/src/app/components/quarter-filter/quarter-filter.component.html @@ -1,12 +1,12 @@ - - {{ getQuarterLabel(quarter, i) }} + + {{ quarter.fullLabel() }} diff --git a/frontend/src/app/components/quarter-filter/quarter-filter.component.spec.ts b/frontend/src/app/components/quarter-filter/quarter-filter.component.spec.ts index e2507f74ce..bcd2e4bdd5 100644 --- a/frontend/src/app/components/quarter-filter/quarter-filter.component.spec.ts +++ b/frontend/src/app/components/quarter-filter/quarter-filter.component.spec.ts @@ -2,7 +2,6 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { QuarterFilterComponent } from './quarter-filter.component'; import { HttpClientTestingModule } from '@angular/common/http/testing'; import { OverviewService } from '../../services/overview.service'; -import { quarter } from '../../shared/testData'; import { Observable, of } from 'rxjs'; import { Quarter } from '../../shared/types/model/Quarter'; import { QuarterService } from '../../services/quarter.service'; @@ -21,16 +20,19 @@ const overviewService = { }; const quarters = [ - { id: 999, label: 'Backlog', startDate: null, endDate: null }, - { ...quarter, id: 2 }, - { ...quarter, id: 5 }, - { ...quarter, id: 7 }, + new Quarter(999, 'Backlog', null, null), + new Quarter(2, '23.02.2025', new Date(), new Date()), + new Quarter(5, '23.02.2025', new Date(), new Date()), + new Quarter(7, '23.02.2025', new Date(), new Date()), ]; const quarterService = { getAllQuarters(): Observable { return of(quarters); }, + getCurrentQuarter(): Observable { + return of(quarters[2]); + }, }; describe('QuarterFilterComponent', () => { @@ -68,13 +70,14 @@ describe('QuarterFilterComponent', () => { it('should set correct default quarter if no route param is defined', async () => { jest.spyOn(component, 'changeDisplayedQuarter'); + jest.spyOn(quarters[2] as any, 'isCurrent').mockReturnValue(true); const quarterSelect = await loader.getHarness(MatSelectHarness); expect(quarterSelect).toBeTruthy(); component.ngOnInit(); fixture.detectChanges(); - expect(component.quarterId).toBe(quarters[2].id); + expect(component.currentQuarterId).toBe(quarters[2].id); expect(await quarterSelect.getValueText()).toBe(quarters[2].label + ' Aktuell'); - expect(component.changeDisplayedQuarter).toHaveBeenCalledTimes(0); + expect(component.changeDisplayedQuarter).toHaveBeenCalledTimes(1); }); it('should set correct value in form according to route param', async () => { @@ -89,7 +92,7 @@ describe('QuarterFilterComponent', () => { component.ngOnInit(); fixture.detectChanges(); - expect(component.quarterId).toBe(quarters[3].id); + expect(component.currentQuarterId).toBe(quarters[3].id); expect(await quarterSelect.getValueText()).toBe(quarters[3].label); expect(component.changeDisplayedQuarter).toHaveBeenCalledTimes(1); }); @@ -106,7 +109,7 @@ describe('QuarterFilterComponent', () => { routerHarness.detectChanges(); component.ngOnInit(); fixture.detectChanges(); - expect(component.quarterId).toBe(quarters[2].id); + expect(component.currentQuarterId).toBe(quarters[2].id); expect(await quarterSelect.getValueText()).toBe(quarters[2].label + ' Aktuell'); expect(component.changeDisplayedQuarter).toHaveBeenCalledTimes(1); expect(router.url).toBe('/?quarter=' + quarters[2].id); diff --git a/frontend/src/app/components/quarter-filter/quarter-filter.component.ts b/frontend/src/app/components/quarter-filter/quarter-filter.component.ts index 1374bbc135..023a242adf 100644 --- a/frontend/src/app/components/quarter-filter/quarter-filter.component.ts +++ b/frontend/src/app/components/quarter-filter/quarter-filter.component.ts @@ -1,10 +1,10 @@ import { ChangeDetectionStrategy, Component, EventEmitter, OnInit, Output } from '@angular/core'; import { QuarterService } from '../../services/quarter.service'; import { Quarter } from '../../shared/types/model/Quarter'; -import { BehaviorSubject } from 'rxjs'; +import { BehaviorSubject, forkJoin } from 'rxjs'; import { ActivatedRoute, Router } from '@angular/router'; -import { getQuarterLabel, getValueFromQuery } from '../../shared/common'; import { RefreshDataService } from '../../services/refresh-data.service'; +import { getValueFromQuery } from '../../shared/common'; @Component({ selector: 'app-quarter-filter', @@ -14,7 +14,7 @@ import { RefreshDataService } from '../../services/refresh-data.service'; export class QuarterFilterComponent implements OnInit { quarters: BehaviorSubject = new BehaviorSubject([]); @Output() quarterLabel$ = new EventEmitter(); - quarterId: number = -1; + currentQuarterId: number = -1; constructor( private quarterService: QuarterService, @@ -24,28 +24,30 @@ export class QuarterFilterComponent implements OnInit { ) {} ngOnInit() { - this.quarterService.getAllQuarters().subscribe((quarters) => { + const allQuarters$ = this.quarterService.getAllQuarters(); + const currentQuarter$ = this.quarterService.getCurrentQuarter(); + forkJoin([allQuarters$, currentQuarter$]).subscribe(([quarters, currentQuarter]) => { this.quarters.next(quarters); const quarterQuery = this.route.snapshot.queryParams['quarter']; const quarterId: number = getValueFromQuery(quarterQuery)[0]; if (quarters.map((quarter) => quarter.id).includes(quarterId)) { - this.quarterId = quarterId; + this.currentQuarterId = quarterId; this.changeDisplayedQuarter(); } else { - this.quarterId = quarters[2].id; - if (quarterQuery !== undefined) { - this.changeDisplayedQuarter(); - } else { + this.currentQuarterId = currentQuarter.id; + this.changeDisplayedQuarter(); + + if (quarterQuery === undefined) { this.refreshDataService.quarterFilterReady.next(); } } - const quarterLabel = quarters.find((e) => e.id == this.quarterId)?.label || ''; + const quarterLabel = quarters.find((e) => e.id == this.currentQuarterId)?.label || ''; this.quarterLabel$.next(quarterLabel); }); } changeDisplayedQuarter() { - const id = this.quarterId; + const id = this.currentQuarterId; const quarterLabel = this.quarters.getValue().find((e) => e.id == id)?.label || ''; this.quarterLabel$.next(quarterLabel); @@ -53,6 +55,4 @@ export class QuarterFilterComponent implements OnInit { .navigate([], { queryParams: { quarter: id } }) .then(() => this.refreshDataService.quarterFilterReady.next()); } - - protected readonly getQuarterLabel = getQuarterLabel; } diff --git a/frontend/src/app/services/quarter.service.ts b/frontend/src/app/services/quarter.service.ts index 3991320476..a811c6052e 100644 --- a/frontend/src/app/services/quarter.service.ts +++ b/frontend/src/app/services/quarter.service.ts @@ -1,7 +1,7 @@ import { Injectable } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { Quarter } from '../shared/types/model/Quarter'; -import { Observable } from 'rxjs'; +import { map, Observable } from 'rxjs'; @Injectable({ providedIn: 'root', @@ -10,6 +10,16 @@ export class QuarterService { constructor(private http: HttpClient) {} getAllQuarters(): Observable { - return this.http.get('/api/v1/quarters'); + return this.http + .get('/api/v2/quarters') + .pipe( + map((quarters) => + quarters.map((quarter) => new Quarter(quarter.id, quarter.label, quarter.startDate, quarter.endDate)), + ), + ); + } + + getCurrentQuarter(): Observable { + return this.http.get('/api/v2/quarters/current'); } } diff --git a/frontend/src/app/shared/common.ts b/frontend/src/app/shared/common.ts index ec0d3de2b3..57f70b2c0e 100644 --- a/frontend/src/app/shared/common.ts +++ b/frontend/src/app/shared/common.ts @@ -87,10 +87,6 @@ export function formInputCheck(form: FormGroup, propertyName: string) { } } -export function getQuarterLabel(quarter: any, index: number): string { - return index == 2 ? quarter.label + ' Aktuell' : quarter.label; -} - export function isMobileDevice() { return window.navigator.userAgent.toLowerCase().includes('mobile'); } diff --git a/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.html b/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.html index 28e461b3bf..742809d8f3 100644 --- a/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.html +++ b/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.html @@ -60,7 +60,7 @@ > diff --git a/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.spec.ts b/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.spec.ts index 47ca3ba9ff..4e667435bc 100644 --- a/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.spec.ts +++ b/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.spec.ts @@ -1,4 +1,4 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing'; import { ObjectiveFormComponent } from './objective-form.component'; import { HttpClientTestingModule } from '@angular/common/http/testing'; @@ -35,14 +35,21 @@ let objectiveService = { deleteObjective: jest.fn(), }; +interface MatDialogDataInterface { + objective: { objectiveId: number | undefined; teamId: number | undefined }; +} + const quarterService = { getAllQuarters(): Observable { return of([ - { id: 1, startDate: quarter.startDate, endDate: quarter.endDate, label: quarter.label }, - { id: 2, startDate: quarter.startDate, endDate: quarter.endDate, label: quarter.label }, - { id: 999, startDate: null, endDate: null, label: 'Backlog' }, + new Quarter(1, quarter.label, quarter.startDate, quarter.endDate), + new Quarter(2, quarter.label, quarter.startDate, quarter.endDate), + new Quarter(999, 'Backlog', null, null), ]); }, + getCurrentQuarter(): Observable { + return of(new Quarter(2, quarter.label, quarter.startDate, quarter.endDate)); + }, }; const teamService = { @@ -58,7 +65,7 @@ const dialogMock = { close: jest.fn(), }; -let matDataMock: { objective: { objectiveId: number | undefined; teamId: number | undefined } } = { +let matDataMock: MatDialogDataInterface = { objective: { objectiveId: undefined, teamId: 1, @@ -115,65 +122,69 @@ describe('ObjectiveDialogComponent', () => { expect(component).toBeTruthy(); }); - it.each([['DRAFT'], ['ONGOING']])('onSubmit create', async (state: string) => { - //Prepare data - let title: string = 'title'; - let description: string = 'description'; - let createKeyresults: boolean = true; - let quarter: number = 0; - let team: number = 0; - teamService.getAllTeams().subscribe((teams) => { - team = teams[0].id; - }); - quarterService.getAllQuarters().subscribe((quarters) => { - quarter = quarters[1].id; - }); - - // Get input elements and set values - const titleInput: HTMLInputElement = fixture.debugElement.query(By.css('[data-testId="title"]')).nativeElement; - titleInput.value = title; - const descriptionInput: HTMLInputElement = fixture.debugElement.query( - By.css('[data-testId="description"]'), - ).nativeElement; - descriptionInput.value = description; - const checkBox = await loader.getHarness(MatCheckboxHarness); - await checkBox.check(); - const quarterSelect: HTMLSelectElement = fixture.debugElement.query(By.css('#quarter')).nativeElement; - quarterSelect.value = quarter.toString(); - - // Trigger update of form - fixture.detectChanges(); - titleInput.dispatchEvent(new Event('input')); - descriptionInput.dispatchEvent(new Event('input')); - quarterSelect.dispatchEvent(new Event('input')); - - const rawFormValue = component.objectiveForm.getRawValue(); - expect(rawFormValue.description).toBe(description); - expect(rawFormValue.quarter).toBe(quarter); - expect(rawFormValue.team).toBe(team); - expect(rawFormValue.title).toBe(title); - expect(rawFormValue.createKeyResults).toBe(createKeyresults); - - objectiveService.createObjective.mockReturnValue(of({ ...objective, state: state })); - component.onSubmit(state); - - expect(dialogMock.close).toHaveBeenCalledWith({ - addKeyResult: createKeyresults, - delete: false, - objective: { - description: description, - id: 5, - version: 1, - quarterId: 2, - quarterLabel: 'GJ 22/23-Q2', - state: State[state as keyof typeof State], - teamId: 2, - title: title, - writeable: true, - }, - teamId: 1, - }); - }); + it.each([['DRAFT'], ['ONGOING']])( + 'onSubmit create', + fakeAsync((state: string) => { + //Prepare data + let title: string = 'title'; + let description: string = 'description'; + let createKeyresults: boolean = true; + let quarter: number = 0; + let team: number = 0; + teamService.getAllTeams().subscribe((teams) => { + team = teams[0].id; + }); + quarterService.getAllQuarters().subscribe((quarters) => { + quarter = quarters[1].id; + }); + + // Get input elements and set values + const titleInput: HTMLInputElement = fixture.debugElement.query(By.css('[data-testId="title"]')).nativeElement; + titleInput.value = title; + const descriptionInput: HTMLInputElement = fixture.debugElement.query( + By.css('[data-testId="description"]'), + ).nativeElement; + descriptionInput.value = description; + loader.getHarness(MatCheckboxHarness).then((checkBox) => checkBox.check()); + tick(200); + const quarterSelect: HTMLSelectElement = fixture.debugElement.query( + By.css('[data-testId="quarterSelect"]'), + ).nativeElement; + quarterSelect.value = quarter.toString(); + // Trigger update of form + fixture.detectChanges(); + titleInput.dispatchEvent(new Event('input')); + descriptionInput.dispatchEvent(new Event('input')); + quarterSelect.dispatchEvent(new Event('change')); + + const rawFormValue = component.objectiveForm.getRawValue(); + expect(rawFormValue.description).toBe(description); + expect(rawFormValue.quarter).toBe(quarter.toString()); + expect(rawFormValue.team).toBe(team); + expect(rawFormValue.title).toBe(title); + expect(rawFormValue.createKeyResults).toBe(createKeyresults); + + objectiveService.createObjective.mockReturnValue(of({ ...objective, state: state })); + component.onSubmit(state); + + expect(dialogMock.close).toHaveBeenCalledWith({ + addKeyResult: createKeyresults, + delete: false, + objective: { + description: description, + id: 5, + version: 1, + quarterId: 2, + quarterLabel: 'GJ 22/23-Q2', + state: State[state as keyof typeof State], + teamId: 2, + title: title, + writeable: true, + }, + teamId: 1, + }); + }), + ); it('should create objective', () => { matDataMock.objective.objectiveId = undefined; @@ -292,12 +303,8 @@ describe('ObjectiveDialogComponent', () => { }); it('should return if option is allowed for quarter select', async () => { - let quarter: Quarter = { - id: 999, - label: 'Backlog', - startDate: null, - endDate: null, - }; + let quarter: Quarter = new Quarter(1, 'Backlog', null, null); + let data = { action: 'duplicate', objective: { @@ -310,7 +317,6 @@ describe('ObjectiveDialogComponent', () => { expect(component.allowedOption(quarter)).toBeTruthy(); - quarter.label = 'Backlog'; expect(component.allowedOption(quarter)).toBeTruthy(); data.action = 'releaseBacklog'; fixture.detectChanges(); diff --git a/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.ts b/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.ts index 33fbb38769..9875795967 100644 --- a/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.ts +++ b/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.ts @@ -10,7 +10,8 @@ import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { State } from '../../types/enums/State'; import { ObjectiveMin } from '../../types/model/ObjectiveMin'; import { Objective } from '../../types/model/Objective'; -import { formInputCheck, getQuarterLabel, getValueFromQuery, hasFormFieldErrors } from '../../common'; +import { ConfirmDialogComponent } from '../confirm-dialog/confirm-dialog.component'; +import { formInputCheck, getValueFromQuery, hasFormFieldErrors, isMobileDevice } from '../../common'; import { ActivatedRoute } from '@angular/router'; import { GJ_REGEX_PATTERN } from '../../constantLibary'; import { TranslateService } from '@ngx-translate/core'; @@ -32,6 +33,7 @@ export class ObjectiveFormComponent implements OnInit, OnDestroy { createKeyResults: new FormControl(false), }); quarters$: Observable = of([]); + currentQuarter$: Observable = of(); quarters: Quarter[] = []; teams$: Observable = of([]); currentTeam: Subject = new Subject(); @@ -82,16 +84,16 @@ export class ObjectiveFormComponent implements OnInit, OnDestroy { const isCreating: boolean = !!this.data.objective.objectiveId; this.teams$ = this.teamService.getAllTeams().pipe(takeUntil(this.unsubscribe$)); this.quarters$ = this.quarterService.getAllQuarters(); + this.currentQuarter$ = this.quarterService.getCurrentQuarter(); const objective$ = isCreating ? this.objectiveService.getFullObjective(this.data.objective.objectiveId!) : of(this.getDefaultObjective()); - - forkJoin([objective$, this.quarters$]).subscribe(([objective, quarters]) => { + forkJoin([objective$, this.quarters$, this.currentQuarter$]).subscribe(([objective, quarters, currentQuarter]) => { this.quarters = quarters; const teamId = isCreating ? objective.teamId : this.data.objective.teamId; - let quarterId = getValueFromQuery(this.route.snapshot.queryParams['quarter'], quarters[1].id)[0]; + const newEditQuarter = isCreating ? currentQuarter.id : objective.quarterId; + let quarterId = getValueFromQuery(this.route.snapshot.queryParams['quarter'], newEditQuarter)[0]; - let currentQuarter: Quarter | undefined = this.quarters.find((quarter) => quarter.id == quarterId); if (currentQuarter && !this.isBacklogQuarter(currentQuarter.label) && this.data.action == 'releaseBacklog') { quarterId = quarters[1].id; } @@ -215,6 +217,4 @@ export class ObjectiveFormComponent implements OnInit, OnDestroy { isBacklogQuarter(label: string) { return GJ_REGEX_PATTERN.test(label); } - - protected readonly getQuarterLabel = getQuarterLabel; } diff --git a/frontend/src/app/shared/testData.ts b/frontend/src/app/shared/testData.ts index 9a4a64044e..48ba98a0de 100644 --- a/frontend/src/app/shared/testData.ts +++ b/frontend/src/app/shared/testData.ts @@ -90,31 +90,13 @@ export const addedAction: Action = { keyResultId: 1, } as Action; -export const quarterMin: Quarter = { - id: 1, - label: 'GJ 23/24-Q1', -} as Quarter; +export const quarterMin: Quarter = new Quarter(1, 'GJ 23/24-Q1', null, null); -export const quarter1: Quarter = { - id: 1, - label: 'GJ 22/23-Q4', - startDate: new Date('2023-04-01'), - endDate: new Date('2023-07-30'), -} as Quarter; +export const quarter1: Quarter = new Quarter(1, 'GJ 22/23-Q4', new Date('2023-04-01'), new Date('2023-07-30')); -export const quarter2: Quarter = { - id: 2, - label: 'GJ 22/23-Q3', - startDate: new Date('2023-01-01'), - endDate: new Date('2023-03-31'), -} as Quarter; - -export const quarterBacklog: Quarter = { - id: 999, - label: 'GJ 23/24-Q1', - startDate: null, - endDate: null, -} as Quarter; +export const quarter2: Quarter = new Quarter(2, 'GJ 22/23-Q3', new Date('2023-01-01'), new Date('2023-03-31')); + +export const quarterBacklog: Quarter = new Quarter(999, 'GJ 23/24-Q1', null, null); export const quarterList: Quarter[] = [quarter1, quarter2, quarterBacklog]; @@ -308,12 +290,7 @@ export const overViewEntityResponse2: any = { export const overviews: OverviewEntity[] = [overViewEntityResponse1, overViewEntityResponse2]; -export const quarter: Quarter = { - id: 1, - label: '23.02.2025', - endDate: new Date(), - startDate: new Date(), -}; +export const quarter: Quarter = new Quarter(1, '23.02.2025', new Date(), new Date()); export const keyResultObjective: KeyResultObjective = { id: 1, @@ -435,12 +412,7 @@ export const keyResult: KeyResultOrdinal = { id: 301, version: 1, state: State.DRAFT, - quarter: { - id: 1, - label: 'GJ 23/24-Q1', - startDate: new Date(), - endDate: new Date(), - } as Quarter, + quarter: new Quarter(1, 'GJ 23/24-Q1', new Date(), new Date()), writeable: true, } as KeyResultObjective, lastCheckIn: { @@ -474,12 +446,7 @@ export const keyResultOrdinal: KeyResultOrdinal = { id: 301, version: 1, state: State.DRAFT, - quarter: { - id: 1, - label: 'GJ 23/24-Q1', - startDate: new Date(), - endDate: new Date(), - } as Quarter, + quarter: new Quarter(1, 'GJ 23/24-Q1', new Date(), new Date()), writeable: true, } as KeyResultObjective, lastCheckIn: { @@ -513,12 +480,7 @@ export const keyResultWriteableFalse: KeyResultOrdinal = { id: 301, version: 1, state: State.DRAFT, - quarter: { - id: 1, - label: 'GJ 23/24-Q1', - startDate: new Date(), - endDate: new Date(), - } as Quarter, + quarter: new Quarter(1, 'GJ 23/24-Q1', new Date(), new Date()), writeable: false, } as KeyResultObjective, lastCheckIn: { @@ -552,12 +514,7 @@ export const keyResultMetric: KeyResultMetric = { id: 302, version: 1, state: State.DRAFT, - quarter: { - id: 1, - label: 'GJ 23/24-Q1', - startDate: new Date(), - endDate: new Date(), - } as Quarter, + quarter: new Quarter(1, 'GJ 23/24-Q1', new Date(), new Date()), writeable: true, } as KeyResultObjective, lastCheckIn: { @@ -590,12 +547,7 @@ export const keyResultActions: KeyResultMetric = { objective: { id: 302, state: State.DRAFT, - quarter: { - id: 1, - label: 'GJ 23/24-Q1', - startDate: new Date(), - endDate: new Date(), - } as Quarter, + quarter: new Quarter(1, 'GJ 23/24-Q1', new Date(), new Date()), writeable: true, } as KeyResultObjective, lastCheckIn: { diff --git a/frontend/src/app/shared/types/model/Quarter.ts b/frontend/src/app/shared/types/model/Quarter.ts index 07d312e084..cc97f785dd 100644 --- a/frontend/src/app/shared/types/model/Quarter.ts +++ b/frontend/src/app/shared/types/model/Quarter.ts @@ -1,6 +1,24 @@ -export interface Quarter { - id: number; - label: string; - startDate: Date | null; - endDate: Date | null; +export class Quarter { + constructor(id: number, label: string, startDate: Date | null, endDate: Date | null) { + this.id = id; + this.label = label; + this.startDate = startDate; + this.endDate = endDate; + } + + readonly id: number; + readonly label: string; + readonly startDate: Date | null; + readonly endDate: Date | null; + + fullLabel(): string { + return this.isCurrent() ? this.label + ' Aktuell' : this.label; + } + + private isCurrent(): boolean { + if (this.startDate === null || this.endDate === null) { + return false; + } + return this.startDate <= new Date() && this.endDate >= new Date(); + } } From 55170f69a121ae474ff34f5c6a492a52d877d21c Mon Sep 17 00:00:00 2001 From: Yanick Minder <79108296+kcinay055679@users.noreply.github.com> Date: Mon, 4 Nov 2024 16:30:48 +0100 Subject: [PATCH 26/33] fix git history (#1124) --- docker/dev-with-prod/docker-compose.yml | 44 +++++++++++++++++----- docker/dev-with-prod/local-prod.Dockerfile | 2 +- pom.xml | 1 - 3 files changed, 36 insertions(+), 11 deletions(-) diff --git a/docker/dev-with-prod/docker-compose.yml b/docker/dev-with-prod/docker-compose.yml index db47c48efa..0fbb4064c0 100644 --- a/docker/dev-with-prod/docker-compose.yml +++ b/docker/dev-with-prod/docker-compose.yml @@ -2,34 +2,60 @@ include: - ../docker-compose.yml services: spring: + tty: true container_name: spring build: context: . dockerfile: local-prod.Dockerfile restart: always environment: - SPRING_PROFILES_ACTIVE: staging - LOGGING_LEVEL_ORG_SPRINGFRAMEWORK: debug - SPRING_FLYWAY_LOCATION: classpath:db/migration,classpath:db/data-migration,classpath:db/callback + SPRING_PROFILES_ACTIVE: dev volumes: - ../../../okr/backend/target:/app-root/backend network_mode: "host" + depends_on: + maven-init: + condition: service_completed_successfully maven: + tty: true container_name: maven + restart: on-failure image: maven:3.9.9-amazoncorretto-21 - command: sh -c "mvn fizzed-watcher:run" + command: mvn fizzed-watcher:run working_dir: /app-root/ volumes: - - ../../../okr:/app-root/ + - ../../../okr:/app-root - ~/.m2/repository:/root/.m2/repository + depends_on: + maven-init: + condition: service_completed_successfully + + maven-init: + tty: true + container_name: maven-init + image: maven:3.9.9-amazoncorretto-21 + command: mvn -B clean package -P build-for-docker,debug,no-formatter + working_dir: /app-root/ + volumes: + - ../../../okr:/app-root + - ~/.m2/repository:/root/.m2/repository + depends_on: + angular: + condition: service_healthy angular: container_name: angular image: node:22 - user: "${UID:-1000}:${GID:-1000}" + tty: true + restart: on-failure volumes: - ../../../okr:/opt - - /etc/passwd:/etc/passwd:ro - - /etc/group:/etc/group:ro - command: [ "/bin/bash", "-c", "cd /opt/frontend && npm ci && npm run watch:prod" ] + command: [ "/bin/bash", "-c", "cd /opt/frontend && rm -rf dist && npm ci && npm run watch:prod" ] + healthcheck: + test: bash -c "[ -f /opt/frontend/dist/frontend/index.html ]" + interval: 10s + retries: 999 + start_period: 30s + timeout: 10s + diff --git a/docker/dev-with-prod/local-prod.Dockerfile b/docker/dev-with-prod/local-prod.Dockerfile index 131874545b..a9bfc82e0f 100644 --- a/docker/dev-with-prod/local-prod.Dockerfile +++ b/docker/dev-with-prod/local-prod.Dockerfile @@ -2,7 +2,7 @@ FROM alpine:3.20 USER root -RUN apk update && apk add --upgrade curl && apk --no-cache add openjdk17 inotify-tools +RUN apk update && apk add --upgrade curl && apk --no-cache add openjdk21 inotify-tools RUN adduser --home /app-root --uid 1001 --disabled-password okr USER 1001 diff --git a/pom.xml b/pom.xml index 0dc3a93bff..deb6767423 100644 --- a/pom.xml +++ b/pom.xml @@ -127,5 +127,4 @@ - From 2035535a98e6380958bed5f401a39d975a4ba7c9 Mon Sep 17 00:00:00 2001 From: Yanick Minder <79108296+kcinay055679@users.noreply.github.com> Date: Mon, 4 Nov 2024 16:51:13 +0100 Subject: [PATCH 27/33] Fix/fix git history (#1125) * fix history * fix hist --- .run/OkrApplication-local-prod-debug.run.xml | 17 ----------------- backend/pom.xml | 2 -- .../resources/application-staging.properties | 6 +++--- 3 files changed, 3 insertions(+), 22 deletions(-) delete mode 100755 .run/OkrApplication-local-prod-debug.run.xml diff --git a/.run/OkrApplication-local-prod-debug.run.xml b/.run/OkrApplication-local-prod-debug.run.xml deleted file mode 100755 index 15ae63f4ef..0000000000 --- a/.run/OkrApplication-local-prod-debug.run.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/backend/pom.xml b/backend/pom.xml index 55b2f44dd9..2e00fc4f7d 100644 --- a/backend/pom.xml +++ b/backend/pom.xml @@ -207,8 +207,6 @@ ${project.build.directory}/jacoco-output/merged.exec - - diff --git a/backend/src/main/resources/application-staging.properties b/backend/src/main/resources/application-staging.properties index 31ce92a3c2..effd67d65a 100644 --- a/backend/src/main/resources/application-staging.properties +++ b/backend/src/main/resources/application-staging.properties @@ -4,7 +4,7 @@ logging.level.org.springframework=debug connect.src=http://localhost:8544 http://localhost:8545 -hibernate.connection.url=jdbc:postgresql://okr-dev-db:5432/okr +hibernate.connection.url=jdbc:postgresql://localhost:5432/okr hibernate.connection.username=user hibernate.connection.password=pwd hibernate.multiTenancy=SCHEMA @@ -16,7 +16,7 @@ okr.datasource.driver-class-name=org.postgresql.Driver okr.user.champion.usernames=peggimann # pitc -okr.tenants.pitc.datasource.url=jdbc:postgresql://okr-dev-db:5432/okr +okr.tenants.pitc.datasource.url=jdbc:postgresql://localhost:5432/okr okr.tenants.pitc.datasource.username=user okr.tenants.pitc.datasource.password=pwd okr.tenants.pitc.datasource.schema=okr_pitc @@ -27,7 +27,7 @@ okr.tenants.pitc.security.oauth2.frontend.client-id=pitc_okr_staging # acme -okr.tenants.acme.datasource.url=jdbc:postgresql://okr-dev-db:5432/okr +okr.tenants.acme.datasource.url=jdbc:postgresql://localhost:5432/okr okr.tenants.acme.datasource.username=user okr.tenants.acme.datasource.password=pwd okr.tenants.acme.datasource.schema=okr_acme From 28e38613771e0403c2df9e7123607782a25f2f51 Mon Sep 17 00:00:00 2001 From: Yanick Minder <79108296+kcinay055679@users.noreply.github.com> Date: Wed, 6 Nov 2024 11:51:55 +0100 Subject: [PATCH 28/33] refactor styles of dialogs (#1072) * add jar debug dev tools only on profile * change log levels * try to fix autorestart of spring * rename intelij config and change log level of spring to debug in staging config * rename folder * use external profile to disable formatter * remove backend/pom.xml from branch * refactor styles of dialogs * remove useless tabindex * refactor styling of dialog-content and dialog-actions * refactor dialog header * clean up * fix invite members test * change default width of dialog * set cypress viewport * remove it.only from cy tests * use diffrent classes for default and small dialogs * remove useless config * clean up styling * remove scroll-shaddow * change height sizing strategie * refactor styling of keyresult edit component * introduce dialog-template-core-component * create base template for dialogs * align close button * comment in dialog component * fix scrolling of dialog * refactor kr-type-form * finish styling of kr-type select * fix scrolling by setting height of action buttons * clean up dialog content size * clean up styles.scss * refactor styles * refactor styles of generic component * move core component to shared module * fix design because of scrollbar * change keyresult type tabs to butotns * delete cancel dialog and refactor related files * refactor confirm dialog component to use template dialog core * refactor styling * refactor style of okr-label * refactor example dialog * refactor objective form component to use template dialog core * refactor objective form dialog * refactor dialog width * refactor keyresult component * fix responsive dialog * refactor objective complete dialog * clean up * refactor objective form component * minder out * fix objective form component * use priority to fetch logos * refactor dialog history compoennt * add spinner to dialog core * refactor dialog history component * refactor check-in componentn * refactor metric check in component * clean up keyresult dialog * clean up * fix kr type component * finish dialog refactoring * remove header component * refactor css classes * refactor styling * insert okr-form-label-input-container * fix dialog service tests * fix jest of objective form compoennt * fix tests of keyresult form * fix pom * fix tests of add-edit team dialog * fix tests keyresult-dialog tests * fix tests of complete dialog * fix jest tests * readd cancel button for check-in create * fix check-in e2e tests * fix e2e tests * remove only from cypress tests * fix teammanagement e2e test * fix duplicate scorring * clean up check-in tests * fix objective duplicate tests * clean up * clean up styles * create a dialog-data-show class * fix check in metric * fix keyresult type * clean up * implement todo --------- Co-authored-by: peggimann --- .../resources/application-staging.properties | 4 +- frontend/cypress/e2e/checkIn.cy.ts | 18 +- frontend/cypress/e2e/duplicated-scoring.cy.ts | 5 +- frontend/cypress/e2e/keyresult.cy.ts | 2 +- frontend/cypress/e2e/objective.cy.ts | 2 +- frontend/cypress/e2e/tab.cy.ts | 2 +- frontend/cypress/e2e/teammanagement.cy.ts | 33 ++- frontend/cypress/support/commands.ts | 1 + frontend/src/app/app.component.scss | 9 - frontend/src/app/app.module.ts | 2 +- .../src/app/callback/callback.component.html | 1 - .../src/app/callback/callback.component.ts | 8 - .../action-plan/action-plan.component.html | 10 +- .../application-top-bar.component.html | 2 +- .../check-in-history-dialog.component.html | 86 +++--- .../check-in-history-dialog.component.scss | 87 +----- .../check-in-history-dialog.component.spec.ts | 14 +- .../check-in-form-metric.component.html | 48 ++-- .../check-in-form-metric.component.scss | 51 ---- .../check-in-form-ordinal.component.html | 6 +- .../check-in-form-ordinal.component.scss | 2 +- .../check-in-form.component.html | 151 ++++++----- .../check-in-form.component.scss | 15 -- .../check-in-form.component.spec.ts | 15 +- .../check-in-form/check-in-form.component.ts | 8 +- .../confidence/confidence.component.html | 9 +- .../confidence/confidence.component.scss | 2 +- .../key-result-form.component.html | 39 +-- .../key-result-form.component.scss | 4 - .../key-result-form.component.spec.ts | 18 +- .../keyresult-detail.component.scss | 2 +- .../keyresult-dialog.component.html | 35 +-- .../keyresult-dialog.component.scss | 10 - .../keyresult-dialog.component.spec.ts | 28 +- .../keyresult-dialog.component.ts | 6 +- .../keyresult-type.component.html | 250 +++++++++--------- .../keyresult-type.component.scss | 63 +---- .../keyresult/keyresult.component.html | 2 +- .../objective-filter.component.scss | 12 - .../src/app/services/dialog.service.spec.ts | 61 ++++- frontend/src/app/services/dialog.service.ts | 17 +- .../dialog-header.component.html | 15 -- .../dialog-header.component.scss | 15 -- .../dialog-header.component.spec.ts | 21 -- .../dialog-header/dialog-header.component.ts | 11 - .../dialog-template-core.component.cy.ts} | 0 .../dialog-template-core.component.html | 35 +++ .../dialog-template-core.component.scss | 24 ++ .../dialog-template-core.component.ts | 22 ++ .../okr-tangram/okr-tangram.component.html | 2 +- .../cancel-dialog.component.html | 18 -- .../cancel-dialog.component.scss | 0 .../cancel-dialog/cancel-dialog.component.ts | 23 -- .../complete-dialog.component.html | 74 +++--- .../complete-dialog.component.scss | 4 - .../complete-dialog.component.spec.ts | 19 +- .../confirm-dialog.component.html | 35 ++- .../confirm-dialog.component.spec.ts | 6 +- .../example-dialog.component.html | 79 +++--- .../objective-form.component.html | 115 ++++---- .../objective-form.component.scss | 42 --- .../objective-form.component.spec.ts | 27 +- .../objective-form.component.ts | 20 ++ frontend/src/app/shared/shared.module.ts | 10 +- .../add-edit-team-dialog.component.html | 35 +-- .../add-edit-team-dialog.component.scss | 4 - .../add-edit-team-dialog.component.spec.ts | 16 +- .../add-edit-team-dialog.component.ts | 4 + .../add-member-to-team-dialog.component.html | 79 +++--- .../invite-user-dialog.component.html | 61 ++--- .../member-detail.component.spec.ts | 25 +- .../member-detail/member-detail.component.ts | 11 +- .../member-list-table.component.spec.ts | 5 +- .../member-list-table.component.ts | 14 +- .../member-list/member-list.component.spec.ts | 15 +- .../member-list/member-list.component.ts | 12 +- frontend/src/assets/i18n/de.json | 8 +- frontend/src/style/_variables.scss | 9 +- .../src/style/custom_angular.components.scss | 14 +- frontend/src/style/custom_bootstrap.scss | 3 +- frontend/src/style/styles.scss | 132 ++++----- 81 files changed, 1015 insertions(+), 1189 deletions(-) delete mode 100644 frontend/src/app/callback/callback.component.html delete mode 100644 frontend/src/app/callback/callback.component.ts delete mode 100644 frontend/src/app/objective-filter/objective-filter.component.scss delete mode 100644 frontend/src/app/shared/custom/dialog-header/dialog-header.component.html delete mode 100644 frontend/src/app/shared/custom/dialog-header/dialog-header.component.scss delete mode 100644 frontend/src/app/shared/custom/dialog-header/dialog-header.component.spec.ts delete mode 100644 frontend/src/app/shared/custom/dialog-header/dialog-header.component.ts rename frontend/src/app/{callback/callback.component.css => shared/custom/dialog-template-core/dialog-template-core.component.cy.ts} (100%) create mode 100644 frontend/src/app/shared/custom/dialog-template-core/dialog-template-core.component.html create mode 100644 frontend/src/app/shared/custom/dialog-template-core/dialog-template-core.component.scss create mode 100644 frontend/src/app/shared/custom/dialog-template-core/dialog-template-core.component.ts delete mode 100644 frontend/src/app/shared/dialog/cancel-dialog/cancel-dialog.component.html delete mode 100644 frontend/src/app/shared/dialog/cancel-dialog/cancel-dialog.component.scss delete mode 100644 frontend/src/app/shared/dialog/cancel-dialog/cancel-dialog.component.ts diff --git a/backend/src/main/resources/application-staging.properties b/backend/src/main/resources/application-staging.properties index effd67d65a..38e4a47571 100644 --- a/backend/src/main/resources/application-staging.properties +++ b/backend/src/main/resources/application-staging.properties @@ -1,5 +1,7 @@ # logging level for staging -logging.level.org.springframework=debug +logging.level.ch.puzzle.okr=DEBUG +#logging.level.org.flywaydb.core=DEBUG + connect.src=http://localhost:8544 http://localhost:8545 diff --git a/frontend/cypress/e2e/checkIn.cy.ts b/frontend/cypress/e2e/checkIn.cy.ts index 45728dedc0..70c2c17a38 100644 --- a/frontend/cypress/e2e/checkIn.cy.ts +++ b/frontend/cypress/e2e/checkIn.cy.ts @@ -163,12 +163,12 @@ describe('OKR Check-in e2e tests', () => { cy.contains(getCurrentDate()); cy.contains('Wert: 30%'); cy.contains('Wert: 50%'); - cy.contains('Confidence: 5 / 10'); - cy.contains('Confidence: 6 / 10'); - cy.contains('Veränderungen: We bought a new house'); - cy.contains('Veränderungen: This was a good idea'); - cy.contains('Massnahmen: We have to buy more PCs'); - cy.contains('Massnahmen: Will be difficult'); + checkForAttribute('Confidence:', '5 / 10'); + checkForAttribute('Confidence:', '6 / 10'); + checkForAttribute('Veränderungen:', 'We bought a new house'); + checkForAttribute('Veränderungen:', 'This was a good idea'); + checkForAttribute('Massnahmen:', 'We have to buy more PCs'); + checkForAttribute('Massnahmen:', 'Will be difficult'); cy.contains('Schliessen'); }); @@ -312,7 +312,7 @@ describe('OKR Check-in e2e tests', () => { cy.wait('@getKeyResultsAfterSave'); cy.getByTestId('add-check-in').first().click(); - cy.get('#old-value label + div').contains('10 %'); + cy.contains('Letzter Wert').siblings('div').contains('10 %'); }); }); }); @@ -359,3 +359,7 @@ function getCurrentDate() { return dd_str + '.' + mm_str + '.' + yyyy; } + +function checkForAttribute(title: string, value: string) { + cy.get('mat-dialog-container').contains(value).parent().should('contain', title); +} diff --git a/frontend/cypress/e2e/duplicated-scoring.cy.ts b/frontend/cypress/e2e/duplicated-scoring.cy.ts index d1152ced59..6a87dbd243 100644 --- a/frontend/cypress/e2e/duplicated-scoring.cy.ts +++ b/frontend/cypress/e2e/duplicated-scoring.cy.ts @@ -9,7 +9,10 @@ describe('e2e test for scoring adjustment on objective duplicate', () => { }); it('Create ordinal checkin and validate value of scoring component', () => { + cy.intercept('POST', '**/keyresults').as('createKeyresult'); cy.createOrdinalKeyresult('stretch keyresult for testing', null); + cy.wait('@createKeyresult'); + cy.contains('stretch keyresult for testing'); cy.getByTestId('keyresult').get(':contains("stretch keyresult for testing")').last().click(); cy.getByTestId('add-check-in').click(); cy.getByTestId(`stretch-radio`).click(); @@ -26,7 +29,7 @@ describe('e2e test for scoring adjustment on objective duplicate', () => { cy.visit('/?quarter=3'); let scoringBlock1 = cy - .getByTestId('objective') + .get('.objective:contains("A duplicated Objective for this tool")') .first() .getByTestId('key-result') .first() diff --git a/frontend/cypress/e2e/keyresult.cy.ts b/frontend/cypress/e2e/keyresult.cy.ts index f7f9ef7b67..aaa5d3b709 100644 --- a/frontend/cypress/e2e/keyresult.cy.ts +++ b/frontend/cypress/e2e/keyresult.cy.ts @@ -294,7 +294,7 @@ describe('OKR Overview', () => { cy.getByTestId('edit-keyResult').click(); cy.getByTestId('delete-keyResult').click(); - cy.getByTestId('confirmYes').click(); + cy.getByTestId('confirm-yes').click(); cy.contains('Puzzle ITC'); cy.get('A keyresult to delete').should('not.exist'); diff --git a/frontend/cypress/e2e/objective.cy.ts b/frontend/cypress/e2e/objective.cy.ts index 758d7687a3..83eb98496e 100644 --- a/frontend/cypress/e2e/objective.cy.ts +++ b/frontend/cypress/e2e/objective.cy.ts @@ -30,7 +30,7 @@ describe('OKR Objective e2e tests', () => { .click(); cy.contains('Objective veröffentlichen'); cy.contains('Soll dieses Objective veröffentlicht werden?'); - cy.getByTestId('confirmYes').click(); + cy.getByTestId('confirm-yes').click(); cy.getByTestId('objective') .filter(':contains(A objective in state draft)') .last() diff --git a/frontend/cypress/e2e/tab.cy.ts b/frontend/cypress/e2e/tab.cy.ts index 4d1cdd714b..ee8143f4eb 100644 --- a/frontend/cypress/e2e/tab.cy.ts +++ b/frontend/cypress/e2e/tab.cy.ts @@ -416,7 +416,7 @@ describe('Tab workflow tests', () => { cy.tabForwardUntil('[data-testId="add-action-plan-line"]'); cy.tabBackward(); cy.realPress('Enter'); - cy.tabForwardUntil('[data-testId="confirmYes"]'); + cy.tabForwardUntil('[data-testId="confirm-yes"]'); cy.realPress('Enter'); cy.tabForward(); cy.tabForwardUntil('[data-testId="submit"]'); diff --git a/frontend/cypress/e2e/teammanagement.cy.ts b/frontend/cypress/e2e/teammanagement.cy.ts index d89d8b06e1..6edf0db28e 100644 --- a/frontend/cypress/e2e/teammanagement.cy.ts +++ b/frontend/cypress/e2e/teammanagement.cy.ts @@ -44,8 +44,8 @@ describe('Team management tests', () => { cy.getByTestId('remove-from-member-list').click(); // dialog - cy.contains(`Jaya Norris wirklich aus Team ${teamName} entfernen?`); - cy.getByTestId('cancelDialog-confirm').click(); + cy.contains(`Möchtest du Jaya Norris wirklich aus dem Team '${teamName}' entfernen?`); + cy.getByTestId('confirm-yes').click(); cy.wait('@removeUser'); @@ -60,8 +60,8 @@ describe('Team management tests', () => { cy.getByTestId('remove-from-member-list').click(); // cancel dialog - cy.contains(`Jaya Norris wirklich aus Team ${teamName} entfernen?`); - cy.getByTestId('cancelDialog-cancel').click(); + cy.contains(`Möchtest du Jaya Norris wirklich aus dem Team '${teamName}' entfernen?`); + cy.getByTestId('confirm-no').click(); cy.get('@removeUser.all').then((interceptions) => { expect(interceptions).to.have.length(0); @@ -96,14 +96,18 @@ describe('Team management tests', () => { cy.getByTestId('teamDeleteButton').click(); // cancel dialog => cancel - cy.contains(`${teamName} wirklich löschen?`); - cy.getByTestId('cancelDialog-cancel').click(); + cy.contains( + `Möchtest du das Team '${teamName}' wirklich löschen? Zugehörige Objectives werden dadurch in allen Quartalen ebenfalls gelöscht!`, + ); + cy.getByTestId('confirm-no').click(); // try again and confirm dialog cy.getByTestId('teamMoreButton').click(); cy.getByTestId('teamDeleteButton').click(); - cy.contains(`${teamName} wirklich löschen?`); - cy.getByTestId('cancelDialog-confirm').click(); + cy.contains( + `Möchtest du das Team '${teamName}' wirklich löschen? Zugehörige Objectives werden dadurch in allen Quartalen ebenfalls gelöscht!`, + ); + cy.getByTestId('confirm-yes').click(); cy.wait(['@saveTeam', '@getUsers']); @@ -149,6 +153,10 @@ describe('Team management tests', () => { const firstNameStefan = uniqueSuffix('Stefan'); cy.getByTestId('invite-member').click(); + cy.wait(1000); // wait for dialog to open + cy.tabForward(); + cy.contains('Members registrieren'); + fillOutNewUser(firstNameClaudia, 'Meier', mailUserClaudia); cy.tabForward(); cy.tabForward(); @@ -305,7 +313,7 @@ describe('Team management tests', () => { // add findus peterson cy.getByTestId('search-member-to-add').click().type('Find', { delay: 1 }); - cy.get(matOption).contains('Findus Peterson').click(); + cy.contains(matOption, 'Findus Peterson').click(); // add robin papierer cy.getByTestId('search-member-to-add').click(); @@ -336,6 +344,7 @@ describe('Team management tests', () => { return; } $row.find(`[data-testId='edit-role']`).click(); + cy.wait(500); // wait for dialog to open }) .then(() => { cy.getByTestId('select-team-role').click(); @@ -356,7 +365,7 @@ describe('Team management tests', () => { it('should remove BBT membership of findus', () => { navigateToUser('Findus Peterson'); cy.getByTestId('delete-team-member').click(); - cy.getByTestId('cancelDialog-confirm').click(); + cy.getByTestId('confirm-yes').click(); cy.get('app-member-detail').contains('/BBT').should('not.exist'); }); @@ -365,12 +374,12 @@ describe('Team management tests', () => { navigateToUser(nameEsha); cy.getByTestId('delete-team-member').eq(0).click(); - cy.getByTestId('cancelDialog-confirm').click(); + cy.getByTestId('confirm-yes').click(); cy.wait('@removeUser'); cy.getByTestId('delete-team-member').eq(0).click(); - cy.getByTestId('cancelDialog-confirm').click(); + cy.getByTestId('confirm-yes').click(); cy.get('app-member-detail').should('not.contain', '/BBT').and('not.contain', 'LoremIpsum'); }); diff --git a/frontend/cypress/support/commands.ts b/frontend/cypress/support/commands.ts index 1fa1df2314..30e6b962c6 100644 --- a/frontend/cypress/support/commands.ts +++ b/frontend/cypress/support/commands.ts @@ -1,6 +1,7 @@ import { validateScoring } from './scoringSupport'; Cypress.Commands.add('loginAsUser', (user: any) => { + cy.viewport(1920, 1080); loginWithCredentials(user.username, user.password); overviewIsLoaded(); }); diff --git a/frontend/src/app/app.component.scss b/frontend/src/app/app.component.scss index 97a874648e..8ba091ef8b 100644 --- a/frontend/src/app/app.component.scss +++ b/frontend/src/app/app.component.scss @@ -1,12 +1,3 @@ -.okr-label { - margin-left: 35px; -} - -.overview-item { - margin-right: 20px; - margin-left: 50px; -} - app-application-top-bar { z-index: 1001; position: relative; diff --git a/frontend/src/app/app.module.ts b/frontend/src/app/app.module.ts index 58fa564e59..6e3150d3c1 100644 --- a/frontend/src/app/app.module.ts +++ b/frontend/src/app/app.module.ts @@ -166,9 +166,9 @@ export const MY_FORMATS = { MatChipsModule, CdkDropList, CdkDrag, - SharedModule, A11yModule, CdkDragHandle, + SharedModule, ], providers: [ { diff --git a/frontend/src/app/callback/callback.component.html b/frontend/src/app/callback/callback.component.html deleted file mode 100644 index 9a5f2cf345..0000000000 --- a/frontend/src/app/callback/callback.component.html +++ /dev/null @@ -1 +0,0 @@ -

You are getting forwarded!

diff --git a/frontend/src/app/callback/callback.component.ts b/frontend/src/app/callback/callback.component.ts deleted file mode 100644 index ac042fdc53..0000000000 --- a/frontend/src/app/callback/callback.component.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { Component } from '@angular/core'; - -@Component({ - selector: 'app-callback', - templateUrl: './callback.component.html', - styleUrl: './callback.component.css', -}) -export class CallbackComponent {} diff --git a/frontend/src/app/components/action-plan/action-plan.component.html b/frontend/src/app/components/action-plan/action-plan.component.html index b056643283..4d7b642f9e 100644 --- a/frontend/src/app/components/action-plan/action-plan.component.html +++ b/frontend/src/app/components/action-plan/action-plan.component.html @@ -1,11 +1,11 @@ -
- +
+
@@ -42,11 +42,11 @@
-
+
-
-
-
- Wert: {{ getMetricKeyResult().unit | unitLabelTransformation }} - {{ +checkIn.value! | unitValueTransformation: getMetricKeyResult().unit }} - Wert: {{ checkIn.value }} +
+

Wert:

+
+ + {{ getMetricKeyResult().unit | unitLabelTransformation }} + {{ +checkIn.value! | unitValueTransformation: getMetricKeyResult().unit }} + + + {{ checkIn.value }} + +
+

Confidence:

+
{{ checkIn.confidence }} / 10
-
- Confidence: {{ checkIn.confidence }} / 10 + +
+

Veränderungen:

+ {{ checkIn.changeInfo }} +
+ +
+

Massnahmen:

+ {{ checkIn.initiatives }}
-
-
- Veränderungen: - {{ checkIn.changeInfo }} -
-
- Massnahmen: - {{ checkIn.initiatives }}
-
- - - - -
+ + + +
+ +
+
+ diff --git a/frontend/src/app/components/check-in-history-dialog/check-in-history-dialog.component.scss b/frontend/src/app/components/check-in-history-dialog/check-in-history-dialog.component.scss index 2d5a6334b6..c2c8998c4f 100644 --- a/frontend/src/app/components/check-in-history-dialog/check-in-history-dialog.component.scss +++ b/frontend/src/app/components/check-in-history-dialog/check-in-history-dialog.component.scss @@ -1,85 +1,4 @@ -@import "../style/variables"; - -.cancel-button { - background-color: $pz-dark-blue; - border-radius: 8px; - width: min-content; -} - -.edit-check-in { - width: 20px; - height: 20px; -} - -.edit-check-in-button { - @extend .edit-check-in; - margin-right: 5px; -} - -.check-in-box { - background-color: #f5f5f5; -} - -.label-width { - width: 19%; -} - -.value-width { - width: 81%; -} - -.dialog-content { - max-height: 28.5vh; -} - -.close-button { - margin-top: 32px; -} - -@media only screen and (max-width: 690px) { - .label-width { - width: 60%; - } -} - -@media only screen and (min-height: 600px) and (max-width: 450px) { - .dialog-content { - max-height: 500px; - } -} - -@media only screen and (min-height: 700px) and (max-width: 450px) { - .dialog-content { - max-height: 590px; - } -} - -@media only screen and (min-height: 800px) and (max-width: 450px) { - .dialog-content { - max-height: 625px; - } -} - -@media only screen and (min-height: 850px) and (max-width: 450px) { - .dialog-content { - max-height: 685px; - } -} - -@media only screen and (min-height: 875px) and (max-width: 450px) { - .dialog-content { - max-height: 715px; - } -} - -@media only screen and (min-height: 900px) and (max-width: 450px) { - .dialog-content { - max-height: 750px; - } -} - -@media only screen and (min-height: 1000px) and (max-width: 450px) { - .dialog-content { - max-height: 550px; - } +//TODO: Just Temporarily so the application look decent +h4 { + font-size: 1rem; } diff --git a/frontend/src/app/components/check-in-history-dialog/check-in-history-dialog.component.spec.ts b/frontend/src/app/components/check-in-history-dialog/check-in-history-dialog.component.spec.ts index cd4b5a7262..ee75865c64 100644 --- a/frontend/src/app/components/check-in-history-dialog/check-in-history-dialog.component.spec.ts +++ b/frontend/src/app/components/check-in-history-dialog/check-in-history-dialog.component.spec.ts @@ -2,15 +2,18 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { CheckInHistoryDialogComponent } from './check-in-history-dialog.component'; import { MAT_DIALOG_DATA, MatDialogModule, MatDialogRef } from '@angular/material/dialog'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { provideHttpClientTesting } from '@angular/common/http/testing'; import { checkInMetric, checkInMetricWriteableFalse, keyResult } from '../../shared/testData'; import { By } from '@angular/platform-browser'; import { DialogService } from '../../services/dialog.service'; import { TranslateModule, TranslateService } from '@ngx-translate/core'; -import { DialogHeaderComponent } from '../../shared/custom/dialog-header/dialog-header.component'; import { MatIconModule } from '@angular/material/icon'; import { SpinnerComponent } from '../../shared/custom/spinner/spinner.component'; import { MatProgressSpinner } from '@angular/material/progress-spinner'; +import { provideRouter } from '@angular/router'; +import { provideHttpClient } from '@angular/common/http'; +import { DialogTemplateCoreComponent } from '../../shared/custom/dialog-template-core/dialog-template-core.component'; +import { MatDividerModule } from '@angular/material/divider'; const checkInService = { getAllCheckInOfKeyResult: jest.fn(), @@ -22,10 +25,13 @@ describe('CheckInHistoryDialogComponent', () => { beforeEach(() => { TestBed.configureTestingModule({ - declarations: [CheckInHistoryDialogComponent, DialogHeaderComponent, SpinnerComponent], + declarations: [CheckInHistoryDialogComponent, DialogTemplateCoreComponent, SpinnerComponent], - imports: [HttpClientTestingModule, TranslateModule.forRoot(), MatIconModule, MatProgressSpinner], + imports: [TranslateModule.forRoot(), MatIconModule, MatProgressSpinner, MatDividerModule, MatDialogModule], providers: [ + provideRouter([]), + provideHttpClient(), + provideHttpClientTesting(), TranslateService, DialogService, { provide: MAT_DIALOG_DATA, useValue: { keyResult: keyResult } }, diff --git a/frontend/src/app/components/checkin/check-in-form-metric/check-in-form-metric.component.html b/frontend/src/app/components/checkin/check-in-form-metric/check-in-form-metric.component.html index 45d9ce03ec..3c796f384f 100644 --- a/frontend/src/app/components/checkin/check-in-form-metric/check-in-form-metric.component.html +++ b/frontend/src/app/components/checkin/check-in-form-metric/check-in-form-metric.component.html @@ -1,28 +1,34 @@ -
-
-
- -
-
- +
+
+
+ +
+ +
+ +
+ + {{ generateUnitLabel() }} - {{ generateUnitLabel() }}
+ + {{ getErrorMessage("MUST_BE_NUMBER", "Neuer Wert") }} +
-
- -
- {{ checkIn.value }} {{ generateUnitLabel() }} +
+ +
+
+

Letzter Wert

+
+
{{ checkIn.value }} {{ generateUnitLabel() }}
- - {{ getErrorMessage("MUST_BE_NUMBER", "Neuer Wert") }} -
diff --git a/frontend/src/app/components/checkin/check-in-form-metric/check-in-form-metric.component.scss b/frontend/src/app/components/checkin/check-in-form-metric/check-in-form-metric.component.scss index 46bd29b25b..e69de29bb2 100644 --- a/frontend/src/app/components/checkin/check-in-form-metric/check-in-form-metric.component.scss +++ b/frontend/src/app/components/checkin/check-in-form-metric/check-in-form-metric.component.scss @@ -1,51 +0,0 @@ -.custom-input { - margin-top: 3px; -} - -mat-label { - color: black; -} - -.value-field { - height: 2rem; - padding: 0.437rem 0.625rem 0.375rem 0.625rem !important; -} - -.unit-label { - margin-left: 10px; -} - -.disabled-field { - background-color: #f5f5f5; - width: 218px; -} - -@media only screen and (min-width: 601px) { - .gap-between { - gap: 5rem; - } -} - -@media only screen and (min-width: 575px) and (max-width: 600px) { - #new-value { - order: 0; - } - #old-value { - order: 1; - } - .gap-between { - gap: 3rem; - } -} - -@media only screen and (max-width: 574px) { - #new-value { - order: 1; - } - #old-value { - order: 0; - } - .gap-between { - gap: 1rem; - } -} diff --git a/frontend/src/app/components/checkin/check-in-form-ordinal/check-in-form-ordinal.component.html b/frontend/src/app/components/checkin/check-in-form-ordinal/check-in-form-ordinal.component.html index 511f0222b1..8a5af56f89 100644 --- a/frontend/src/app/components/checkin/check-in-form-ordinal/check-in-form-ordinal.component.html +++ b/frontend/src/app/components/checkin/check-in-form-ordinal/check-in-form-ordinal.component.html @@ -1,7 +1,7 @@
-
- - +
+ +
Fail:   Commit / Target / Stretch noch nicht erreicht diff --git a/frontend/src/app/components/checkin/check-in-form-ordinal/check-in-form-ordinal.component.scss b/frontend/src/app/components/checkin/check-in-form-ordinal/check-in-form-ordinal.component.scss index 55e3fe921d..62b280fbef 100644 --- a/frontend/src/app/components/checkin/check-in-form-ordinal/check-in-form-ordinal.component.scss +++ b/frontend/src/app/components/checkin/check-in-form-ordinal/check-in-form-ordinal.component.scss @@ -17,6 +17,6 @@ mat-label { color: black; } .radio-text-container { - background: $show-element; + background: $display-element; height: auto; } diff --git a/frontend/src/app/components/checkin/check-in-form/check-in-form.component.html b/frontend/src/app/components/checkin/check-in-form/check-in-form.component.html index c821f83b3e..5a2ee63747 100644 --- a/frontend/src/app/components/checkin/check-in-form/check-in-form.component.html +++ b/frontend/src/app/components/checkin/check-in-form/check-in-form.component.html @@ -1,42 +1,45 @@ -
- - -
- - -
- -
-

Key Result

-

{{ keyResult.title }}

+ + + +
+

Key Result

+ +

{{ keyResult.title }}

+
-
- - - - - {{ action.action }} + +
+

Action Plan:

+
+ + + +
+ {{ action.action }} +
- +
-
- - - - {{ getErrorMessage("MAX_VALUE", "Kommentar / Veränderung", 4096) }} - +
+ +
+ + + {{ getErrorMessage("MAX_VALUE", "Kommentar / Veränderung", 4096) }} + +
-
- - - - {{ getErrorMessage("MAX_VALUE", "Massnahmen", 4096) }} - +
+ +
+ + + {{ getErrorMessage("MAX_VALUE", "Massnahmen", 4096) }} + +
-
-
- + - - - - + +
+ + +
+
+ diff --git a/frontend/src/app/components/checkin/check-in-form/check-in-form.component.scss b/frontend/src/app/components/checkin/check-in-form/check-in-form.component.scss index 669c394785..010e8abc58 100644 --- a/frontend/src/app/components/checkin/check-in-form/check-in-form.component.scss +++ b/frontend/src/app/components/checkin/check-in-form/check-in-form.component.scss @@ -1,14 +1,3 @@ -@import "../style/variables"; - -.action-list-item { - background-color: $show-element; - height: auto; - min-height: 40px; - padding: 10px; - margin: auto; - color: black; -} - .confidence-label { margin-bottom: -7px; } @@ -16,7 +5,3 @@ app-check-in-form-ordinal { padding-top: 0.1rem; } - -.dialog-content { - max-height: 45vh; -} diff --git a/frontend/src/app/components/checkin/check-in-form/check-in-form.component.spec.ts b/frontend/src/app/components/checkin/check-in-form/check-in-form.component.spec.ts index 65f71b851e..55819aace2 100644 --- a/frontend/src/app/components/checkin/check-in-form/check-in-form.component.spec.ts +++ b/frontend/src/app/components/checkin/check-in-form/check-in-form.component.spec.ts @@ -11,7 +11,6 @@ import { keyResultMetric, keyResultOrdinal, } from '../../../shared/testData'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; import { MatIconModule } from '@angular/material/icon'; import { MatFormFieldModule } from '@angular/material/form-field'; import { MatSelectModule } from '@angular/material/select'; @@ -23,11 +22,16 @@ import { ParseUnitValuePipe } from '../../../shared/pipes/parse-unit-value/parse import { CheckInService } from '../../../services/check-in.service'; import { of } from 'rxjs'; import { ActionService } from '../../../services/action.service'; +// @ts-ignore import * as de from '../../../../assets/i18n/de.json'; import { TranslateTestingModule } from 'ngx-translate-testing'; -import { DialogHeaderComponent } from '../../../shared/custom/dialog-header/dialog-header.component'; import { ConfidenceComponent } from '../../confidence/confidence.component'; import { MatSliderModule } from '@angular/material/slider'; +import { provideRouter } from '@angular/router'; +import { provideHttpClient } from '@angular/common/http'; +import { provideHttpClientTesting } from '@angular/common/http/testing'; +import { DialogTemplateCoreComponent } from '../../../shared/custom/dialog-template-core/dialog-template-core.component'; +import { MatDividerModule } from '@angular/material/divider'; const dialogMock = { close: jest.fn(), @@ -48,7 +52,6 @@ describe('CheckInFormComponent', () => { beforeEach(() => { TestBed.configureTestingModule({ imports: [ - HttpClientTestingModule, MatDialogModule, MatIconModule, MatFormFieldModule, @@ -63,15 +66,19 @@ describe('CheckInFormComponent', () => { TranslateTestingModule.withTranslations({ de: de, }), + MatDividerModule, ], providers: [ + provideRouter([]), + provideHttpClient(), + provideHttpClientTesting(), { provide: MAT_DIALOG_DATA, useValue: { keyResult: {} } }, { provide: MatDialogRef, useValue: dialogMock }, { provide: CheckInService, useValue: checkInServiceMock }, { provide: ActionService, useValue: actionServiceMock }, ParseUnitValuePipe, ], - declarations: [CheckInFormComponent, DialogHeaderComponent, ConfidenceComponent], + declarations: [CheckInFormComponent, DialogTemplateCoreComponent, ConfidenceComponent], }); fixture = TestBed.createComponent(CheckInFormComponent); component = fixture.componentInstance; diff --git a/frontend/src/app/components/checkin/check-in-form/check-in-form.component.ts b/frontend/src/app/components/checkin/check-in-form/check-in-form.component.ts index baa3b8b1a0..a47c0b1da7 100644 --- a/frontend/src/app/components/checkin/check-in-form/check-in-form.component.ts +++ b/frontend/src/app/components/checkin/check-in-form/check-in-form.component.ts @@ -111,8 +111,8 @@ export class CheckInFormComponent implements OnInit { return this.keyResult as KeyResultOrdinal; } - getActions(): Action[] | null { - return this.dialogForm.controls['actionList'].value; + getActions(): Action[] { + return this.dialogForm.controls['actionList'].value || []; } changeIsChecked(event: any, index: number) { @@ -120,4 +120,8 @@ export class CheckInFormComponent implements OnInit { actions[index] = { ...actions[index], isChecked: event.checked }; this.dialogForm.patchValue({ actionList: actions }); } + + getDialogTitle(): string { + return this.checkIn.id ? 'Check-in bearbeiten' : 'Check-in erfassen'; + } } diff --git a/frontend/src/app/components/confidence/confidence.component.html b/frontend/src/app/components/confidence/confidence.component.html index aa8b974de0..286d8e3cf4 100644 --- a/frontend/src/app/components/confidence/confidence.component.html +++ b/frontend/src/app/components/confidence/confidence.component.html @@ -1,13 +1,14 @@
-

{{ checkIn.confidence }}/{{ max }}

+

+ {{ checkIn.confidence }}/{{ max }} +

-
-
- + +
+ +
- - {{ getErrorMessage("SIZE_BETWEEN", "Commit Zone", 1, 400) }} - -
+ +
+
+ +
+ + + {{ getErrorMessage("SIZE_BETWEEN", "Commit Zone", 1, 400) }} + +
+
+
-
- - - - {{ getErrorMessage("SIZE_BETWEEN", "Target Zone", 1, 400) }} - -
+
+
+ +
+ + + {{ getErrorMessage("SIZE_BETWEEN", "Target Zone", 1, 400) }} + +
+
+
-
- - - - {{ getErrorMessage("SIZE_BETWEEN", "Stretch Zone", 1, 400) }} - -
-
+
+
+ +
+ + + {{ getErrorMessage("SIZE_BETWEEN", "Stretch Zone", 1, 400) }} +
diff --git a/frontend/src/app/components/keyresult-type/keyresult-type.component.scss b/frontend/src/app/components/keyresult-type/keyresult-type.component.scss index 9b3e87d79e..0edb31b8a7 100644 --- a/frontend/src/app/components/keyresult-type/keyresult-type.component.scss +++ b/frontend/src/app/components/keyresult-type/keyresult-type.component.scss @@ -1,7 +1,11 @@ +@import "custom_bootstrap"; + .active { border-left: #909090 1px solid; border-top: #909090 1px solid; border-right: #909090 1px solid; + border-bottom: #909090 0 solid; + border-top-left-radius: 5px; border-top-right-radius: 5px; background: white; @@ -10,22 +14,21 @@ .non-active { color: #9c9c9c; + background: white; + border: #909090 0 solid; border-bottom: #909090 1px solid; } .tab-title { - display: flex; - justify-content: center; - align-items: center; - height: 39px; - margin-bottom: 16px; + @extend .p-2; + @extend .tab-focus; } .buffer { border-bottom: #909090 1px solid; } -.tabfocus { +.tab-focus { outline: none; &:focus-visible { border-radius: 5px; @@ -33,55 +36,7 @@ } } -.metric { - width: 87px; -} - -.ordinal { - width: 75px; -} - -.ordinal-input-fields { - height: 4.3rem; - padding: 0.437rem 0.625rem 0.375rem 0.625rem !important; -} - -.metric-fields { - height: 2rem; - padding: 0.437rem 0.625rem 0.375rem 0.625rem !important; -} - -.input-style { - width: 31%; -} - .unit-dropdown { - @extend .metric-fields; border: solid 1px #909090; cursor: pointer; } - -@media only screen and (min-width: 820px) { - .input-alignments { - display: flex; - justify-content: space-between; - } - - .input-style { - margin-bottom: 0; - } -} - -@media only screen and (max-width: 820px) { - .metric-fields { - width: 100%; - } - - .ordinal-input-fields { - width: 100%; - } - - .input-style { - width: auto; - } -} diff --git a/frontend/src/app/components/keyresult/keyresult.component.html b/frontend/src/app/components/keyresult/keyresult.component.html index 926322cd1c..3abe4ac6e6 100644 --- a/frontend/src/app/components/keyresult/keyresult.component.html +++ b/frontend/src/app/components/keyresult/keyresult.component.html @@ -24,7 +24,7 @@

{{ keyResult.title }}

[edit]="false" [isDetail]="false" [checkIn]="keyResult.lastCheckIn!" - class="ms-2 bg-keyResult-attribute" + class="bg-keyResult-attribute" >
diff --git a/frontend/src/app/objective-filter/objective-filter.component.scss b/frontend/src/app/objective-filter/objective-filter.component.scss deleted file mode 100644 index 0f3d371327..0000000000 --- a/frontend/src/app/objective-filter/objective-filter.component.scss +++ /dev/null @@ -1,12 +0,0 @@ -.search-button { - border-radius: 0 4px 4px 0; - padding-left: 15px; -} - -#objective-form-field { - width: 300px; -} - -.search-scale { - transform: scale(1.2); -} diff --git a/frontend/src/app/services/dialog.service.spec.ts b/frontend/src/app/services/dialog.service.spec.ts index 16b4888d16..b9f540874f 100644 --- a/frontend/src/app/services/dialog.service.spec.ts +++ b/frontend/src/app/services/dialog.service.spec.ts @@ -2,16 +2,26 @@ import { TestBed } from '@angular/core/testing'; import { DialogService } from './dialog.service'; import { TranslateModule, TranslateService } from '@ngx-translate/core'; -import { MatDialogRef } from '@angular/material/dialog'; +import { MatDialog, MatDialogRef } from '@angular/material/dialog'; import { ConfirmDialogComponent } from '../shared/dialog/confirm-dialog/confirm-dialog.component'; -import { TeamComponent } from '../components/team/team.component'; +import { AddEditTeamDialog } from '../team-management/add-edit-team-dialog/add-edit-team-dialog.component'; +import { provideHttpClientTesting } from '@angular/common/http/testing'; +import { provideHttpClient } from '@angular/common/http'; describe('DialogService', () => { let service: DialogService; + let matDialogSpy: MatDialog; + let translateServiceSpy: TranslateService; beforeEach(() => { - TestBed.configureTestingModule({ imports: [TranslateModule.forRoot()], providers: [TranslateService] }); + TestBed.configureTestingModule({ + imports: [TranslateModule.forRoot()], + providers: [provideHttpClient(), provideHttpClientTesting(), TranslateService], + }); + matDialogSpy = TestBed.inject(MatDialog); service = TestBed.inject(DialogService); + translateServiceSpy = TestBed.inject(TranslateService); + jest.spyOn(matDialogSpy, 'open'); }); it('should be created', () => { @@ -19,20 +29,51 @@ describe('DialogService', () => { }); it('should open dialog', () => { - const dialog = service.open(TeamComponent); + const dialog = service.open(AddEditTeamDialog, { + data: { + aProperty: 'aValue', + }, + }); expect(dialog).toBeInstanceOf(MatDialogRef); - expect(dialog._containerInstance._config.panelClass).toEqual(service.DIALOG_CONFIG.panelClass); - expect(dialog._containerInstance._config.maxWidth).toEqual(service.DIALOG_CONFIG.maxWidth); - expect(dialog.componentInstance).toBeInstanceOf(TeamComponent); + expect(dialog.componentInstance).toBeInstanceOf(AddEditTeamDialog); + + expect(matDialogSpy.open).toHaveBeenCalledWith(AddEditTeamDialog, { + panelClass: service.DIALOG_PANEL_CLASS_DEFAULT, + ...service.DIALOG_CONFIG, + data: { + aProperty: 'aValue', + }, + }); }); it('should open confirm dialog', () => { + const i18nData = { + team: 'the a-team', + }; jest.spyOn(service, 'open'); - const dialog = service.openConfirmDialog('DELETE.ACTION'); + jest.spyOn(translateServiceSpy, 'instant'); + const dialog = service.openConfirmDialog('DELETE.TEAM', i18nData); + + //Call function of own service expect(service.open).toHaveBeenCalledTimes(1); expect(dialog).toBeInstanceOf(MatDialogRef); + expect(translateServiceSpy.instant).toHaveBeenCalledTimes(2); + expect(translateServiceSpy.instant).toHaveBeenCalledWith('DELETE.TEAM.TITLE', i18nData); + expect(translateServiceSpy.instant).toHaveBeenCalledWith('DELETE.TEAM.TEXT', i18nData); + + //Call function of angular material dialog + expect(matDialogSpy.open).toHaveBeenCalledTimes(1); expect(dialog.componentInstance).toBeInstanceOf(ConfirmDialogComponent); - expect(dialog.componentInstance.data.title).toBe('DELETE.ACTION.TITLE'); - expect(dialog.componentInstance.data.text).toBe('DELETE.ACTION.TEXT'); + expect(dialog.componentInstance.data.title).toBe('DELETE.TEAM.TITLE'); + expect(dialog.componentInstance.data.text).toBe('DELETE.TEAM.TEXT'); + + expect(matDialogSpy.open).toHaveBeenCalledWith(ConfirmDialogComponent, { + panelClass: service.DIALOG_PANEL_CLASS_SMALL, + ...service.DIALOG_CONFIG, + data: { + title: 'DELETE.TEAM.TITLE', + text: 'DELETE.TEAM.TEXT', + }, + }); }); }); diff --git a/frontend/src/app/services/dialog.service.ts b/frontend/src/app/services/dialog.service.ts index 5bfa310482..a1236133ea 100644 --- a/frontend/src/app/services/dialog.service.ts +++ b/frontend/src/app/services/dialog.service.ts @@ -13,9 +13,12 @@ export interface ConfirmDialogData { providedIn: 'root', }) export class DialogService { - DIALOG_CONFIG = { - panelClass: 'okr-dialog-panel', - maxWidth: '100vw', + DIALOG_PANEL_CLASS_DEFAULT = 'okr-dialog-panel-default'; + DIALOG_PANEL_CLASS_SMALL = 'okr-dialog-panel-small'; + + DIALOG_CONFIG: MatDialogConfig = { + maxWidth: '100vw', // Used to override the default maxWidth of angular material dialog + autoFocus: 'first-tabbable', }; constructor( @@ -25,15 +28,17 @@ export class DialogService { open(component: ComponentType, config?: MatDialogConfig): MatDialogRef { return this.dialog.open(component, { + panelClass: this.DIALOG_PANEL_CLASS_DEFAULT, ...this.DIALOG_CONFIG, ...config, }); } - openConfirmDialog(translationKey: string) { - const title = this.translationService.instant(`${translationKey}.TITLE`); - const text = this.translationService.instant(`${translationKey}.TEXT`); + openConfirmDialog(translationKey: string, i18nData?: Object): MatDialogRef { + const title = this.translationService.instant(`${translationKey}.TITLE`, i18nData); + const text = this.translationService.instant(`${translationKey}.TEXT`, i18nData); return this.open(ConfirmDialogComponent, { + panelClass: this.DIALOG_PANEL_CLASS_SMALL, data: { title: title, text: text, diff --git a/frontend/src/app/shared/custom/dialog-header/dialog-header.component.html b/frontend/src/app/shared/custom/dialog-header/dialog-header.component.html deleted file mode 100644 index 80a7f62d05..0000000000 --- a/frontend/src/app/shared/custom/dialog-header/dialog-header.component.html +++ /dev/null @@ -1,15 +0,0 @@ -
- - {{ dialogTitle }} - -
- -
-
diff --git a/frontend/src/app/shared/custom/dialog-header/dialog-header.component.scss b/frontend/src/app/shared/custom/dialog-header/dialog-header.component.scss deleted file mode 100644 index 40bb4dbcda..0000000000 --- a/frontend/src/app/shared/custom/dialog-header/dialog-header.component.scss +++ /dev/null @@ -1,15 +0,0 @@ -.closing-button { - right: 0; -} - -@media only screen and (min-width: 800px) { - .dialog-header { - margin-bottom: 1rem; - } -} - -mat-icon { - display: flex; - justify-content: center; - align-items: center; -} diff --git a/frontend/src/app/shared/custom/dialog-header/dialog-header.component.spec.ts b/frontend/src/app/shared/custom/dialog-header/dialog-header.component.spec.ts deleted file mode 100644 index 2065d3916a..0000000000 --- a/frontend/src/app/shared/custom/dialog-header/dialog-header.component.spec.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing'; - -import { DialogHeaderComponent } from './dialog-header.component'; - -describe('DialogHeaderComponent', () => { - let component: DialogHeaderComponent; - let fixture: ComponentFixture; - - beforeEach(() => { - TestBed.configureTestingModule({ - declarations: [DialogHeaderComponent], - }); - fixture = TestBed.createComponent(DialogHeaderComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); -}); diff --git a/frontend/src/app/shared/custom/dialog-header/dialog-header.component.ts b/frontend/src/app/shared/custom/dialog-header/dialog-header.component.ts deleted file mode 100644 index 3808dc9bb7..0000000000 --- a/frontend/src/app/shared/custom/dialog-header/dialog-header.component.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { Component, Input } from '@angular/core'; - -@Component({ - selector: 'app-dialog-header', - templateUrl: './dialog-header.component.html', - styleUrls: ['./dialog-header.component.scss'], -}) -export class DialogHeaderComponent { - @Input() - dialogTitle!: string; -} diff --git a/frontend/src/app/callback/callback.component.css b/frontend/src/app/shared/custom/dialog-template-core/dialog-template-core.component.cy.ts similarity index 100% rename from frontend/src/app/callback/callback.component.css rename to frontend/src/app/shared/custom/dialog-template-core/dialog-template-core.component.cy.ts diff --git a/frontend/src/app/shared/custom/dialog-template-core/dialog-template-core.component.html b/frontend/src/app/shared/custom/dialog-template-core/dialog-template-core.component.html new file mode 100644 index 0000000000..c74e127ca2 --- /dev/null +++ b/frontend/src/app/shared/custom/dialog-template-core/dialog-template-core.component.html @@ -0,0 +1,35 @@ +
+
+
+

+ {{ title }} + +

+
+
+
+ + + + + +
+ +
+
+ + + +
+ + + + +
+
+ +
+
+
diff --git a/frontend/src/app/shared/custom/dialog-template-core/dialog-template-core.component.scss b/frontend/src/app/shared/custom/dialog-template-core/dialog-template-core.component.scss new file mode 100644 index 0000000000..769a21b288 --- /dev/null +++ b/frontend/src/app/shared/custom/dialog-template-core/dialog-template-core.component.scss @@ -0,0 +1,24 @@ +@import "custom_bootstrap"; + +mat-dialog-content { + //scrollbar-gutter: stable both-edges; + $dialog-header-height: 48px; + $full-dialog-content-height: calc( + 100vh - $top-bar-height - $dialog-header-height - $dialog-content-padding-y - $dialog-action-buttons-div-height + ); + @extend .d-flex; + @extend .flex-column; + @extend .flex-wrap; + @extend .container-fluid; + max-height: $full-dialog-content-height !important; + + @include media-breakpoint-down(sm) { + height: $full-dialog-content-height !important; + } +} + +mat-dialog-actions { + @extend .d-flex; + @extend .justify-content-between; + height: $dialog-action-buttons-div-height !important; +} diff --git a/frontend/src/app/shared/custom/dialog-template-core/dialog-template-core.component.ts b/frontend/src/app/shared/custom/dialog-template-core/dialog-template-core.component.ts new file mode 100644 index 0000000000..95f651d3dd --- /dev/null +++ b/frontend/src/app/shared/custom/dialog-template-core/dialog-template-core.component.ts @@ -0,0 +1,22 @@ +import { Component, Input } from '@angular/core'; +import { Observable, of } from 'rxjs'; + +@Component({ + selector: 'app-dialog-template-core', + templateUrl: './dialog-template-core.component.html', + styleUrl: './dialog-template-core.component.scss', +}) +export class DialogTemplateCoreComponent { + @Input() observable: Observable = of({}); + @Input() title: string = ''; + + isValueReady(obj: any): boolean { + if (obj == null) { + return false; + } + if (Array.isArray(obj)) { + return obj.length > 0; + } + return true; + } +} diff --git a/frontend/src/app/shared/custom/okr-tangram/okr-tangram.component.html b/frontend/src/app/shared/custom/okr-tangram/okr-tangram.component.html index abd7776d79..92bb32f252 100644 --- a/frontend/src/app/shared/custom/okr-tangram/okr-tangram.component.html +++ b/frontend/src/app/shared/custom/okr-tangram/okr-tangram.component.html @@ -1 +1 @@ -okr-logo +okr-logo diff --git a/frontend/src/app/shared/dialog/cancel-dialog/cancel-dialog.component.html b/frontend/src/app/shared/dialog/cancel-dialog/cancel-dialog.component.html deleted file mode 100644 index b3d5fd8848..0000000000 --- a/frontend/src/app/shared/dialog/cancel-dialog/cancel-dialog.component.html +++ /dev/null @@ -1,18 +0,0 @@ -
- -
- - - @if (data.dialogText) { -
- {{ this.data.dialogText }} -
- } -
- - - - - diff --git a/frontend/src/app/shared/dialog/cancel-dialog/cancel-dialog.component.scss b/frontend/src/app/shared/dialog/cancel-dialog/cancel-dialog.component.scss deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/frontend/src/app/shared/dialog/cancel-dialog/cancel-dialog.component.ts b/frontend/src/app/shared/dialog/cancel-dialog/cancel-dialog.component.ts deleted file mode 100644 index c2a33fc611..0000000000 --- a/frontend/src/app/shared/dialog/cancel-dialog/cancel-dialog.component.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { Component, Inject } from '@angular/core'; -import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; - -export type CancelDialogData = { - dialogTitle: string; - dialogText?: string; -}; - -@Component({ - selector: 'app-cancel-dialog', - templateUrl: './cancel-dialog.component.html', - styleUrl: './cancel-dialog.component.scss', -}) -export class CancelDialogComponent { - constructor( - @Inject(MAT_DIALOG_DATA) public data: CancelDialogData, - private dialogRef: MatDialogRef, - ) {} - - closeAndConfirm() { - this.dialogRef.close(true); - } -} diff --git a/frontend/src/app/shared/dialog/complete-dialog/complete-dialog.component.html b/frontend/src/app/shared/dialog/complete-dialog/complete-dialog.component.html index 5a0cb5d688..e4eae92721 100644 --- a/frontend/src/app/shared/dialog/complete-dialog/complete-dialog.component.html +++ b/frontend/src/app/shared/dialog/complete-dialog/complete-dialog.component.html @@ -1,20 +1,18 @@ -
- -
- - -
-
-

Objective

-

{{ data.objectiveTitle }}

+ + +
+

Objective

+ +

{{ data.objectiveTitle }}

+
-
- -
-
+

Bewertung

+
+
-
+
+
-
- - - - {{ getErrorMessage("MAX_VALUE", "Kommentar", 4096) }} - +
+ +
+ + + {{ getErrorMessage("MAX_VALUE", "Kommentar", 4096) }} + +
-
- + - -
-
+ +
- +
-
- + + diff --git a/frontend/src/app/shared/dialog/complete-dialog/complete-dialog.component.scss b/frontend/src/app/shared/dialog/complete-dialog/complete-dialog.component.scss index 2244ba1bf0..f757130a0f 100644 --- a/frontend/src/app/shared/dialog/complete-dialog/complete-dialog.component.scss +++ b/frontend/src/app/shared/dialog/complete-dialog/complete-dialog.component.scss @@ -73,7 +73,3 @@ div:hover { @extend .card-shadow; background-color: #ba3838; } - -.dialog-content { - max-height: 30vh; -} diff --git a/frontend/src/app/shared/dialog/complete-dialog/complete-dialog.component.spec.ts b/frontend/src/app/shared/dialog/complete-dialog/complete-dialog.component.spec.ts index e949a38b9a..72ce7381c6 100644 --- a/frontend/src/app/shared/dialog/complete-dialog/complete-dialog.component.spec.ts +++ b/frontend/src/app/shared/dialog/complete-dialog/complete-dialog.component.spec.ts @@ -2,10 +2,14 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { CompleteDialogComponent } from './complete-dialog.component'; import { MAT_DIALOG_DATA, MatDialogModule, MatDialogRef } from '@angular/material/dialog'; -import { DialogHeaderComponent } from '../../custom/dialog-header/dialog-header.component'; import { TranslateService } from '@ngx-translate/core'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { MatIconModule } from '@angular/material/icon'; +import { DialogTemplateCoreComponent } from '../../custom/dialog-template-core/dialog-template-core.component'; +import { MatDividerModule } from '@angular/material/divider'; +import { provideRouter } from '@angular/router'; +import { provideHttpClient } from '@angular/common/http'; +import { provideHttpClientTesting } from '@angular/common/http/testing'; const dialogMock = { close: jest.fn(), @@ -21,12 +25,16 @@ let matDataMock: { objective: { objectiveId: number | undefined; teamId: number describe('CompleteDialogComponent', () => { let component: CompleteDialogComponent; let fixture: ComponentFixture; + let debugElement: any; beforeEach(() => { TestBed.configureTestingModule({ - imports: [FormsModule, ReactiveFormsModule, MatDialogModule, MatIconModule], - declarations: [CompleteDialogComponent, DialogHeaderComponent], + imports: [FormsModule, ReactiveFormsModule, MatDialogModule, MatIconModule, MatDividerModule], + declarations: [CompleteDialogComponent, DialogTemplateCoreComponent], providers: [ + provideRouter([]), + provideHttpClient(), + provideHttpClientTesting(), { provide: MatDialogRef, useValue: dialogMock }, { provide: MAT_DIALOG_DATA, useValue: matDataMock }, { provide: TranslateService, useValue: {} }, @@ -35,6 +43,7 @@ describe('CompleteDialogComponent', () => { fixture = TestBed.createComponent(CompleteDialogComponent); component = fixture.componentInstance; fixture.detectChanges(); + debugElement = fixture.debugElement.nativeElement; }); it('should create', () => { @@ -45,7 +54,7 @@ describe('CompleteDialogComponent', () => { let elements = document.querySelectorAll('.valuation-card'); let successful = document.querySelectorAll('.card-hover-successful'); let notSuccessful = document.querySelectorAll('.card-hover-not-successful'); - let submitButton = document.querySelectorAll('button')[1]; + let submitButton = debugElement.querySelector('[data-testid="submit"]'); expect(elements.length).toEqual(2); expect(successful.length).toEqual(1); @@ -59,7 +68,7 @@ describe('CompleteDialogComponent', () => { it('should change isSuccessful value on card click and remove class card-hover', () => { component.switchSuccessState('successful'); let elements = document.querySelectorAll('.card-hover'); - let submitButton = document.querySelectorAll('button')[1]; + let submitButton = debugElement.querySelector('[data-testid="submit"]'); expect(component.completeForm.value.isSuccessful).toBeTruthy(); expect(component.completeForm.invalid).toBeFalsy(); diff --git a/frontend/src/app/shared/dialog/confirm-dialog/confirm-dialog.component.html b/frontend/src/app/shared/dialog/confirm-dialog/confirm-dialog.component.html index 60f6120ae7..c34a464c2b 100644 --- a/frontend/src/app/shared/dialog/confirm-dialog/confirm-dialog.component.html +++ b/frontend/src/app/shared/dialog/confirm-dialog/confirm-dialog.component.html @@ -1,16 +1,23 @@ -
- -
+ + +
+ {{ this.dialogText.trim() }} +
+
- -
- {{ this.dialogText.trim() }} -
-
+ +
+ - - - - + +
+
+
diff --git a/frontend/src/app/shared/dialog/confirm-dialog/confirm-dialog.component.spec.ts b/frontend/src/app/shared/dialog/confirm-dialog/confirm-dialog.component.spec.ts index 7ed74ba54e..22c02998c8 100644 --- a/frontend/src/app/shared/dialog/confirm-dialog/confirm-dialog.component.spec.ts +++ b/frontend/src/app/shared/dialog/confirm-dialog/confirm-dialog.component.spec.ts @@ -11,9 +11,10 @@ import { MatInputModule } from '@angular/material/input'; import { MatRadioModule } from '@angular/material/radio'; import { ReactiveFormsModule } from '@angular/forms'; import { TranslateModule, TranslateService } from '@ngx-translate/core'; -import { DialogHeaderComponent } from '../../custom/dialog-header/dialog-header.component'; import { MatIconModule } from '@angular/material/icon'; import { ConfirmDialogData } from '../../../services/dialog.service'; +import { DialogTemplateCoreComponent } from '../../custom/dialog-template-core/dialog-template-core.component'; +import { MatDividerModule } from '@angular/material/divider'; const dialogRefMock = { close: jest.fn(), @@ -35,8 +36,9 @@ describe('ConfirmDialogComponent', () => { ReactiveFormsModule, TranslateModule.forRoot(), MatIconModule, + MatDividerModule, ], - declarations: [ConfirmDialogComponent, DialogHeaderComponent], + declarations: [ConfirmDialogComponent, DialogTemplateCoreComponent], providers: [ TranslateService, { provide: MAT_DIALOG_DATA, useValue: { title: '', text: '' } as ConfirmDialogData }, diff --git a/frontend/src/app/shared/dialog/example-dialog/example-dialog.component.html b/frontend/src/app/shared/dialog/example-dialog/example-dialog.component.html index 83957d9873..37a8bc048e 100644 --- a/frontend/src/app/shared/dialog/example-dialog/example-dialog.component.html +++ b/frontend/src/app/shared/dialog/example-dialog/example-dialog.component.html @@ -1,50 +1,55 @@ -

This is an example dialog

- -
- - Name - -
- + + This is an example dialog + + + + + Name + +
+ + {{ errorMessages[errorKey.toUpperCase()] }} + +
+
+ + + Male + Female + Other + +
+ {{ errorMessages[errorKey.toUpperCase()] }}
- - - - Male - Female - Other - -
- - {{ errorMessages[errorKey.toUpperCase()] }} - -
- - Please select a hobby - - {{ hobby }} - - -
- - {{ errorMessages[errorKey.toUpperCase()] }} - -
+ + Please select a hobby + + {{ hobby }} + + +
+ + {{ errorMessages[errorKey.toUpperCase()] }} + +
+ +
- - + +
- - - + +
+
+
diff --git a/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.html b/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.html index 742809d8f3..8479d497d8 100644 --- a/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.html +++ b/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.html @@ -1,30 +1,9 @@ -
-
- -
-
- -
-
- - -
-
- - -
-
-
-
- + + + +
+ +