Skip to content

Commit 081ec2a

Browse files
committed
feat: integrate branch-specific tracking into project activity and modified entities handling
1 parent 742ff13 commit 081ec2a

File tree

16 files changed

+465
-16
lines changed

16 files changed

+465
-16
lines changed

backend/api/src/main/kotlin/io/tolgee/api/v2/controllers/ProjectActivityController.kt

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import io.tolgee.model.views.activity.ProjectActivityView
2020
import io.tolgee.security.ProjectHolder
2121
import io.tolgee.security.authentication.AllowApiAccess
2222
import io.tolgee.security.authorization.RequiresProjectPermissions
23+
import io.tolgee.service.branching.BranchService
2324
import org.springdoc.core.annotations.ParameterObject
2425
import org.springframework.data.domain.Pageable
2526
import org.springframework.data.web.PagedResourcesAssembler
@@ -43,15 +44,24 @@ class ProjectActivityController(
4344
private val modificationResourcesAssembler: PagedResourcesAssembler<ModifiedEntityView>,
4445
private val projectActivityModelAssembler: ProjectActivityModelAssembler,
4546
private val modifiedEntityModelAssembler: ModifiedEntityModelAssembler,
47+
private val branchService: BranchService,
4648
) {
4749
@Operation(summary = "Get project activity")
4850
@GetMapping("")
4951
@RequiresProjectPermissions([Scope.ACTIVITY_VIEW])
5052
@AllowApiAccess
5153
fun getActivity(
5254
@ParameterObject pageable: Pageable,
55+
@RequestParam(required = false) branch: String? = null,
5356
): PagedModel<ProjectActivityModel> {
54-
val views = activityService.findProjectActivity(projectId = projectHolder.project.id, pageable)
57+
val branchEntity =
58+
branch?.let { branchService.getActiveBranch(projectHolder.project.id, it) }
59+
val views =
60+
activityService.findProjectActivity(
61+
projectId = projectHolder.project.id,
62+
pageable = pageable,
63+
branchId = branchEntity?.id,
64+
)
5565
return activityPagedResourcesAssembler.toModel(views, projectActivityModelAssembler)
5666
}
5767

@@ -61,9 +71,16 @@ class ProjectActivityController(
6171
@AllowApiAccess
6272
fun getSingleRevision(
6373
@PathVariable revisionId: Long,
74+
@RequestParam(required = false) branch: String? = null,
6475
): ProjectActivityModel {
76+
val branchEntity =
77+
branch?.let { branchService.getActiveBranch(projectHolder.project.id, it) }
6578
val views =
66-
activityService.findProjectActivity(projectId = projectHolder.project.id, revisionId)
79+
activityService.findProjectActivity(
80+
projectId = projectHolder.project.id,
81+
revisionId = revisionId,
82+
branchId = branchEntity?.id,
83+
)
6784
?: throw NotFoundException()
6885
return projectActivityModelAssembler.toModel(views)
6986
}
@@ -80,13 +97,18 @@ class ProjectActivityController(
8097
)
8198
@RequestParam(required = false)
8299
filterEntityClass: List<String>?,
100+
@RequestParam(required = false)
101+
branch: String? = null,
83102
): PagedModel<ModifiedEntityModel> {
103+
val branchEntity =
104+
branch?.let { branchService.getActiveBranch(projectHolder.project.id, it) }
84105
val page =
85106
activityService.getRevisionModifications(
86107
projectId = projectHolder.project.id,
87108
revisionId,
88109
pageable,
89110
filterEntityClass,
111+
branchEntity?.id,
90112
)
91113
return modificationResourcesAssembler.toModel(page, modifiedEntityModelAssembler)
92114
}

backend/app/src/test/kotlin/io/tolgee/service/ActivityVIewByRevisionsProviderTest.kt

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,4 +41,49 @@ class ActivityVIewByRevisionsProviderTest : ProjectAuthControllerTest() {
4141
.size.assert
4242
.isEqualTo(7)
4343
}
44+
45+
@Test
46+
fun `it includes default branch modified entities when branchId is null`() {
47+
val testData = ImportTestData()
48+
testData.setAllResolved()
49+
testData.setAllOverride()
50+
testData.addDefaultBranch()
51+
testDataService.saveTestData(testData.root)
52+
val user =
53+
testData.root.data.userAccounts[0]
54+
.self
55+
val projectId = testData.project.id
56+
loginAsUser(user.username)
57+
58+
performAuthPost(
59+
"/v2/projects/$projectId/translations",
60+
mapOf(
61+
"key" to "default_key",
62+
"translations" to mapOf(testData.english.tag to "default_text"),
63+
),
64+
).andIsOk
65+
val revision =
66+
entityManager
67+
.createQuery(
68+
"from ActivityRevision ar order by ar.id desc limit 1",
69+
ActivityRevision::class.java,
70+
).resultList
71+
72+
val branchIds =
73+
entityManager
74+
.createQuery(
75+
"select ame.branchId from ActivityModifiedEntity ame where ame.activityRevision.id = :revisionId",
76+
java.lang.Long::class.java,
77+
).setParameter("revisionId", revision.first().id)
78+
.resultList
79+
80+
branchIds.any { it == testData.defaultBranch.id }.assert.isTrue()
81+
82+
val views = ActivityViewByRevisionsProvider(applicationContext, revision, null, onlyCountInListAbove = 1).get()
83+
views
84+
.first()
85+
.modifications!!
86+
.size.assert
87+
.isGreaterThan(0)
88+
}
4489
}

backend/data/src/main/kotlin/io/tolgee/activity/ActivityService.kt

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -59,8 +59,8 @@ class ActivityService(
5959
jdbcTemplate.batchUpdate(
6060
"INSERT INTO activity_modified_entity " +
6161
"(entity_class, entity_id, describing_data, " +
62-
"describing_relations, modifications, revision_type, activity_revision_id) " +
63-
"VALUES (?, ?, ?, ?, ?, ?, ?)",
62+
"describing_relations, modifications, revision_type, activity_revision_id, branch_id) " +
63+
"VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
6464
list,
6565
100,
6666
) { ps, entity ->
@@ -71,6 +71,7 @@ class ActivityService(
7171
ps.setObject(5, getJsonbObject(entity.modifications))
7272
ps.setInt(6, RevisionType.values().indexOf(entity.revisionType))
7373
ps.setLong(7, entity.activityRevision.id)
74+
ps.setObject(8, entity.branchId)
7475
}
7576

7677
return list
@@ -113,11 +114,13 @@ class ActivityService(
113114
fun findProjectActivity(
114115
projectId: Long,
115116
pageable: Pageable,
117+
branchId: Long? = null,
116118
): Page<ProjectActivityView> {
117119
return ProjectActivityViewByPageableProvider(
118120
applicationContext = applicationContext,
119121
projectId = projectId,
120122
pageable = pageable,
123+
branchId = branchId,
121124
).get()
122125
}
123126

@@ -126,18 +129,21 @@ class ActivityService(
126129
return ProjectActivityViewByRevisionProvider(
127130
applicationContext = applicationContext,
128131
revisionId = revisionId,
132+
branchId = null,
129133
).get()
130134
}
131135

132136
@Transactional
133137
fun findProjectActivity(
134138
projectId: Long,
135139
revisionId: Long,
140+
branchId: Long? = null,
136141
): ProjectActivityView? {
137142
return ProjectActivityViewByRevisionProvider(
138143
applicationContext = applicationContext,
139144
revisionId = revisionId,
140145
projectId = projectId,
146+
branchId = branchId,
141147
).get()
142148
}
143149

@@ -158,9 +164,17 @@ class ActivityService(
158164
revisionId: Long,
159165
pageable: Pageable,
160166
filterEntityClass: List<String>?,
167+
branchId: Long? = null,
161168
): Page<ModifiedEntityView> {
162169
val provider =
163-
ModificationsByRevisionsProvider(applicationContext, projectId, listOf(revisionId), pageable, filterEntityClass)
170+
ModificationsByRevisionsProvider(
171+
applicationContext,
172+
projectId,
173+
listOf(revisionId),
174+
pageable,
175+
filterEntityClass,
176+
branchId,
177+
)
164178
return provider.get()
165179
}
166180

backend/data/src/main/kotlin/io/tolgee/activity/iterceptor/InterceptedEventsManager.kt

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,17 @@ import io.tolgee.activity.propChangesProvider.PropChangesProvider
1313
import io.tolgee.component.ActivityHolderProvider
1414
import io.tolgee.events.OnProjectActivityEvent
1515
import io.tolgee.model.EntityWithId
16+
import io.tolgee.model.Language
17+
import io.tolgee.model.Project
1618
import io.tolgee.model.activity.ActivityDescribingEntity
1719
import io.tolgee.model.activity.ActivityModifiedEntity
1820
import io.tolgee.model.activity.ActivityRevision
21+
import io.tolgee.model.branching.BranchVersionedEntity
22+
import io.tolgee.model.key.KeyComment
23+
import io.tolgee.model.key.screenshotReference.KeyScreenshotReference
24+
import io.tolgee.model.task.Task
1925
import jakarta.persistence.EntityManager
26+
import jakarta.persistence.FlushModeType
2027
import org.apache.commons.lang3.exception.ExceptionUtils.getRootCause
2128
import org.hibernate.Transaction
2229
import org.hibernate.action.spi.BeforeTransactionCompletionProcess
@@ -165,9 +172,71 @@ class InterceptedEventsManager(
165172
).also { it.revisionType = revisionType }
166173
}
167174

175+
activityModifiedEntity.branchId = resolveBranchId(entity)
176+
168177
return activityModifiedEntity
169178
}
170179

180+
private fun resolveBranchId(entity: EntityWithId): Long? {
181+
val branchableKey =
182+
(entity as? BranchVersionedEntity<*, *>)?.resolveKey()
183+
if (branchableKey != null) {
184+
return branchableKey.branch?.id ?: defaultBranchId(branchableKey.project.id)
185+
}
186+
187+
return when (entity) {
188+
is KeyComment -> {
189+
entity.keyMeta.key
190+
?.branch
191+
?.id ?: entity.keyMeta.key
192+
?.project
193+
?.id
194+
?.let(::defaultBranchId)
195+
}
196+
197+
is KeyScreenshotReference -> {
198+
entity.key.branch?.id ?: defaultBranchId(entity.key.project.id)
199+
}
200+
201+
is Task -> {
202+
entity.branch?.id ?: defaultBranchId(entity.project.id)
203+
}
204+
205+
is Project -> {
206+
defaultBranchId(entity.id)
207+
}
208+
209+
is Language -> {
210+
defaultBranchId(entity.project.id)
211+
}
212+
213+
else -> {
214+
null
215+
}
216+
}
217+
}
218+
219+
private fun defaultBranchId(projectId: Long?): Long? {
220+
if (projectId == null) {
221+
return null
222+
}
223+
return entityManager
224+
.createQuery(
225+
"""
226+
select b.id
227+
from Branch b
228+
where b.project.id = :projectId
229+
and b.isDefault = true
230+
and b.archivedAt is null
231+
and b.deletedAt is null
232+
""",
233+
Long::class.java,
234+
).setParameter("projectId", projectId)
235+
.setFlushMode(FlushModeType.COMMIT)
236+
.resultList
237+
.firstOrNull()
238+
}
239+
171240
private fun getChangeEntityDescription(
172241
entity: EntityWithId,
173242
activityRevision: ActivityRevision,

backend/data/src/main/kotlin/io/tolgee/activity/projectActivity/ActivityViewByRevisionsProvider.kt

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
package io.tolgee.activity.projectActivity
22

33
import io.tolgee.activity.data.ActivityType
4+
import io.tolgee.model.Project_
45
import io.tolgee.model.UserAccount
56
import io.tolgee.model.activity.ActivityDescribingEntity
67
import io.tolgee.model.activity.ActivityModifiedEntity
78
import io.tolgee.model.activity.ActivityModifiedEntity_
89
import io.tolgee.model.activity.ActivityRevision
910
import io.tolgee.model.activity.ActivityRevision_
11+
import io.tolgee.model.branching.Branch
12+
import io.tolgee.model.branching.Branch_
1013
import io.tolgee.model.views.activity.ModifiedEntityView
1114
import io.tolgee.model.views.activity.ProjectActivityView
1215
import io.tolgee.repository.activity.ActivityRevisionRepository
@@ -21,6 +24,7 @@ import org.springframework.context.ApplicationContext
2124
class ActivityViewByRevisionsProvider(
2225
private val applicationContext: ApplicationContext,
2326
private val revisions: Collection<ActivityRevision>,
27+
private val branchId: Long? = null,
2428
/**
2529
* For Activities, which have onlyCountInList = true in io.tolgee.activity.data.ActivityType,
2630
* this parameter specifies what's the maximum per-entity modification to be included in the list.
@@ -112,7 +116,8 @@ class ActivityViewByRevisionsProvider(
112116
activityRevisionRepository
113117
.getModifiedEntityTypeCounts(
114118
revisionIds = revisionIds,
115-
allowedTypes,
119+
allowedTypes = allowedTypes,
120+
branchId = branchId,
116121
).forEach { (revisionId, entityClass, count) ->
117122
counts
118123
.computeIfAbsent(revisionId as Long) { mutableMapOf() }
@@ -153,6 +158,7 @@ class ActivityViewByRevisionsProvider(
153158
val whereConditions = mutableListOf<Predicate>()
154159
whereConditions.add(filter)
155160
whereConditions.add(revision.get(ActivityRevision_.id).`in`(revisionIds))
161+
whereConditions.add(getBranchPredicate(cb, query, root, revision))
156162
ActivityType.entries.forEach {
157163
it.restrictEntitiesInList?.let { restrictEntitiesInList ->
158164
val restrictedEntityNames = restrictEntitiesInList.map { it.simpleName }
@@ -169,6 +175,32 @@ class ActivityViewByRevisionsProvider(
169175
return entityManager.createQuery(query).resultList
170176
}
171177

178+
private fun getBranchPredicate(
179+
cb: CriteriaBuilder,
180+
query: jakarta.persistence.criteria.CriteriaQuery<*>,
181+
root: Root<ActivityModifiedEntity>,
182+
revision: Join<ActivityModifiedEntity, ActivityRevision>,
183+
): Predicate {
184+
val branchPath = root.get(ActivityModifiedEntity_.branchId)
185+
if (branchId != null) {
186+
return cb.equal(branchPath, branchId)
187+
}
188+
189+
val subquery = query.subquery(Long::class.java)
190+
val branch = subquery.from(Branch::class.java)
191+
subquery.select(branch.get(Branch_.id))
192+
subquery.where(
193+
cb.equal(branch.get(Branch_.project).get(Project_.id), revision.get(ActivityRevision_.projectId)),
194+
cb.isTrue(branch.get(Branch_.isDefault)),
195+
cb.isNull(branch.get(Branch_.archivedAt)),
196+
)
197+
198+
return cb.or(
199+
cb.isNull(branchPath),
200+
branchPath.`in`(subquery),
201+
)
202+
}
203+
172204
private fun getClassesToExpandFilter(
173205
cb: CriteriaBuilder,
174206
revision: Join<ActivityModifiedEntity, ActivityRevision>,

backend/data/src/main/kotlin/io/tolgee/activity/projectActivity/ModificationsByRevisionsProvider.kt

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ class ModificationsByRevisionsProvider(
1717
private val revisionIds: List<Long>,
1818
private val pageable: Pageable,
1919
private val filterEntityClass: List<String>?,
20+
private val branchId: Long?,
2021
) {
2122
val userAccountService: UserAccountService =
2223
applicationContext.getBean(UserAccountService::class.java)
@@ -57,6 +58,12 @@ class ModificationsByRevisionsProvider(
5758
}
5859

5960
private fun getModifiedEntitiesRaw(): Page<ActivityModifiedEntity> {
60-
return activityModifiedEntityRepository.getModifiedEntities(projectId, revisionIds, filterEntityClass, pageable)
61+
return activityModifiedEntityRepository.getModifiedEntities(
62+
projectId,
63+
revisionIds,
64+
filterEntityClass,
65+
pageable,
66+
branchId,
67+
)
6168
}
6269
}

0 commit comments

Comments
 (0)