-
-
Notifications
You must be signed in to change notification settings - Fork 366
feat: key archiving #3486
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
Merged
Merged
feat: key archiving #3486
Changes from all commits
Commits
Show all changes
77 commits
Select commit
Hold shift + click to select a range
69e899e
feat: implement soft-delete for keys (move to trash)
bdshadow 1a8905f
test: add soft-delete edge case tests for branching, namespaces, and …
bdshadow 52c03be
feat: add trash UI and refactor key search for soft-deleted keys
bdshadow b18662d
fix: handle soft-deleted keys in branching operations
bdshadow f9c7f9e
feat: add red dot badge to trash icon when keys are in trash
bdshadow 42123c8
feat: add trash translation keys to en.json and remove inline defaults
bdshadow 5aaaffa
feat: add batch restore and hard-delete operations for trash
bdshadow 4bb3134
fix: invalidate trash count cache after batch operations
bdshadow 8a49aef
feat: add screenshots to trash view with proper sizing and text wrapping
bdshadow b87d83f
feat: include isPlural in trash API for proper plural rendering
bdshadow 4f4d479
fix: add display names for restore and hard-delete batch operations
bdshadow ab1392c
fix: make TrashedKeyWithTranslationsModelAssembler thread-safe
bdshadow 32bf473
fix: add activity logging for permanent key deletion
bdshadow 6832222
feat: add activity log labels for trash operations
bdshadow 10a20d9
fix: scope restoreKeys to project and include key name in conflict error
bdshadow 1b4986a
fix: include deletedAt in Key.isModified for branch revision tracking
bdshadow 66433e6
fix: add data-cy and aria-label to TrashBanner close button
bdshadow b16ee87
refactor: remove @ts-ignore and as-any casts now that schema includes…
bdshadow 31aaea3
perf: fix N+1 queries, unbounded purge, missing index, and redundant …
bdshadow 65a9814
chore: prettify UI
bdshadow 501869a
fix: update websocket test and namespace endpoint for soft-delete
bdshadow e144f99
fix: mark key as deleted in UI when soft-delete websocket event arrives
bdshadow ba1b8a6
chore: prettify UI
bdshadow 36c6803
fix: exclude soft-deleted keys from import resolution and translation…
bdshadow 85651cb
fix: prevent duplicate ORDER BY in trash search query
bdshadow 97f802e
test: add trash search and list tests
bdshadow 3f84b36
fix: equalize spacing between sort and trash icons
bdshadow e80b6d6
fix: translations according to design
bdshadow cf7558b
fix: invalidate trash count after batch delete finishes
bdshadow 22155f5
feat: show deleted-by user avatar in trash
bdshadow edf9a3c
fix: align trash column content to top
bdshadow 75f9f0c
fix: improve trash view layout alignment and banner spacing
bdshadow 0441e2d
feat: reuse translation view infrastructure for trash filtering
bdshadow d3e1d2b
fix: resolve NPE in softDeleteMultiple default parameter on CGLIB proxy
bdshadow acd9245
fix: trash table header styling and full-width layout
bdshadow a53f9a8
fix: narrow key column to snug trashed column to the left
bdshadow b4d8d80
refactor: extract shared KeyCellContent and TranslationCellReadOnly c…
bdshadow a2ad2fd
fix: hard-delete soft-deleted keys before merge addition to prevent c…
bdshadow 485f841
fix: filter trash count by branch and exclude soft-deleted keys from …
bdshadow b2bc1b2
fix: propagate KEY_EXISTS error to UI during batch restore keys
bdshadow e31ab90
feat: add branch selector to trash view
bdshadow 16f2444
fix: preserve current branch when navigating to trash view
bdshadow 2874665
fix: hide broken "show failed keys" link for batch restore jobs
bdshadow 07dff04
test: add e2e test for trashing duplicate key names
bdshadow ace53c0
feat: improve trash view UX
bdshadow 12cc271
feat: add sort menu to trash view
bdshadow ff75970
chore: ktlint
bdshadow d7b1749
chore: regenerate API schema and remove unnecessary type casts
bdshadow 39f1d79
chore: ts pretify
bdshadow e72c4c5
refactor: extract UserSearchSelect as non-EE component for trash filter
bdshadow 64bad1e
fix: use static translation keys in TrashRow for Tolgee extractor
bdshadow ed163e2
fix: prefix unused key2Id variable in e2e test
bdshadow a60e8dc
fix: preserve soft-deleted keys during branch merge addition
bdshadow 8c4de4e
test: add e2e test for soft-deleted key exclusion from tasks
bdshadow f0429a4
fix: address CodeRabbit review findings
bdshadow bc9fe32
test: add tests for trash purge, export exclusion, permissions, and b…
bdshadow 80250cf
fix: add deterministic ORDER BY to findSoftDeletedByProjectId query
bdshadow d257ef1
fix: replace super-auth user search with lightweight deleters endpoin…
bdshadow 01fc722
feat: add horizontal scroll arrows to trash view
bdshadow 2eeefd5
feat: add trash navigation links in activity detail for soft-deleted …
bdshadow cfecab0
chore: revert en.json updates
bdshadow 1ea817a
refactor: consolidate RETENTION_DAYS constant into KeyTrashPurgeSched…
bdshadow 807d471
fix: refine generic type
bdshadow 6cd0c45
refactor: extract KeyTrashService and make assembler a singleton comp…
bdshadow c6e2051
fix: replace any types with proper types in trash components
bdshadow 034b99c
refactor: use invalidateUrlPrefix instead of refetch in TrashPage
bdshadow b1f56d0
refactor: move hard delete logic from controller to KeyService
bdshadow 2ac8bc6
refactor: replace API calls with UI interactions in soft delete e2e t…
bdshadow c9019ce
refactor: use TestData pattern for soft delete e2e tests
bdshadow 945fcc6
fix: resolve N+1 query in restoreKeys batch operation
bdshadow e10b92b
refactor: split TrashPage and TrashRow into smaller components
bdshadow 7147d0c
fix: reuse deletedBy join between QueryBase and QueryGlobalFiltering
bdshadow f349eb7
fix: reuse branch join and fetch branch eagerly in restore query
bdshadow 6123a4c
chore: pretify typescript
bdshadow bb6d524
refactor: use generated TrashedKeyWithTranslationsModel type
bdshadow 8a01734
test: add e2e test for filtering trashed keys by deleter
bdshadow 24ae990
chore: revert unintended en.json whitespace change
bdshadow File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
127 changes: 127 additions & 0 deletions
127
backend/api/src/main/kotlin/io/tolgee/api/v2/controllers/keys/KeyTrashController.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,127 @@ | ||
| package io.tolgee.api.v2.controllers.keys | ||
|
|
||
| import io.swagger.v3.oas.annotations.Operation | ||
| import io.tolgee.activity.RequestActivity | ||
| import io.tolgee.activity.data.ActivityType | ||
| import io.tolgee.api.v2.controllers.IController | ||
| import io.tolgee.dtos.queryResults.KeyView | ||
| import io.tolgee.dtos.request.translation.TranslationFilters | ||
| import io.tolgee.hateoas.key.KeyModel | ||
| import io.tolgee.hateoas.key.KeyModelAssembler | ||
| import io.tolgee.hateoas.key.trash.TrashedKeyWithTranslationsModel | ||
| import io.tolgee.hateoas.key.trash.TrashedKeyWithTranslationsModelAssembler | ||
| import io.tolgee.hateoas.userAccount.SimpleUserAccountModel | ||
| import io.tolgee.hateoas.userAccount.SimpleUserAccountModelAssembler | ||
| import io.tolgee.model.enums.Scope | ||
| import io.tolgee.model.views.KeyWithTranslationsView | ||
| import io.tolgee.security.ProjectHolder | ||
| import io.tolgee.security.authentication.AllowApiAccess | ||
| import io.tolgee.security.authentication.AuthenticationFacade | ||
| import io.tolgee.security.authorization.RequiresProjectPermissions | ||
| import io.tolgee.service.key.KeyService | ||
| import io.tolgee.service.key.KeyTrashService | ||
| import org.springdoc.core.annotations.ParameterObject | ||
| import org.springframework.data.domain.Pageable | ||
| import org.springframework.data.web.PagedResourcesAssembler | ||
| import org.springframework.data.web.SortDefault | ||
| import org.springframework.hateoas.CollectionModel | ||
| import org.springframework.hateoas.PagedModel | ||
| import org.springframework.transaction.annotation.Transactional | ||
| import org.springframework.web.bind.annotation.CrossOrigin | ||
| import org.springframework.web.bind.annotation.DeleteMapping | ||
| import org.springframework.web.bind.annotation.GetMapping | ||
| import org.springframework.web.bind.annotation.ModelAttribute | ||
| import org.springframework.web.bind.annotation.PathVariable | ||
| import org.springframework.web.bind.annotation.PutMapping | ||
| import org.springframework.web.bind.annotation.RequestMapping | ||
| import org.springframework.web.bind.annotation.RequestParam | ||
| import org.springframework.web.bind.annotation.RestController | ||
|
|
||
| @Suppress("MVCPathVariableInspection") | ||
| @RestController | ||
| @CrossOrigin(origins = ["*"]) | ||
| @RequestMapping( | ||
| value = [ | ||
| "/v2/projects/{projectId:[0-9]+}/keys/trash", | ||
| "/v2/projects/keys/trash", | ||
| ], | ||
| ) | ||
| @KeysDocsTag | ||
| class KeyTrashController( | ||
| private val keyService: KeyService, | ||
| private val keyTrashService: KeyTrashService, | ||
| private val projectHolder: ProjectHolder, | ||
| private val keyModelAssembler: KeyModelAssembler, | ||
| private val authenticationFacade: AuthenticationFacade, | ||
| private val trashedKeyWithTranslationsModelAssembler: TrashedKeyWithTranslationsModelAssembler, | ||
| private val simpleUserAccountModelAssembler: SimpleUserAccountModelAssembler, | ||
| @Suppress("SpringJavaInjectionPointsAutowiringInspection") | ||
| private val pagedResourcesAssembler: PagedResourcesAssembler<KeyWithTranslationsView>, | ||
| ) : IController { | ||
| @GetMapping("") | ||
| @Operation(summary = "List trashed keys") | ||
| @RequiresProjectPermissions([Scope.KEYS_VIEW]) | ||
| @AllowApiAccess | ||
| fun list( | ||
| @ParameterObject | ||
| @SortDefault("deletedAt") | ||
| pageable: Pageable, | ||
| @ParameterObject | ||
| @ModelAttribute | ||
| params: TranslationFilters, | ||
| ): PagedModel<TrashedKeyWithTranslationsModel> { | ||
| val data = | ||
| keyTrashService.getTrashedKeysWithTranslations( | ||
| projectId = projectHolder.project.id, | ||
| userId = authenticationFacade.authenticatedUser.id, | ||
| pageable = pageable, | ||
| params = params, | ||
| ) | ||
| return pagedResourcesAssembler.toModel(data, trashedKeyWithTranslationsModelAssembler) | ||
| } | ||
|
|
||
| @GetMapping("/deleters") | ||
| @Operation(summary = "List users who deleted keys") | ||
| @RequiresProjectPermissions([Scope.KEYS_VIEW]) | ||
| @AllowApiAccess | ||
| fun listDeleters( | ||
| @RequestParam(required = false) branch: String?, | ||
| ): CollectionModel<SimpleUserAccountModel> { | ||
| val users = keyService.findDistinctDeleters(projectHolder.project.id, branch) | ||
| return simpleUserAccountModelAssembler.toCollectionModel(users) | ||
| } | ||
|
|
||
| @PutMapping("/{keyId}/restore") | ||
| @Transactional | ||
| @Operation(summary = "Restore a trashed key") | ||
| @RequestActivity(ActivityType.KEY_RESTORE) | ||
| @RequiresProjectPermissions([Scope.KEYS_CREATE]) | ||
| @AllowApiAccess | ||
| fun restore( | ||
| @PathVariable keyId: Long, | ||
| ): KeyModel { | ||
| val key = keyService.restoreKey(projectHolder.project.id, keyId) | ||
| val view = | ||
| KeyView( | ||
| key.id, | ||
| key.name, | ||
| key.namespace?.name, | ||
| key.keyMeta?.description, | ||
| key.keyMeta?.custom, | ||
| key.branch?.name, | ||
| ) | ||
| return keyModelAssembler.toModel(view) | ||
| } | ||
|
|
||
| @DeleteMapping("/{keyId}") | ||
| @Transactional | ||
| @Operation(summary = "Permanently delete a trashed key") | ||
| @RequestActivity(ActivityType.KEY_HARD_DELETE) | ||
| @RequiresProjectPermissions([Scope.KEYS_DELETE]) | ||
| @AllowApiAccess | ||
| fun permanentlyDelete( | ||
| @PathVariable keyId: Long, | ||
| ) { | ||
| keyService.hardDeleteSingleTrashedKey(projectHolder.project.id, keyId) | ||
| } | ||
|
bdshadow marked this conversation as resolved.
|
||
| } | ||
26 changes: 26 additions & 0 deletions
26
backend/api/src/main/kotlin/io/tolgee/hateoas/key/trash/TrashedKeyModel.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,26 @@ | ||
| package io.tolgee.hateoas.key.trash | ||
|
|
||
| import io.swagger.v3.oas.annotations.media.Schema | ||
| import io.tolgee.hateoas.userAccount.SimpleUserAccountModel | ||
| import org.springframework.hateoas.RepresentationModel | ||
| import org.springframework.hateoas.server.core.Relation | ||
| import java.io.Serializable | ||
| import java.util.Date | ||
|
|
||
| @Suppress("unused") | ||
| @Relation(collectionRelation = "keys", itemRelation = "key") | ||
| class TrashedKeyModel( | ||
| @Schema(description = "Id of key record") | ||
| val id: Long, | ||
| @Schema(description = "Name of key", example = "this_is_super_key") | ||
| val name: String, | ||
| @Schema(description = "Namespace of key", example = "homepage") | ||
| val namespace: String?, | ||
| @Schema(description = "When the key was deleted") | ||
| val deletedAt: Date, | ||
| @Schema(description = "When the key will be permanently deleted") | ||
| val permanentDeleteAt: Date, | ||
| @Schema(description = "User who deleted the key") | ||
| val deletedBy: SimpleUserAccountModel?, | ||
| ) : RepresentationModel<TrashedKeyModel>(), | ||
| Serializable |
44 changes: 44 additions & 0 deletions
44
backend/api/src/main/kotlin/io/tolgee/hateoas/key/trash/TrashedKeyModelAssembler.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,44 @@ | ||
| package io.tolgee.hateoas.key.trash | ||
|
|
||
| import io.tolgee.api.v2.controllers.keys.KeyTrashController | ||
| import io.tolgee.hateoas.userAccount.SimpleUserAccountModel | ||
| import io.tolgee.service.AvatarService | ||
| import io.tolgee.service.key.KeySearchResultView | ||
| import io.tolgee.service.key.KeyTrashPurgeScheduler | ||
| import io.tolgee.util.addDays | ||
| import org.springframework.hateoas.server.mvc.RepresentationModelAssemblerSupport | ||
| import org.springframework.stereotype.Component | ||
|
|
||
| @Component | ||
| class TrashedKeyModelAssembler( | ||
| private val avatarService: AvatarService, | ||
| ) : RepresentationModelAssemblerSupport<KeySearchResultView, TrashedKeyModel>( | ||
| KeyTrashController::class.java, | ||
| TrashedKeyModel::class.java, | ||
| ) { | ||
| override fun toModel(entity: KeySearchResultView): TrashedKeyModel { | ||
| val deletedAt = | ||
| entity.deletedAt | ||
| ?: throw IllegalStateException("Cannot assemble TrashedKeyModel: deletedAt is null for key ${entity.id}") | ||
| return TrashedKeyModel( | ||
| id = entity.id, | ||
| name = entity.name, | ||
| namespace = entity.namespace, | ||
| deletedAt = deletedAt, | ||
| permanentDeleteAt = deletedAt.addDays(KeyTrashPurgeScheduler.RETENTION_DAYS), | ||
| deletedBy = buildDeletedByModel(entity), | ||
| ) | ||
| } | ||
|
|
||
| private fun buildDeletedByModel(entity: KeySearchResultView): SimpleUserAccountModel? { | ||
| return entity.deletedByUserId?.let { userId -> | ||
| SimpleUserAccountModel( | ||
| id = userId, | ||
| username = entity.deletedByUserUsername ?: "", | ||
| name = entity.deletedByUserName, | ||
| avatar = avatarService.getAvatarLinks(entity.deletedByUserAvatarHash), | ||
| deleted = entity.deletedByUserDeleted ?: false, | ||
| ) | ||
| } | ||
| } | ||
| } |
39 changes: 39 additions & 0 deletions
39
backend/api/src/main/kotlin/io/tolgee/hateoas/key/trash/TrashedKeyWithTranslationsModel.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,39 @@ | ||
| package io.tolgee.hateoas.key.trash | ||
|
|
||
| import io.swagger.v3.oas.annotations.media.Schema | ||
| import io.tolgee.api.v2.hateoas.invitation.TagModel | ||
| import io.tolgee.hateoas.screenshot.ScreenshotModel | ||
| import io.tolgee.hateoas.translations.TranslationViewModel | ||
| import io.tolgee.hateoas.userAccount.SimpleUserAccountModel | ||
| import org.springframework.hateoas.RepresentationModel | ||
| import org.springframework.hateoas.server.core.Relation | ||
| import java.io.Serializable | ||
| import java.util.Date | ||
|
|
||
| @Suppress("unused") | ||
| @Relation(collectionRelation = "keys", itemRelation = "key") | ||
| class TrashedKeyWithTranslationsModel( | ||
| @Schema(description = "Id of key record") | ||
| val id: Long, | ||
| @Schema(description = "Name of key", example = "this_is_super_key") | ||
| val name: String, | ||
| @Schema(description = "Namespace of key", example = "homepage") | ||
| val namespace: String?, | ||
| @Schema(description = "When the key was deleted") | ||
| val deletedAt: Date, | ||
| @Schema(description = "When the key will be permanently deleted") | ||
| val permanentDeleteAt: Date, | ||
| @Schema(description = "Description of the key") | ||
| val description: String?, | ||
| @Schema(description = "Tags of key") | ||
| val tags: List<TagModel>, | ||
| @Schema(description = "Translations object keyed by language tag") | ||
| val translations: Map<String, TranslationViewModel>, | ||
| @Schema(description = "Screenshots of the key") | ||
| val screenshots: List<ScreenshotModel>, | ||
| @Schema(description = "Whether the key is plural") | ||
| val isPlural: Boolean, | ||
| @Schema(description = "User who deleted the key") | ||
| val deletedBy: SimpleUserAccountModel?, | ||
| ) : RepresentationModel<TrashedKeyWithTranslationsModel>(), | ||
| Serializable |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.