Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Enhanced activity & notifications #2330

Draft
wants to merge 52 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
52 commits
Select commit Hold shift + click to select a range
bc0f83c
feat: notifications core service draft
cyyynthia Nov 14, 2023
1ec0527
chore: cleanup batch job internal events
cyyynthia Nov 19, 2023
eefcec0
feat: core user notification dispatch
cyyynthia Nov 19, 2023
c4a183c
chore: lint
cyyynthia Nov 19, 2023
fcecce7
feat: finish user notification dispatch logic
cyyynthia Nov 20, 2023
bd8bfab
fix: make fetching translation to language maps work again
cyyynthia Nov 21, 2023
a850303
tests: test notification dispatching
cyyynthia Nov 24, 2023
3f10a64
Merge remote-tracking branch 'origin/main' into cynthia/notifications…
cyyynthia Nov 24, 2023
3b31359
feat: redo notifications the right way this time
cyyynthia Dec 3, 2023
397dd08
chore(deps): solve ij classpath issues with json-unit & assertj
cyyynthia Dec 3, 2023
9f1c7cd
feat: debounce key creation and initial string creations
cyyynthia Dec 3, 2023
c97c9e9
feat: notification preference configuration
cyyynthia Dec 3, 2023
85149be
feat: respect notification subscription settings
cyyynthia Dec 3, 2023
f235131
tests: fix json number matching
cyyynthia Dec 4, 2023
caa4ce4
fix: handle modified entity relations using rdbms-level cascade
cyyynthia Dec 4, 2023
2384df9
chore: lint
cyyynthia Dec 4, 2023
be30fc9
fix: hibernate proxy behavior
cyyynthia Dec 4, 2023
16de53b
tests: remove unused mockbean
cyyynthia Dec 4, 2023
19d7c6e
fix: hibernate lazy initialization problems
cyyynthia Dec 4, 2023
e612c0c
feat: implement notification http endpoints
cyyynthia Dec 13, 2023
4aebb94
fix: notification updates
cyyynthia Dec 13, 2023
621113a
Merge remote-tracking branch 'origin/main' into cynthia/notifications…
cyyynthia Dec 13, 2023
38e938a
fix: key creation test
cyyynthia Dec 13, 2023
9569892
Merge remote-tracking branch 'origin/main' into cynthia/notifications…
cyyynthia Jan 22, 2024
8823857
Merge remote-tracking branch 'origin/main' into cynthia/notifications…
cyyynthia Jan 24, 2024
1d54b2e
fix: migrate notifications to Spring Boot 3
cyyynthia Jan 26, 2024
49b8710
Merge remote-tracking branch 'origin/main' into cynthia/notifications…
cyyynthia Jan 26, 2024
1698670
chore: format
cyyynthia Jan 26, 2024
cda1335
fix(tests): handle bad transactions
cyyynthia Jan 26, 2024
b838b7f
fix: make language permission fetching more efficient
cyyynthia Jan 27, 2024
c845dda
Merge remote-tracking branch 'origin/cynthia/notifications-core' into…
cyyynthia Jan 27, 2024
b841451
chore: ktlint
cyyynthia Jan 27, 2024
c208be0
fix: bad aggregation result
cyyynthia Jan 27, 2024
0601b71
fix: bad permission mock
cyyynthia Jan 27, 2024
9cb51f4
chore(webapp): cleanup & update some deps
cyyynthia Feb 5, 2024
ac25e49
feat: add notification bell and root view
cyyynthia Feb 5, 2024
ca62a12
fix: workaround the fact npm is a bad pkg manager
cyyynthia Feb 5, 2024
a6e0362
feat: skeleton of the notifications view
cyyynthia Feb 14, 2024
f018b9b
Merge remote-tracking branch 'origin/cynthia/notifications-core' into…
cyyynthia Feb 14, 2024
cbb7633
Merge branch 'main' into cynthia/notifications-core
cyyynthia Apr 29, 2024
f1e0ee9
fix: use listagg instead of array_agg
cyyynthia Apr 29, 2024
0f2dca5
fix: Minor updates
JanCizmar May 15, 2024
2c3c8f7
feat: Provide swagger schemas of activity modifications
JanCizmar May 17, 2024
c6ed341
feat: Activity type definitions, activity grouping
JanCizmar May 30, 2024
dab8b16
feat: Grouper
JanCizmar May 30, 2024
5fb7b38
fix: Add JOOQ, activity grouping stuff
JanCizmar Jun 6, 2024
4e7166a
Merge branch 'main' into jancizmar/notifications-activity-grouping
JanCizmar Jul 29, 2024
3a8d10c
feat: Project creation group backend
JanCizmar Jul 31, 2024
80b59d4
feat: "Create Key" group with items
JanCizmar Aug 22, 2024
0431b7a
feat: "Create Key" group with items
JanCizmar Aug 22, 2024
59cf99c
feat: "Create Key" group items and their relations
JanCizmar Sep 4, 2024
59bc144
feat: Store base translation value for key
JanCizmar Sep 14, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import io.tolgee.api.EeSubscriptionProvider
import io.tolgee.component.PreferredOrganizationFacade
import io.tolgee.hateoas.InitialDataModel
import io.tolgee.hateoas.ee.IEeSubscriptionModelAssembler
import io.tolgee.notifications.UserNotificationService
import io.tolgee.openApiDocs.OpenApiHideFromPublicDocs
import io.tolgee.security.authentication.AuthenticationFacade
import io.tolgee.service.security.UserPreferencesService
Expand All @@ -32,6 +33,7 @@ class InitialDataController(
private val eeSubscriptionModelAssembler: IEeSubscriptionModelAssembler,
private val eeSubscriptionProvider: EeSubscriptionProvider,
private val announcementController: AnnouncementController,
private val userNotificationService: UserNotificationService,
) : IController {
@GetMapping(value = [""])
@Operation(summary = "Get initial data", description = "Returns initial data required by the UI to load")
Expand All @@ -53,6 +55,7 @@ class InitialDataController(
data.preferredOrganization = preferredOrganizationFacade.getPreferred()
data.languageTag = userPreferencesService.find(userAccount.id)?.language
data.announcement = announcementController.getLatest()
data.unreadNotifications = userNotificationService.getUnreadNotificationsCount(userAccount.id)
}

return data
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,12 @@ import io.swagger.v3.oas.annotations.Parameter
import io.swagger.v3.oas.annotations.media.ExampleObject
import io.swagger.v3.oas.annotations.tags.Tag
import io.tolgee.activity.ActivityService
import io.tolgee.activity.groups.ActivityGroupService
import io.tolgee.dtos.queryResults.ActivityGroupView
import io.tolgee.dtos.request.ActivityGroupFilters
import io.tolgee.exceptions.NotFoundException
import io.tolgee.hateoas.activity.ActivityGroupModel
import io.tolgee.hateoas.activity.ActivityGroupModelAssembler
import io.tolgee.hateoas.activity.ModifiedEntityModel
import io.tolgee.hateoas.activity.ModifiedEntityModelAssembler
import io.tolgee.hateoas.activity.ProjectActivityModel
Expand All @@ -31,6 +36,7 @@ import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RequestParam
import org.springframework.web.bind.annotation.RestController
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping

@Suppress("MVCPathVariableInspection", "SpringJavaInjectionPointsAutowiringInspection")
@RestController
Expand All @@ -44,6 +50,10 @@ class ProjectActivityController(
private val modificationResourcesAssembler: PagedResourcesAssembler<ModifiedEntityView>,
private val projectActivityModelAssembler: ProjectActivityModelAssembler,
private val modifiedEntityModelAssembler: ModifiedEntityModelAssembler,
private val activityGroupService: ActivityGroupService,
private val groupPagedResourcesAssembler: PagedResourcesAssembler<ActivityGroupView>,
private val groupModelAssembler: ActivityGroupModelAssembler,
private val requestMappingHandlerMapping: RequestMappingHandlerMapping,
) {
@Operation(summary = "Get project activity")
@GetMapping("", produces = [MediaTypes.HAL_JSON_VALUE])
Expand All @@ -52,7 +62,7 @@ class ProjectActivityController(
fun getActivity(
@ParameterObject pageable: Pageable,
): PagedModel<ProjectActivityModel> {
val views = activityService.findProjectActivity(projectId = projectHolder.project.id, pageable)
val views = activityService.getProjectActivity(projectId = projectHolder.project.id, pageable)
return activityPagedResourcesAssembler.toModel(views, projectActivityModelAssembler)
}

Expand All @@ -64,7 +74,7 @@ class ProjectActivityController(
@PathVariable revisionId: Long,
): ProjectActivityModel {
val views =
activityService.findProjectActivity(projectId = projectHolder.project.id, revisionId)
activityService.getProjectActivity(projectId = projectHolder.project.id, revisionId)
?: throw NotFoundException()
return projectActivityModelAssembler.toModel(views)
}
Expand All @@ -91,4 +101,24 @@ class ProjectActivityController(
)
return modificationResourcesAssembler.toModel(page, modifiedEntityModelAssembler)
}

@Operation(
summary = "Get project activity groups",
description = "This endpoints returns the activity grouped by time windows so it's easier to read on the frontend.",
)
@GetMapping("/groups", produces = [MediaTypes.HAL_JSON_VALUE])
@RequiresProjectPermissions([Scope.ACTIVITY_VIEW])
@AllowApiAccess
fun getActivityGroups(
@ParameterObject pageable: Pageable,
@ParameterObject activityGroupFilters: ActivityGroupFilters,
): PagedModel<ActivityGroupModel> {
val views =
activityGroupService.getProjectActivityGroups(
projectId = projectHolder.project.id,
pageable,
activityGroupFilters,
)
return groupPagedResourcesAssembler.toModel(views, groupModelAssembler)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/*
* Copyright (c) 2020. Tolgee
*/

package io.tolgee.api.v2.controllers

import io.swagger.v3.oas.annotations.Operation
import io.swagger.v3.oas.annotations.tags.Tag
import io.tolgee.activity.groups.viewProviders.createKey.CreateKeyGroupItemModel
import io.tolgee.activity.groups.viewProviders.createKey.CreateKeyGroupModelProvider
import io.tolgee.model.enums.Scope
import io.tolgee.security.authentication.AllowApiAccess
import io.tolgee.security.authorization.RequiresProjectPermissions
import org.springdoc.core.annotations.ParameterObject
import org.springframework.data.domain.Pageable
import org.springframework.hateoas.MediaTypes
import org.springframework.hateoas.PagedModel
import org.springframework.hateoas.PagedModel.PageMetadata
import org.springframework.web.bind.annotation.CrossOrigin
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController

@Suppress("MVCPathVariableInspection")
@RestController
@CrossOrigin(origins = ["*"])
@RequestMapping(value = ["/v2/projects/{projectId}/activity/group-items", "/v2/projects/activity/group-items"])
@Tag(name = "Activity Groups")
class ProjectActivityGroupItemsController(
private val createKeyGroupModelProvider: CreateKeyGroupModelProvider,
) {
@Operation(
summary = "Get CREATE_KEY group items",
)
@GetMapping("/create-key/{groupId}", produces = [MediaTypes.HAL_JSON_VALUE])
@RequiresProjectPermissions([Scope.ACTIVITY_VIEW])
@AllowApiAccess
fun getCreateKeyItems(
@ParameterObject pageable: Pageable,
@PathVariable groupId: Long,
): PagedModel<CreateKeyGroupItemModel> {
val data = createKeyGroupModelProvider.provideItems(groupId, pageable)
return PagedModel.of(
data.content,
PageMetadata(data.pageable.pageSize.toLong(), data.pageable.pageNumber.toLong(), data.totalElements),
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
/**
* Copyright (C) 2023 Tolgee s.r.o. and contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package io.tolgee.api.v2.controllers.notifications

import io.swagger.v3.oas.annotations.Operation
import io.swagger.v3.oas.annotations.tags.Tag
import io.tolgee.notifications.NotificationPreferencesService
import io.tolgee.notifications.dto.NotificationPreferencesDto
import io.tolgee.security.authentication.AuthenticationFacade
import org.springframework.http.HttpHeaders
import org.springframework.http.HttpStatus
import org.springframework.http.ResponseEntity
import org.springframework.validation.annotation.Validated
import org.springframework.web.bind.annotation.DeleteMapping
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.PutMapping
import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.ResponseStatus
import org.springframework.web.bind.annotation.RestController

@RestController
@RequestMapping(value = ["/v2/notifications/preferences"])
@Tag(name = "Notification preferences")
class NotificationPreferencesController(
private val authenticationFacade: AuthenticationFacade,
private val notificationPreferencesService: NotificationPreferencesService,
) {
@GetMapping("")
@Operation(summary = "Fetch the global preferences and all overrides of the current user")
fun getAllPreferences(): Map<String, NotificationPreferencesDto> {
return notificationPreferencesService.getAllPreferences(authenticationFacade.authenticatedUser.id)
}

@GetMapping("/global")
@Operation(summary = "Fetch the global preferences for the current user")
fun getGlobalPreferences(): NotificationPreferencesDto {
return notificationPreferencesService.getGlobalPreferences(authenticationFacade.authenticatedUser.id)
}

@PutMapping("/global")
@Operation(summary = "Update the global notification preferences of the current user")
fun updateGlobalPreferences(
@RequestBody @Validated preferencesDto: NotificationPreferencesDto,
): NotificationPreferencesDto {
val updated =
notificationPreferencesService.setPreferencesOfUser(
authenticationFacade.authenticatedUser.id,
preferencesDto,
)

return NotificationPreferencesDto.fromEntity(updated)
}

@GetMapping("/project/{id}")
@Operation(summary = "Fetch the notification preferences of the current user for a specific project")
fun getPerProjectPreferences(
@PathVariable("id") id: Long,
): NotificationPreferencesDto {
return notificationPreferencesService.getProjectPreferences(
authenticationFacade.authenticatedUser.id,
id,
)
}

@PutMapping("/project/{id}")
@Operation(summary = "Update the notification preferences of the current user for a specific project")
fun updatePerProjectPreferences(
@PathVariable("id") id: Long,
@RequestBody @Validated preferencesDto: NotificationPreferencesDto,
): NotificationPreferencesDto {
val updated =
notificationPreferencesService.setProjectPreferencesOfUser(
authenticationFacade.authenticatedUser.id,
id,
preferencesDto,
)

return NotificationPreferencesDto.fromEntity(updated)
}

@DeleteMapping("/project/{id}")
@ResponseStatus(HttpStatus.NO_CONTENT)
@Operation(summary = "Delete the notification preferences of the current user for a specific project")
fun deletePerProjectPreferences(
@PathVariable("id") id: Long,
) {
notificationPreferencesService.deleteProjectPreferencesOfUser(
authenticationFacade.authenticatedUser.id,
id,
)
}

@PostMapping("/project/{id}/subscribe")
@Operation(summary = "Subscribe to notifications for a given project")
fun subscribeToProject(
@PathVariable("id") id: Long,
): ResponseEntity<String> {
return ResponseEntity(
"Coming soon! Please see https://github.com/tolgee/tolgee-platform/issues/1360 for progress on this. :D",
HttpHeaders().also {
@Suppress("UastIncorrectHttpHeaderInspection")
it.add(
"x-hey-curious-reader",
"oh hey there, didn't expect you here... " +
"if you're here, might as well join us! https://tolgee.io/career",
)
},
HttpStatus.NOT_IMPLEMENTED,
)
}
}
Loading