Skip to content

Commit ea41ebf

Browse files
committed
feat: shared infrastructure for plan migration
Platform-side building blocks consumed by the billing plan-migration feature (the feature itself lives in the billing repo): - email custom-template engine (renderer, placeholders extractor) and EmailTemplateModel/EmailPlaceholderModel DTOs - SchedulingManager cron scheduling and CurrentDateProvider local-date accessor - plan-migration error codes (Message, I18n_en) + frontend error translations - reusable UI: FullWidthTooltip, FormatedDateTooltip, optional table headers in PaginatedHateoasTable, HeaderBar custom buttons, validation + links additions - e2e textarea helpers - registration email greeting placeholder fix (username -> recipientName)
1 parent 786feff commit ea41ebf

29 files changed

Lines changed: 382 additions & 52 deletions

File tree

backend/data/src/main/kotlin/io/tolgee/component/CurrentDateProvider.kt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ import org.springframework.stereotype.Component
1616
import org.springframework.transaction.PlatformTransactionManager
1717
import java.sql.Timestamp
1818
import java.time.Duration
19+
import java.time.LocalDate
20+
import java.time.ZoneId.systemDefault
1921
import java.time.ZonedDateTime
2022
import java.time.format.DateTimeFormatter
2123
import java.time.temporal.TemporalAccessor
@@ -86,6 +88,11 @@ class CurrentDateProvider(
8688
return forcedDate ?: Date()
8789
}
8890

91+
val localDate: LocalDate
92+
get() {
93+
return date.toInstant().atZone(systemDefault()).toLocalDate()
94+
}
95+
8996
override fun getNow(): Optional<TemporalAccessor> {
9097
return Optional.of(date.toInstant())
9198
}

backend/data/src/main/kotlin/io/tolgee/component/SchedulingManager.kt

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package io.tolgee.component
33
import io.tolgee.util.Logging
44
import io.tolgee.util.logger
55
import jakarta.annotation.PreDestroy
6+
import org.springframework.scheduling.support.CronTrigger
67
import org.springframework.stereotype.Component
78
import java.time.Duration
89
import java.util.UUID
@@ -46,6 +47,19 @@ class SchedulingManager(
4647
return id
4748
}
4849

50+
fun scheduleWithCron(
51+
runnable: Runnable,
52+
cron: String,
53+
): String {
54+
val future = taskScheduler.schedule(runnable, CronTrigger(cron))
55+
if (future == null) {
56+
throw IllegalStateException("Future from scheduler was null")
57+
}
58+
val id = UUID.randomUUID().toString()
59+
scheduledTasks[id] = future
60+
return id
61+
}
62+
4963
@PreDestroy
5064
fun cancelAll() {
5165
Companion.cancelAll()
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package io.tolgee.component.email.customTemplate
2+
3+
import io.tolgee.component.email.customTemplate.placeholder.EmailPlaceholdersExtractor
4+
import org.springframework.stereotype.Component
5+
import java.text.MessageFormat
6+
import java.util.Locale
7+
import kotlin.reflect.KClass
8+
9+
@Component
10+
class EmailTemplateRenderer(
11+
private val placeholderExtractor: EmailPlaceholdersExtractor,
12+
) {
13+
fun render(
14+
template: String,
15+
variables: EmailTemplateVariables,
16+
): String {
17+
@Suppress("UNCHECKED_CAST")
18+
val entries =
19+
placeholderExtractor.getEntries(
20+
variables::class as KClass<EmailTemplateVariables>,
21+
)
22+
23+
val parameters =
24+
entries
25+
.map { entry ->
26+
entry.accessor(variables) ?: ""
27+
}.toTypedArray()
28+
29+
return MessageFormat(template, Locale.ENGLISH).format(parameters)
30+
}
31+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
package io.tolgee.component.email.customTemplate
2+
3+
/**
4+
* Marker interface for classes that describe email template variables via [io.tolgee.component.email.customTemplate.placeholder.EmailPlaceholder] annotations.
5+
*/
6+
interface EmailTemplateVariables
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package io.tolgee.component.email.customTemplate.placeholder
2+
3+
@Target(AnnotationTarget.PROPERTY)
4+
@Retention(AnnotationRetention.RUNTIME)
5+
annotation class EmailPlaceholder(
6+
val position: Int,
7+
val placeholder: String,
8+
val description: String,
9+
val exampleValue: String,
10+
)
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package io.tolgee.component.email.customTemplate.placeholder
2+
3+
import io.tolgee.component.email.customTemplate.EmailTemplateVariables
4+
5+
data class EmailPlaceholderDefinition(
6+
val position: Int,
7+
val placeholder: String,
8+
val description: String,
9+
val exampleValue: String,
10+
)
11+
12+
data class EmailPlaceholderEntry<T : EmailTemplateVariables>(
13+
val definition: EmailPlaceholderDefinition,
14+
val accessor: (T) -> String?,
15+
)
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
package io.tolgee.component.email.customTemplate.placeholder
2+
3+
import io.tolgee.component.email.customTemplate.EmailTemplateVariables
4+
import io.tolgee.util.I18n
5+
import org.springframework.stereotype.Component
6+
import java.util.concurrent.ConcurrentHashMap
7+
import kotlin.collections.set
8+
import kotlin.reflect.KClass
9+
import kotlin.reflect.full.findAnnotation
10+
import kotlin.reflect.full.memberProperties
11+
12+
@Component
13+
class EmailPlaceholdersExtractor(
14+
private val i18n: I18n,
15+
) {
16+
private val cache =
17+
ConcurrentHashMap<KClass<*>, List<EmailPlaceholderEntry<*>>>()
18+
19+
fun <T : EmailTemplateVariables> getEntries(kClass: KClass<T>): List<EmailPlaceholderEntry<T>> {
20+
val existing = cache[kClass]
21+
if (existing != null) {
22+
@Suppress("UNCHECKED_CAST")
23+
return existing as List<EmailPlaceholderEntry<T>>
24+
}
25+
26+
val extracted = extract(kClass)
27+
cache[kClass] = extracted
28+
return extracted
29+
}
30+
31+
fun <T : EmailTemplateVariables> getDefinitions(kClass: KClass<T>): List<EmailPlaceholderDefinition> {
32+
return getEntries(kClass).map { it.definition }
33+
}
34+
35+
private fun <T : EmailTemplateVariables> extract(kClass: KClass<T>): List<EmailPlaceholderEntry<T>> {
36+
return kClass.memberProperties
37+
.mapNotNull { property ->
38+
val annotation = property.findAnnotation<EmailPlaceholder>() ?: return@mapNotNull null
39+
40+
EmailPlaceholderEntry(
41+
definition =
42+
EmailPlaceholderDefinition(
43+
position = annotation.position,
44+
placeholder = annotation.placeholder,
45+
description = i18n.translate(annotation.description),
46+
exampleValue = annotation.exampleValue,
47+
),
48+
accessor = { instance: T ->
49+
property.get(instance)?.toString()
50+
},
51+
)
52+
}.sortedBy { it.definition.position }
53+
}
54+
}

backend/data/src/main/kotlin/io/tolgee/constants/Message.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -347,6 +347,9 @@ enum class Message {
347347
TRANSLATION_EXCEEDS_CHAR_LIMIT,
348348
URL_NOT_VALID,
349349
QA_CHECKS_NOT_ENABLED,
350+
PLAN_MIGRATION_NOT_FOUND,
351+
PLAN_HAS_MIGRATIONS,
352+
SOURCE_AND_TARGET_PLAN_MUST_BE_DIFFERENT,
350353
;
351354

352355
val code: String
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package io.tolgee.dtos.misc
2+
3+
data class EmailPlaceholderModel(
4+
val placeholder: String,
5+
val description: String,
6+
val exampleValue: String,
7+
)
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
package io.tolgee.dtos.misc
2+
3+
data class EmailTemplateModel(
4+
val body: String,
5+
val placeholders: List<EmailPlaceholderModel>,
6+
)

0 commit comments

Comments
 (0)