Skip to content

Commit 6a7f116

Browse files
authored
Added notification display (#2816)
* Added notification display
1 parent 3f597d6 commit 6a7f116

6 files changed

Lines changed: 203 additions & 4 deletions

File tree

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
package com.saveourtool.save.backend.controllers
2+
3+
import com.saveourtool.save.authservice.utils.username
4+
import com.saveourtool.save.backend.service.NotificationService
5+
import com.saveourtool.save.configs.ApiSwaggerSupport
6+
import com.saveourtool.save.entities.NotificationDto
7+
import com.saveourtool.save.utils.StringResponse
8+
import com.saveourtool.save.utils.blockingToFlux
9+
import com.saveourtool.save.utils.blockingToMono
10+
import com.saveourtool.save.v1
11+
import io.swagger.v3.oas.annotations.Operation
12+
import io.swagger.v3.oas.annotations.responses.ApiResponse
13+
import io.swagger.v3.oas.annotations.tags.Tag
14+
import io.swagger.v3.oas.annotations.tags.Tags
15+
import org.springframework.http.HttpStatus
16+
import org.springframework.http.ResponseEntity
17+
import org.springframework.security.access.prepost.PreAuthorize
18+
import org.springframework.security.core.Authentication
19+
import org.springframework.web.bind.annotation.*
20+
import org.springframework.web.server.ResponseStatusException
21+
import reactor.core.publisher.Flux
22+
import reactor.core.publisher.Mono
23+
24+
/**
25+
* Controller for working with notifications.
26+
*/
27+
@ApiSwaggerSupport
28+
@Tags(
29+
Tag(name = "notifications"),
30+
)
31+
@RestController
32+
@RequestMapping(path = ["/api/$v1/notifications"])
33+
class NotificationController(
34+
private val notificationService: NotificationService,
35+
) {
36+
@GetMapping("/get-all-by-user")
37+
@Operation(
38+
method = "GET",
39+
summary = "Get all notifications by user name.",
40+
description = "Get user notifications.",
41+
)
42+
@ApiResponse(responseCode = "200", description = "Successfully fetched all notifications by user name")
43+
fun getAllNotifications(
44+
authentication: Authentication?,
45+
): Flux<NotificationDto> = blockingToFlux {
46+
authentication?.let {
47+
notificationService.getAllByUserName(authentication.username()).map { it.toDto() }
48+
}.orEmpty()
49+
}
50+
51+
@DeleteMapping("/delete-by-id")
52+
@Operation(
53+
method = "DELETE",
54+
summary = "Delete notification by id.",
55+
description = "Delete notification by id.",
56+
)
57+
@PreAuthorize("permitAll()")
58+
@ApiResponse(responseCode = "200", description = "Successfully deleted notification by id")
59+
fun deleteById(
60+
@RequestParam id: Long,
61+
authentication: Authentication?,
62+
): Mono<StringResponse> = blockingToMono {
63+
val notification = notificationService.getById(id)
64+
notification?.let {
65+
if (authentication?.username() == notification.user.name) {
66+
notificationService.deleteById(id)
67+
ResponseEntity.ok("Successfully deleted requested notification.")
68+
} else {
69+
throw ResponseStatusException(HttpStatus.FORBIDDEN, "it is not your notification")
70+
}
71+
} ?: throw ResponseStatusException(HttpStatus.NOT_FOUND, "notification is not found")
72+
}
73+
}

save-backend/src/main/kotlin/com/saveourtool/save/backend/repository/NotificationRepository.kt

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,10 @@ import org.springframework.stereotype.Repository
88
* Repository to access data about user notification
99
*/
1010
@Repository
11-
interface NotificationRepository : BaseEntityRepository<Notification>
11+
interface NotificationRepository : BaseEntityRepository<Notification> {
12+
/**
13+
* @param name
14+
* @return list of notification
15+
*/
16+
fun findByUserName(name: String): List<Notification>
17+
}

save-backend/src/main/kotlin/com/saveourtool/save/backend/service/NotificationService.kt

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package com.saveourtool.save.backend.service
22

33
import com.saveourtool.save.backend.repository.NotificationRepository
44
import com.saveourtool.save.entities.Notification
5+
import org.springframework.data.repository.findByIdOrNull
56
import org.springframework.stereotype.Service
67
import org.springframework.transaction.annotation.Transactional
78

@@ -26,4 +27,22 @@ class NotificationService(
2627
*/
2728
@Transactional
2829
fun saveAll(notifications: List<Notification>): List<Notification> = notificationRepository.saveAll(notifications)
30+
31+
/**
32+
* @param name name of user
33+
* @return list of notifications
34+
*/
35+
fun getAllByUserName(name: String): List<Notification> = notificationRepository.findByUserName(name)
36+
37+
/**
38+
* @param id of notification
39+
*/
40+
@Transactional
41+
fun deleteById(id: Long) = notificationRepository.deleteById(id)
42+
43+
/**
44+
* @param id of notification
45+
* @return notification by id
46+
*/
47+
fun getById(id: Long) = notificationRepository.findByIdOrNull(id)
2948
}

save-cloud-common/src/commonMain/kotlin/com/saveourtool/save/entities/NotificationDto.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,13 @@ import kotlinx.datetime.LocalDateTime
44
import kotlinx.serialization.Serializable
55

66
/**
7+
* @property id
78
* @property message
89
* @property createDate
910
*/
1011
@Serializable
1112
class NotificationDto(
13+
val id: Long,
1214
val message: String,
1315
val createDate: LocalDateTime?,
1416
)

save-cloud-common/src/jvmMain/kotlin/com/saveourtool/save/entities/Notification.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ class Notification(
2121
var user: User,
2222
) : BaseEntityWithDateAndDto<NotificationDto>() {
2323
override fun toDto() = NotificationDto(
24+
id = requiredId(),
2425
message = message,
2526
createDate = createDate?.toKotlinLocalDateTime(),
2627
)

save-frontend/src/main/kotlin/com/saveourtool/save/frontend/components/views/index/IndexViewUserInfo.kt

Lines changed: 101 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,24 +4,71 @@
44

55
package com.saveourtool.save.frontend.components.views.index
66

7+
import com.saveourtool.save.entities.NotificationDto
8+
import com.saveourtool.save.frontend.components.basic.markdown
9+
import com.saveourtool.save.frontend.externals.fontawesome.faTimes
710
import com.saveourtool.save.frontend.externals.i18next.useTranslation
8-
import com.saveourtool.save.frontend.utils.UserInfoAwareProps
11+
import com.saveourtool.save.frontend.utils.*
12+
import com.saveourtool.save.frontend.utils.noopLoadingHandler
13+
import com.saveourtool.save.utils.toUnixCalendarFormat
14+
915
import js.core.jso
1016
import react.ChildrenBuilder
1117
import react.FC
18+
import react.StateSetter
1219
import react.dom.html.ReactHTML.div
1320
import react.dom.html.ReactHTML.h2
1421
import react.dom.html.ReactHTML.h4
1522
import react.dom.html.ReactHTML.img
1623
import react.dom.html.ReactHTML.p
24+
import react.dom.html.ReactHTML.span
25+
import react.useState
1726
import web.cssom.*
1827

28+
import kotlinx.browser.window
29+
import kotlinx.datetime.TimeZone
30+
1931
const val INDEX_VIEW_CUSTOM_BG = "rgb(247, 250, 253)"
32+
private const val DEFAULT_MAX_NOTIFICATION_AMOUNT = 5
2033

2134
@Suppress("IDENTIFIER_LENGTH")
2235
val indexViewInfo: FC<UserInfoAwareProps> = FC { props ->
2336
val (t) = useTranslation("index")
2437

38+
val (notifications, setNotifications) = useState(emptyList<NotificationDto>())
39+
val (isAllNotificationsShown, setIsAllNotificationsShown) = useState(false)
40+
val (notificationForDeletion, setNotificationForDeletion) = useState<NotificationDto?>(null)
41+
42+
useRequest {
43+
val newNotifications = get(
44+
url = "$apiUrl/notifications/get-all-by-user",
45+
headers = jsonHeaders,
46+
loadingHandler = ::noopLoadingHandler,
47+
).unsafeMap {
48+
it.decodeFromJsonString<List<NotificationDto>>()
49+
}
50+
51+
setNotifications(newNotifications)
52+
}
53+
54+
useRequest(arrayOf(notificationForDeletion)) {
55+
notificationForDeletion?.let { notification ->
56+
delete(
57+
url = "$apiUrl/notifications/delete-by-id",
58+
params = jso<dynamic> {
59+
id = notification.id
60+
},
61+
headers = jsonHeaders,
62+
loadingHandler = ::noopLoadingHandler,
63+
).run {
64+
if (ok) {
65+
setNotifications { it.minus(notification) }
66+
setNotificationForDeletion(null)
67+
}
68+
}
69+
}
70+
}
71+
2572
div {
2673
className = ClassName("row justify-content-center mt-5 text-gray-900")
2774
h2 {
@@ -51,8 +98,25 @@ val indexViewInfo: FC<UserInfoAwareProps> = FC { props ->
5198
className = ClassName("card mb-4 mr-3 ml-3")
5299
div {
53100
className = ClassName("card-body")
54-
p {
55-
+"Your notifications will be located here.".t()
101+
if (notifications.isEmpty()) {
102+
p {
103+
+"Your notifications will be located here.".t()
104+
}
105+
} else {
106+
if (notifications.size > DEFAULT_MAX_NOTIFICATION_AMOUNT && !isAllNotificationsShown) {
107+
display(notifications.take(DEFAULT_MAX_NOTIFICATION_AMOUNT), setNotificationForDeletion)
108+
div {
109+
className = ClassName("col-12 mt-3 px-0")
110+
onClick = { setIsAllNotificationsShown(true) }
111+
style = jso { cursor = "pointer".unsafeCast<Cursor>() }
112+
h4 {
113+
className = ClassName("text-center card p-2 shadow")
114+
+"Show all ${notifications.size} comments"
115+
}
116+
}
117+
} else {
118+
display(notifications, setNotificationForDeletion)
119+
}
56120
}
57121
}
58122
}
@@ -71,3 +135,37 @@ internal fun ChildrenBuilder.cardImage(img: String) {
71135
}
72136
}
73137
}
138+
139+
/**
140+
* @param notifications list of notification
141+
* @param setNotificationForDeletion [StateSetter] for delete notifications
142+
*/
143+
internal fun ChildrenBuilder.display(
144+
notifications: List<NotificationDto>,
145+
setNotificationForDeletion: StateSetter<NotificationDto?>
146+
) {
147+
notifications.forEach { notification ->
148+
div {
149+
className = ClassName("shadow-none card text-left border-0")
150+
div {
151+
className = ClassName("flex-wrap d-flex justify-content-between")
152+
style = jso { background = "#f1f1f1".unsafeCast<Background>() }
153+
span {
154+
className = ClassName("ml-1")
155+
+(notification.createDate?.toUnixCalendarFormat(TimeZone.currentSystemDefault()) ?: "Unknown")
156+
}
157+
div {
158+
buttonBuilder(faTimes, style = "", classes = "btn-sm") {
159+
if (window.confirm("Are you sure you want to delete a notification?")) {
160+
setNotificationForDeletion(notification)
161+
}
162+
}
163+
}
164+
}
165+
div {
166+
className = ClassName("shadow-none card card-body border-0")
167+
markdown(notification.message.split("\n").joinToString("\n\n"))
168+
}
169+
}
170+
}
171+
}

0 commit comments

Comments
 (0)