From 8a1ca837d6342bfbb13fdc6ff456304365a681b8 Mon Sep 17 00:00:00 2001 From: "d.svitak" Date: Wed, 12 Aug 2026 16:01:51 +0200 Subject: [PATCH 01/14] MIG-584 Base template deployment to Flex - initial impl --- .../example/common/DeployBaseTemplates.groovy | 38 ++++++ .../com/quadient/migration/api/Migration.kt | 2 + .../migration/service/deploy/DeployClient.kt | 2 + .../service/deploy/DesignerDeployClient.kt | 4 + .../service/deploy/EvolveDeployClient.kt | 8 ++ .../service/deploy/InteractiveDeployClient.kt | 30 +++++ .../deploy/utility/ConflictDetector.kt | 1 + .../deploy/utility/DeploymentResult.kt | 2 +- .../service/deploy/utility/PostProcess.kt | 1 + .../InspireBaseTemplateBuilder.kt | 56 +++++++++ .../service/deploy/EvolveDeployClientTest.kt | 3 + .../deploy/InteractiveDeployClientTest.kt | 4 +- .../InspireBaseTemplateBuilderTest.kt | 111 ++++++++++++++++++ 13 files changed, 260 insertions(+), 2 deletions(-) create mode 100644 migration-examples/src/main/groovy/com/quadient/migration/example/common/DeployBaseTemplates.groovy create mode 100644 migration-library/src/main/kotlin/com/quadient/migration/service/inspirebuilder/InspireBaseTemplateBuilder.kt create mode 100644 migration-library/src/test/kotlin/com/quadient/migration/service/inspirebuilder/InspireBaseTemplateBuilderTest.kt diff --git a/migration-examples/src/main/groovy/com/quadient/migration/example/common/DeployBaseTemplates.groovy b/migration-examples/src/main/groovy/com/quadient/migration/example/common/DeployBaseTemplates.groovy new file mode 100644 index 00000000..0f859577 --- /dev/null +++ b/migration-examples/src/main/groovy/com/quadient/migration/example/common/DeployBaseTemplates.groovy @@ -0,0 +1,38 @@ +//! --- +//! displayName: Deploy Base Templates +//! category: Deployment +//! description: Deploys all base templates +//! --- +package com.quadient.migration.example.common + +import groovy.transform.Field +import org.slf4j.Logger +import org.slf4j.LoggerFactory + +import static com.quadient.migration.example.common.util.InitMigration.initMigration + +def migration = initMigration(this.binding) +def start = System.currentTimeMillis() + +deploymentResult = migration.deployClient.deployBaseTemplates() +@Field static Logger log = LoggerFactory.getLogger(this.class.name) + +if (!deploymentResult.deployed.empty) { + for (def item : deploymentResult.deployed) { + log.info "Deployed ${item.type.toString()} '${item.id}' to '${item.targetPath}'" + } +} + +if (!deploymentResult.warnings.empty) { + for (def item : deploymentResult.warnings) { + log.warn "Item '${item.id}' deployed with warning: ${item.message}" + } +} + +if (!deploymentResult.errors.empty) { + for (def item : deploymentResult.errors) { + log.error "Item '${item.id}' failed to deploy with error: ${item.message}" + } +} +log.info "Deployment finished. Deployed ${deploymentResult.deployed.size()} items with ${deploymentResult.warnings.size()} warnings and ${deploymentResult.errors.size()} errors" +log.info "Deployment took ${System.currentTimeMillis() - start} ms" diff --git a/migration-library/src/main/kotlin/com/quadient/migration/api/Migration.kt b/migration-library/src/main/kotlin/com/quadient/migration/api/Migration.kt index 40841f58..1d7a4ef2 100644 --- a/migration-library/src/main/kotlin/com/quadient/migration/api/Migration.kt +++ b/migration-library/src/main/kotlin/com/quadient/migration/api/Migration.kt @@ -28,6 +28,7 @@ import com.quadient.migration.service.deploy.utility.ConflictDetectorImpl import com.quadient.migration.service.deploy.utility.DeployOrderImpl import com.quadient.migration.service.deploy.utility.ProgressReporterImpl import com.quadient.migration.service.inspirebuilder.InspireDocumentObjectBuilder +import com.quadient.migration.service.inspirebuilder.InspireBaseTemplateBuilder import com.quadient.migration.service.ipsclient.IpsService import com.quadient.migration.service.ipsclient.Version import com.quadient.migration.service.ipsclient.display @@ -115,6 +116,7 @@ class Migration(val config: MigConfig, val projectConfig: ProjectConfig) { single() single() single() + single() } private val koinApp: KoinApplication = koinApplication { diff --git a/migration-library/src/main/kotlin/com/quadient/migration/service/deploy/DeployClient.kt b/migration-library/src/main/kotlin/com/quadient/migration/service/deploy/DeployClient.kt index 72a55127..7a08c162 100644 --- a/migration-library/src/main/kotlin/com/quadient/migration/service/deploy/DeployClient.kt +++ b/migration-library/src/main/kotlin/com/quadient/migration/service/deploy/DeployClient.kt @@ -108,6 +108,8 @@ sealed class DeployClient( abstract fun shouldIncludeDependency(documentObject: DocumentObject): Boolean + abstract fun deployBaseTemplates(): DeploymentResult + fun deployDocumentObjects(): DeploymentResult { val tracker = ResultTrackerImpl(statusTrackingRepository, projectConfig.inspireOutput) val ordered = deployOrder(getAllDocumentObjectsToDeploy()) diff --git a/migration-library/src/main/kotlin/com/quadient/migration/service/deploy/DesignerDeployClient.kt b/migration-library/src/main/kotlin/com/quadient/migration/service/deploy/DesignerDeployClient.kt index 7d2f9268..a5c8dd4e 100644 --- a/migration-library/src/main/kotlin/com/quadient/migration/service/deploy/DesignerDeployClient.kt +++ b/migration-library/src/main/kotlin/com/quadient/migration/service/deploy/DesignerDeployClient.kt @@ -121,6 +121,10 @@ class DesignerDeployClient( return documentObject.type != DocumentObjectType.Page && documentObject.internal != true } + override fun deployBaseTemplates(): DeploymentResult { + error("Base template deployment is not supported for Designer output.") + } + override fun uploadDocumentObject(obj: DocumentObject, targetPath: IcmPath, wfdXml: String): OperationResult { return ipsService.xml2wfd(wfdXml, targetPath) } diff --git a/migration-library/src/main/kotlin/com/quadient/migration/service/deploy/EvolveDeployClient.kt b/migration-library/src/main/kotlin/com/quadient/migration/service/deploy/EvolveDeployClient.kt index 35326d44..60b4b1b0 100644 --- a/migration-library/src/main/kotlin/com/quadient/migration/service/deploy/EvolveDeployClient.kt +++ b/migration-library/src/main/kotlin/com/quadient/migration/service/deploy/EvolveDeployClient.kt @@ -23,8 +23,10 @@ import com.quadient.migration.service.ResourcePathProvider import com.quadient.migration.service.getBaseTemplateFullPath import com.quadient.migration.service.deploy.utility.ConflictDetectorImpl import com.quadient.migration.service.deploy.utility.DeployOrderImpl +import com.quadient.migration.service.deploy.utility.DeploymentResult import com.quadient.migration.service.deploy.utility.ProgressReporterImpl import com.quadient.migration.service.inspirebuilder.InspireDocumentObjectBuilder +import com.quadient.migration.service.inspirebuilder.InspireBaseTemplateBuilder import com.quadient.migration.service.ipsclient.IpsService import com.quadient.migration.service.ipsclient.OperationResult import com.quadient.migration.service.ipsclient.Version @@ -56,6 +58,7 @@ class EvolveDeployClient( variableStructureRepository: VariableStructureRepository, baseTemplateRepository: BaseTemplateRepository, documentObjectBuilder: InspireDocumentObjectBuilder, + baseTemplateBuilder: InspireBaseTemplateBuilder, ipsService: IpsService, storage: Storage, ) : InteractiveDeployClient( @@ -77,6 +80,7 @@ class EvolveDeployClient( variableStructureRepository, baseTemplateRepository, documentObjectBuilder, + baseTemplateBuilder, ipsService, storage, ) { @@ -250,6 +254,10 @@ class EvolveDeployClient( error("Styles deployment is not currently supported in Evolve output") } + override fun deployBaseTemplates(): DeploymentResult { + error("Base template deployment is not currently supported in Evolve output") + } + private fun HttpResult<*, ApiBadRequestException>.toOperationResult(): OperationResult = when (this) { is HttpResult.Success -> OperationResult.Success is HttpResult.Failure -> OperationResult.Failure("CA API error ${error.status}: ${error.title} - ${error.detail}") diff --git a/migration-library/src/main/kotlin/com/quadient/migration/service/deploy/InteractiveDeployClient.kt b/migration-library/src/main/kotlin/com/quadient/migration/service/deploy/InteractiveDeployClient.kt index da803cb3..4451aad1 100644 --- a/migration-library/src/main/kotlin/com/quadient/migration/service/deploy/InteractiveDeployClient.kt +++ b/migration-library/src/main/kotlin/com/quadient/migration/service/deploy/InteractiveDeployClient.kt @@ -29,6 +29,7 @@ import com.quadient.migration.api.repository.VariableStructureRepository import com.quadient.migration.persistence.table.DocumentObjectTable import com.quadient.migration.service.Storage import com.quadient.migration.service.deploy.utility.DeploymentError +import com.quadient.migration.service.deploy.utility.DeploymentInfo import com.quadient.migration.service.deploy.utility.DeploymentResult import com.quadient.migration.service.deploy.utility.MetadataValidatorImpl import com.quadient.migration.service.deploy.utility.PostProcessImpl @@ -40,6 +41,7 @@ import com.quadient.migration.service.deploy.utility.ConflictDetectorImpl import com.quadient.migration.service.deploy.utility.DeployOrderImpl import com.quadient.migration.service.deploy.utility.ProgressReporterImpl import com.quadient.migration.service.inspirebuilder.InspireDocumentObjectBuilder +import com.quadient.migration.service.inspirebuilder.InspireBaseTemplateBuilder import com.quadient.migration.service.ipsclient.IpsService import com.quadient.migration.service.ipsclient.OperationResult import com.quadient.migration.service.resolveTarget @@ -79,6 +81,7 @@ open class InteractiveDeployClient( variableStructureRepository: VariableStructureRepository, baseTemplateRepository: BaseTemplateRepository, documentObjectBuilder: InspireDocumentObjectBuilder, + private val baseTemplateBuilder: InspireBaseTemplateBuilder, ipsService: IpsService, storage: Storage, ) : DeployClient( @@ -185,6 +188,33 @@ open class InteractiveDeployClient( return documentObject.internal != true } + override fun deployBaseTemplates(): DeploymentResult { + val deploymentResult = DeploymentResult(Uuid.random()) + + val baseTemplates = baseTemplateRepository.listAll() + logger.info("Found ${baseTemplates.size} base template(s) in the repository.") + + for (baseTemplate in baseTemplates) { + val targetPath = resourcePathProvider.getBaseTemplatePath(baseTemplate) + val wfdXml = baseTemplateBuilder.buildBaseTemplate(baseTemplate) + + when (val result = ipsService.xml2wfd(wfdXml, targetPath)) { + is OperationResult.Success -> { + logger.debug("Deployment of base template '${baseTemplate.nameOrId()}' to $targetPath is successful.") + deploymentResult.deployed.add(DeploymentInfo(baseTemplate.id, ResourceType.BaseTemplate, targetPath)) + } + + is OperationResult.Failure -> { + val message = "Failed to deploy base template '${baseTemplate.nameOrId()}' to $targetPath." + logger.error(message) + deploymentResult.errors.add(DeploymentError(baseTemplate.id, message)) + } + } + } + + return deploymentResult + } + override fun getAllDocumentObjectsToDeploy(): List { return documentObjectRepository.list( (DocumentObjectTable.type inList listOf( diff --git a/migration-library/src/main/kotlin/com/quadient/migration/service/deploy/utility/ConflictDetector.kt b/migration-library/src/main/kotlin/com/quadient/migration/service/deploy/utility/ConflictDetector.kt index 5635caed..f129823e 100644 --- a/migration-library/src/main/kotlin/com/quadient/migration/service/deploy/utility/ConflictDetector.kt +++ b/migration-library/src/main/kotlin/com/quadient/migration/service/deploy/utility/ConflictDetector.kt @@ -105,6 +105,7 @@ class ConflictDetectorImpl( ResourceType.TextStyle -> resourcePathProvider.getStyleDefinitionPath() ResourceType.ParagraphStyle -> resourcePathProvider.getStyleDefinitionPath() + ResourceType.BaseTemplate -> null } }.getOrNull() } diff --git a/migration-library/src/main/kotlin/com/quadient/migration/service/deploy/utility/DeploymentResult.kt b/migration-library/src/main/kotlin/com/quadient/migration/service/deploy/utility/DeploymentResult.kt index 6d179238..f10790c9 100644 --- a/migration-library/src/main/kotlin/com/quadient/migration/service/deploy/utility/DeploymentResult.kt +++ b/migration-library/src/main/kotlin/com/quadient/migration/service/deploy/utility/DeploymentResult.kt @@ -30,7 +30,7 @@ data class DeploymentInfo( ) enum class ResourceType { - DocumentObject, Image, Attachment, TextStyle, ParagraphStyle, DisplayRule + DocumentObject, Image, Attachment, TextStyle, ParagraphStyle, DisplayRule, BaseTemplate } data class DeploymentError(val id: String, val message: String) diff --git a/migration-library/src/main/kotlin/com/quadient/migration/service/deploy/utility/PostProcess.kt b/migration-library/src/main/kotlin/com/quadient/migration/service/deploy/utility/PostProcess.kt index 6c425db1..9f9e7ee3 100644 --- a/migration-library/src/main/kotlin/com/quadient/migration/service/deploy/utility/PostProcess.kt +++ b/migration-library/src/main/kotlin/com/quadient/migration/service/deploy/utility/PostProcess.kt @@ -77,6 +77,7 @@ class PostProcessImpl( ResourceType.Attachment -> attachmentRepository.find(info.id) ResourceType.TextStyle -> textStyleRepository.find(info.id) ResourceType.ParagraphStyle -> paragraphStyleRepository.find(info.id) + ResourceType.BaseTemplate -> null } if (obj == null) { val msg = "Failed to run '$name' post processor, '${info.type}' with id '${info.id}' at path '${info.targetPath}'." diff --git a/migration-library/src/main/kotlin/com/quadient/migration/service/inspirebuilder/InspireBaseTemplateBuilder.kt b/migration-library/src/main/kotlin/com/quadient/migration/service/inspirebuilder/InspireBaseTemplateBuilder.kt new file mode 100644 index 00000000..e867109d --- /dev/null +++ b/migration-library/src/main/kotlin/com/quadient/migration/service/inspirebuilder/InspireBaseTemplateBuilder.kt @@ -0,0 +1,56 @@ +package com.quadient.migration.service.inspirebuilder + +import com.quadient.migration.api.dto.migrationmodel.BaseTemplate +import com.quadient.migration.tools.logger +import com.quadient.wfdxml.WfdXmlBuilder +import com.quadient.wfdxml.api.layoutnodes.Flow +import com.quadient.wfdxml.api.layoutnodes.Flow.WebEditingType.SECTION +import com.quadient.wfdxml.api.layoutnodes.Pages + +class InspireBaseTemplateBuilder { + private val logger by logger() + + fun buildBaseTemplate(baseTemplate: BaseTemplate): String { + logger.debug("Starting to build base template '${baseTemplate.nameOrId()}'.") + + val builder = WfdXmlBuilder() + val layout = builder.addLayout() + layout.setName("DocumentLayout") + layout.addRoot() + + val interactiveFlows = mutableListOf() + var mainFlow: Flow? = null + + baseTemplate.pages.forEach { page -> + val wfdPage = layout.addPage().setType(Pages.PageConditionType.SIMPLE) + page.name?.let { wfdPage.setName(it) } + page.pageWidth?.let { wfdPage.setWidth(it.toMeters()) } + page.pageHeight?.let { wfdPage.setHeight(it.toMeters()) } + + page.areas.forEach { area -> + val flow = layout.addFlow() + .setId("Def.InteractiveFlow${interactiveFlows.size}") + .setName(area.interactiveFlowName) + .setType(Flow.Type.SIMPLE) + .setSectionFlow(true) + .setWebEditingType(SECTION) + interactiveFlows.add(flow) + + // TODO: main flow selection is a placeholder until we decide the real rule for it. + if (mainFlow == null) mainFlow = flow + + val flowArea = wfdPage.addFlowArea().setFlow(flow).setFlowToNextPage(area.flowToNextPage) + area.position?.let { + flowArea.setPosX(it.x.toMeters()).setPosY(it.y.toMeters()).setWidth(it.width.toMeters()) + .setHeight(it.height.toMeters()) + } + } + } + + layout.pages.setInteractiveFlows(interactiveFlows) + mainFlow?.let { layout.pages.setMainFlow(it) } + + logger.debug("Successfully built base template '${baseTemplate.nameOrId()}'.") + return builder.build() + } +} diff --git a/migration-library/src/test/kotlin/com/quadient/migration/service/deploy/EvolveDeployClientTest.kt b/migration-library/src/test/kotlin/com/quadient/migration/service/deploy/EvolveDeployClientTest.kt index e956821a..edb06b66 100644 --- a/migration-library/src/test/kotlin/com/quadient/migration/service/deploy/EvolveDeployClientTest.kt +++ b/migration-library/src/test/kotlin/com/quadient/migration/service/deploy/EvolveDeployClientTest.kt @@ -25,6 +25,7 @@ import com.quadient.migration.service.deploy.utility.ProgressReporterImpl import com.quadient.migration.service.inspirebuilder.InteractiveDocumentObjectBuilder import com.quadient.migration.service.InteractiveResourcePathProvider import com.quadient.migration.service.deploy.utility.DeployOrderImpl +import com.quadient.migration.service.inspirebuilder.InspireBaseTemplateBuilder import com.quadient.migration.service.ipsclient.IpsService import com.quadient.migration.service.ipsclient.OperationResult import com.quadient.migration.service.ipsclient.Version @@ -56,6 +57,7 @@ class EvolveDeployClientTest { val baseTemplateRepository = mockk() val statusTrackingRepository = mockk() val documentObjectBuilder = mockk() + val baseTemplateBuilder = mockk() val ipsService = mockk() val storage = mockk() val caClient = mockk() @@ -106,6 +108,7 @@ class EvolveDeployClientTest { variableStructureRepository, baseTemplateRepository, documentObjectBuilder, + baseTemplateBuilder, ipsService, storage, ) diff --git a/migration-library/src/test/kotlin/com/quadient/migration/service/deploy/InteractiveDeployClientTest.kt b/migration-library/src/test/kotlin/com/quadient/migration/service/deploy/InteractiveDeployClientTest.kt index 75a1488d..dcd0877a 100644 --- a/migration-library/src/test/kotlin/com/quadient/migration/service/deploy/InteractiveDeployClientTest.kt +++ b/migration-library/src/test/kotlin/com/quadient/migration/service/deploy/InteractiveDeployClientTest.kt @@ -44,6 +44,7 @@ import com.quadient.migration.service.deploy.utility.ResultTrackerImpl import com.quadient.migration.service.inspirebuilder.InteractiveDocumentObjectBuilder import com.quadient.migration.service.InteractiveResourcePathProvider import com.quadient.migration.service.deploy.utility.DeployOrderImpl +import com.quadient.migration.service.inspirebuilder.InspireBaseTemplateBuilder import com.quadient.migration.service.ipsclient.IpsService import com.quadient.migration.service.ipsclient.OperationResult import com.quadient.migration.service.resolveTargetDir @@ -60,7 +61,6 @@ import com.quadient.migration.shared.MetadataValue import com.quadient.migration.shared.SkipOptions import com.quadient.migration.shared.toIcmPath import com.quadient.migration.tools.aActiveStatus -import com.quadient.migration.tools.aBlockModel import com.quadient.migration.tools.aDeployedStatus import com.quadient.migration.tools.aErrorStatus import com.quadient.migration.tools.aProjectConfig @@ -103,6 +103,7 @@ class InteractiveDeployClientTest { val baseTemplateRepository = mockk() val statusTrackingRepository = mockk() val documentObjectBuilder = mockk() + val baseTemplateBuilder = mockk() val ipsService = mockk() val storage = mockk() val config = aProjectConfig( @@ -135,6 +136,7 @@ class InteractiveDeployClientTest { variableStructureRepository, baseTemplateRepository, documentObjectBuilder, + baseTemplateBuilder, ipsService, storage, ) diff --git a/migration-library/src/test/kotlin/com/quadient/migration/service/inspirebuilder/InspireBaseTemplateBuilderTest.kt b/migration-library/src/test/kotlin/com/quadient/migration/service/inspirebuilder/InspireBaseTemplateBuilderTest.kt new file mode 100644 index 00000000..d5797901 --- /dev/null +++ b/migration-library/src/test/kotlin/com/quadient/migration/service/inspirebuilder/InspireBaseTemplateBuilderTest.kt @@ -0,0 +1,111 @@ +package com.quadient.migration.service.inspirebuilder + +import com.quadient.migration.api.dto.migrationmodel.builder.BaseTemplateBuilder +import com.quadient.migration.shared.millimeters +import com.quadient.migration.tools.shouldBeEqualTo +import com.quadient.migration.tools.shouldBeNull +import org.junit.jupiter.api.Test +import tools.jackson.dataformat.xml.XmlMapper +import tools.jackson.module.kotlin.KotlinModule + +class InspireBaseTemplateBuilderTest { + private val subject = InspireBaseTemplateBuilder() + private val xmlMapper = XmlMapper.builder().addModule(KotlinModule.Builder().build()).build() + + @Test + fun `buildBaseTemplate creates page with name and size`() { + // given + val baseTemplate = BaseTemplateBuilder("BT_1").addPage { + name("Page 1") + pageSize(210.millimeters(), 297.millimeters()) + }.build() + + // when + val result = subject.buildBaseTemplate(baseTemplate).let { xmlMapper.readTree(it.trimIndent()) }["Layout"]["Layout"] + + // then + val pageId = result["Page"].first { it["Name"].stringValue() == "Page 1" }["Id"].stringValue() + val pageData = result["Page"].last { it["Id"].stringValue() == pageId } + pageData["Width"].stringValue().shouldBeEqualTo(210.millimeters().toMeters().toString()) + pageData["Height"].stringValue().shouldBeEqualTo(297.millimeters().toMeters().toString()) + } + + @Test + fun `buildBaseTemplate creates interactive flow and flow area per area`() { + // given + val baseTemplate = BaseTemplateBuilder("BT_1").addPage { + name("Page 1") + addArea("Body") { + flowToNextPage(true) + position { + left(10.millimeters()) + top(20.millimeters()) + width(180.millimeters()) + height(250.millimeters()) + } + } + }.build() + + // when + val result = subject.buildBaseTemplate(baseTemplate).let { xmlMapper.readTree(it.trimIndent()) }["Layout"]["Layout"] + + // then + val flowId = "Def.InteractiveFlow0" + + val flowAreaId = result["FlowArea"].first { it["FlowId"]?.stringValue() == flowId }["Id"].stringValue() + val flowArea = result["FlowArea"].last { it["Id"].stringValue() == flowAreaId } + flowArea["FlowingToNextPage"].stringValue().shouldBeEqualTo("True") + flowArea["Pos"]["X"].stringValue().shouldBeEqualTo(10.millimeters().toMeters().toString()) + flowArea["Pos"]["Y"].stringValue().shouldBeEqualTo(20.millimeters().toMeters().toString()) + flowArea["Size"]["X"].stringValue().shouldBeEqualTo(180.millimeters().toMeters().toString()) + flowArea["Size"]["Y"].stringValue().shouldBeEqualTo(250.millimeters().toMeters().toString()) + + result["Pages"]["MainFlow"].stringValue().shouldBeEqualTo(flowId) + result["Pages"]["InteractiveFlow"]["FlowId"].stringValue().shouldBeEqualTo(flowId) + } + + @Test + fun `buildBaseTemplate with multiple pages and areas creates one interactive flow per area`() { + // given + val baseTemplate = BaseTemplateBuilder("BT_1") + .addPage { name("Page 1"); addArea("Header"); addArea("Body") } + .addPage { name("Page 2"); addArea("Footer") } + .build() + + // when + val result = subject.buildBaseTemplate(baseTemplate).let { xmlMapper.readTree(it.trimIndent()) }["Layout"]["Layout"] + + // then + result["Page"].filter { it["Name"] != null }.size.shouldBeEqualTo(2) + result["Flow"].size().shouldBeEqualTo(3) + result["FlowArea"].filter { it["FlowId"] != null }.size.shouldBeEqualTo(3) + result["Pages"]["InteractiveFlow"].size().shouldBeEqualTo(3) + } + + @Test + fun `buildBaseTemplate with page without areas creates no flow`() { + // given + val baseTemplate = BaseTemplateBuilder("BT_1").addPage { name("Page 1") }.build() + + // when + val result = subject.buildBaseTemplate(baseTemplate).let { xmlMapper.readTree(it.trimIndent()) }["Layout"]["Layout"] + + // then + result["Flow"].shouldBeNull() + result["FlowArea"].shouldBeNull() + result["Page"].first()["Name"].stringValue().shouldBeEqualTo("Page 1") + } + + @Test + fun `buildBaseTemplate without pages creates empty layout`() { + // given + val baseTemplate = BaseTemplateBuilder("BT_1").build() + + // when + val result = subject.buildBaseTemplate(baseTemplate).let { xmlMapper.readTree(it.trimIndent()) }["Layout"]["Layout"] + + // then + result["Page"].shouldBeNull() + result["Flow"].shouldBeNull() + } +} From 48b057b983f3df786504e66d4ee06af0ef6a99e3 Mon Sep 17 00:00:00 2001 From: "d.svitak" Date: Thu, 13 Aug 2026 10:06:18 +0200 Subject: [PATCH 02/14] MIG-584 Base template deployment to Flex - WIP - naming areas and interactive flows, using source base template for creation of other modules, settings up other prerequisites... --- .../service/deploy/InteractiveDeployClient.kt | 2 + .../DesignerDocumentObjectBuilder.kt | 50 +---------- .../InspireBaseTemplateBuilder.kt | 38 +++++--- .../inspirebuilder/InspireBuilderUtils.kt | 55 +++++++++++- .../deploy/InteractiveDeployClientTest.kt | 40 +++++++++ .../InspireBaseTemplateBuilderTest.kt | 87 ++++++++++++++++++- 6 files changed, 208 insertions(+), 64 deletions(-) diff --git a/migration-library/src/main/kotlin/com/quadient/migration/service/deploy/InteractiveDeployClient.kt b/migration-library/src/main/kotlin/com/quadient/migration/service/deploy/InteractiveDeployClient.kt index 4451aad1..f6fe0ce2 100644 --- a/migration-library/src/main/kotlin/com/quadient/migration/service/deploy/InteractiveDeployClient.kt +++ b/migration-library/src/main/kotlin/com/quadient/migration/service/deploy/InteractiveDeployClient.kt @@ -212,6 +212,8 @@ open class InteractiveDeployClient( } } + runPostProcessors(deploymentResult) + return deploymentResult } diff --git a/migration-library/src/main/kotlin/com/quadient/migration/service/inspirebuilder/DesignerDocumentObjectBuilder.kt b/migration-library/src/main/kotlin/com/quadient/migration/service/inspirebuilder/DesignerDocumentObjectBuilder.kt index cb85e8c1..620ce988 100644 --- a/migration-library/src/main/kotlin/com/quadient/migration/service/inspirebuilder/DesignerDocumentObjectBuilder.kt +++ b/migration-library/src/main/kotlin/com/quadient/migration/service/inspirebuilder/DesignerDocumentObjectBuilder.kt @@ -36,16 +36,6 @@ import com.quadient.wfdxml.api.module.Layout import com.quadient.wfdxml.internal.layoutnodes.FlowAreaImpl import com.quadient.wfdxml.internal.layoutnodes.PageImpl import com.quadient.wfdxml.internal.layoutnodes.PagesImpl -import org.w3c.dom.Document -import org.w3c.dom.Element -import org.w3c.dom.Node -import org.xml.sax.InputSource -import java.io.StringReader -import java.io.StringWriter -import javax.xml.parsers.DocumentBuilderFactory -import javax.xml.transform.TransformerFactory.newInstance -import javax.xml.transform.dom.DOMSource -import javax.xml.transform.stream.StreamResult class DesignerDocumentObjectBuilder( documentObjectRepository: DocumentObjectRepository, @@ -176,7 +166,7 @@ class DesignerDocumentObjectBuilder( return if (projectConfig.sourceBaseTemplatePath.isNullOrBlank()) { documentObjectXml } else { - enrichLayoutWithSourceBaseTemplate(documentObjectXml, projectConfig.sourceBaseTemplatePath.toIcmPath()) + enrichLayoutWithSourceBaseTemplate(icmDataCache, documentObjectXml, projectConfig.sourceBaseTemplatePath.toIcmPath()) } } @@ -454,44 +444,6 @@ class DesignerDocumentObjectBuilder( } } - private fun enrichLayoutWithSourceBaseTemplate(documentObjectXml: String, sourceBaseTemplatePath: IcmPath): String { - val sourceBaseTemplateXml = icmDataCache.wfd2Xml(sourceBaseTemplatePath) - - val sourceBaseTemplateDoc = sourceBaseTemplateXml.toXmlDocument() - val documentObjectDoc = documentObjectXml.toXmlDocument() - - val sourceBaseLayoutNode = sourceBaseTemplateDoc.getElementsByTagName("Layout").item(0) as? Element - ?: error("Source base template '$sourceBaseTemplatePath' does not contain a Layout element.") - val sourceBaseInnerLayoutNode = sourceBaseLayoutNode.firstElementChildByTag("Layout") - ?: error("Source base template '$sourceBaseTemplatePath' does not contain an inner Layout element.") - - val documentObjectInnerLayoutNode = - documentObjectDoc.getElementsByTagName("Layout").item(0)?.firstElementChildByTag("Layout") - ?.let { sourceBaseTemplateDoc.importNode(it, true) } - ?: error("Document object does not contain an inner Layout element.") - - sourceBaseLayoutNode.replaceChild(documentObjectInnerLayoutNode, sourceBaseInnerLayoutNode) - return sourceBaseTemplateDoc.toXmlString() - } - - private fun String.toXmlDocument(): Document = - DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(InputSource(StringReader(this))) - - private fun Document.toXmlString(): String { - val result = StringWriter() - val transformer = newInstance().newTransformer() - transformer.transform(DOMSource(this), StreamResult(result)) - return result.toString().replace(Regex("([\\s\\S]*?)")) { matchResult -> - val value = matchResult.groupValues[1] - val encoded = value.replace("\n", " ").replace("\r", "") - "$encoded" - } - } - - private fun Node.firstElementChildByTag(tag: String): Element? = - (0 until childNodes.length).asSequence().map { childNodes.item(it) }.filterIsInstance() - .firstOrNull { it.tagName == tag } - private sealed interface PageContent { data class AreaContent(val content: Area): PageContent data class PathObjectContent(val content: Shape): PageContent diff --git a/migration-library/src/main/kotlin/com/quadient/migration/service/inspirebuilder/InspireBaseTemplateBuilder.kt b/migration-library/src/main/kotlin/com/quadient/migration/service/inspirebuilder/InspireBaseTemplateBuilder.kt index e867109d..da143b58 100644 --- a/migration-library/src/main/kotlin/com/quadient/migration/service/inspirebuilder/InspireBaseTemplateBuilder.kt +++ b/migration-library/src/main/kotlin/com/quadient/migration/service/inspirebuilder/InspireBaseTemplateBuilder.kt @@ -1,13 +1,20 @@ package com.quadient.migration.service.inspirebuilder +import com.quadient.migration.api.ProjectConfig import com.quadient.migration.api.dto.migrationmodel.BaseTemplate +import com.quadient.migration.service.IcmDataCache +import com.quadient.migration.shared.IcmPath +import com.quadient.migration.shared.toIcmPath import com.quadient.migration.tools.logger import com.quadient.wfdxml.WfdXmlBuilder import com.quadient.wfdxml.api.layoutnodes.Flow import com.quadient.wfdxml.api.layoutnodes.Flow.WebEditingType.SECTION import com.quadient.wfdxml.api.layoutnodes.Pages -class InspireBaseTemplateBuilder { +class InspireBaseTemplateBuilder( + private val projectConfig: ProjectConfig, + private val icmDataCache: IcmDataCache, +) { private val logger by logger() fun buildBaseTemplate(baseTemplate: BaseTemplate): String { @@ -15,13 +22,13 @@ class InspireBaseTemplateBuilder { val builder = WfdXmlBuilder() val layout = builder.addLayout() - layout.setName("DocumentLayout") - layout.addRoot() + layout.setName("DocumentLayout").addRoot().setAllowRuntimeModifications(true) val interactiveFlows = mutableListOf() var mainFlow: Flow? = null + var mainFlowSize = -1.0 - baseTemplate.pages.forEach { page -> + baseTemplate.pages.forEachIndexed { pageIndex, page -> val wfdPage = layout.addPage().setType(Pages.PageConditionType.SIMPLE) page.name?.let { wfdPage.setName(it) } page.pageWidth?.let { wfdPage.setWidth(it.toMeters()) } @@ -29,20 +36,25 @@ class InspireBaseTemplateBuilder { page.areas.forEach { area -> val flow = layout.addFlow() - .setId("Def.InteractiveFlow${interactiveFlows.size}") .setName(area.interactiveFlowName) .setType(Flow.Type.SIMPLE) .setSectionFlow(true) .setWebEditingType(SECTION) interactiveFlows.add(flow) - // TODO: main flow selection is a placeholder until we decide the real rule for it. - if (mainFlow == null) mainFlow = flow + val flowArea = wfdPage.addFlowArea().setName("${area.interactiveFlowName}Area").setFlow(flow) + .setFlowToNextPage(area.flowToNextPage) - val flowArea = wfdPage.addFlowArea().setFlow(flow).setFlowToNextPage(area.flowToNextPage) + var areaSize = -1.0 area.position?.let { flowArea.setPosX(it.x.toMeters()).setPosY(it.y.toMeters()).setWidth(it.width.toMeters()) .setHeight(it.height.toMeters()) + areaSize = it.width.toMeters() * it.height.toMeters() + } + + if (pageIndex == 0 && areaSize > mainFlowSize) { + mainFlow = flow + mainFlowSize = areaSize } } } @@ -50,7 +62,13 @@ class InspireBaseTemplateBuilder { layout.pages.setInteractiveFlows(interactiveFlows) mainFlow?.let { layout.pages.setMainFlow(it) } - logger.debug("Successfully built base template '${baseTemplate.nameOrId()}'.") - return builder.build() + val baseTemplateXml = builder.build() + val sourceBaseTemplatePath = if (projectConfig.sourceBaseTemplatePath.isNullOrBlank()) { + IcmPath.root().join("Interactive").join("StandardPackage").join("Sources").join("SourceTemplate.wfd") + } else { + projectConfig.sourceBaseTemplatePath.toIcmPath() + } + + return enrichLayoutWithSourceBaseTemplate(icmDataCache, baseTemplateXml, sourceBaseTemplatePath) } } diff --git a/migration-library/src/main/kotlin/com/quadient/migration/service/inspirebuilder/InspireBuilderUtils.kt b/migration-library/src/main/kotlin/com/quadient/migration/service/inspirebuilder/InspireBuilderUtils.kt index e099a20e..8360b0f6 100644 --- a/migration-library/src/main/kotlin/com/quadient/migration/service/inspirebuilder/InspireBuilderUtils.kt +++ b/migration-library/src/main/kotlin/com/quadient/migration/service/inspirebuilder/InspireBuilderUtils.kt @@ -1,12 +1,13 @@ package com.quadient.migration.service.inspirebuilder +import com.quadient.migration.service.IcmDataCache import com.quadient.migration.shared.Color +import com.quadient.migration.shared.IcmPath import com.quadient.wfdxml.api.layoutnodes.FillStyle import com.quadient.wfdxml.api.layoutnodes.Flow import com.quadient.wfdxml.api.layoutnodes.Font import com.quadient.wfdxml.api.layoutnodes.Image import com.quadient.wfdxml.api.layoutnodes.data.DataType -import com.quadient.wfdxml.api.layoutnodes.data.Variable import com.quadient.wfdxml.api.module.Layout import com.quadient.wfdxml.internal.Group import com.quadient.wfdxml.internal.layoutnodes.FlowImpl @@ -15,6 +16,16 @@ import com.quadient.wfdxml.internal.layoutnodes.ImageImpl import com.quadient.wfdxml.internal.layoutnodes.data.DataImpl import com.quadient.wfdxml.internal.layoutnodes.data.VariableImpl import com.quadient.wfdxml.internal.module.layout.LayoutImpl +import org.w3c.dom.Document +import org.w3c.dom.Element +import org.w3c.dom.Node +import org.xml.sax.InputSource +import java.io.StringReader +import java.io.StringWriter +import javax.xml.parsers.DocumentBuilderFactory +import javax.xml.transform.TransformerFactory.newInstance +import javax.xml.transform.dom.DOMSource +import javax.xml.transform.stream.StreamResult import com.quadient.migration.shared.DataType as DataTypeModel fun getDataType(dataType: DataTypeModel): DataType { @@ -221,4 +232,44 @@ fun appendExtensionIfMissing(fileName: String, sourcePath: String?): String { fun toScriptStringLiteral(value: String): String = "'${ value.replace("\\", "\\\\").replace("\"", "\\\"").replace("'", "\\'") -}'" \ No newline at end of file +}'" + +fun enrichLayoutWithSourceBaseTemplate( + icmDataCache: IcmDataCache, documentObjectXml: String, sourceBaseTemplatePath: IcmPath, +): String { + val sourceBaseTemplateXml = icmDataCache.wfd2Xml(sourceBaseTemplatePath) + + val sourceBaseTemplateDoc = sourceBaseTemplateXml.toXmlDocument() + val documentObjectDoc = documentObjectXml.toXmlDocument() + + val sourceBaseLayoutNode = sourceBaseTemplateDoc.getElementsByTagName("Layout").item(0) as? Element + ?: error("Source base template '$sourceBaseTemplatePath' does not contain a Layout element.") + val sourceBaseInnerLayoutNode = sourceBaseLayoutNode.firstElementChildByTag("Layout") + ?: error("Source base template '$sourceBaseTemplatePath' does not contain an inner Layout element.") + + val documentObjectInnerLayoutNode = + documentObjectDoc.getElementsByTagName("Layout").item(0)?.firstElementChildByTag("Layout") + ?.let { sourceBaseTemplateDoc.importNode(it, true) } + ?: error("Document object does not contain an inner Layout element.") + + sourceBaseLayoutNode.replaceChild(documentObjectInnerLayoutNode, sourceBaseInnerLayoutNode) + return sourceBaseTemplateDoc.toXmlString() +} + +private fun String.toXmlDocument(): Document = + DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(InputSource(StringReader(this))) + +private fun Document.toXmlString(): String { + val result = StringWriter() + val transformer = newInstance().newTransformer() + transformer.transform(DOMSource(this), StreamResult(result)) + return result.toString().replace(Regex("([\\s\\S]*?)")) { matchResult -> + val value = matchResult.groupValues[1] + val encoded = value.replace("\n", " ").replace("\r", "") + "$encoded" + } +} + +private fun Node.firstElementChildByTag(tag: String): Element? = + (0 until childNodes.length).asSequence().map { childNodes.item(it) }.filterIsInstance() + .firstOrNull { it.tagName == tag } \ No newline at end of file diff --git a/migration-library/src/test/kotlin/com/quadient/migration/service/deploy/InteractiveDeployClientTest.kt b/migration-library/src/test/kotlin/com/quadient/migration/service/deploy/InteractiveDeployClientTest.kt index dcd0877a..9a36eba1 100644 --- a/migration-library/src/test/kotlin/com/quadient/migration/service/deploy/InteractiveDeployClientTest.kt +++ b/migration-library/src/test/kotlin/com/quadient/migration/service/deploy/InteractiveDeployClientTest.kt @@ -12,6 +12,7 @@ import com.quadient.migration.api.dto.migrationmodel.Paragraph import com.quadient.migration.api.dto.migrationmodel.StringValue import com.quadient.migration.api.dto.migrationmodel.VariableRef import com.quadient.migration.api.dto.migrationmodel.builder.AttachmentBuilder +import com.quadient.migration.api.dto.migrationmodel.builder.BaseTemplateBuilder import com.quadient.migration.api.dto.migrationmodel.builder.DisplayRuleBuilder import com.quadient.migration.api.dto.migrationmodel.builder.DocumentObjectBuilder import com.quadient.migration.api.dto.migrationmodel.builder.ImageBuilder @@ -522,6 +523,45 @@ class InteractiveDeployClientTest { verify(exactly = 0) { ipsService.setProductionApprovalState(any>()) } } + @Test + fun `deployBaseTemplates uploads base templates and sets production approval state`() { + // given + val baseTemplate = BaseTemplateBuilder("BT_1").build() + every { baseTemplateRepository.listAll() } returns listOf(baseTemplate) + every { baseTemplateBuilder.buildBaseTemplate(baseTemplate) } returns "" + every { ipsService.xml2wfd(any(), any()) } returns OperationResult.Success + every { ipsService.setProductionApprovalState(any>()) } returns OperationResult.Success + + val targetPath = "icm://Interactive/BaseTemplates/BT_1.wfd".toIcmPath() + every { resourcePathProvider.getBaseTemplatePath(baseTemplate) } returns targetPath + + // when + val result = subject.deployBaseTemplates() + + // then + result.deployed.shouldBeOfSize(1) + verify { ipsService.xml2wfd(eq(""), eq(targetPath)) } + verify { ipsService.setProductionApprovalState(eq(listOf(targetPath))) } + } + + @Test + fun `deployBaseTemplates does not set production approval state when upload fails`() { + // given + val baseTemplate = BaseTemplateBuilder("BT_1").build() + every { baseTemplateRepository.listAll() } returns listOf(baseTemplate) + every { baseTemplateBuilder.buildBaseTemplate(baseTemplate) } returns "" + every { ipsService.xml2wfd(any(), any()) } returns OperationResult.Failure("Problem") + every { ipsService.setProductionApprovalState(any>()) } returns OperationResult.Success + every { resourcePathProvider.getBaseTemplatePath(baseTemplate) } returns "icm://Interactive/BaseTemplates/BT_1.wfd".toIcmPath() + + // when + val result = subject.deployBaseTemplates() + + // then + result.errors.shouldBeOfSize(1) + verify { ipsService.setProductionApprovalState(eq(emptyList())) } + } + @Test fun `deploy list of document objects validates that no document objects are unsupported`() { val spy = spyk(subject) diff --git a/migration-library/src/test/kotlin/com/quadient/migration/service/inspirebuilder/InspireBaseTemplateBuilderTest.kt b/migration-library/src/test/kotlin/com/quadient/migration/service/inspirebuilder/InspireBaseTemplateBuilderTest.kt index d5797901..57015119 100644 --- a/migration-library/src/test/kotlin/com/quadient/migration/service/inspirebuilder/InspireBaseTemplateBuilderTest.kt +++ b/migration-library/src/test/kotlin/com/quadient/migration/service/inspirebuilder/InspireBaseTemplateBuilderTest.kt @@ -1,17 +1,44 @@ package com.quadient.migration.service.inspirebuilder import com.quadient.migration.api.dto.migrationmodel.builder.BaseTemplateBuilder +import com.quadient.migration.service.DesignerIcmDataCache +import com.quadient.migration.service.DesignerResourcePathProvider +import com.quadient.migration.service.ipsclient.IpsService +import com.quadient.migration.shared.IcmPath import com.quadient.migration.shared.millimeters +import com.quadient.migration.shared.toIcmPath +import com.quadient.migration.tools.aProjectConfig import com.quadient.migration.tools.shouldBeEqualTo import com.quadient.migration.tools.shouldBeNull +import io.mockk.every +import io.mockk.mockk +import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import tools.jackson.dataformat.xml.XmlMapper import tools.jackson.module.kotlin.KotlinModule class InspireBaseTemplateBuilderTest { - private val subject = InspireBaseTemplateBuilder() + private val ipsService = mockk() + private val config = aProjectConfig() + private val resourcePathProvider = DesignerResourcePathProvider(config) + private val icmDataCache = DesignerIcmDataCache(ipsService, resourcePathProvider) + private val subject = InspireBaseTemplateBuilder(config, icmDataCache) private val xmlMapper = XmlMapper.builder().addModule(KotlinModule.Builder().build()).build() + @BeforeEach + fun setUp() { + every { ipsService.wfd2xml(any()) } returns """ + + + Layout1 + Layout1 + + + + + """.trimIndent() + } + @Test fun `buildBaseTemplate creates page with name and size`() { // given @@ -50,9 +77,11 @@ class InspireBaseTemplateBuilderTest { val result = subject.buildBaseTemplate(baseTemplate).let { xmlMapper.readTree(it.trimIndent()) }["Layout"]["Layout"] // then - val flowId = "Def.InteractiveFlow0" + val flowId = result["Flow"].first { it["Name"]?.stringValue() == "Body" }["Id"].stringValue() val flowAreaId = result["FlowArea"].first { it["FlowId"]?.stringValue() == flowId }["Id"].stringValue() + val flowAreaStub = result["FlowArea"].first { it["Id"].stringValue() == flowAreaId && it["Name"] != null } + flowAreaStub["Name"].stringValue().shouldBeEqualTo("BodyArea") val flowArea = result["FlowArea"].last { it["Id"].stringValue() == flowAreaId } flowArea["FlowingToNextPage"].stringValue().shouldBeEqualTo("True") flowArea["Pos"]["X"].stringValue().shouldBeEqualTo(10.millimeters().toMeters().toString()) @@ -77,11 +106,63 @@ class InspireBaseTemplateBuilderTest { // then result["Page"].filter { it["Name"] != null }.size.shouldBeEqualTo(2) - result["Flow"].size().shouldBeEqualTo(3) + result["Flow"].filter { it["Name"] != null }.size.shouldBeEqualTo(3) result["FlowArea"].filter { it["FlowId"] != null }.size.shouldBeEqualTo(3) result["Pages"]["InteractiveFlow"].size().shouldBeEqualTo(3) } + @Test + fun `buildBaseTemplate picks the largest area as the main flow`() { + // given + val baseTemplate = BaseTemplateBuilder("BT_1") + .addPage { + name("Page 1") + addArea("Header") { + position { left(0.millimeters()); top(0.millimeters()); width(210.millimeters()); height(20.millimeters()) } + } + addArea("Body") { + position { left(0.millimeters()); top(20.millimeters()); width(210.millimeters()); height(250.millimeters()) } + } + } + .build() + + // when + val result = subject.buildBaseTemplate(baseTemplate).let { xmlMapper.readTree(it.trimIndent()) }["Layout"]["Layout"] + + // then + val bodyFlowId = result["Flow"].first { it["Name"]?.stringValue() == "Body" }["Id"].stringValue() + result["Pages"]["MainFlow"].stringValue().shouldBeEqualTo(bodyFlowId) + } + + @Test + fun `buildBaseTemplate picks the largest area on the first page only as the main flow`() { + // given + val baseTemplate = BaseTemplateBuilder("BT_1") + .addPage { + name("Title page") + addArea("Title") { + position { left(0.millimeters()); top(0.millimeters()); width(210.millimeters()); height(297.millimeters()) } + } + } + .addPage { + name("Page 2") + addArea("Header") { + position { left(0.millimeters()); top(0.millimeters()); width(210.millimeters()); height(20.millimeters()) } + } + addArea("Body") { + position { left(0.millimeters()); top(20.millimeters()); width(210.millimeters()); height(250.millimeters()) } + } + } + .build() + + // when + val result = subject.buildBaseTemplate(baseTemplate).let { xmlMapper.readTree(it.trimIndent()) }["Layout"]["Layout"] + + // then + val titleFlowId = result["Flow"].first { it["Name"]?.stringValue() == "Title" }["Id"].stringValue() + result["Pages"]["MainFlow"].stringValue().shouldBeEqualTo(titleFlowId) + } + @Test fun `buildBaseTemplate with page without areas creates no flow`() { // given From ebd40b176618654d66839e40ff390564a2559809 Mon Sep 17 00:00:00 2001 From: "d.svitak" Date: Thu, 13 Aug 2026 11:32:00 +0200 Subject: [PATCH 03/14] MIG-584 Base template deployment to Flex - WIP - Font and style definition resolution --- .../InspireBaseTemplateBuilder.kt | 17 +++++- .../inspirebuilder/InspireBuilderUtils.kt | 21 ++++++++ .../InspireDocumentObjectBuilder.kt | 19 +------ .../InspireBaseTemplateBuilderTest.kt | 52 ++++++++++++++++--- 4 files changed, 85 insertions(+), 24 deletions(-) diff --git a/migration-library/src/main/kotlin/com/quadient/migration/service/inspirebuilder/InspireBaseTemplateBuilder.kt b/migration-library/src/main/kotlin/com/quadient/migration/service/inspirebuilder/InspireBaseTemplateBuilder.kt index da143b58..86cf4d78 100644 --- a/migration-library/src/main/kotlin/com/quadient/migration/service/inspirebuilder/InspireBaseTemplateBuilder.kt +++ b/migration-library/src/main/kotlin/com/quadient/migration/service/inspirebuilder/InspireBaseTemplateBuilder.kt @@ -3,6 +3,7 @@ package com.quadient.migration.service.inspirebuilder import com.quadient.migration.api.ProjectConfig import com.quadient.migration.api.dto.migrationmodel.BaseTemplate import com.quadient.migration.service.IcmDataCache +import com.quadient.migration.service.ResourcePathProvider import com.quadient.migration.shared.IcmPath import com.quadient.migration.shared.toIcmPath import com.quadient.migration.tools.logger @@ -14,15 +15,29 @@ import com.quadient.wfdxml.api.layoutnodes.Pages class InspireBaseTemplateBuilder( private val projectConfig: ProjectConfig, private val icmDataCache: IcmDataCache, + private val resourcePathProvider: ResourcePathProvider, ) { private val logger by logger() + private val resolvedStyleDefinitionPath: IcmPath? by lazy { + val path = resourcePathProvider.getStyleDefinitionPath() + try { + if (icmDataCache.fileExists(path)) path else null + } catch (e: Exception) { + throw RuntimeException("Failed to check for style definition existence", e) + } + } + fun buildBaseTemplate(baseTemplate: BaseTemplate): String { logger.debug("Starting to build base template '${baseTemplate.nameOrId()}'.") val builder = WfdXmlBuilder() val layout = builder.addLayout() - layout.setName("DocumentLayout").addRoot().setAllowRuntimeModifications(true) + val root = layout.setName("DocumentLayout").addRoot().setAllowRuntimeModifications(true) + if (resolvedStyleDefinitionPath != null) { + root.setExternalStylesLayout(resolvedStyleDefinitionPath.toString()) + } + resolveArialFont(layout, icmDataCache) val interactiveFlows = mutableListOf() var mainFlow: Flow? = null diff --git a/migration-library/src/main/kotlin/com/quadient/migration/service/inspirebuilder/InspireBuilderUtils.kt b/migration-library/src/main/kotlin/com/quadient/migration/service/inspirebuilder/InspireBuilderUtils.kt index 8360b0f6..f6b97721 100644 --- a/migration-library/src/main/kotlin/com/quadient/migration/service/inspirebuilder/InspireBuilderUtils.kt +++ b/migration-library/src/main/kotlin/com/quadient/migration/service/inspirebuilder/InspireBuilderUtils.kt @@ -7,7 +7,9 @@ import com.quadient.wfdxml.api.layoutnodes.FillStyle import com.quadient.wfdxml.api.layoutnodes.Flow import com.quadient.wfdxml.api.layoutnodes.Font import com.quadient.wfdxml.api.layoutnodes.Image +import com.quadient.wfdxml.api.layoutnodes.LocationType import com.quadient.wfdxml.api.layoutnodes.data.DataType +import com.quadient.wfdxml.api.layoutnodes.font.SubFont import com.quadient.wfdxml.api.module.Layout import com.quadient.wfdxml.internal.Group import com.quadient.wfdxml.internal.layoutnodes.FlowImpl @@ -234,6 +236,25 @@ fun toScriptStringLiteral(value: String): String = "'${ value.replace("\\", "\\\\").replace("\"", "\\\"").replace("'", "\\'") }'" +fun resolveArialFont(layout: Layout, icmDataCache: IcmDataCache) { + val arialFont = getFontByName(layout, "Arial") + require(arialFont != null) { "Layout must contain Arial font." } + arialFont.setName("Arial").setFontName("Arial") + upsertSubFont(icmDataCache, arialFont, isBold = false, isItalic = false) +} + +fun upsertSubFont(icmDataCache: IcmDataCache, font: Font, isBold: Boolean, isItalic: Boolean): SubFont? { + val subFontName = buildFontName(isBold, isItalic) + + val fontLocation = icmDataCache.font[FontKey(font.name, subFontName)] + ?: icmDataCache.font[FontKey(font.name, buildFontName(bold = false, italic = false))] + ?: return null + + font.subFonts.removeAll { it.name == subFontName } + return font.addSubfont().setName(subFontName).setBold(isBold).setItalic(isItalic) + .setLocation(fontLocation, LocationType.ICM) +} + fun enrichLayoutWithSourceBaseTemplate( icmDataCache: IcmDataCache, documentObjectXml: String, sourceBaseTemplatePath: IcmPath, ): String { diff --git a/migration-library/src/main/kotlin/com/quadient/migration/service/inspirebuilder/InspireDocumentObjectBuilder.kt b/migration-library/src/main/kotlin/com/quadient/migration/service/inspirebuilder/InspireDocumentObjectBuilder.kt index 6a37bd84..a3673bba 100644 --- a/migration-library/src/main/kotlin/com/quadient/migration/service/inspirebuilder/InspireDocumentObjectBuilder.kt +++ b/migration-library/src/main/kotlin/com/quadient/migration/service/inspirebuilder/InspireDocumentObjectBuilder.kt @@ -525,23 +525,8 @@ abstract class InspireDocumentObjectBuilder( } } - private fun upsertSubFont(font: Font, isBold: Boolean, isItalic: Boolean): SubFont? { - val subFontName = buildFontName(isBold, isItalic) - - val fontLocation = icmDataCache.font[FontKey(font.name, subFontName)] - ?: icmDataCache.font[FontKey(font.name, buildFontName(bold = false, italic = false))] - ?: return null - - font.subFonts.removeAll { it.name == subFontName } - return font.addSubfont().setName(subFontName).setBold(isBold).setItalic(isItalic) - .setLocation(fontLocation, LocationType.ICM) - } - fun buildTextStyles(layout: Layout, textStyleModels: List) { - val arialFont = getFontByName(layout, "Arial") - require(arialFont != null) { "Layout must contain Arial font." } - arialFont.setName("Arial").setFontName("Arial") - upsertSubFont(arialFont, isBold = false, isItalic = false) + resolveArialFont(layout, icmDataCache) textStyleModels.forEach { styleModel -> val definition = styleModel.resolve().definition @@ -556,7 +541,7 @@ abstract class InspireDocumentObjectBuilder( val font = getFontByName(layout, fontFamily) ?: layout.addFont().setName(fontFamily).setFontName(fontFamily) textStyle.setFont(font) - val subFont = upsertSubFont(font, definition.bold, definition.italic) + val subFont = upsertSubFont(icmDataCache, font, definition.bold, definition.italic) if (subFont != null) { textStyle.setSubFont(subFont) } diff --git a/migration-library/src/test/kotlin/com/quadient/migration/service/inspirebuilder/InspireBaseTemplateBuilderTest.kt b/migration-library/src/test/kotlin/com/quadient/migration/service/inspirebuilder/InspireBaseTemplateBuilderTest.kt index 57015119..215411a0 100644 --- a/migration-library/src/test/kotlin/com/quadient/migration/service/inspirebuilder/InspireBaseTemplateBuilderTest.kt +++ b/migration-library/src/test/kotlin/com/quadient/migration/service/inspirebuilder/InspireBaseTemplateBuilderTest.kt @@ -1,12 +1,11 @@ package com.quadient.migration.service.inspirebuilder import com.quadient.migration.api.dto.migrationmodel.builder.BaseTemplateBuilder -import com.quadient.migration.service.DesignerIcmDataCache -import com.quadient.migration.service.DesignerResourcePathProvider +import com.quadient.migration.service.InteractiveIcmDataCache +import com.quadient.migration.service.InteractiveResourcePathProvider import com.quadient.migration.service.ipsclient.IpsService import com.quadient.migration.shared.IcmPath import com.quadient.migration.shared.millimeters -import com.quadient.migration.shared.toIcmPath import com.quadient.migration.tools.aProjectConfig import com.quadient.migration.tools.shouldBeEqualTo import com.quadient.migration.tools.shouldBeNull @@ -20,9 +19,9 @@ import tools.jackson.module.kotlin.KotlinModule class InspireBaseTemplateBuilderTest { private val ipsService = mockk() private val config = aProjectConfig() - private val resourcePathProvider = DesignerResourcePathProvider(config) - private val icmDataCache = DesignerIcmDataCache(ipsService, resourcePathProvider) - private val subject = InspireBaseTemplateBuilder(config, icmDataCache) + private val resourcePathProvider = InteractiveResourcePathProvider(config) + private val icmDataCache = InteractiveIcmDataCache(ipsService, resourcePathProvider) + private val subject = InspireBaseTemplateBuilder(config, icmDataCache, resourcePathProvider) private val xmlMapper = XmlMapper.builder().addModule(KotlinModule.Builder().build()).build() @BeforeEach @@ -37,6 +36,47 @@ class InspireBaseTemplateBuilderTest { """.trimIndent() + every { ipsService.fileExists(any()) } returns false + every { ipsService.gatherFontData(any()) } returns "Arial,Regular,icm://Fonts/arial.ttf;" + } + + @Test + fun `buildBaseTemplate sets external styles layout when style definition exists`() { + // given + every { ipsService.fileExists(any()) } returns true + val baseTemplate = BaseTemplateBuilder("BT_1").build() + + // when + val result = subject.buildBaseTemplate(baseTemplate).let { xmlMapper.readTree(it.trimIndent()) }["Layout"]["Layout"] + + // then + val styleDefinitionPath = resourcePathProvider.getStyleDefinitionPath() + result["Root"]["ExternalStylesLayout"].stringValue().shouldBeEqualTo("VCSLocation,$styleDefinitionPath") + } + + @Test + fun `buildBaseTemplate does not set external styles layout when style definition does not exist`() { + // given + every { ipsService.fileExists(any()) } returns false + val baseTemplate = BaseTemplateBuilder("BT_1").build() + + // when + val result = subject.buildBaseTemplate(baseTemplate).let { xmlMapper.readTree(it.trimIndent()) }["Layout"]["Layout"] + + // then + result["Root"]["ExternalStylesLayout"].shouldBeNull() + } + + @Test + fun `buildBaseTemplate redirects Arial font to ICM location`() { + // given + val baseTemplate = BaseTemplateBuilder("BT_1").build() + + // when + val result = subject.buildBaseTemplate(baseTemplate).let { xmlMapper.readTree(it.trimIndent()) }["Layout"]["Layout"] + + // then + result["Font"]["SubFont"]["FontLocation"].stringValue().shouldBeEqualTo("VCSLocation,icm://Fonts/arial.ttf") } @Test From 372c3789b0d44e1e8ab1065d2dbe6cddb0bbfd48 Mon Sep 17 00:00:00 2001 From: "d.svitak" Date: Thu, 13 Aug 2026 15:14:14 +0200 Subject: [PATCH 04/14] MIG-584 Base template deployment to Flex - enabling base template ref during document object deployment, fixing forgotten mapping condition in document objects mapping for variable structure, and fixed potential clash of flow names by appending "Flow" to generated names of interactive flows in Layout index.html --- migration-examples/layout/index.html | 2 +- .../mapping/DocumentObjectsImport.groovy | 4 +--- .../DocumentObjectsMappingImportTest.groovy | 20 +++++++++++++++++++ .../migration/service/ResourcePathProvider.kt | 10 +--------- .../migration/service/DeployPhaseUtilsTest.kt | 17 ++++++---------- 5 files changed, 29 insertions(+), 24 deletions(-) diff --git a/migration-examples/layout/index.html b/migration-examples/layout/index.html index 85e2602d..254def7f 100644 --- a/migration-examples/layout/index.html +++ b/migration-examples/layout/index.html @@ -945,7 +945,7 @@ representativeDrafts.forEach((draft, areaGroupIndex) => { const flowToNextPage = draft.areaIndices.some(ai => representativePage.areas[ai]?.flowToNextPage); - let flowName = draft.name; + let flowName = `${draft.name}Flow`; if (usedFlowNames.has(flowName)) { flowName = `${flowName} (Area ${areaGroupIndex + 1})`; } diff --git a/migration-examples/src/main/groovy/com/quadient/migration/example/common/mapping/DocumentObjectsImport.groovy b/migration-examples/src/main/groovy/com/quadient/migration/example/common/mapping/DocumentObjectsImport.groovy index 3c30cd55..410fd515 100644 --- a/migration-examples/src/main/groovy/com/quadient/migration/example/common/mapping/DocumentObjectsImport.groovy +++ b/migration-examples/src/main/groovy/com/quadient/migration/example/common/mapping/DocumentObjectsImport.groovy @@ -70,9 +70,7 @@ static void run(Migration migration, Path documentObjFilePath) { Mapping.mapProp(existingMapping, existingDocObject, "type", newType) def varStructureRef = Csv.deserialize(values.get("variableStructureId"), String.class) - if (varStructureRef != existingDocObject.variableStructureRef?.id && varStructureRef != existingMapping.variableStructureRef) { - existingMapping.variableStructureRef = varStructureRef - } + existingMapping.variableStructureRef = varStructureRef def csvStatus = values.get("status") if (status != null && csvStatus == "Active" && status.class.simpleName != "Active") { diff --git a/migration-examples/src/test/groovy/DocumentObjectsMappingImportTest.groovy b/migration-examples/src/test/groovy/DocumentObjectsMappingImportTest.groovy index c460150d..b3b192fc 100644 --- a/migration-examples/src/test/groovy/DocumentObjectsMappingImportTest.groovy +++ b/migration-examples/src/test/groovy/DocumentObjectsMappingImportTest.groovy @@ -65,6 +65,26 @@ class DocumentObjectsMappingImportTest { verify(migration.mappingRepository, times(1)).applyAllDocumentObjectMappings() } + @Test + void clearsVariableStructureRefWhenCsvValueIsBlank() { + def migration = Utils.mockMigration() + Path mappingFile = Paths.get(dir.path, "testProject-variables.csv") + def input = """\ + id,name,type,internal,originLocation,baseTemplate,targetFolder,variableStructureId,status + cleared,,Block,false,[],,,,Active + """.stripIndent() + mappingFile.toFile().write(input) + givenExistingDocumentObject(migration, "cleared", null, false, null, null, null, "previousVarStructure") + givenExistingDocumentObjectMapping(migration, "cleared", null, null, null, null, null, "previousVarStructure") + + DocumentObjectsImport.run(migration, mappingFile) + + verify(migration.mappingRepository, times(1)).upsertBatch([ + "cleared": new MappingItem.DocumentObject(null, false, null, null, DocumentObjectType.Block, null, new SkipOptions(false, null, null)) + ]) + verify(migration.mappingRepository, times(1)).applyAllDocumentObjectMappings() + } + static void givenExistingDocumentObject(Migration mig, String id, String name, Boolean internal, String baseTemplate, String targetFolder, DocumentObjectType type, String varStructureRef) { def builder = new DocumentObjectBuilder(id, type ?: DocumentObjectType.Block) if (name != null) { diff --git a/migration-library/src/main/kotlin/com/quadient/migration/service/ResourcePathProvider.kt b/migration-library/src/main/kotlin/com/quadient/migration/service/ResourcePathProvider.kt index a3c8cbdd..a95c3c50 100644 --- a/migration-library/src/main/kotlin/com/quadient/migration/service/ResourcePathProvider.kt +++ b/migration-library/src/main/kotlin/com/quadient/migration/service/ResourcePathProvider.kt @@ -16,10 +16,6 @@ import com.quadient.migration.shared.IcmPath import com.quadient.migration.shared.ImageType import com.quadient.migration.shared.orDefault import com.quadient.migration.shared.toIcmPath -import org.slf4j.LoggerFactory -import java.lang.invoke.MethodHandles - -private val logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()) interface ResourcePathProvider { fun getDocumentObjectPath(nameOrId: String, type: DocumentObjectType, targetFolder: IcmPath?): IcmPath @@ -72,11 +68,7 @@ fun ResourcePathProvider.getBaseTemplateFullPath( is BaseTemplateRef -> { val baseTemplate = findBaseTemplate(documentObjectBaseTemplate.id) - val baseTemplatePath = getBaseTemplatePath(baseTemplate) - val message = - "Base template '$baseTemplatePath' cannot be used because referencing base templates by id is not yet supported during deployment." - logger.error(message) - error(message) + return getBaseTemplatePath(baseTemplate) } null -> config.baseTemplatePath diff --git a/migration-library/src/test/kotlin/com/quadient/migration/service/DeployPhaseUtilsTest.kt b/migration-library/src/test/kotlin/com/quadient/migration/service/DeployPhaseUtilsTest.kt index 3ddd9f52..529cb609 100644 --- a/migration-library/src/test/kotlin/com/quadient/migration/service/DeployPhaseUtilsTest.kt +++ b/migration-library/src/test/kotlin/com/quadient/migration/service/DeployPhaseUtilsTest.kt @@ -54,23 +54,18 @@ class DeployPhaseUtilsTest { } @Test - fun `base template referenced by id fails because it is not yet supported`() { + fun `base template referenced by id resolves to its deployed path`() { val baseTemplate = BaseTemplate( id = "bt-1", name = "AddressBaseTemplate", customFields = CustomFieldMap(), ) - try { - resourcePathProvider.getBaseTemplateFullPath( - projectConfig, BaseTemplateRef(baseTemplate.id) - ) { id -> if (id == baseTemplate.id) baseTemplate else error("Unexpected id '$id'") } - error("Expected an exception to be thrown") - } catch (e: IllegalStateException) { - e.message.shouldBeEqualTo( - "Base template 'icm://Interactive/StandardPackage/BaseTemplates/AddressBaseTemplate.wfd' cannot be used because referencing base templates by id is not yet supported during deployment." - ) - } + val result = resourcePathProvider.getBaseTemplateFullPath( + projectConfig, BaseTemplateRef(baseTemplate.id) + ) { id -> if (id == baseTemplate.id) baseTemplate else error("Unexpected id '$id'") }.toString() + + result.shouldBeEqualTo("icm://Interactive/StandardPackage/BaseTemplates/AddressBaseTemplate.wfd") } @Test From 72df4b2ac7d9b5b1f5c44c0eed877883a902f8aa Mon Sep 17 00:00:00 2001 From: "d.svitak" Date: Fri, 14 Aug 2026 10:50:30 +0200 Subject: [PATCH 05/14] MIG-584 Base template deployment to Flex - WIP - updating the ecosystem like status tracking, etc. --- .../api/repository/BaseTemplateRepository.kt | 27 ++++++++++++++----- .../service/deploy/InteractiveDeployClient.kt | 18 ++++++++----- .../deploy/utility/DeploymentResult.kt | 27 +++++++++++++++++++ .../deploy/InteractiveDeployClientTest.kt | 8 ++++++ .../migration/tools/TestObjectBuilders.kt | 2 +- 5 files changed, 68 insertions(+), 14 deletions(-) diff --git a/migration-library/src/main/kotlin/com/quadient/migration/api/repository/BaseTemplateRepository.kt b/migration-library/src/main/kotlin/com/quadient/migration/api/repository/BaseTemplateRepository.kt index c57ee9aa..2f45de07 100644 --- a/migration-library/src/main/kotlin/com/quadient/migration/api/repository/BaseTemplateRepository.kt +++ b/migration-library/src/main/kotlin/com/quadient/migration/api/repository/BaseTemplateRepository.kt @@ -2,24 +2,28 @@ package com.quadient.migration.api.repository import com.quadient.migration.api.ProjectName import com.quadient.migration.api.dto.migrationmodel.BaseTemplate -import com.quadient.migration.api.dto.migrationmodel.BaseTemplateRef import com.quadient.migration.api.dto.migrationmodel.CustomFieldMap import com.quadient.migration.api.dto.migrationmodel.MigrationObject import com.quadient.migration.persistence.table.BaseTemplateTable import com.quadient.migration.persistence.table.DocumentObjectTable +import com.quadient.migration.service.deploy.utility.ResourceType import com.quadient.migration.tools.concat import kotlin.time.Clock import kotlin.time.toJavaInstant import kotlinx.serialization.json.Json import java.sql.Types import org.jetbrains.exposed.v1.core.ResultRow +import org.jetbrains.exposed.v1.core.and import org.jetbrains.exposed.v1.core.eq import org.jetbrains.exposed.v1.jdbc.selectAll import org.jetbrains.exposed.v1.jdbc.transactions.transaction import org.jetbrains.exposed.v1.jdbc.upsertReturning +import org.jetbrains.exposed.v1.json.extract -class BaseTemplateRepository(projectName: ProjectName) : - Repository(BaseTemplateTable, projectName.name) { +class BaseTemplateRepository( + projectName: ProjectName, + private val statusTrackingRepository: StatusTrackingRepository, +) : Repository(BaseTemplateTable, projectName.name) { override fun fromDb(row: ResultRow): BaseTemplate { return BaseTemplate( @@ -36,10 +40,11 @@ class BaseTemplateRepository(projectName: ProjectName) : override fun findUsages(id: String): List { return transaction { - DocumentObjectTable.selectAll().where { DocumentObjectTable.projectName eq projectName } - .map { DocumentObjectTable.fromResultRow(it) } - .filter { it.collectRefs().any { ref -> ref is BaseTemplateRef && ref.id == id } } - .distinct() + DocumentObjectTable.selectAll().where { + (DocumentObjectTable.projectName eq projectName) and + (DocumentObjectTable.baseTemplate.extract("type") eq "BaseTemplateRef") and + (DocumentObjectTable.baseTemplate.extract("id") eq id) + }.map { DocumentObjectTable.fromResultRow(it) } } } @@ -49,6 +54,10 @@ class BaseTemplateRepository(projectName: ProjectName) : val now = Clock.System.now() + if (existingItem == null) { + statusTrackingRepository.active(dto.id, ResourceType.BaseTemplate) + } + table.upsertReturning(table.id, table.projectName) { it[BaseTemplateTable.id] = dto.id it[BaseTemplateTable.projectName] = this@BaseTemplateRepository.projectName @@ -79,6 +88,10 @@ class BaseTemplateRepository(projectName: ProjectName) : dtos.forEach { dto -> val existingItem = find(dto.id) + if (existingItem == null) { + statusTrackingRepository.active(dto.id, ResourceType.BaseTemplate) + } + stmt.setString(index++, dto.id) stmt.setString(index++, this@BaseTemplateRepository.projectName) stmt.setString(index++, dto.name) diff --git a/migration-library/src/main/kotlin/com/quadient/migration/service/deploy/InteractiveDeployClient.kt b/migration-library/src/main/kotlin/com/quadient/migration/service/deploy/InteractiveDeployClient.kt index f6fe0ce2..95902ff6 100644 --- a/migration-library/src/main/kotlin/com/quadient/migration/service/deploy/InteractiveDeployClient.kt +++ b/migration-library/src/main/kotlin/com/quadient/migration/service/deploy/InteractiveDeployClient.kt @@ -29,12 +29,12 @@ import com.quadient.migration.api.repository.VariableStructureRepository import com.quadient.migration.persistence.table.DocumentObjectTable import com.quadient.migration.service.Storage import com.quadient.migration.service.deploy.utility.DeploymentError -import com.quadient.migration.service.deploy.utility.DeploymentInfo import com.quadient.migration.service.deploy.utility.DeploymentResult import com.quadient.migration.service.deploy.utility.MetadataValidatorImpl import com.quadient.migration.service.deploy.utility.PostProcessImpl import com.quadient.migration.service.deploy.utility.ResourceType import com.quadient.migration.service.deploy.utility.ResultTracker +import com.quadient.migration.service.deploy.utility.ResultTrackerImpl import com.quadient.migration.service.ResourcePathProvider import com.quadient.migration.service.getBaseTemplateFullPath import com.quadient.migration.service.deploy.utility.ConflictDetectorImpl @@ -189,32 +189,38 @@ open class InteractiveDeployClient( } override fun deployBaseTemplates(): DeploymentResult { - val deploymentResult = DeploymentResult(Uuid.random()) + val tracker = ResultTrackerImpl(statusTrackingRepository, projectConfig.inspireOutput) val baseTemplates = baseTemplateRepository.listAll() logger.info("Found ${baseTemplates.size} base template(s) in the repository.") for (baseTemplate in baseTemplates) { val targetPath = resourcePathProvider.getBaseTemplatePath(baseTemplate) + + if (!shouldDeployObject(baseTemplate.id, ResourceType.BaseTemplate, targetPath, tracker.deploymentResult)) { + logger.info("Skipping deployment of '${baseTemplate.id}' as it is not marked for deployment.") + continue + } + val wfdXml = baseTemplateBuilder.buildBaseTemplate(baseTemplate) when (val result = ipsService.xml2wfd(wfdXml, targetPath)) { is OperationResult.Success -> { logger.debug("Deployment of base template '${baseTemplate.nameOrId()}' to $targetPath is successful.") - deploymentResult.deployed.add(DeploymentInfo(baseTemplate.id, ResourceType.BaseTemplate, targetPath)) + tracker.deployedBaseTemplate(baseTemplate.id, targetPath) } is OperationResult.Failure -> { val message = "Failed to deploy base template '${baseTemplate.nameOrId()}' to $targetPath." logger.error(message) - deploymentResult.errors.add(DeploymentError(baseTemplate.id, message)) + tracker.errorBaseTemplate(baseTemplate.id, targetPath, message) } } } - runPostProcessors(deploymentResult) + runPostProcessors(tracker.deploymentResult) - return deploymentResult + return tracker.deploymentResult } override fun getAllDocumentObjectsToDeploy(): List { diff --git a/migration-library/src/main/kotlin/com/quadient/migration/service/deploy/utility/DeploymentResult.kt b/migration-library/src/main/kotlin/com/quadient/migration/service/deploy/utility/DeploymentResult.kt index f10790c9..62d39217 100644 --- a/migration-library/src/main/kotlin/com/quadient/migration/service/deploy/utility/DeploymentResult.kt +++ b/migration-library/src/main/kotlin/com/quadient/migration/service/deploy/utility/DeploymentResult.kt @@ -52,6 +52,8 @@ interface ResultTracker { fun deployedDisplayRule(id: String, targetPath: IcmPath) fun warningDisplayRule(id: String, path: IcmPath, message: String) fun errorDisplayRule(id: String, path: IcmPath, message: String) + fun deployedBaseTemplate(id: String, targetPath: IcmPath) + fun errorBaseTemplate(id: String, targetPath: IcmPath?, message: String) } class ResultTrackerImpl( @@ -209,4 +211,29 @@ class ResultTrackerImpl( ) deploymentResult.errors.add(DeploymentError(id, message)) } + + override fun deployedBaseTemplate(id: String, targetPath: IcmPath) { + statusTrackingRepository?.deployed( + id = id, + deploymentId = deploymentId, + timestamp = timestamp, + resourceType = ResourceType.BaseTemplate, + output = inspireOutput, + icmPath = targetPath, + ) + deploymentResult.deployed.add(DeploymentInfo(id, ResourceType.BaseTemplate, targetPath)) + } + + override fun errorBaseTemplate(id: String, targetPath: IcmPath?, message: String) { + statusTrackingRepository?.error( + id = id, + deploymentId = deploymentId, + timestamp = timestamp, + resourceType = ResourceType.BaseTemplate, + output = inspireOutput, + icmPath = targetPath, + message = message, + ) + deploymentResult.errors.add(DeploymentError(id, message)) + } } \ No newline at end of file diff --git a/migration-library/src/test/kotlin/com/quadient/migration/service/deploy/InteractiveDeployClientTest.kt b/migration-library/src/test/kotlin/com/quadient/migration/service/deploy/InteractiveDeployClientTest.kt index 9a36eba1..fc6af6d4 100644 --- a/migration-library/src/test/kotlin/com/quadient/migration/service/deploy/InteractiveDeployClientTest.kt +++ b/migration-library/src/test/kotlin/com/quadient/migration/service/deploy/InteractiveDeployClientTest.kt @@ -531,6 +531,10 @@ class InteractiveDeployClientTest { every { baseTemplateBuilder.buildBaseTemplate(baseTemplate) } returns "" every { ipsService.xml2wfd(any(), any()) } returns OperationResult.Success every { ipsService.setProductionApprovalState(any>()) } returns OperationResult.Success + every { statusTrackingRepository.findLastEventRelevantToOutput(any(), any(), any()) } returns Active() + every { + statusTrackingRepository.deployed(any(), any(), any(), any(), any(), any()) + } returns aDeployedStatus("id") val targetPath = "icm://Interactive/BaseTemplates/BT_1.wfd".toIcmPath() every { resourcePathProvider.getBaseTemplatePath(baseTemplate) } returns targetPath @@ -553,6 +557,10 @@ class InteractiveDeployClientTest { every { ipsService.xml2wfd(any(), any()) } returns OperationResult.Failure("Problem") every { ipsService.setProductionApprovalState(any>()) } returns OperationResult.Success every { resourcePathProvider.getBaseTemplatePath(baseTemplate) } returns "icm://Interactive/BaseTemplates/BT_1.wfd".toIcmPath() + every { statusTrackingRepository.findLastEventRelevantToOutput(any(), any(), any()) } returns Active() + every { + statusTrackingRepository.error(any(), any(), any(), any(), any(), any(), any()) + } returns aDeployedStatus("id") // when val result = subject.deployBaseTemplates() diff --git a/migration-library/src/test/kotlin/com/quadient/migration/tools/TestObjectBuilders.kt b/migration-library/src/test/kotlin/com/quadient/migration/tools/TestObjectBuilders.kt index bba9108f..e4f5144b 100644 --- a/migration-library/src/test/kotlin/com/quadient/migration/tools/TestObjectBuilders.kt +++ b/migration-library/src/test/kotlin/com/quadient/migration/tools/TestObjectBuilders.kt @@ -530,4 +530,4 @@ fun aTextStyleRepository() = TextStyleRepository(ProjectName(aProjectConfig().na fun aDisplayRuleRepository() = DisplayRuleRepository(ProjectName(aProjectConfig().name), statusRepo) fun aImageRepository() = ImageRepository(ProjectName(aProjectConfig().name), statusRepo) fun aAttachmentRepository() = AttachmentRepository(ProjectName(aProjectConfig().name), statusRepo) -fun aBaseTemplateRepository() = BaseTemplateRepository(ProjectName(aProjectConfig().name)) +fun aBaseTemplateRepository() = BaseTemplateRepository(ProjectName(aProjectConfig().name), statusRepo) From 93e802199c533301c42bb15b6e3379c401f788a1 Mon Sep 17 00:00:00 2001 From: "d.svitak" Date: Fri, 14 Aug 2026 13:17:07 +0200 Subject: [PATCH 06/14] MIG-584 Base template deployment to Flex - including base templates into validations and hierarchy --- migration-examples/hierarchy/index.html | 23 ++++++++++++++++++- .../example/common/report/Hierarchy.groovy | 11 +++++++++ .../api/dto/migrationmodel/BaseTemplate.kt | 4 +++- .../migration/service/ReferenceValidator.kt | 3 ++- 4 files changed, 38 insertions(+), 3 deletions(-) diff --git a/migration-examples/hierarchy/index.html b/migration-examples/hierarchy/index.html index be962983..0abee186 100644 --- a/migration-examples/hierarchy/index.html +++ b/migration-examples/hierarchy/index.html @@ -183,6 +183,10 @@ color: #40a02b; } + .label.base_template { + color: #8839ef; + } + .label mark { background: var(--hl); padding: 0 2px; @@ -247,6 +251,8 @@ + +
@@ -289,6 +295,7 @@ const variableStructuresChkbox = /** @type {HTMLInputElement} */ (document.getElementById("variableStructures")); const attachmentsChkbox = /** @type {HTMLInputElement} */ (document.getElementById("attachments")); const imagesChkbox = /** @type {HTMLInputElement} */ (document.getElementById("images")); + const baseTemplatesChkbox = /** @type {HTMLInputElement} */ (document.getElementById("baseTemplates")); function refresh() { indexes = buildIndexes(data); @@ -307,6 +314,7 @@ variableStructuresChkbox.addEventListener("change", refresh); attachmentsChkbox.addEventListener("change", refresh); imagesChkbox.addEventListener("change", refresh); + baseTemplatesChkbox.addEventListener("change", refresh); treeEl.addEventListener("click", onTreeClick); @@ -489,6 +497,9 @@ case 'VARIABLE_STRUCTURE': { return variableStructuresChkbox.checked; }; + case 'BASE_TEMPLATE': { + return baseTemplatesChkbox.checked; + }; } } @@ -916,6 +927,7 @@ addBucket(root.images, "IMAGE"); addBucket(root.variables, "VARIABLE"); addBucket(root.variableStructures, "VARIABLE_STRUCTURE"); + addBucket(root.baseTemplates, "BASE_TEMPLATE"); return { nodeByKey, childrenByKey, parentsByKey }; } @@ -967,6 +979,9 @@ case 'VARIABLE_STRUCTURE': { return root.variableStructures[ref.id]; }; + case 'BASE_TEMPLATE': { + return root.baseTemplates[ref.id]; + }; } } @@ -1012,6 +1027,10 @@ label = "VS" break; }; + case 'BASE_TEMPLATE': { + label = "BT" + break; + }; } let eid = id; @@ -1040,6 +1059,7 @@ * @property {Object.} images * @property {Object.} variables * @property {Object.} variableStructures + * @property {Object.} baseTemplates */ /** @@ -1073,7 +1093,8 @@ * | 'ATTACHMENT' * | 'IMAGE' * | 'VARIABLE' - * | 'VARIABLE_STRUCTURE'} ChildType + * | 'VARIABLE_STRUCTURE' + * | 'BASE_TEMPLATE'} ChildType */ diff --git a/migration-examples/src/main/groovy/com/quadient/migration/example/common/report/Hierarchy.groovy b/migration-examples/src/main/groovy/com/quadient/migration/example/common/report/Hierarchy.groovy index 46798a0c..f42b055b 100644 --- a/migration-examples/src/main/groovy/com/quadient/migration/example/common/report/Hierarchy.groovy +++ b/migration-examples/src/main/groovy/com/quadient/migration/example/common/report/Hierarchy.groovy @@ -27,6 +27,7 @@ def attachments = migration.attachmentRepository.listAll() def images = migration.imageRepository.listAll() def variables = migration.variableRepository.listAll() def variableStructures = migration.variableStructureRepository.listAll() +def baseTemplates = migration.baseTemplateRepository.listAll() def root = new Root() @@ -87,6 +88,12 @@ for (variableStructure in variableStructures) { root.variableStructures[node.id] = node } +for (baseTemplate in baseTemplates) { + def node = new Leaf(id: baseTemplate.id, name: baseTemplate.name, type: ChildType.BASE_TEMPLATE) + + root.baseTemplates[node.id] = node +} + // Collect parents for (node in root.documentObjects.values()) { def parentNode = new Reference(id: node.id, type: ChildType.DOCUMENT_OBJECT) @@ -121,6 +128,7 @@ static void collectParents(Root root, Node node, Reference parentNode) { case ChildType.IMAGE -> root.images case ChildType.VARIABLE -> root.variables case ChildType.VARIABLE_STRUCTURE -> root.variableStructures + case ChildType.BASE_TEMPLATE -> root.baseTemplates default -> throw new IllegalStateException("Unknown parent type: ${child.type}") } @@ -139,6 +147,7 @@ static void collectChildren(Node node, Set refs) { case ImageRef -> ChildType.IMAGE case VariableRef -> ChildType.VARIABLE case VariableStructureRef -> ChildType.VARIABLE_STRUCTURE + case BaseTemplateRef -> ChildType.BASE_TEMPLATE default -> throw new IllegalStateException("Unknown reference type: ${child.class}") } @@ -155,6 +164,7 @@ class Root { Map images = [:] Map variables = [:] Map variableStructures = [:] + Map baseTemplates = [:] } class Node { @@ -186,4 +196,5 @@ enum ChildType { IMAGE, VARIABLE, VARIABLE_STRUCTURE, + BASE_TEMPLATE, } \ No newline at end of file diff --git a/migration-library/src/main/kotlin/com/quadient/migration/api/dto/migrationmodel/BaseTemplate.kt b/migration-library/src/main/kotlin/com/quadient/migration/api/dto/migrationmodel/BaseTemplate.kt index e2c9cc29..fe36a345 100644 --- a/migration-library/src/main/kotlin/com/quadient/migration/api/dto/migrationmodel/BaseTemplate.kt +++ b/migration-library/src/main/kotlin/com/quadient/migration/api/dto/migrationmodel/BaseTemplate.kt @@ -12,4 +12,6 @@ data class BaseTemplate( var pages: List = emptyList(), override var created: Instant? = null, override var lastUpdated: Instant? = null, -) : MigrationObject +) : MigrationObject, RefValidatable { + override fun collectRefs(): Set = emptySet() +} diff --git a/migration-library/src/main/kotlin/com/quadient/migration/service/ReferenceValidator.kt b/migration-library/src/main/kotlin/com/quadient/migration/service/ReferenceValidator.kt index a6bfa11b..0c002bfe 100644 --- a/migration-library/src/main/kotlin/com/quadient/migration/service/ReferenceValidator.kt +++ b/migration-library/src/main/kotlin/com/quadient/migration/service/ReferenceValidator.kt @@ -37,10 +37,11 @@ class ReferenceValidator( val displayRules = displayRuleRepository.listAll() val images = imageRepository.listAll() val attachments = attachmentRepository.listAll() + val baseTemplates = baseTemplateRepository.listAll() val alreadyValidatedRefs = mutableSetOf() val missingRefs = - (documentObjects + variables + paragraphStyles + textStyles + dataStructures + displayRules + images + attachments).mapNotNull { + (documentObjects + variables + paragraphStyles + textStyles + dataStructures + displayRules + images + attachments + baseTemplates).mapNotNull { validate(it, alreadyValidatedRefs).missingRefs.ifEmpty { null } }.flatten() From de69a785cbc8fb269883fc9f4d89805f66f6b70d Mon Sep 17 00:00:00 2001 From: "d.svitak" Date: Fri, 14 Aug 2026 16:49:58 +0200 Subject: [PATCH 07/14] MIG-584 Base template deployment to Flex - introduced RefInheritanceService that during deployment passes base templates (and also variable structures) down the hierarchy (template -> page -> external block A ...) if they don't have explicitly assigned base template themselves. --- .../migration/example/example/Import.groovy | 8 -- .../com/quadient/migration/api/Migration.kt | 2 + .../migration/service/deploy/DeployClient.kt | 8 +- .../service/deploy/DesignerDeployClient.kt | 2 + .../service/deploy/EvolveDeployClient.kt | 3 + .../service/deploy/InteractiveDeployClient.kt | 37 ++++++--- .../deploy/utility/RefInheritanceService.kt | 72 +++++++++++++++++ .../deploy/DesignerDeployClientTest.kt | 1 + .../service/deploy/EvolveDeployClientTest.kt | 6 +- .../deploy/InteractiveDeployClientTest.kt | 80 +++++++++++++++++++ .../utility/RefInheritanceServiceTest.kt | 71 ++++++++++++++++ 11 files changed, 269 insertions(+), 21 deletions(-) create mode 100644 migration-library/src/main/kotlin/com/quadient/migration/service/deploy/utility/RefInheritanceService.kt create mode 100644 migration-library/src/test/kotlin/com/quadient/migration/service/deploy/utility/RefInheritanceServiceTest.kt diff --git a/migration-examples/src/main/groovy/com/quadient/migration/example/example/Import.groovy b/migration-examples/src/main/groovy/com/quadient/migration/example/example/Import.groovy index a96dbbc4..0664d5d3 100644 --- a/migration-examples/src/main/groovy/com/quadient/migration/example/example/Import.groovy +++ b/migration-examples/src/main/groovy/com/quadient/migration/example/example/Import.groovy @@ -119,7 +119,6 @@ def displayAddressRule = new DisplayRuleBuilder("displayAddressRule") .internal(false) .subject("External display rule") .metadata("key") { it.string("value") } - .variableStructureRef(variableStructure) .group { it.operator(GroupOp.Or) it.comparison { it.variable(nameVariable).notEquals().value("") } @@ -403,7 +402,6 @@ def address = new DocumentObjectBuilder("address", DocumentObjectType.Block) .paragraph { it.styleRef(compactParagraphStyle).text { it.styleRef(normalStyle).variableRef(addressVariable) } } .paragraph { it.styleRef(compactParagraphStyle).text { it.styleRef(normalStyle).variableRef(cityVariable) } } .paragraph { it.styleRef(compactParagraphStyle).text { it.styleRef(normalStyle).variableRef(stateVariable) } } - .variableStructureRef(variableStructure) .build() // Footer of the document containing a signature. @@ -411,7 +409,6 @@ def signature = new DocumentObjectBuilder("signature", DocumentObjectType.Block) .paragraph { it.styleRef(compactParagraphStyle).text { it.styleRef(normalStyle).string("Sincerely,") } } .paragraph { it.styleRef(compactParagraphStyle).text { it.styleRef(normalStyle).string("John Migration") } } .paragraph { it.styleRef(compactParagraphStyle).text { it.styleRef(normalStyle).string("CEO of Lorem ipsum") } } - .variableStructureRef(variableStructure) .build() // Sample paragraph containing a heading using headingStyle style, @@ -490,7 +487,6 @@ def conditionalParagraph = new DocumentObjectBuilder("conditionalParagraph", Doc it.styleRef(normalStyle).string("Integer quis quam semper, accumsan neque at, pellentesque diam. Etiam in blandit dolor. Maecenas sit amet interdum augue, vel pellentesque erat. Suspendisse ut sem in justo rhoncus placerat vitae ut lacus. Etiam consequat bibendum justo ut posuere. Donec aliquam posuere nibh, vehicula pulvinar lectus dictum et. Nullam rhoncus ultrices ipsum et consectetur. Nam tincidunt id purus ac viverra. ") } } - .variableStructureRef(variableStructure) .build() def firstMatchBlock = new DocumentObjectBuilder("firstMatch", DocumentObjectType.Block) @@ -534,7 +530,6 @@ def snippet = new SnippetBuilder("snippet") .simple() .string("Lorem ipsum: ") .variableRef(nameVariable) - .variableStructureRef(variableStructure) .build() // A simple first match snippet example @@ -551,7 +546,6 @@ def fmSnippet = new SnippetBuilder("firstMatchSnippet") } .defaultString("For more information visit ") } - .variableStructureRef(variableStructure) .build() @@ -649,7 +643,6 @@ def page = new DocumentObjectBuilder("page1", DocumentObjectType.Page) .attachmentRef(exampleAttachment) .flowToNextPage(true) } - .variableStructureRef(variableStructure) .build() def sms = new SmsObjectBuilder("sms") @@ -724,7 +717,6 @@ def templateEmailSms = new DocumentObjectBuilder("templateEmailSms", DocumentObj .documentObjectRef(sms) .documentObjectRef(email) .baseTemplate("vcs://Interactive/StandardPackage/BaseTemplates/ResponsiveEmailBaseTemplate.wfd") - .variableStructureRef(variableStructure) .build() def template = new DocumentObjectBuilder("template", DocumentObjectType.Template) diff --git a/migration-library/src/main/kotlin/com/quadient/migration/api/Migration.kt b/migration-library/src/main/kotlin/com/quadient/migration/api/Migration.kt index 1d7a4ef2..f9b787eb 100644 --- a/migration-library/src/main/kotlin/com/quadient/migration/api/Migration.kt +++ b/migration-library/src/main/kotlin/com/quadient/migration/api/Migration.kt @@ -26,6 +26,7 @@ import com.quadient.migration.service.PreviewProvider import com.quadient.migration.service.ResourcePathProvider import com.quadient.migration.service.deploy.utility.ConflictDetectorImpl import com.quadient.migration.service.deploy.utility.DeployOrderImpl +import com.quadient.migration.service.deploy.utility.RefInheritanceServiceImpl import com.quadient.migration.service.deploy.utility.ProgressReporterImpl import com.quadient.migration.service.inspirebuilder.InspireDocumentObjectBuilder import com.quadient.migration.service.inspirebuilder.InspireBaseTemplateBuilder @@ -111,6 +112,7 @@ class Migration(val config: MigConfig, val projectConfig: ProjectConfig) { single() single() single() + single() single() single() diff --git a/migration-library/src/main/kotlin/com/quadient/migration/service/deploy/DeployClient.kt b/migration-library/src/main/kotlin/com/quadient/migration/service/deploy/DeployClient.kt index 7a08c162..80e13ac7 100644 --- a/migration-library/src/main/kotlin/com/quadient/migration/service/deploy/DeployClient.kt +++ b/migration-library/src/main/kotlin/com/quadient/migration/service/deploy/DeployClient.kt @@ -48,6 +48,7 @@ import com.quadient.migration.service.inspirebuilder.InspireDocumentObjectBuilde import com.quadient.migration.service.ResourcePathProvider import com.quadient.migration.service.deploy.utility.DeployOrder import com.quadient.migration.service.deploy.utility.DeployOrderImpl +import com.quadient.migration.service.deploy.utility.RefInheritanceService import com.quadient.migration.service.ipsclient.IpsService import com.quadient.migration.service.ipsclient.OperationResult import com.quadient.migration.service.readSafely @@ -67,6 +68,7 @@ sealed class DeployClient( private val conflictDetector: ConflictDetectorImpl, private val progressReporter: ProgressReporterImpl, private val deployOrder: DeployOrderImpl, + private val refInheritanceService: RefInheritanceService, private val resourcePathProvider: ResourcePathProvider, protected val documentObjectRepository: DocumentObjectRepository, protected val imageRepository: ImageRepository, @@ -112,7 +114,7 @@ sealed class DeployClient( fun deployDocumentObjects(): DeploymentResult { val tracker = ResultTrackerImpl(statusTrackingRepository, projectConfig.inspireOutput) - val ordered = deployOrder(getAllDocumentObjectsToDeploy()) + val ordered = refInheritanceService.apply(deployOrder(getAllDocumentObjectsToDeploy())) val result = deployDocumentObjectsInternal(ordered, tracker, ::uploadDocumentObject, ::uploadImage, ::uploadAttachment, ::uploadDisplayRule) runPostProcessors(result) @@ -124,11 +126,11 @@ sealed class DeployClient( val documentObjects = getDocumentObjectsToDeploy(documentObjectIds) val tracker = ResultTrackerImpl(statusTrackingRepository, projectConfig.inspireOutput) val result = if (skipDependencies) { - val ordered = deployOrder(documentObjects) + val ordered = refInheritanceService.apply(deployOrder(documentObjects)) deployDocumentObjectsInternal(ordered, tracker, ::uploadDocumentObject, ::uploadImage, ::uploadAttachment, ::uploadDisplayRule) } else { val dependencies = documentObjects.flatMap { it.findDependencies() }.filter { it.internal != true } - val ordered = deployOrder((documentObjects + dependencies).toSet().toList()) + val ordered = refInheritanceService.apply(deployOrder((documentObjects + dependencies).toSet().toList())) deployDocumentObjectsInternal(ordered, tracker, ::uploadDocumentObject, ::uploadImage, ::uploadAttachment, ::uploadDisplayRule) } diff --git a/migration-library/src/main/kotlin/com/quadient/migration/service/deploy/DesignerDeployClient.kt b/migration-library/src/main/kotlin/com/quadient/migration/service/deploy/DesignerDeployClient.kt index a5c8dd4e..d3da53e4 100644 --- a/migration-library/src/main/kotlin/com/quadient/migration/service/deploy/DesignerDeployClient.kt +++ b/migration-library/src/main/kotlin/com/quadient/migration/service/deploy/DesignerDeployClient.kt @@ -25,6 +25,7 @@ import com.quadient.migration.service.deploy.utility.ResultTracker import com.quadient.migration.service.ResourcePathProvider import com.quadient.migration.service.deploy.utility.ConflictDetectorImpl import com.quadient.migration.service.deploy.utility.DeployOrderImpl +import com.quadient.migration.service.deploy.utility.NoopRefInheritanceService import com.quadient.migration.service.deploy.utility.ProgressReporterImpl import com.quadient.migration.service.inspirebuilder.InspireDocumentObjectBuilder import com.quadient.migration.service.ipsclient.IpsService @@ -69,6 +70,7 @@ class DesignerDeployClient( conflictDetector, progressReporter, deployOrder, + NoopRefInheritanceService(), resourcePathProvider, documentObjectRepository, imageRepository, diff --git a/migration-library/src/main/kotlin/com/quadient/migration/service/deploy/EvolveDeployClient.kt b/migration-library/src/main/kotlin/com/quadient/migration/service/deploy/EvolveDeployClient.kt index 60b4b1b0..d9acee4b 100644 --- a/migration-library/src/main/kotlin/com/quadient/migration/service/deploy/EvolveDeployClient.kt +++ b/migration-library/src/main/kotlin/com/quadient/migration/service/deploy/EvolveDeployClient.kt @@ -24,6 +24,7 @@ import com.quadient.migration.service.getBaseTemplateFullPath import com.quadient.migration.service.deploy.utility.ConflictDetectorImpl import com.quadient.migration.service.deploy.utility.DeployOrderImpl import com.quadient.migration.service.deploy.utility.DeploymentResult +import com.quadient.migration.service.deploy.utility.RefInheritanceServiceImpl import com.quadient.migration.service.deploy.utility.ProgressReporterImpl import com.quadient.migration.service.inspirebuilder.InspireDocumentObjectBuilder import com.quadient.migration.service.inspirebuilder.InspireBaseTemplateBuilder @@ -47,6 +48,7 @@ class EvolveDeployClient( conflictDetector: ConflictDetectorImpl, progressReporter: ProgressReporterImpl, deployOrder: DeployOrderImpl, + refInheritanceService: RefInheritanceServiceImpl, documentObjectRepository: DocumentObjectRepository, imageRepository: ImageRepository, attachmentRepository: AttachmentRepository, @@ -69,6 +71,7 @@ class EvolveDeployClient( conflictDetector, progressReporter, deployOrder, + refInheritanceService, documentObjectRepository, imageRepository, attachmentRepository, diff --git a/migration-library/src/main/kotlin/com/quadient/migration/service/deploy/InteractiveDeployClient.kt b/migration-library/src/main/kotlin/com/quadient/migration/service/deploy/InteractiveDeployClient.kt index 95902ff6..d07826b5 100644 --- a/migration-library/src/main/kotlin/com/quadient/migration/service/deploy/InteractiveDeployClient.kt +++ b/migration-library/src/main/kotlin/com/quadient/migration/service/deploy/InteractiveDeployClient.kt @@ -5,6 +5,7 @@ import com.quadient.migration.api.repository.StatusTrackingRepository import com.quadient.migration.api.dto.migrationmodel.DocumentObject import com.quadient.migration.api.dto.migrationmodel.Attachment import com.quadient.migration.api.dto.migrationmodel.AttachmentRef +import com.quadient.migration.api.dto.migrationmodel.BaseTemplateLocation import com.quadient.migration.api.dto.migrationmodel.BaseTemplateRef import com.quadient.migration.api.dto.migrationmodel.CustomFieldMap import com.quadient.migration.api.dto.migrationmodel.DisplayRule @@ -39,6 +40,7 @@ import com.quadient.migration.service.ResourcePathProvider import com.quadient.migration.service.getBaseTemplateFullPath import com.quadient.migration.service.deploy.utility.ConflictDetectorImpl import com.quadient.migration.service.deploy.utility.DeployOrderImpl +import com.quadient.migration.service.deploy.utility.RefInheritanceServiceImpl import com.quadient.migration.service.deploy.utility.ProgressReporterImpl import com.quadient.migration.service.inspirebuilder.InspireDocumentObjectBuilder import com.quadient.migration.service.inspirebuilder.InspireBaseTemplateBuilder @@ -70,6 +72,7 @@ open class InteractiveDeployClient( conflictDetector: ConflictDetectorImpl, progressReporter: ProgressReporterImpl, deployOrder: DeployOrderImpl, + refInheritanceService: RefInheritanceServiceImpl, documentObjectRepository: DocumentObjectRepository, imageRepository: ImageRepository, attachmentRepository: AttachmentRepository, @@ -91,6 +94,7 @@ open class InteractiveDeployClient( conflictDetector, progressReporter, deployOrder, + refInheritanceService, resourcePathProvider, documentObjectRepository, imageRepository, @@ -272,7 +276,7 @@ open class InteractiveDeployClient( tracker: ResultTracker, deployDisplayRule: (DisplayRule, IcmPath, ByteArray) -> OperationResult, ) { - val rules = documentObjects + val enrichedRules = documentObjects .flatMap { try { it.getAllExternalDisplayRules() @@ -281,10 +285,14 @@ open class InteractiveDeployClient( emptyList() } } - .distinctBy { it.id } - - for (r in rules) { - val rule = r.resolveTarget(displayRuleRepository::findOrFail) + .distinctBy { it.rule.id } + + for (enrichedRule in enrichedRules) { + val resolvedRule = enrichedRule.rule.resolveTarget(displayRuleRepository::findOrFail) + val rule = resolvedRule.copy( + baseTemplate = resolvedRule.baseTemplate ?: enrichedRule.inheritedBaseTemplate, + variableStructureRef = resolvedRule.variableStructureRef ?: enrichedRule.inheritedVariableStructureRef, + ) val targetPath = resourcePathProvider.getDisplayRulePath(rule) if (!shouldDeployObject(rule.id, ResourceType.DisplayRule, targetPath, tracker.deploymentResult)) { @@ -491,8 +499,13 @@ open class InteractiveDeployClient( } } - private fun DocumentObject.getAllExternalDisplayRules(): List { - val resources = mutableListOf() + private fun DocumentObject.getAllExternalDisplayRules( + inheritedBaseTemplate: BaseTemplateLocation? = null, + inheritedVariableStructureRef: VariableStructureRef? = null, + ): List { + val resources = mutableListOf() + val effectiveBaseTemplate = this.baseTemplate ?: inheritedBaseTemplate + val effectiveVariableStructureRef = this.variableStructureRef ?: inheritedVariableStructureRef this.collectRefs().forEach { when (it) { @@ -502,14 +515,14 @@ open class InteractiveDeployClient( val resolvedModel = model.resolveTarget(displayRuleRepository::findOrFail) if (!resolvedModel.internal) { - resources.add(model) + resources.add(EnrichedDisplayRule(model, effectiveBaseTemplate, effectiveVariableStructureRef)) } } is DocumentObjectRef -> { val model = documentObjectRepository.find(it.id) ?: error("Unable to collect resource references because inner document object '${it.id}' was not found.") - resources.addAll(model.getAllExternalDisplayRules()) + resources.addAll(model.getAllExternalDisplayRules(effectiveBaseTemplate, effectiveVariableStructureRef)) } is ParagraphStyleRef, is AttachmentRef, is ImageRef, is TextStyleRef, is VariableRef, is VariableStructureRef, is BaseTemplateRef -> {} } @@ -517,4 +530,10 @@ open class InteractiveDeployClient( return resources } + + protected data class EnrichedDisplayRule( + val rule: DisplayRule, + val inheritedBaseTemplate: BaseTemplateLocation?, + val inheritedVariableStructureRef: VariableStructureRef?, + ) } diff --git a/migration-library/src/main/kotlin/com/quadient/migration/service/deploy/utility/RefInheritanceService.kt b/migration-library/src/main/kotlin/com/quadient/migration/service/deploy/utility/RefInheritanceService.kt new file mode 100644 index 00000000..90637780 --- /dev/null +++ b/migration-library/src/main/kotlin/com/quadient/migration/service/deploy/utility/RefInheritanceService.kt @@ -0,0 +1,72 @@ +package com.quadient.migration.service.deploy.utility + +import com.quadient.migration.api.dto.migrationmodel.BaseTemplateLocation +import com.quadient.migration.api.dto.migrationmodel.DocumentObject +import com.quadient.migration.api.dto.migrationmodel.DocumentObjectRef +import com.quadient.migration.api.dto.migrationmodel.VariableStructureRef +import com.quadient.migration.api.repository.DocumentObjectRepository + +interface RefInheritanceService { + fun apply(documentObjects: List): List +} + +class RefInheritanceServiceImpl( + private val documentObjectRepository: DocumentObjectRepository, +) : RefInheritanceService { + override fun apply(documentObjects: List): List { + val parentsById = mutableMapOf>() + for (obj in documentObjectRepository.listAll()) { + for (ref in obj.collectRefs()) { + if (ref is DocumentObjectRef) { + parentsById.getOrPut(ref.id) { mutableListOf() }.add(obj.id) + } + } + } + + val baseTemplateCache = mutableMapOf() + val variableStructureCache = mutableMapOf() + + fun resolveBaseTemplate(id: String, visiting: MutableSet): BaseTemplateLocation? { + baseTemplateCache[id]?.let { return it } + if (!visiting.add(id)) return null + + val obj = documentObjectRepository.find(id) + val resolved = obj?.baseTemplate ?: parentsById[id]?.firstNotNullOfOrNull { parentId -> + resolveBaseTemplate(parentId, visiting) + } + + visiting.remove(id) + if (resolved != null) baseTemplateCache[id] = resolved + return resolved + } + + fun resolveVariableStructureRef(id: String, visiting: MutableSet): VariableStructureRef? { + variableStructureCache[id]?.let { return it } + if (!visiting.add(id)) return null + + val obj = documentObjectRepository.find(id) + val resolved = obj?.variableStructureRef ?: parentsById[id]?.firstNotNullOfOrNull { parentId -> + resolveVariableStructureRef(parentId, visiting) + } + + visiting.remove(id) + if (resolved != null) variableStructureCache[id] = resolved + return resolved + } + + return documentObjects.map { obj -> + val effectiveBaseTemplate = obj.baseTemplate ?: resolveBaseTemplate(obj.id, mutableSetOf()) + val effectiveVariableStructureRef = obj.variableStructureRef + ?: resolveVariableStructureRef(obj.id, mutableSetOf()) + + obj.copy( + baseTemplate = effectiveBaseTemplate, + variableStructureRef = effectiveVariableStructureRef, + ) + } + } +} + +class NoopRefInheritanceService : RefInheritanceService { + override fun apply(documentObjects: List): List = documentObjects +} diff --git a/migration-library/src/test/kotlin/com/quadient/migration/service/deploy/DesignerDeployClientTest.kt b/migration-library/src/test/kotlin/com/quadient/migration/service/deploy/DesignerDeployClientTest.kt index bde382fc..6aa6b90f 100644 --- a/migration-library/src/test/kotlin/com/quadient/migration/service/deploy/DesignerDeployClientTest.kt +++ b/migration-library/src/test/kotlin/com/quadient/migration/service/deploy/DesignerDeployClientTest.kt @@ -138,6 +138,7 @@ class DesignerDeployClientTest { } every { ipsService.writeMetadata(any>()) } just runs every { documentObjectRepository.find(any()) } returns null + every { documentObjectRepository.listAll() } returns emptyList() } @Test diff --git a/migration-library/src/test/kotlin/com/quadient/migration/service/deploy/EvolveDeployClientTest.kt b/migration-library/src/test/kotlin/com/quadient/migration/service/deploy/EvolveDeployClientTest.kt index edb06b66..819d9024 100644 --- a/migration-library/src/test/kotlin/com/quadient/migration/service/deploy/EvolveDeployClientTest.kt +++ b/migration-library/src/test/kotlin/com/quadient/migration/service/deploy/EvolveDeployClientTest.kt @@ -25,6 +25,7 @@ import com.quadient.migration.service.deploy.utility.ProgressReporterImpl import com.quadient.migration.service.inspirebuilder.InteractiveDocumentObjectBuilder import com.quadient.migration.service.InteractiveResourcePathProvider import com.quadient.migration.service.deploy.utility.DeployOrderImpl +import com.quadient.migration.service.deploy.utility.RefInheritanceServiceImpl import com.quadient.migration.service.inspirebuilder.InspireBaseTemplateBuilder import com.quadient.migration.service.ipsclient.IpsService import com.quadient.migration.service.ipsclient.OperationResult @@ -63,7 +64,6 @@ class EvolveDeployClientTest { val caClient = mockk() val resourcePathProvider = mockk() val postProcess = mockk(relaxed = true) - val deployOrder = DeployOrderImpl(documentObjectRepository) val evolveConfig = EvolveConfig( apiRetryDelayMs = 0L, @@ -84,6 +84,9 @@ class EvolveDeployClientTest { targetDefaultFolder = "defaultFolder" ) + val deployOrder = DeployOrderImpl(documentObjectRepository) + val refInheritanceService = RefInheritanceServiceImpl(documentObjectRepository) + val conflictDetector = ConflictDetectorImpl(documentObjectRepository, imageRepository, attachmentRepository, displayRuleRepository, statusTrackingRepository, resourcePathProvider, projectConfig.inspireOutput) val progressReporter = ProgressReporterImpl(documentObjectRepository, imageRepository, attachmentRepository, displayRuleRepository, documentObjectBuilder, statusTrackingRepository, resourcePathProvider, projectConfig.inspireOutput) @@ -97,6 +100,7 @@ class EvolveDeployClientTest { conflictDetector, progressReporter, deployOrder, + refInheritanceService, documentObjectRepository, imageRepository, attachmentRepository, diff --git a/migration-library/src/test/kotlin/com/quadient/migration/service/deploy/InteractiveDeployClientTest.kt b/migration-library/src/test/kotlin/com/quadient/migration/service/deploy/InteractiveDeployClientTest.kt index fc6af6d4..aa4f844c 100644 --- a/migration-library/src/test/kotlin/com/quadient/migration/service/deploy/InteractiveDeployClientTest.kt +++ b/migration-library/src/test/kotlin/com/quadient/migration/service/deploy/InteractiveDeployClientTest.kt @@ -5,6 +5,7 @@ import com.quadient.migration.api.dto.migrationmodel.Attachment import com.quadient.migration.api.dto.migrationmodel.AttachmentRef import com.quadient.migration.api.dto.migrationmodel.DisplayRule import com.quadient.migration.api.dto.migrationmodel.DisplayRuleRef +import com.quadient.migration.api.dto.migrationmodel.LiteralBaseTemplatePath import com.quadient.migration.api.dto.migrationmodel.DocumentObject import com.quadient.migration.api.dto.migrationmodel.Image import com.quadient.migration.api.dto.migrationmodel.ImageRef @@ -45,6 +46,7 @@ import com.quadient.migration.service.deploy.utility.ResultTrackerImpl import com.quadient.migration.service.inspirebuilder.InteractiveDocumentObjectBuilder import com.quadient.migration.service.InteractiveResourcePathProvider import com.quadient.migration.service.deploy.utility.DeployOrderImpl +import com.quadient.migration.service.deploy.utility.RefInheritanceServiceImpl import com.quadient.migration.service.inspirebuilder.InspireBaseTemplateBuilder import com.quadient.migration.service.ipsclient.IpsService import com.quadient.migration.service.ipsclient.OperationResult @@ -117,6 +119,7 @@ class InteractiveDeployClientTest { val conflictDetector = ConflictDetectorImpl(documentObjectRepository, imageRepository, attachmentRepository, displayRuleRepository, statusTrackingRepository, resourcePathProvider, config.inspireOutput) val progressReporter = ProgressReporterImpl(documentObjectRepository, imageRepository, attachmentRepository, displayRuleRepository, documentObjectBuilder, statusTrackingRepository, resourcePathProvider, config.inspireOutput) val deployOrder = DeployOrderImpl(documentObjectRepository) + val refInheritanceService = RefInheritanceServiceImpl(documentObjectRepository) private val subject = InteractiveDeployClient( config, @@ -126,6 +129,7 @@ class InteractiveDeployClientTest { conflictDetector, progressReporter, deployOrder, + refInheritanceService, documentObjectRepository, imageRepository, attachmentRepository, @@ -151,6 +155,7 @@ class InteractiveDeployClientTest { every { ipsService.writeMetadata(any()) } just runs every { ipsService.setProductionApprovalState(any>()) } returns OperationResult.Success every { documentObjectRepository.find(any()) } returns null + every { documentObjectRepository.listAll() } returns emptyList() } @Test @@ -975,6 +980,81 @@ class InteractiveDeployClientTest { )) } + @Test + fun `deployDocumentObjects deploys a standalone block inheriting baseTemplate from its ancestor template`() { + // given + val block = mockObj(aDocObj("B_1", DocumentObjectType.Block, internal = false)) + val template = mockObj( + aDocObj( + "T_1", DocumentObjectType.Template, + content = listOf(aDocumentObjectRef(block.id)), + internal = false, + baseTemplate = "icm://Interactive/tenant/BaseTemplates/inherited.wfd", + ) + ) + every { documentObjectRepository.list(any>()) } returns listOf(block) + every { documentObjectRepository.listAll() } returns listOf(block, template) + every { documentObjectBuilder.buildDocumentObject(any()) } answers { firstArg().id } + every { resourcePathProvider.getDocumentObjectPath(any()) } answers { "icm://${firstArg().id}".toIcmPath() } + mockBasicSuccessfulIpsOperations() + every { statusTrackingRepository.findLastEventRelevantToOutput(any(), any(), any()) } returns Active() + every { + statusTrackingRepository.deployed(any(), any(), any(), any(), any(), any(), any()) + } returns aDeployedStatus("id") + + // when + subject.deployDocumentObjects(listOf(block.id), true) + + // then + verify { + documentObjectBuilder.buildDocumentObject( + withArg { it.baseTemplate.shouldBeEqualTo(LiteralBaseTemplatePath("icm://Interactive/tenant/BaseTemplates/inherited.wfd")) } + ) + } + } + + @Test + fun `deployDocumentObjects deploys an external display rule inheriting baseTemplate from the document object that introduced it`() { + // given + val rule = DisplayRuleBuilder("R_1") + .comparison { value("a").equals().value("b") } + .internal(false) + .build() + .mock() + val block = mockObj( + aDocObj( + "B_1", DocumentObjectType.Block, + content = listOf(aParagraph(displayRuleRef = DisplayRuleRef(rule.id))), + internal = false, + baseTemplate = "icm://Interactive/tenant/BaseTemplates/inherited.wfd", + ) + ) + every { documentObjectRepository.list(any>()) } returns listOf(block) + every { documentObjectRepository.listAll() } returns listOf(block) + every { documentObjectBuilder.buildDocumentObject(any()) } returns "" + every { resourcePathProvider.getDocumentObjectPath(any()) } answers { "icm://${firstArg().id}".toIcmPath() } + every { resourcePathProvider.getDisplayRulePath(any()) } returns "icm://Interactive/$tenant/Rules/defaultFolder/${rule.id}.jrd".toIcmPath() + every { ipsService.tryUpload(any(), any()) } returns OperationResult.Success + mockBasicSuccessfulIpsOperations() + every { statusTrackingRepository.findLastEventRelevantToOutput(any(), any(), any()) } returns Active() + every { + statusTrackingRepository.deployed(any(), any(), any(), any(), any(), any(), any()) + } returns aDeployedStatus("id") + + // when + subject.deployDocumentObjects() + + // then + verify { + ipsService.tryUpload( + "icm://Interactive/$tenant/Rules/defaultFolder/${rule.id}.jrd".toIcmPath(), + withArg { + String(it).contains("map://interactive/BaseTemplates/inherited.wfd").shouldBeEqualTo(true) + } + ) + } + } + private fun mockBasicDocumentObjects() { val blocks = List(2) { mockDocumentObject( diff --git a/migration-library/src/test/kotlin/com/quadient/migration/service/deploy/utility/RefInheritanceServiceTest.kt b/migration-library/src/test/kotlin/com/quadient/migration/service/deploy/utility/RefInheritanceServiceTest.kt new file mode 100644 index 00000000..ce135760 --- /dev/null +++ b/migration-library/src/test/kotlin/com/quadient/migration/service/deploy/utility/RefInheritanceServiceTest.kt @@ -0,0 +1,71 @@ +package com.quadient.migration.service.deploy.utility + +import com.quadient.migration.api.dto.migrationmodel.BaseTemplateRef +import com.quadient.migration.api.dto.migrationmodel.DocumentObject +import com.quadient.migration.api.dto.migrationmodel.VariableStructureRef +import com.quadient.migration.api.dto.migrationmodel.builder.DocumentObjectBuilder +import com.quadient.migration.api.repository.DocumentObjectRepository +import com.quadient.migration.shared.DocumentObjectType +import com.quadient.migration.tools.shouldBeEqualTo +import com.quadient.migration.tools.shouldBeNull +import io.mockk.every +import io.mockk.mockk +import org.junit.jupiter.api.Test + +class RefInheritanceServiceTest { + val documentObjectRepository = mockk() + val subject = RefInheritanceServiceImpl(documentObjectRepository) + val allObjects = mutableListOf() + + init { + every { documentObjectRepository.listAll() } answers { allObjects.toList() } + } + + @Test + fun `block inherits baseTemplate and variableStructureRef from its page and template ancestors`() { + val block = DocumentObjectBuilder("block", DocumentObjectType.Block).mock() + val page = DocumentObjectBuilder("page", DocumentObjectType.Page) + .documentObjectRef(block) + .variableStructureRef("vs1") + .mock() + DocumentObjectBuilder("template", DocumentObjectType.Template) + .documentObjectRef(page) + .baseTemplateRef("bt1") + .mock() + + val result = subject.apply(listOf(block)) + + result.single().baseTemplate.shouldBeEqualTo(BaseTemplateRef("bt1")) + result.single().variableStructureRef.shouldBeEqualTo(VariableStructureRef("vs1")) + } + + @Test + fun `explicit baseTemplate on the object itself takes priority over inherited value`() { + val block = DocumentObjectBuilder("block", DocumentObjectType.Block).baseTemplateRef("explicit").mock() + DocumentObjectBuilder("template", DocumentObjectType.Template) + .documentObjectRef(block) + .baseTemplateRef("inherited") + .mock() + + val result = subject.apply(listOf(block)) + + result.single().baseTemplate.shouldBeEqualTo(BaseTemplateRef("explicit")) + } + + @Test + fun `standalone block with no ancestors has no resolved baseTemplate`() { + val block = DocumentObjectBuilder("block", DocumentObjectType.Block).mock() + + val result = subject.apply(listOf(block)) + + result.single().baseTemplate.shouldBeNull() + result.single().variableStructureRef.shouldBeNull() + } + + private fun DocumentObjectBuilder.mock(): DocumentObject { + val obj = this.build() + every { documentObjectRepository.find(obj.id) } returns obj + allObjects.add(obj) + return obj + } +} From df7e406b6633c049c99a4edcdb104e08a9f9670e Mon Sep 17 00:00:00 2001 From: "d.svitak" Date: Mon, 17 Aug 2026 12:28:07 +0200 Subject: [PATCH 08/14] MIG-584 Base template deployment to Flex - Basic registration of Email and SMS flows based on the document object usage lookup --- .../migration/example/example/Import.groovy | 11 ++- .../InspireBaseTemplateBuilder.kt | 49 +++++++++++++- .../InspireBaseTemplateBuilderTest.kt | 67 ++++++++++++++++++- .../wfdxml/api/layoutnodes/Pages.java | 7 ++ .../internal/layoutnodes/PagesImpl.java | 35 +++++++--- .../internal/layoutnodes/PagesImplTest.groovy | 9 ++- 6 files changed, 157 insertions(+), 21 deletions(-) diff --git a/migration-examples/src/main/groovy/com/quadient/migration/example/example/Import.groovy b/migration-examples/src/main/groovy/com/quadient/migration/example/example/Import.groovy index 0664d5d3..d12d764a 100644 --- a/migration-examples/src/main/groovy/com/quadient/migration/example/example/Import.groovy +++ b/migration-examples/src/main/groovy/com/quadient/migration/example/example/Import.groovy @@ -568,6 +568,7 @@ def separator = new ShapeBuilder() def paragraph1TopMargin = topMargin + Size.ofMillimeters(25) def signatureTopMargin = pageHeight - Size.ofCentimeters(3) def page = new DocumentObjectBuilder("page1", DocumentObjectType.Page) + .internal(true) .options(new PageOptions(pageWidth, pageHeight)) .shape(separator) .area { @@ -713,14 +714,10 @@ def email = new EmailObjectBuilder("email") } .build() -def templateEmailSms = new DocumentObjectBuilder("templateEmailSms", DocumentObjectType.Template) - .documentObjectRef(sms) - .documentObjectRef(email) - .baseTemplate("vcs://Interactive/StandardPackage/BaseTemplates/ResponsiveEmailBaseTemplate.wfd") - .build() - def template = new DocumentObjectBuilder("template", DocumentObjectType.Template) .documentObjectRef(page) + .documentObjectRef(sms) + .documentObjectRef(email) .subject("Document example template") .pdfMetadata { it.author(new VariableRef(nameVariable.id)) @@ -733,7 +730,7 @@ def template = new DocumentObjectBuilder("template", DocumentObjectType.Template .build() // Insert all content into the database to be used in the deploy task -for (item in [address, signature, paragraph1, paragraph2, conditionalParagraph, page, template, firstMatchBlock, selectByLanguageBlock, jobListBlock, snippet, fmSnippet, sms, email, templateEmailSms]) { +for (item in [address, signature, paragraph1, paragraph2, conditionalParagraph, page, template, firstMatchBlock, selectByLanguageBlock, jobListBlock, snippet, fmSnippet, sms, email]) { migration.documentObjectRepository.upsert(item) } for (item in [headingStyle, normalStyle]) { diff --git a/migration-library/src/main/kotlin/com/quadient/migration/service/inspirebuilder/InspireBaseTemplateBuilder.kt b/migration-library/src/main/kotlin/com/quadient/migration/service/inspirebuilder/InspireBaseTemplateBuilder.kt index 86cf4d78..7e715622 100644 --- a/migration-library/src/main/kotlin/com/quadient/migration/service/inspirebuilder/InspireBaseTemplateBuilder.kt +++ b/migration-library/src/main/kotlin/com/quadient/migration/service/inspirebuilder/InspireBaseTemplateBuilder.kt @@ -2,8 +2,13 @@ package com.quadient.migration.service.inspirebuilder import com.quadient.migration.api.ProjectConfig import com.quadient.migration.api.dto.migrationmodel.BaseTemplate +import com.quadient.migration.api.dto.migrationmodel.DocumentObject +import com.quadient.migration.api.dto.migrationmodel.DocumentObjectRef +import com.quadient.migration.api.repository.BaseTemplateRepository +import com.quadient.migration.api.repository.DocumentObjectRepository import com.quadient.migration.service.IcmDataCache import com.quadient.migration.service.ResourcePathProvider +import com.quadient.migration.shared.DocumentObjectType import com.quadient.migration.shared.IcmPath import com.quadient.migration.shared.toIcmPath import com.quadient.migration.tools.logger @@ -11,11 +16,15 @@ import com.quadient.wfdxml.WfdXmlBuilder import com.quadient.wfdxml.api.layoutnodes.Flow import com.quadient.wfdxml.api.layoutnodes.Flow.WebEditingType.SECTION import com.quadient.wfdxml.api.layoutnodes.Pages +import com.quadient.wfdxml.api.layoutnodes.email.EmailComponentPlaceHolder +import com.quadient.wfdxml.api.module.Layout class InspireBaseTemplateBuilder( private val projectConfig: ProjectConfig, private val icmDataCache: IcmDataCache, private val resourcePathProvider: ResourcePathProvider, + private val baseTemplateRepository: BaseTemplateRepository, + private val documentObjectRepository: DocumentObjectRepository, ) { private val logger by logger() @@ -39,7 +48,6 @@ class InspireBaseTemplateBuilder( } resolveArialFont(layout, icmDataCache) - val interactiveFlows = mutableListOf() var mainFlow: Flow? = null var mainFlowSize = -1.0 @@ -55,7 +63,7 @@ class InspireBaseTemplateBuilder( .setType(Flow.Type.SIMPLE) .setSectionFlow(true) .setWebEditingType(SECTION) - interactiveFlows.add(flow) + layout.pages.addInteractiveFlow(flow, Pages.InteractiveFlowType.NORMAL) val flowArea = wfdPage.addFlowArea().setName("${area.interactiveFlowName}Area").setFlow(flow) .setFlowToNextPage(area.flowToNextPage) @@ -74,9 +82,10 @@ class InspireBaseTemplateBuilder( } } - layout.pages.setInteractiveFlows(interactiveFlows) mainFlow?.let { layout.pages.setMainFlow(it) } + enrichFromDocumentObjects(baseTemplate, layout) + val baseTemplateXml = builder.build() val sourceBaseTemplatePath = if (projectConfig.sourceBaseTemplatePath.isNullOrBlank()) { IcmPath.root().join("Interactive").join("StandardPackage").join("Sources").join("SourceTemplate.wfd") @@ -86,4 +95,38 @@ class InspireBaseTemplateBuilder( return enrichLayoutWithSourceBaseTemplate(icmDataCache, baseTemplateXml, sourceBaseTemplatePath) } + + private fun enrichFromDocumentObjects(baseTemplate: BaseTemplate, layout: Layout) { + val usages = baseTemplateRepository.findUsages(baseTemplate.id).filterIsInstance() + + var needsEmail = false + var needsSms = false + + for (usage in usages) { + for (content in usage.content) { + val referencedType = (content as? DocumentObjectRef)?.id?.let(documentObjectRepository::find)?.type + when (referencedType) { + DocumentObjectType.Email -> needsEmail = true + DocumentObjectType.Sms -> needsSms = true + else -> Unit + } + } + } + + if (needsSms) { + val smsFlow = layout.addFlow().setSectionFlow(true).setWebEditingType(SECTION) + .addCustomProperty("customName", "SMS Content") + layout.pages.addInteractiveFlow(smsFlow, Pages.InteractiveFlowType.NORMAL) + layout.addSmsRoot().setContent(smsFlow) + } + + if (needsEmail) { + val emailBodyRootFlow = layout.addFlow().setSectionFlow(true).setWebEditingType(SECTION) + .addCustomProperty("customName", "Body Content") + layout.pages.addInteractiveFlow(emailBodyRootFlow, Pages.InteractiveFlowType.HTML) + layout.addEmailComponentRoot().setEmailComponentsText(layout.addEmailTMText()) + layout.addEmailComponentPlaceHolder().setId("Def.EmailsBody").setType(EmailComponentPlaceHolder.Type.BODY) + .setContent(emailBodyRootFlow) + } + } } diff --git a/migration-library/src/test/kotlin/com/quadient/migration/service/inspirebuilder/InspireBaseTemplateBuilderTest.kt b/migration-library/src/test/kotlin/com/quadient/migration/service/inspirebuilder/InspireBaseTemplateBuilderTest.kt index 215411a0..9ded4464 100644 --- a/migration-library/src/test/kotlin/com/quadient/migration/service/inspirebuilder/InspireBaseTemplateBuilderTest.kt +++ b/migration-library/src/test/kotlin/com/quadient/migration/service/inspirebuilder/InspireBaseTemplateBuilderTest.kt @@ -1,14 +1,20 @@ package com.quadient.migration.service.inspirebuilder import com.quadient.migration.api.dto.migrationmodel.builder.BaseTemplateBuilder +import com.quadient.migration.api.repository.BaseTemplateRepository +import com.quadient.migration.api.repository.DocumentObjectRepository import com.quadient.migration.service.InteractiveIcmDataCache import com.quadient.migration.service.InteractiveResourcePathProvider import com.quadient.migration.service.ipsclient.IpsService +import com.quadient.migration.shared.DocumentObjectType import com.quadient.migration.shared.IcmPath import com.quadient.migration.shared.millimeters import com.quadient.migration.tools.aProjectConfig +import com.quadient.migration.tools.model.aDocObj +import com.quadient.migration.tools.model.aDocumentObjectRef import com.quadient.migration.tools.shouldBeEqualTo import com.quadient.migration.tools.shouldBeNull +import com.quadient.migration.tools.shouldNotBeNull import io.mockk.every import io.mockk.mockk import org.junit.jupiter.api.BeforeEach @@ -21,7 +27,11 @@ class InspireBaseTemplateBuilderTest { private val config = aProjectConfig() private val resourcePathProvider = InteractiveResourcePathProvider(config) private val icmDataCache = InteractiveIcmDataCache(ipsService, resourcePathProvider) - private val subject = InspireBaseTemplateBuilder(config, icmDataCache, resourcePathProvider) + private val baseTemplateRepository = mockk() + private val documentObjectRepository = mockk() + private val subject = InspireBaseTemplateBuilder( + config, icmDataCache, resourcePathProvider, baseTemplateRepository, documentObjectRepository + ) private val xmlMapper = XmlMapper.builder().addModule(KotlinModule.Builder().build()).build() @BeforeEach @@ -38,6 +48,7 @@ class InspireBaseTemplateBuilderTest { """.trimIndent() every { ipsService.fileExists(any()) } returns false every { ipsService.gatherFontData(any()) } returns "Arial,Regular,icm://Fonts/arial.ttf;" + every { baseTemplateRepository.findUsages(any()) } returns emptyList() } @Test @@ -217,6 +228,60 @@ class InspireBaseTemplateBuilderTest { result["Page"].first()["Name"].stringValue().shouldBeEqualTo("Page 1") } + @Test + fun `buildBaseTemplate registers SMS root when a usage references an Sms document object`() { + // given + val baseTemplate = BaseTemplateBuilder("BT_1").build() + val smsModel = aDocObj("Sms_1", DocumentObjectType.Sms) + val template = aDocObj("T_1", DocumentObjectType.Template, content = listOf(aDocumentObjectRef(smsModel.id))) + every { baseTemplateRepository.findUsages(baseTemplate.id) } returns listOf(template) + every { documentObjectRepository.find(smsModel.id) } returns smsModel + + // when + val result = subject.buildBaseTemplate(baseTemplate).let { xmlMapper.readTree(it.trimIndent()) }["Layout"]["Layout"] + + // then + result["SMSRoot"]["FlowId"].shouldNotBeNull() + result["Pages"]["InteractiveFlow"]["FlowType"].stringValue().shouldBeEqualTo("Normal") + result["ECPlaceHolder"].shouldBeNull() + } + + @Test + fun `buildBaseTemplate registers email component root and HTML interactive flow when a usage references an Email document object`() { + // given + val baseTemplate = BaseTemplateBuilder("BT_1").build() + val emailModel = aDocObj("Email_1", DocumentObjectType.Email) + val template = aDocObj("T_1", DocumentObjectType.Template, content = listOf(aDocumentObjectRef(emailModel.id))) + every { baseTemplateRepository.findUsages(baseTemplate.id) } returns listOf(template) + every { documentObjectRepository.find(emailModel.id) } returns emailModel + + // when + val result = subject.buildBaseTemplate(baseTemplate).let { xmlMapper.readTree(it.trimIndent()) }["Layout"]["Layout"] + + // then + result["ECRoot"].shouldNotBeNull() + result["ECPlaceHolder"]["Id"].stringValue().shouldBeEqualTo("Def.EmailsBody") + result["Pages"]["InteractiveFlow"]["FlowType"].stringValue().shouldBeEqualTo("HTML") + result["SMSRoot"].shouldBeNull() + } + + @Test + fun `buildBaseTemplate registers neither email nor sms modules when no usage references them`() { + // given + val baseTemplate = BaseTemplateBuilder("BT_1").build() + val block = aDocObj("B_1", DocumentObjectType.Block) + val template = aDocObj("T_1", DocumentObjectType.Template, content = listOf(aDocumentObjectRef(block.id))) + every { baseTemplateRepository.findUsages(baseTemplate.id) } returns listOf(template) + every { documentObjectRepository.find(block.id) } returns block + + // when + val result = subject.buildBaseTemplate(baseTemplate).let { xmlMapper.readTree(it.trimIndent()) }["Layout"]["Layout"] + + // then + result["SMSRoot"].shouldBeNull() + result["ECRoot"].shouldBeNull() + } + @Test fun `buildBaseTemplate without pages creates empty layout`() { // given diff --git a/wfd-xml/api/src/main/java/com/quadient/wfdxml/api/layoutnodes/Pages.java b/wfd-xml/api/src/main/java/com/quadient/wfdxml/api/layoutnodes/Pages.java index bf6679ca..41c65064 100644 --- a/wfd-xml/api/src/main/java/com/quadient/wfdxml/api/layoutnodes/Pages.java +++ b/wfd-xml/api/src/main/java/com/quadient/wfdxml/api/layoutnodes/Pages.java @@ -17,6 +17,8 @@ public interface Pages extends Node { Pages setInteractiveFlows(List interactiveFlows); + Pages addInteractiveFlow(Flow flow, InteractiveFlowType type); + Pages addSheetName(SheetNameType type, Variable variable); enum PageOrder { @@ -25,6 +27,11 @@ enum PageOrder { DATA_VARIABLE_SELECTION, } + enum InteractiveFlowType { + NORMAL, + HTML, + } + enum PageConditionType { SIMPLE, SELECT_BY_INTEGER, diff --git a/wfd-xml/impl/src/main/java/com/quadient/wfdxml/internal/layoutnodes/PagesImpl.java b/wfd-xml/impl/src/main/java/com/quadient/wfdxml/internal/layoutnodes/PagesImpl.java index 84ff4f3f..2f1feaae 100644 --- a/wfd-xml/impl/src/main/java/com/quadient/wfdxml/internal/layoutnodes/PagesImpl.java +++ b/wfd-xml/impl/src/main/java/com/quadient/wfdxml/internal/layoutnodes/PagesImpl.java @@ -8,6 +8,7 @@ import com.quadient.wfdxml.internal.Tree; import com.quadient.wfdxml.internal.xml.export.XmlExporter; +import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -23,9 +24,11 @@ public class PagesImpl extends Tree implements Pages { private PageConditionType type = SIMPLE; private PageImpl page = null; private Flow mainFlow = null; - private List interactiveFlows = null; + private final List interactiveFlows = new ArrayList<>(); private final Map sheetNames = new HashMap<>(); + private record InteractiveFlowEntry(Flow flow, InteractiveFlowType type) {} + public static String pageConditionTypeToXml(PageConditionType type) { switch (type) { case SIMPLE: @@ -110,7 +113,16 @@ public Pages setMainFlow(Flow mainFlow) { @Override public Pages setInteractiveFlows(List interactiveFlows) { - this.interactiveFlows = interactiveFlows; + this.interactiveFlows.clear(); + for (Flow flow : interactiveFlows) { + this.interactiveFlows.add(new InteractiveFlowEntry(flow, Pages.InteractiveFlowType.NORMAL)); + } + return this; + } + + @Override + public Pages addInteractiveFlow(Flow flow, Pages.InteractiveFlowType type) { + this.interactiveFlows.add(new InteractiveFlowEntry(flow, type)); return this; } @@ -136,13 +148,11 @@ public void export(XmlExporter exporter) { exporter.addElementWithIface("MainFlow", mainFlow); exporter.addElementWithBoolData("UseAnotherFlowAsInteractiveMainFlow", false); } - if (interactiveFlows != null){ - for (Flow flow:interactiveFlows) { - exporter.beginElement("InteractiveFlow"); - exporter.addElementWithIface("FlowId", flow); - exporter.addElementWithStringData("FlowType", "Normal"); - exporter.endElement(); - } + for (InteractiveFlowEntry entry : interactiveFlows) { + exporter.beginElement("InteractiveFlow"); + exporter.addElementWithIface("FlowId", entry.flow()); + exporter.addElementWithStringData("FlowType", interactiveFlowTypeToXml(entry.type())); + exporter.endElement(); } if (pageSelectionType == VARIABLE) { exporter.addElementWithStringData("ConditionType", pageConditionTypeToXml(type)); @@ -187,6 +197,13 @@ private void exportSheetNames(XmlExporter exporter) { } } + public static String interactiveFlowTypeToXml(Pages.InteractiveFlowType type) { + return switch (type) { + case NORMAL -> "Normal"; + case HTML -> "HTML"; + }; + } + public String pageSelectionTypeToXml() { switch (pageSelectionType) { case SIMPLE: diff --git a/wfd-xml/impl/src/test/groovy/com/quadient/wfdxml/internal/layoutnodes/PagesImplTest.groovy b/wfd-xml/impl/src/test/groovy/com/quadient/wfdxml/internal/layoutnodes/PagesImplTest.groovy index a6d458d3..0677b7f7 100644 --- a/wfd-xml/impl/src/test/groovy/com/quadient/wfdxml/internal/layoutnodes/PagesImplTest.groovy +++ b/wfd-xml/impl/src/test/groovy/com/quadient/wfdxml/internal/layoutnodes/PagesImplTest.groovy @@ -88,9 +88,12 @@ class PagesImplTest extends Specification { Flow mainFlow = new FlowImpl() Flow interactiveFlow1 = new FlowImpl() Flow interactiveFlow2 = new FlowImpl() + Flow htmlFlow = new FlowImpl() PagesImpl pages = new PagesImpl() .setMainFlow(mainFlow) - .setInteractiveFlows([interactiveFlow1, interactiveFlow2]) + .addInteractiveFlow(interactiveFlow1, Pages.InteractiveFlowType.NORMAL) + .addInteractiveFlow(interactiveFlow2, Pages.InteractiveFlowType.NORMAL) + .addInteractiveFlow(htmlFlow, Pages.InteractiveFlowType.HTML) when: pages.export(exporter) @@ -108,6 +111,10 @@ class PagesImplTest extends Specification { SR_3 Normal + + SR_4 + HTML + """) } From 85fda1b5be9f72f743329c78adba8132f39ee4cb Mon Sep 17 00:00:00 2001 From: "d.svitak" Date: Mon, 17 Aug 2026 16:24:26 +0200 Subject: [PATCH 09/14] MIG-584 Base template deployment to Flex - Add variable structure to base template and fill email/sms sheetNames --- .../src/test/groovy/LayoutExportTest.groovy | 2 +- .../src/test/groovy/LayoutImportTest.groovy | 2 +- .../com/quadient/migration/api/Migration.kt | 2 + .../api/dto/migrationmodel/BaseTemplate.kt | 3 +- .../builder/BaseTemplateBuilder.kt | 7 +- .../api/repository/BaseTemplateRepository.kt | 6 +- .../persistence/table/BaseTemplateTable.kt | 1 + .../V17__base_template_variable_structure.kt | 18 ++ .../DesignerDocumentObjectBuilder.kt | 13 +- .../InspireBaseTemplateBuilder.kt | 36 +++- .../InspireDocumentObjectBuilder.kt | 102 +----------- .../InspireVariableStructureBuilder.kt | 156 ++++++++++++++++++ .../InteractiveDocumentObjectBuilder.kt | 13 +- .../DesignerDocumentObjectBuilderTest.kt | 2 +- .../InspireBaseTemplateBuilderTest.kt | 75 ++++++++- .../InspireDocumentObjectBuilderTest.kt | 4 +- .../InteractiveDocumentObjectBuilderTest.kt | 2 +- 17 files changed, 312 insertions(+), 132 deletions(-) create mode 100644 migration-library/src/main/kotlin/com/quadient/migration/persistence/upgrade/V17__base_template_variable_structure.kt create mode 100644 migration-library/src/main/kotlin/com/quadient/migration/service/inspirebuilder/InspireVariableStructureBuilder.kt diff --git a/migration-examples/src/test/groovy/LayoutExportTest.groovy b/migration-examples/src/test/groovy/LayoutExportTest.groovy index 04a86599..1cdda21f 100644 --- a/migration-examples/src/test/groovy/LayoutExportTest.groovy +++ b/migration-examples/src/test/groovy/LayoutExportTest.groovy @@ -117,7 +117,7 @@ class LayoutExportTest { new BaseTemplatePage("Page 2", Size.ofMillimeters(210), Size.ofMillimeters(99), [ new BaseTemplateArea("Area 1", new Position(Size.ofMillimeters(0), Size.ofMillimeters(0), Size.ofMillimeters(210), Size.ofMillimeters(99)), false), ]), - ], null, null) + ], null, null, null) when(migration.baseTemplateRepository.listAll()).thenReturn([baseTemplate]) LayoutExport.run(migration, mappingFile) diff --git a/migration-examples/src/test/groovy/LayoutImportTest.groovy b/migration-examples/src/test/groovy/LayoutImportTest.groovy index bf8f38b7..51b326d2 100644 --- a/migration-examples/src/test/groovy/LayoutImportTest.groovy +++ b/migration-examples/src/test/groovy/LayoutImportTest.groovy @@ -229,7 +229,7 @@ class LayoutImportTest { void importUpdatesExistingBaseTemplateMappingButKeepsItsOtherFields() { Path mappingFile = Paths.get(dir.path, "testProject.csv") - def existing = new BaseTemplate("G1", "Old name", ["origin.wfd"], new CustomFieldMap(new HashMap()), "target/folder", [], null, null) + def existing = new BaseTemplate("G1", "Old name", ["origin.wfd"], new CustomFieldMap(new HashMap()), "target/folder", [], null, null, null) when(migration.baseTemplateRepository.find("G1")).thenReturn(existing) when(migration.mappingRepository.getBaseTemplateMapping("G1")).thenReturn(new MappingItem.BaseTemplate(null, null, [])) diff --git a/migration-library/src/main/kotlin/com/quadient/migration/api/Migration.kt b/migration-library/src/main/kotlin/com/quadient/migration/api/Migration.kt index f9b787eb..bb1f760f 100644 --- a/migration-library/src/main/kotlin/com/quadient/migration/api/Migration.kt +++ b/migration-library/src/main/kotlin/com/quadient/migration/api/Migration.kt @@ -30,6 +30,7 @@ import com.quadient.migration.service.deploy.utility.RefInheritanceServiceImpl import com.quadient.migration.service.deploy.utility.ProgressReporterImpl import com.quadient.migration.service.inspirebuilder.InspireDocumentObjectBuilder import com.quadient.migration.service.inspirebuilder.InspireBaseTemplateBuilder +import com.quadient.migration.service.inspirebuilder.InspireVariableStructureBuilder import com.quadient.migration.service.ipsclient.IpsService import com.quadient.migration.service.ipsclient.Version import com.quadient.migration.service.ipsclient.display @@ -118,6 +119,7 @@ class Migration(val config: MigConfig, val projectConfig: ProjectConfig) { single() single() single() + single() single() } diff --git a/migration-library/src/main/kotlin/com/quadient/migration/api/dto/migrationmodel/BaseTemplate.kt b/migration-library/src/main/kotlin/com/quadient/migration/api/dto/migrationmodel/BaseTemplate.kt index fe36a345..432e11f3 100644 --- a/migration-library/src/main/kotlin/com/quadient/migration/api/dto/migrationmodel/BaseTemplate.kt +++ b/migration-library/src/main/kotlin/com/quadient/migration/api/dto/migrationmodel/BaseTemplate.kt @@ -10,8 +10,9 @@ data class BaseTemplate( override var customFields: CustomFieldMap, var targetFolder: String? = null, var pages: List = emptyList(), + var variableStructureRef: VariableStructureRef? = null, override var created: Instant? = null, override var lastUpdated: Instant? = null, ) : MigrationObject, RefValidatable { - override fun collectRefs(): Set = emptySet() + override fun collectRefs(): Set = setOfNotNull(variableStructureRef) } diff --git a/migration-library/src/main/kotlin/com/quadient/migration/api/dto/migrationmodel/builder/BaseTemplateBuilder.kt b/migration-library/src/main/kotlin/com/quadient/migration/api/dto/migrationmodel/builder/BaseTemplateBuilder.kt index 42501644..e6659971 100644 --- a/migration-library/src/main/kotlin/com/quadient/migration/api/dto/migrationmodel/builder/BaseTemplateBuilder.kt +++ b/migration-library/src/main/kotlin/com/quadient/migration/api/dto/migrationmodel/builder/BaseTemplateBuilder.kt @@ -1,8 +1,10 @@ package com.quadient.migration.api.dto.migrationmodel.builder import com.quadient.migration.api.dto.migrationmodel.BaseTemplate +import com.quadient.migration.api.dto.migrationmodel.VariableStructureRef import com.quadient.migration.api.dto.migrationmodel.builder.components.HasPosition import com.quadient.migration.api.dto.migrationmodel.builder.components.HasTargetFolder +import com.quadient.migration.api.dto.migrationmodel.builder.components.HasVariableStructureRef import com.quadient.migration.shared.BaseTemplateArea import com.quadient.migration.shared.BaseTemplatePage import com.quadient.migration.shared.Position @@ -13,8 +15,10 @@ annotation class BaseTemplateBuilderDsl @BaseTemplateBuilderDsl class BaseTemplateBuilder(id: String) : DtoBuilderBase(id), - HasTargetFolder { + HasTargetFolder, + HasVariableStructureRef { override var targetFolder: String? = null + override var variableStructureRef: VariableStructureRef? = null val pages = mutableListOf() /** Creates a new [Page], appends it, and returns it for further configuration. */ @@ -46,6 +50,7 @@ class BaseTemplateBuilder(id: String) : DtoBuilderBase>("pages", Json) + val variableStructureRef = varchar("variable_structure_ref", 255).nullable() } diff --git a/migration-library/src/main/kotlin/com/quadient/migration/persistence/upgrade/V17__base_template_variable_structure.kt b/migration-library/src/main/kotlin/com/quadient/migration/persistence/upgrade/V17__base_template_variable_structure.kt new file mode 100644 index 00000000..2d92f60c --- /dev/null +++ b/migration-library/src/main/kotlin/com/quadient/migration/persistence/upgrade/V17__base_template_variable_structure.kt @@ -0,0 +1,18 @@ +package com.quadient.migration.persistence.upgrade + +import org.flywaydb.core.api.migration.BaseJavaMigration +import org.flywaydb.core.api.migration.Context + +class V17__base_template_variable_structure : BaseJavaMigration() { + override fun migrate(context: Context) { + val connection = context.connection + connection.createStatement().use { stmt -> + stmt.execute( + """ + ALTER TABLE base_template + ADD COLUMN IF NOT EXISTS variable_structure_ref VARCHAR(255) + """.trimIndent() + ) + } + } +} diff --git a/migration-library/src/main/kotlin/com/quadient/migration/service/inspirebuilder/DesignerDocumentObjectBuilder.kt b/migration-library/src/main/kotlin/com/quadient/migration/service/inspirebuilder/DesignerDocumentObjectBuilder.kt index 620ce988..78b0d952 100644 --- a/migration-library/src/main/kotlin/com/quadient/migration/service/inspirebuilder/DesignerDocumentObjectBuilder.kt +++ b/migration-library/src/main/kotlin/com/quadient/migration/service/inspirebuilder/DesignerDocumentObjectBuilder.kt @@ -11,7 +11,6 @@ import com.quadient.migration.api.repository.ImageRepository import com.quadient.migration.api.repository.ParagraphStyleRepository import com.quadient.migration.api.repository.TextStyleRepository import com.quadient.migration.api.repository.VariableRepository -import com.quadient.migration.api.repository.VariableStructureRepository import com.quadient.migration.service.IcmDataCache import com.quadient.migration.service.ResourcePathProvider import com.quadient.migration.service.resolveAliases @@ -42,7 +41,7 @@ class DesignerDocumentObjectBuilder( textStyleRepository: TextStyleRepository, paragraphStyleRepository: ParagraphStyleRepository, variableRepository: VariableRepository, - variableStructureRepository: VariableStructureRepository, + variableStructureBuilder: InspireVariableStructureBuilder, displayRuleRepository: DisplayRuleRepository, imageRepository: ImageRepository, attachmentRepository: AttachmentRepository, @@ -55,7 +54,7 @@ class DesignerDocumentObjectBuilder( textStyleRepository, paragraphStyleRepository, variableRepository, - variableStructureRepository, + variableStructureBuilder, displayRuleRepository, imageRepository, attachmentRepository, @@ -90,7 +89,7 @@ class DesignerDocumentObjectBuilder( var smsModel: DocumentObject? = null val virtualPageContent = mutableListOf() - val variableStructure = initVariableStructure(layout, documentObject.variableStructureRef?.id) + val variableStructure = variableStructureBuilder.initVariableStructure(layout, documentObject.variableStructureRef?.id) val languages = collectLanguages(documentObject) val languageVariable = variableStructure.languageVariable @@ -105,7 +104,7 @@ class DesignerDocumentObjectBuilder( layout.data.setLanguageVariable(variable) } - layout.addPdfMetadataToPages(documentObject, variableStructure) + variableStructureBuilder.addPdfMetadataToPages(layout, documentObject, variableStructure) documentObject.content.paragraphIfEmpty().forEach { val model = (it as? DocumentObjectRef)?.id?.let(documentObjectRepository::findOrFail) @@ -309,7 +308,7 @@ class DesignerDocumentObjectBuilder( } private fun DocumentObject.buildSmsRoot(layout: Layout, varStructure: VariableStructure, languages: List) { - layout.addSmsNumberToPages(this, varStructure) + variableStructureBuilder.addSmsNumberToPages(layout, this, varStructure) val smsRoot = layout.addSmsRoot() val flow = buildDocumentContentAsSingleFlow( @@ -325,7 +324,7 @@ class DesignerDocumentObjectBuilder( smsRoot.setContent(flow) } private fun DocumentObject.buildEmailRoot(layout: Layout, varStructure: VariableStructure, languages: List) { - layout.addEmailMetadataToPages(this, varStructure) + variableStructureBuilder.addEmailMetadataToPages(layout, this, varStructure) val emailRoot = layout.addEmailComponentRoot() val emailBodyRootFlow = layout.addFlow().setSectionFlow(true) val emailTmText = layout.addEmailTMText() diff --git a/migration-library/src/main/kotlin/com/quadient/migration/service/inspirebuilder/InspireBaseTemplateBuilder.kt b/migration-library/src/main/kotlin/com/quadient/migration/service/inspirebuilder/InspireBaseTemplateBuilder.kt index 7e715622..b121299b 100644 --- a/migration-library/src/main/kotlin/com/quadient/migration/service/inspirebuilder/InspireBaseTemplateBuilder.kt +++ b/migration-library/src/main/kotlin/com/quadient/migration/service/inspirebuilder/InspireBaseTemplateBuilder.kt @@ -4,10 +4,13 @@ import com.quadient.migration.api.ProjectConfig import com.quadient.migration.api.dto.migrationmodel.BaseTemplate import com.quadient.migration.api.dto.migrationmodel.DocumentObject import com.quadient.migration.api.dto.migrationmodel.DocumentObjectRef +import com.quadient.migration.api.dto.migrationmodel.EmailOptions +import com.quadient.migration.api.dto.migrationmodel.SmsOptions import com.quadient.migration.api.repository.BaseTemplateRepository import com.quadient.migration.api.repository.DocumentObjectRepository import com.quadient.migration.service.IcmDataCache import com.quadient.migration.service.ResourcePathProvider +import com.quadient.migration.service.deploy.utility.RefInheritanceServiceImpl import com.quadient.migration.shared.DocumentObjectType import com.quadient.migration.shared.IcmPath import com.quadient.migration.shared.toIcmPath @@ -25,6 +28,8 @@ class InspireBaseTemplateBuilder( private val resourcePathProvider: ResourcePathProvider, private val baseTemplateRepository: BaseTemplateRepository, private val documentObjectRepository: DocumentObjectRepository, + private val variableStructureBuilder: InspireVariableStructureBuilder, + private val refInheritanceService: RefInheritanceServiceImpl, ) { private val logger by logger() @@ -99,34 +104,49 @@ class InspireBaseTemplateBuilder( private fun enrichFromDocumentObjects(baseTemplate: BaseTemplate, layout: Layout) { val usages = baseTemplateRepository.findUsages(baseTemplate.id).filterIsInstance() - var needsEmail = false - var needsSms = false + var emailModel: DocumentObject? = null + var smsModel: DocumentObject? = null for (usage in usages) { for (content in usage.content) { - val referencedType = (content as? DocumentObjectRef)?.id?.let(documentObjectRepository::find)?.type - when (referencedType) { - DocumentObjectType.Email -> needsEmail = true - DocumentObjectType.Sms -> needsSms = true + val referenced = (content as? DocumentObjectRef)?.id?.let(documentObjectRepository::find) ?: continue + when (referenced.type) { + DocumentObjectType.Email -> if (emailModel == null) emailModel = referenced + DocumentObjectType.Sms -> if (smsModel == null) smsModel = referenced else -> Unit } } } - if (needsSms) { + if (emailModel == null && smsModel == null) return + + val resolvedVariableStructureId = baseTemplate.variableStructureRef?.id + ?: listOfNotNull(emailModel, smsModel).firstNotNullOfOrNull { + refInheritanceService.apply(listOf(it)).first().variableStructureRef?.id + } + + val variableStructure = variableStructureBuilder.initVariableStructure(layout, resolvedVariableStructureId) + + if (smsModel != null) { val smsFlow = layout.addFlow().setSectionFlow(true).setWebEditingType(SECTION) .addCustomProperty("customName", "SMS Content") layout.pages.addInteractiveFlow(smsFlow, Pages.InteractiveFlowType.NORMAL) layout.addSmsRoot().setContent(smsFlow) + variableStructureBuilder.addSmsNumberToPages( + layout, smsModel.options as? SmsOptions, variableStructure + ) } - if (needsEmail) { + if (emailModel != null) { val emailBodyRootFlow = layout.addFlow().setSectionFlow(true).setWebEditingType(SECTION) .addCustomProperty("customName", "Body Content") layout.pages.addInteractiveFlow(emailBodyRootFlow, Pages.InteractiveFlowType.HTML) layout.addEmailComponentRoot().setEmailComponentsText(layout.addEmailTMText()) layout.addEmailComponentPlaceHolder().setId("Def.EmailsBody").setType(EmailComponentPlaceHolder.Type.BODY) .setContent(emailBodyRootFlow) + variableStructureBuilder.addEmailMetadataToPages( + layout, emailModel.options as? EmailOptions, variableStructure + ) } } } diff --git a/migration-library/src/main/kotlin/com/quadient/migration/service/inspirebuilder/InspireDocumentObjectBuilder.kt b/migration-library/src/main/kotlin/com/quadient/migration/service/inspirebuilder/InspireDocumentObjectBuilder.kt index a3673bba..b19fd7b1 100644 --- a/migration-library/src/main/kotlin/com/quadient/migration/service/inspirebuilder/InspireDocumentObjectBuilder.kt +++ b/migration-library/src/main/kotlin/com/quadient/migration/service/inspirebuilder/InspireDocumentObjectBuilder.kt @@ -4,7 +4,6 @@ import com.quadient.migration.api.InspireOutput import com.quadient.migration.api.ProjectConfig import com.quadient.migration.api.dto.migrationmodel.Area import com.quadient.migration.api.dto.migrationmodel.ColumnLayout -import com.quadient.migration.api.dto.migrationmodel.CustomFieldMap import com.quadient.migration.api.dto.migrationmodel.DisplayRule import com.quadient.migration.api.dto.migrationmodel.DisplayRuleRef import com.quadient.migration.api.dto.migrationmodel.DocumentContent @@ -68,7 +67,6 @@ import com.quadient.migration.shared.TableAction import com.quadient.migration.shared.TableAlignment import com.quadient.wfdxml.WfdXmlBuilder import com.quadient.wfdxml.api.layoutnodes.Flow -import com.quadient.wfdxml.api.layoutnodes.Font import com.quadient.wfdxml.api.layoutnodes.email.EmailComponentGrid import com.quadient.migration.shared.ColumnDistribution import com.quadient.wfdxml.api.layoutnodes.Image as WfdXmlImage @@ -77,14 +75,12 @@ import com.quadient.wfdxml.api.layoutnodes.Pages import com.quadient.wfdxml.api.layoutnodes.ParagraphStyle as WfdXmlParagraphStyle import com.quadient.wfdxml.api.layoutnodes.ParagraphStyle.LineSpacingType.* import com.quadient.wfdxml.api.layoutnodes.Section as WfdXmlSection -import com.quadient.wfdxml.api.layoutnodes.SheetNameType import com.quadient.wfdxml.api.layoutnodes.TabulatorType import com.quadient.wfdxml.api.layoutnodes.data.Data import com.quadient.wfdxml.api.layoutnodes.data.DataType import com.quadient.wfdxml.api.layoutnodes.data.Variable as WfdXmlVariable import com.quadient.wfdxml.api.layoutnodes.data.VariableKind import com.quadient.wfdxml.api.layoutnodes.flow.Text as WfdXmlText -import com.quadient.wfdxml.api.layoutnodes.font.SubFont import com.quadient.wfdxml.api.layoutnodes.tables.GeneralRowSet import com.quadient.wfdxml.api.layoutnodes.tables.RowSet import com.quadient.wfdxml.api.layoutnodes.tables.Table as WfdXmlTable @@ -97,12 +93,9 @@ import com.quadient.wfdxml.api.layoutnodes.tables.BorderStyle import com.quadient.wfdxml.api.layoutnodes.tables.Cell import com.quadient.wfdxml.api.layoutnodes.flow.Paragraph as WfdXmlParagraph import com.quadient.wfdxml.api.module.Layout -import com.quadient.wfdxml.internal.data.WorkFlowTreeDefinition import com.quadient.wfdxml.internal.layoutnodes.TextStyleImpl import com.quadient.wfdxml.internal.layoutnodes.data.DataImpl import com.quadient.wfdxml.internal.layoutnodes.data.WorkFlowTreeEnums.NodeOptionality -import com.quadient.wfdxml.internal.layoutnodes.data.WorkFlowTreeEnums.NodeType.SUB_TREE -import kotlin.time.Clock import com.quadient.migration.tools.logger import kotlin.collections.ifEmpty import com.quadient.migration.shared.DataType as DataTypeModel @@ -115,17 +108,14 @@ import com.quadient.migration.api.repository.BaseTemplateRepository import com.quadient.migration.api.repository.DisplayRuleRepository import com.quadient.migration.api.repository.ImageRepository import com.quadient.migration.api.repository.VariableRepository -import com.quadient.migration.api.repository.VariableStructureRepository import com.quadient.migration.service.IcmDataCache import com.quadient.migration.service.ResourcePathProvider import com.quadient.migration.shared.VariablePath import com.quadient.migration.shared.LiteralPath import com.quadient.migration.shared.VariableRefPath import com.quadient.migration.service.resolveTarget -import com.quadient.migration.api.dto.migrationmodel.EmailOptions import com.quadient.migration.api.dto.migrationmodel.QrCode import com.quadient.migration.shared.Size -import com.quadient.migration.api.dto.migrationmodel.SmsOptions import com.quadient.wfdxml.api.layoutnodes.Flow.WebEditingType.SECTION import com.quadient.wfdxml.api.layoutnodes.email.EmailComponentContent @@ -134,7 +124,7 @@ abstract class InspireDocumentObjectBuilder( protected val textStyleRepository: TextStyleRepository, protected val paragraphStyleRepository: ParagraphStyleRepository, protected val variableRepository: VariableRepository, - protected val variableStructureRepository: VariableStructureRepository, + protected val variableStructureBuilder: InspireVariableStructureBuilder, protected val displayRuleRepository: DisplayRuleRepository, protected val imageRepository: ImageRepository, protected val attachmentRepository: AttachmentRepository, @@ -474,57 +464,6 @@ abstract class InspireDocumentObjectBuilder( ) } - protected fun initVariableStructure(layout: Layout, variableStructureId: String?): VariableStructure { - val variableStructureId = variableStructureId ?: projectConfig.defaultVariableStructure - - val variableStructureModel = - variableStructureId?.let { variableStructureRepository.findOrFail(it) } ?: VariableStructure( - id = "defaultVariableStructure", - lastUpdated = Clock.System.now(), - created = Clock.System.now(), - structure = mutableMapOf(), - customFields = CustomFieldMap(), - languageVariable = null, - ) - - val normalizedVariablePaths = variableStructureModel.structure.map { (variableId, variablePathData) -> - val literalPath = variablePathData.path.resolve(variableStructureModel, variableRepository::findOrFail) - ?: error("Variable '$variableId' referenced as array path has no resolvable literal path in structure") - removeDataFromVariablePath(literalPath) - }.filter { it.isNotBlank() }.filter { it != "SystemVariable" && !it.startsWith("SystemVariable.") } - - val variableTree = buildVariableTree(normalizedVariablePaths) - - val workflowTreeDefinition = WorkFlowTreeDefinition("Root", SUB_TREE, NodeOptionality.ARRAY).also { - buildVariablePathPart(it, variableTree) - } - - val layoutData = layout.data - layoutData.importDataDefinition(workflowTreeDefinition) - if (variableTree.isNotEmpty() && variableTree.values.first() is ArrayVariable) { - layoutData.setRepeatedBy("Data.${variableTree.keys.first()}") - } - - return variableStructureModel - } - - private fun buildVariablePathPart( - parentNode: WorkFlowTreeDefinition, currentMap: Map - ) { - currentMap.forEach { - val variablePathPart = it.value - val optionality = - if (variablePathPart is ArrayVariable) NodeOptionality.ARRAY else NodeOptionality.MUST_EXIST - - val node = WorkFlowTreeDefinition(variablePathPart.name, SUB_TREE, optionality) - parentNode.addSubNode(node) - - if (variablePathPart.children.isNotEmpty()) { - buildVariablePathPart(node, variablePathPart.children) - } - } - } - fun buildTextStyles(layout: Layout, textStyleModels: List) { resolveArialFont(layout, icmDataCache) @@ -1846,43 +1785,6 @@ abstract class InspireDocumentObjectBuilder( } } - protected fun Layout.addEmailMetadataToPages(documentObject: DocumentObject, variableStructure: VariableStructure) { - val emailOptions = documentObject.options as? EmailOptions ?: return - this.addSheetNameVariable(variableStructure, SheetNameType.EMAIL_FROM, "EmailFrom", emailOptions.from) - this.addSheetNameVariable(variableStructure, SheetNameType.EMAIL_FROM_NAME, "EmailFromName", emailOptions.fromName) - this.addSheetNameVariable(variableStructure, SheetNameType.EMAIL_SUBJECT, "EmailSubject", emailOptions.subject) - this.addSheetNameVariable(variableStructure, SheetNameType.EMAIL_TO, "EmailTo", emailOptions.to) - } - - protected fun Layout.addSmsNumberToPages(documentObject: DocumentObject, variableStructure: VariableStructure) { - val smsOptions = documentObject.options as? SmsOptions ?: return - this.addSheetNameVariable(variableStructure, SheetNameType.SMS_NUMBER_TO, "NumberTo", smsOptions.numberTo) - } - - protected fun Layout.addPdfMetadataToPages(documentObject: DocumentObject, variableStructure: VariableStructure) { - val pdfMetadata = documentObject.pdfMetadata ?: return - this.addSheetNameVariable(variableStructure, SheetNameType.PDF_TITLE, "TaggingTitle", pdfMetadata.title) - this.addSheetNameVariable(variableStructure, SheetNameType.PDF_AUTHOR, "TaggingAuthor", pdfMetadata.author) - this.addSheetNameVariable(variableStructure, SheetNameType.PDF_SUBJECT, "TaggingSubject", pdfMetadata.subject) - this.addSheetNameVariable(variableStructure, SheetNameType.PDF_KEYWORDS, "TaggingKeywords", pdfMetadata.keywords) - this.addSheetNameVariable(variableStructure, SheetNameType.PDF_PRODUCER, "TaggingProduce", pdfMetadata.producer) - } - - private fun Layout.addSheetNameVariable( - variableStructure: VariableStructure, - type: SheetNameType, - variableName: String, - value: List?, - ) { - if (value.isNullOrEmpty()) return - val variable = this.data - .addVariable() - .setName(variableName) - .setKind(VariableKind.CALCULATED) - .setScript(variableStringContentToScript(value, this, variableStructure, variableRepository::findOrFail)) - this.pages.addSheetName(type, variable) - } - sealed interface ScriptResult { data class Success(val variableScript: String) : ScriptResult { override fun toString() = variableScript @@ -2071,7 +1973,7 @@ internal fun VariablePath.resolve(variableStructure: VariableStructure, findVari } } -private fun variableStringContentToScript( +internal fun variableStringContentToScript( variableStringContent: List, layout: Layout, variableStructure: VariableStructure, diff --git a/migration-library/src/main/kotlin/com/quadient/migration/service/inspirebuilder/InspireVariableStructureBuilder.kt b/migration-library/src/main/kotlin/com/quadient/migration/service/inspirebuilder/InspireVariableStructureBuilder.kt new file mode 100644 index 00000000..4c990efe --- /dev/null +++ b/migration-library/src/main/kotlin/com/quadient/migration/service/inspirebuilder/InspireVariableStructureBuilder.kt @@ -0,0 +1,156 @@ +package com.quadient.migration.service.inspirebuilder + +import com.quadient.migration.api.ProjectConfig +import com.quadient.migration.api.dto.migrationmodel.CustomFieldMap +import com.quadient.migration.api.dto.migrationmodel.DocumentObject +import com.quadient.migration.api.dto.migrationmodel.EmailOptions +import com.quadient.migration.api.dto.migrationmodel.SmsOptions +import com.quadient.migration.api.dto.migrationmodel.VariableStringContent +import com.quadient.migration.api.dto.migrationmodel.VariableStructure +import com.quadient.migration.api.repository.VariableRepository +import com.quadient.migration.api.repository.VariableStructureRepository +import com.quadient.wfdxml.api.layoutnodes.SheetNameType +import com.quadient.wfdxml.api.layoutnodes.data.VariableKind +import com.quadient.wfdxml.api.module.Layout +import com.quadient.wfdxml.internal.data.WorkFlowTreeDefinition +import com.quadient.wfdxml.internal.layoutnodes.data.WorkFlowTreeEnums.NodeOptionality +import com.quadient.wfdxml.internal.layoutnodes.data.WorkFlowTreeEnums.NodeType.SUB_TREE +import kotlin.time.Clock + +class InspireVariableStructureBuilder( + private val variableRepository: VariableRepository, + private val variableStructureRepository: VariableStructureRepository, + private val projectConfig: ProjectConfig, +) { + fun initVariableStructure(layout: Layout, variableStructureId: String?): VariableStructure { + val variableStructureId = variableStructureId ?: projectConfig.defaultVariableStructure + + val variableStructureModel = + variableStructureId?.let { variableStructureRepository.findOrFail(it) } ?: VariableStructure( + id = "defaultVariableStructure", + lastUpdated = Clock.System.now(), + created = Clock.System.now(), + structure = mutableMapOf(), + customFields = CustomFieldMap(), + languageVariable = null, + ) + + val normalizedVariablePaths = variableStructureModel.structure.map { (variableId, variablePathData) -> + val literalPath = variablePathData.path.resolve(variableStructureModel, variableRepository::findOrFail) + ?: error("Variable '$variableId' referenced as array path has no resolvable literal path in structure") + removeDataFromVariablePath(literalPath) + }.filter { it.isNotBlank() }.filter { it != "SystemVariable" && !it.startsWith("SystemVariable.") } + + val variableTree = buildVariableTree(normalizedVariablePaths) + + val workflowTreeDefinition = WorkFlowTreeDefinition("Root", SUB_TREE, NodeOptionality.ARRAY).also { + buildVariablePathPart(it, variableTree) + } + + val layoutData = layout.data + layoutData.importDataDefinition(workflowTreeDefinition) + if (variableTree.isNotEmpty() && variableTree.values.first() is ArrayVariable) { + layoutData.setRepeatedBy("Data.${variableTree.keys.first()}") + } + + return variableStructureModel + } + + private fun buildVariablePathPart( + parentNode: WorkFlowTreeDefinition, currentMap: Map + ) { + currentMap.forEach { + val variablePathPart = it.value + val optionality = + if (variablePathPart is ArrayVariable) NodeOptionality.ARRAY else NodeOptionality.MUST_EXIST + + val node = WorkFlowTreeDefinition(variablePathPart.name, SUB_TREE, optionality) + parentNode.addSubNode(node) + + if (variablePathPart.children.isNotEmpty()) { + buildVariablePathPart(node, variablePathPart.children) + } + } + } + + fun addEmailMetadataToPages(layout: Layout, documentObject: DocumentObject, variableStructure: VariableStructure) = + addEmailMetadataToPages(layout, documentObject.options as? EmailOptions, variableStructure) + + fun addEmailMetadataToPages(layout: Layout, emailOptions: EmailOptions?, variableStructure: VariableStructure) { + addSheetNameVariable( + layout, variableStructure, SheetNameType.EMAIL_FROM, "EmailFrom", emailOptions?.from, emitEmpty = true + ) + addSheetNameVariable( + layout, + variableStructure, + SheetNameType.EMAIL_FROM_NAME, + "EmailFromName", + emailOptions?.fromName, + emitEmpty = true, + ) + addSheetNameVariable( + layout, + variableStructure, + SheetNameType.EMAIL_SUBJECT, + "EmailSubject", + emailOptions?.subject, + emitEmpty = true, + ) + addSheetNameVariable( + layout, variableStructure, SheetNameType.EMAIL_TO, "EmailTo", emailOptions?.to, emitEmpty = true + ) + } + + fun addSmsNumberToPages(layout: Layout, documentObject: DocumentObject, variableStructure: VariableStructure) = + addSmsNumberToPages(layout, documentObject.options as? SmsOptions, variableStructure) + + fun addSmsNumberToPages(layout: Layout, smsOptions: SmsOptions?, variableStructure: VariableStructure) { + addSheetNameVariable( + layout, + variableStructure, + SheetNameType.SMS_NUMBER_TO, + "NumberTo", + smsOptions?.numberTo, + emitEmpty = true, + ) + } + + fun addPdfMetadataToPages(layout: Layout, documentObject: DocumentObject, variableStructure: VariableStructure) { + val pdfMetadata = documentObject.pdfMetadata ?: return + addSheetNameVariable(layout, variableStructure, SheetNameType.PDF_TITLE, "TaggingTitle", pdfMetadata.title) + addSheetNameVariable(layout, variableStructure, SheetNameType.PDF_AUTHOR, "TaggingAuthor", pdfMetadata.author) + addSheetNameVariable( + layout, variableStructure, SheetNameType.PDF_SUBJECT, "TaggingSubject", pdfMetadata.subject + ) + addSheetNameVariable( + layout, variableStructure, SheetNameType.PDF_KEYWORDS, "TaggingKeywords", pdfMetadata.keywords + ) + addSheetNameVariable( + layout, variableStructure, SheetNameType.PDF_PRODUCER, "TaggingProduce", pdfMetadata.producer + ) + } + + private fun addSheetNameVariable( + layout: Layout, + variableStructure: VariableStructure, + type: SheetNameType, + variableName: String, + value: List?, + emitEmpty: Boolean = false, + ) { + if (value.isNullOrEmpty() && !emitEmpty) return + + val script = if (value.isNullOrEmpty()) { + "return \"\";" + } else { + variableStringContentToScript(value, layout, variableStructure, variableRepository::findOrFail) + } + + val variable = layout.data + .addVariable() + .setName(variableName) + .setKind(VariableKind.CALCULATED) + .setScript(script) + layout.pages.addSheetName(type, variable) + } +} diff --git a/migration-library/src/main/kotlin/com/quadient/migration/service/inspirebuilder/InteractiveDocumentObjectBuilder.kt b/migration-library/src/main/kotlin/com/quadient/migration/service/inspirebuilder/InteractiveDocumentObjectBuilder.kt index 01da4100..18bce636 100644 --- a/migration-library/src/main/kotlin/com/quadient/migration/service/inspirebuilder/InteractiveDocumentObjectBuilder.kt +++ b/migration-library/src/main/kotlin/com/quadient/migration/service/inspirebuilder/InteractiveDocumentObjectBuilder.kt @@ -15,7 +15,6 @@ import com.quadient.migration.api.repository.ImageRepository import com.quadient.migration.api.repository.ParagraphStyleRepository import com.quadient.migration.api.repository.TextStyleRepository import com.quadient.migration.api.repository.VariableRepository -import com.quadient.migration.api.repository.VariableStructureRepository import com.quadient.migration.service.IcmDataCache import com.quadient.migration.service.ResourcePathProvider import com.quadient.migration.service.getBaseTemplateFullPath @@ -34,7 +33,7 @@ class InteractiveDocumentObjectBuilder( textStyleRepository: TextStyleRepository, paragraphStyleRepository: ParagraphStyleRepository, variableRepository: VariableRepository, - variableStructureRepository: VariableStructureRepository, + variableStructureBuilder: InspireVariableStructureBuilder, displayRuleRepository: DisplayRuleRepository, imageRepository: ImageRepository, attachmentRepository: AttachmentRepository, @@ -47,7 +46,7 @@ class InteractiveDocumentObjectBuilder( textStyleRepository, paragraphStyleRepository, variableRepository, - variableStructureRepository, + variableStructureBuilder, displayRuleRepository, imageRepository, attachmentRepository, @@ -103,9 +102,9 @@ class InteractiveDocumentObjectBuilder( ?: error("Unable to deploy document object ${documentObject.id}. Base template '$baseTemplatePath' does not exist.") val languages = collectLanguages(documentObject) - val variableStructure = initVariableStructure(layout, documentObject.variableStructureRef?.id) + val variableStructure = variableStructureBuilder.initVariableStructure(layout, documentObject.variableStructureRef?.id) - layout.addPdfMetadataToPages(documentObject, variableStructure) + variableStructureBuilder.addPdfMetadataToPages(layout, documentObject, variableStructure) val interactiveFlowsWithContent = mutableMapOf>() var usedSmsModel: DocumentObject? = null @@ -169,11 +168,11 @@ class InteractiveDocumentObjectBuilder( val hasMultipleFlows = interactiveFlowsWithContent.size > 1 if (usedSmsModel != null) { - layout.addSmsNumberToPages(usedSmsModel, variableStructure) + variableStructureBuilder.addSmsNumberToPages(layout, usedSmsModel, variableStructure) } if (usedEmailModel != null) { - layout.addEmailMetadataToPages(usedEmailModel, variableStructure) + variableStructureBuilder.addEmailMetadataToPages(layout, usedEmailModel, variableStructure) } interactiveFlowsWithContent.forEach { diff --git a/migration-library/src/test/kotlin/com/quadient/migration/service/inspirebuilder/DesignerDocumentObjectBuilderTest.kt b/migration-library/src/test/kotlin/com/quadient/migration/service/inspirebuilder/DesignerDocumentObjectBuilderTest.kt index 90d820b3..0aae2767 100644 --- a/migration-library/src/test/kotlin/com/quadient/migration/service/inspirebuilder/DesignerDocumentObjectBuilderTest.kt +++ b/migration-library/src/test/kotlin/com/quadient/migration/service/inspirebuilder/DesignerDocumentObjectBuilderTest.kt @@ -1278,7 +1278,7 @@ class DesignerDocumentObjectBuilderTest { textStyleRepository, paragraphStyleRepository, variableRepository, - variableStructureRepository, + InspireVariableStructureBuilder(variableRepository, variableStructureRepository, config), displayRuleRepository, imageRepository, attachmentRepository, diff --git a/migration-library/src/test/kotlin/com/quadient/migration/service/inspirebuilder/InspireBaseTemplateBuilderTest.kt b/migration-library/src/test/kotlin/com/quadient/migration/service/inspirebuilder/InspireBaseTemplateBuilderTest.kt index 9ded4464..b9c4529b 100644 --- a/migration-library/src/test/kotlin/com/quadient/migration/service/inspirebuilder/InspireBaseTemplateBuilderTest.kt +++ b/migration-library/src/test/kotlin/com/quadient/migration/service/inspirebuilder/InspireBaseTemplateBuilderTest.kt @@ -1,12 +1,19 @@ package com.quadient.migration.service.inspirebuilder +import com.quadient.migration.api.dto.migrationmodel.EmailOptions +import com.quadient.migration.api.dto.migrationmodel.SmsOptions +import com.quadient.migration.api.dto.migrationmodel.StringValue import com.quadient.migration.api.dto.migrationmodel.builder.BaseTemplateBuilder import com.quadient.migration.api.repository.BaseTemplateRepository import com.quadient.migration.api.repository.DocumentObjectRepository +import com.quadient.migration.api.repository.VariableRepository +import com.quadient.migration.api.repository.VariableStructureRepository import com.quadient.migration.service.InteractiveIcmDataCache import com.quadient.migration.service.InteractiveResourcePathProvider +import com.quadient.migration.service.deploy.utility.RefInheritanceServiceImpl import com.quadient.migration.service.ipsclient.IpsService import com.quadient.migration.shared.DocumentObjectType +import com.quadient.migration.shared.Color import com.quadient.migration.shared.IcmPath import com.quadient.migration.shared.millimeters import com.quadient.migration.tools.aProjectConfig @@ -19,6 +26,7 @@ import io.mockk.every import io.mockk.mockk import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test +import tools.jackson.databind.JsonNode import tools.jackson.dataformat.xml.XmlMapper import tools.jackson.module.kotlin.KotlinModule @@ -29,8 +37,17 @@ class InspireBaseTemplateBuilderTest { private val icmDataCache = InteractiveIcmDataCache(ipsService, resourcePathProvider) private val baseTemplateRepository = mockk() private val documentObjectRepository = mockk() + private val variableRepository = mockk() + private val variableStructureRepository = mockk() + private val refInheritanceService = RefInheritanceServiceImpl(documentObjectRepository) private val subject = InspireBaseTemplateBuilder( - config, icmDataCache, resourcePathProvider, baseTemplateRepository, documentObjectRepository + config, + icmDataCache, + resourcePathProvider, + baseTemplateRepository, + documentObjectRepository, + InspireVariableStructureBuilder(variableRepository, variableStructureRepository, config), + refInheritanceService, ) private val xmlMapper = XmlMapper.builder().addModule(KotlinModule.Builder().build()).build() @@ -49,6 +66,7 @@ class InspireBaseTemplateBuilderTest { every { ipsService.fileExists(any()) } returns false every { ipsService.gatherFontData(any()) } returns "Arial,Regular,icm://Fonts/arial.ttf;" every { baseTemplateRepository.findUsages(any()) } returns emptyList() + every { documentObjectRepository.listAll() } returns emptyList() } @Test @@ -244,6 +262,25 @@ class InspireBaseTemplateBuilderTest { result["SMSRoot"]["FlowId"].shouldNotBeNull() result["Pages"]["InteractiveFlow"]["FlowType"].stringValue().shouldBeEqualTo("Normal") result["ECPlaceHolder"].shouldBeNull() + sheetNameScript(result, 0).shouldBeEqualTo("return \"\";") + } + + @Test + fun `buildBaseTemplate uses sms options to build NumberTo sheet name script when present`() { + // given + val baseTemplate = BaseTemplateBuilder("BT_1").build() + val smsModel = aDocObj( + "Sms_1", DocumentObjectType.Sms, options = SmsOptions(numberTo = listOf(StringValue("+1234567890"))) + ) + val template = aDocObj("T_1", DocumentObjectType.Template, content = listOf(aDocumentObjectRef(smsModel.id))) + every { baseTemplateRepository.findUsages(baseTemplate.id) } returns listOf(template) + every { documentObjectRepository.find(smsModel.id) } returns smsModel + + // when + val result = subject.buildBaseTemplate(baseTemplate).let { xmlMapper.readTree(it.trimIndent()) }["Layout"]["Layout"] + + // then + sheetNameScript(result, 0).shouldBeEqualTo("return '+1234567890';") } @Test @@ -263,6 +300,37 @@ class InspireBaseTemplateBuilderTest { result["ECPlaceHolder"]["Id"].stringValue().shouldBeEqualTo("Def.EmailsBody") result["Pages"]["InteractiveFlow"]["FlowType"].stringValue().shouldBeEqualTo("HTML") result["SMSRoot"].shouldBeNull() + sheetNameScript(result, 0).shouldBeEqualTo("return \"\";") + sheetNameScript(result, 2).shouldBeEqualTo("return \"\";") + } + + @Test + fun `buildBaseTemplate uses email options to build sheet name scripts when present`() { + // given + val baseTemplate = BaseTemplateBuilder("BT_1").build() + val emailModel = aDocObj( + "Email_1", + DocumentObjectType.Email, + options = EmailOptions( + width = null, + backgroundFill = Color(255, 255, 255), + from = listOf(StringValue("from@quadient.com")), + fromName = emptyList(), + subject = listOf(StringValue("Hello")), + to = emptyList(), + ), + ) + val template = aDocObj("T_1", DocumentObjectType.Template, content = listOf(aDocumentObjectRef(emailModel.id))) + every { baseTemplateRepository.findUsages(baseTemplate.id) } returns listOf(template) + every { documentObjectRepository.find(emailModel.id) } returns emailModel + + // when + val result = subject.buildBaseTemplate(baseTemplate).let { xmlMapper.readTree(it.trimIndent()) }["Layout"]["Layout"] + + // then + sheetNameScript(result, 0).shouldBeEqualTo("return 'from@quadient.com';") + sheetNameScript(result, 1).shouldBeEqualTo("return \"\";") + sheetNameScript(result, 2).shouldBeEqualTo("return 'Hello';") } @Test @@ -294,4 +362,9 @@ class InspireBaseTemplateBuilderTest { result["Page"].shouldBeNull() result["Flow"].shouldBeNull() } + + private fun sheetNameScript(result: JsonNode, index: Int): String { + val calculatedScripts = result["Variable"].filter { it["Script"] != null }.map { it["Script"].stringValue() } + return calculatedScripts[index] + } } diff --git a/migration-library/src/test/kotlin/com/quadient/migration/service/inspirebuilder/InspireDocumentObjectBuilderTest.kt b/migration-library/src/test/kotlin/com/quadient/migration/service/inspirebuilder/InspireDocumentObjectBuilderTest.kt index 06c81179..69b87670 100644 --- a/migration-library/src/test/kotlin/com/quadient/migration/service/inspirebuilder/InspireDocumentObjectBuilderTest.kt +++ b/migration-library/src/test/kotlin/com/quadient/migration/service/inspirebuilder/InspireDocumentObjectBuilderTest.kt @@ -95,7 +95,7 @@ class InspireDocumentObjectBuilderTest { textStyleRepository, paragraphStyleRepository, variableRepository, - variableStructureRepository, + InspireVariableStructureBuilder(variableRepository, variableStructureRepository, config), displayRuleRepository, imageRepository, attachmentRepository, @@ -1518,7 +1518,7 @@ class InspireDocumentObjectBuilderTest { textStyleRepository, paragraphStyleRepository, variableRepository, - variableStructureRepository, + InspireVariableStructureBuilder(variableRepository, variableStructureRepository, config), displayRuleRepository, imageRepository, attachmentRepository, diff --git a/migration-library/src/test/kotlin/com/quadient/migration/service/inspirebuilder/InteractiveDocumentObjectBuilderTest.kt b/migration-library/src/test/kotlin/com/quadient/migration/service/inspirebuilder/InteractiveDocumentObjectBuilderTest.kt index aed6c918..7a443d35 100644 --- a/migration-library/src/test/kotlin/com/quadient/migration/service/inspirebuilder/InteractiveDocumentObjectBuilderTest.kt +++ b/migration-library/src/test/kotlin/com/quadient/migration/service/inspirebuilder/InteractiveDocumentObjectBuilderTest.kt @@ -1945,7 +1945,7 @@ class InteractiveDocumentObjectBuilderTest { textStyleRepository, paragraphStyleRepository, variableRepository, - variableStructureRepository, + InspireVariableStructureBuilder(variableRepository, variableStructureRepository, config), displayRuleRepository, imageRepository, attachmentRepository, From 386df249cec5349b0bac6ad9854868f272bdb568 Mon Sep 17 00:00:00 2001 From: "d.svitak" Date: Tue, 18 Aug 2026 07:31:28 +0200 Subject: [PATCH 10/14] MIG-584 Base template deployment to Flex - slightly update AcknowledgementLetterFromSource.groovy to be compatible with latest best practices --- .../example/example/AcknowledgementLetterFromSource.groovy | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/migration-examples/src/main/groovy/com/quadient/migration/example/example/AcknowledgementLetterFromSource.groovy b/migration-examples/src/main/groovy/com/quadient/migration/example/example/AcknowledgementLetterFromSource.groovy index 33065742..d943db9d 100644 --- a/migration-examples/src/main/groovy/com/quadient/migration/example/example/AcknowledgementLetterFromSource.groovy +++ b/migration-examples/src/main/groovy/com/quadient/migration/example/example/AcknowledgementLetterFromSource.groovy @@ -335,7 +335,8 @@ def mainFlow = new DocumentObjectBuilder("mainFlow", DocumentObjectType.Block) .build() migration.documentObjectRepository.upsert(mainFlow) -def page = new DocumentObjectBuilder("page1", DocumentObjectType.Page) +def page = new DocumentObjectBuilder("page", DocumentObjectType.Page) + .internal(true) .options(new PageOptions(Size.ofMillimeters(210), Size.ofMillimeters(297))) .area { it.position { From 0d9f520e2437119f78a1945627d1085f505cb2f4 Mon Sep 17 00:00:00 2001 From: "d.svitak" Date: Tue, 18 Aug 2026 09:15:23 +0200 Subject: [PATCH 11/14] MIG-584 Base template deployment to Flex - slight refactor from image area attempt (that was reverted) --- .../service/inspirebuilder/InspireBaseTemplateBuilder.kt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/migration-library/src/main/kotlin/com/quadient/migration/service/inspirebuilder/InspireBaseTemplateBuilder.kt b/migration-library/src/main/kotlin/com/quadient/migration/service/inspirebuilder/InspireBaseTemplateBuilder.kt index b121299b..a6ad6f42 100644 --- a/migration-library/src/main/kotlin/com/quadient/migration/service/inspirebuilder/InspireBaseTemplateBuilder.kt +++ b/migration-library/src/main/kotlin/com/quadient/migration/service/inspirebuilder/InspireBaseTemplateBuilder.kt @@ -53,6 +53,8 @@ class InspireBaseTemplateBuilder( } resolveArialFont(layout, icmDataCache) + val usages = baseTemplateRepository.findUsages(baseTemplate.id).filterIsInstance() + var mainFlow: Flow? = null var mainFlowSize = -1.0 @@ -89,7 +91,7 @@ class InspireBaseTemplateBuilder( mainFlow?.let { layout.pages.setMainFlow(it) } - enrichFromDocumentObjects(baseTemplate, layout) + enrichFromDocumentObjects(baseTemplate, usages, layout) val baseTemplateXml = builder.build() val sourceBaseTemplatePath = if (projectConfig.sourceBaseTemplatePath.isNullOrBlank()) { @@ -101,9 +103,7 @@ class InspireBaseTemplateBuilder( return enrichLayoutWithSourceBaseTemplate(icmDataCache, baseTemplateXml, sourceBaseTemplatePath) } - private fun enrichFromDocumentObjects(baseTemplate: BaseTemplate, layout: Layout) { - val usages = baseTemplateRepository.findUsages(baseTemplate.id).filterIsInstance() - + private fun enrichFromDocumentObjects(baseTemplate: BaseTemplate, usages: List, layout: Layout) { var emailModel: DocumentObject? = null var smsModel: DocumentObject? = null From 9af424aeed7450c0c04486f70b52b32628c2d9aa Mon Sep 17 00:00:00 2001 From: "d.svitak" Date: Tue, 18 Aug 2026 10:59:52 +0200 Subject: [PATCH 12/14] MIG-584 Base template deployment to Flex - remove confusing read-only indicators on the layout mapping files --- .../example/common/mapping/LayoutExport.groovy | 18 +++++++++--------- .../src/test/groovy/LayoutExportTest.groovy | 10 +++++----- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/migration-examples/src/main/groovy/com/quadient/migration/example/common/mapping/LayoutExport.groovy b/migration-examples/src/main/groovy/com/quadient/migration/example/common/mapping/LayoutExport.groovy index 12289238..1a139998 100644 --- a/migration-examples/src/main/groovy/com/quadient/migration/example/common/mapping/LayoutExport.groovy +++ b/migration-examples/src/main/groovy/com/quadient/migration/example/common/mapping/LayoutExport.groovy @@ -41,20 +41,20 @@ static void run(Migration migration, Path path) { areasFile.withWriter { writer -> def headers = [ Mapping.displayHeader("templateId", false), - Mapping.displayHeader("templateName", true), + Mapping.displayHeader("templateName", false), Mapping.displayHeader("pageId", false), - Mapping.displayHeader("pageName", true), + Mapping.displayHeader("pageName", false), Mapping.displayHeader("type", false), Mapping.displayHeader("baseTemplateTargetId", false), Mapping.displayHeader("interactiveFlowName", false), Mapping.displayHeader("flowToNextPage", false), - Mapping.displayHeader("areaIndex", true), - Mapping.displayHeader("x", true), - Mapping.displayHeader("y", true), - Mapping.displayHeader("width", true), - Mapping.displayHeader("height", true), - Mapping.displayHeader("pageWidth", true), - Mapping.displayHeader("pageHeight", true), + Mapping.displayHeader("areaIndex", false), + Mapping.displayHeader("x", false), + Mapping.displayHeader("y", false), + Mapping.displayHeader("width", false), + Mapping.displayHeader("height", false), + Mapping.displayHeader("pageWidth", false), + Mapping.displayHeader("pageHeight", false), Mapping.displayHeader("contentPreview", true), ] writer.writeLine(headers.join(",")) diff --git a/migration-examples/src/test/groovy/LayoutExportTest.groovy b/migration-examples/src/test/groovy/LayoutExportTest.groovy index 1cdda21f..8967af04 100644 --- a/migration-examples/src/test/groovy/LayoutExportTest.groovy +++ b/migration-examples/src/test/groovy/LayoutExportTest.groovy @@ -48,7 +48,7 @@ class LayoutExportTest { LayoutExport.run(migration, mappingFile) def expected = """\ - templateId,templateName (read-only),pageId,pageName (read-only),type,baseTemplateTargetId,interactiveFlowName,flowToNextPage,areaIndex (read-only),x (read-only),y (read-only),width (read-only),height (read-only),pageWidth (read-only),pageHeight (read-only),contentPreview (read-only) + templateId,templateName,pageId,pageName,type,baseTemplateTargetId,interactiveFlowName,flowToNextPage,areaIndex,x,y,width,height,pageWidth,pageHeight,contentPreview (read-only) full tmpl,,full page,,Standard,,test flow2,false,0,0mm,0mm,0mm,0mm,,, full tmpl,,full page,,Standard,,test flow3,true,1,0mm,0mm,0mm,0mm,,, full tmpl,,full page,,Standard,,,false,2,0mm,0mm,0mm,0mm,,, @@ -71,7 +71,7 @@ class LayoutExportTest { LayoutExport.run(migration, mappingFile) def expected = """\ - templateId,templateName (read-only),pageId,pageName (read-only),type,baseTemplateTargetId,interactiveFlowName,flowToNextPage,areaIndex (read-only),x (read-only),y (read-only),width (read-only),height (read-only),pageWidth (read-only),pageHeight (read-only),contentPreview (read-only) + templateId,templateName,pageId,pageName,type,baseTemplateTargetId,interactiveFlowName,flowToNextPage,areaIndex,x,y,width,height,pageWidth,pageHeight,contentPreview (read-only) tmpl with areas,,,,Standard,,Address Content,false,0,0mm,0mm,0mm,0mm,,, tmpl with areas,,,,Standard,,,true,1,0mm,0mm,0mm,0mm,,, tmpl with areas,,,,Standard,,Footer,false,2,0mm,0mm,0mm,0mm,,, @@ -97,7 +97,7 @@ class LayoutExportTest { LayoutExport.run(migration, mappingFile) def expected = """\ - templateId,templateName (read-only),pageId,pageName (read-only),type,baseTemplateTargetId,interactiveFlowName,flowToNextPage,areaIndex (read-only),x (read-only),y (read-only),width (read-only),height (read-only),pageWidth (read-only),pageHeight (read-only),contentPreview (read-only) + templateId,templateName,pageId,pageName,type,baseTemplateTargetId,interactiveFlowName,flowToNextPage,areaIndex,x,y,width,height,pageWidth,pageHeight,contentPreview (read-only) tmpl with base,,page with own base,,Standard,\$G1,test flow,false,0,0mm,0mm,0mm,0mm,,, """.stripIndent() Assertions.assertEquals(expected, mappingFile.toFile().text.replaceAll("\\r\\n|\\r", "\n")) @@ -123,7 +123,7 @@ class LayoutExportTest { LayoutExport.run(migration, mappingFile) def expected = """\ - templateId,templateName (read-only),pageId,pageName (read-only),type,baseTemplateTargetId,interactiveFlowName,flowToNextPage,areaIndex (read-only),x (read-only),y (read-only),width (read-only),height (read-only),pageWidth (read-only),pageHeight (read-only),contentPreview (read-only) + templateId,templateName,pageId,pageName,type,baseTemplateTargetId,interactiveFlowName,flowToNextPage,areaIndex,x,y,width,height,pageWidth,pageHeight,contentPreview (read-only) bt-1,Base template 1,page-1,Page 1,Base,,address,false,0,1cm,1cm,190mm,20mm,210mm,297mm, bt-1,Base template 1,page-1,Page 1,Base,,Area 2,true,1,1cm,30mm,190mm,50mm,210mm,297mm, bt-1,Base template 1,page-2,Page 2,Base,,Area 1,false,0,0mm,0mm,210mm,99mm,210mm,99mm, @@ -154,7 +154,7 @@ class LayoutExportTest { LayoutExport.run(migration, mappingFile) def expected = """\ - templateId,templateName (read-only),pageId,pageName (read-only),type,baseTemplateTargetId,interactiveFlowName,flowToNextPage,areaIndex (read-only),x (read-only),y (read-only),width (read-only),height (read-only),pageWidth (read-only),pageHeight (read-only),contentPreview (read-only) + templateId,templateName,pageId,pageName,type,baseTemplateTargetId,interactiveFlowName,flowToNextPage,areaIndex,x,y,width,height,pageWidth,pageHeight,contentPreview (read-only) ,,page with preview,,Standard,,test flow,false,0,0mm,0mm,0mm,0mm,,,docRef: Block One;imageRef: Image One;docRef: Block Two;(+2 more) """.stripIndent() Assertions.assertEquals(expected, mappingFile.toFile().text.replaceAll("\\r\\n|\\r", "\n")) From bc0e0dfa2322c6bd196c643eaea3adc82ab6e199 Mon Sep 17 00:00:00 2001 From: "d.svitak" Date: Tue, 18 Aug 2026 13:49:29 +0200 Subject: [PATCH 13/14] MIG-584 Base template deployment to Flex - stronger deduplication algo on interactive flow names for base templates --- migration-examples/layout/index.html | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/migration-examples/layout/index.html b/migration-examples/layout/index.html index 254def7f..d1292f6e 100644 --- a/migration-examples/layout/index.html +++ b/migration-examples/layout/index.html @@ -929,6 +929,10 @@ const baseTemplateId = `bt-${templateGroupIndex + 1}`; const baseTemplateName = `${templateGroup.templateNames[0]}BaseTemplate`; + const usedFlowNames = new Set(); + /** @type {Map} */ + const firstPageGroupIndexForName = new Map(); + templateGroup.pageGroups.forEach((pageGroup, pageGroupIndex) => { const basePageId = `page-${pageGroupIndex + 1}`; const basePageName = `Page ${pageGroupIndex + 1}`; @@ -941,14 +945,25 @@ /** @type {string[]} */ const flowNamesByAreaGroupIndex = []; - const usedFlowNames = new Set(); representativeDrafts.forEach((draft, areaGroupIndex) => { const flowToNextPage = draft.areaIndices.some(ai => representativePage.areas[ai]?.flowToNextPage); - let flowName = `${draft.name}Flow`; - if (usedFlowNames.has(flowName)) { - flowName = `${flowName} (Area ${areaGroupIndex + 1})`; + const baseFlowName = `${draft.name}Flow`; + let flowName = baseFlowName; + if (usedFlowNames.has(baseFlowName)) { + flowName = firstPageGroupIndexForName.get(baseFlowName) === pageGroupIndex + ? `${baseFlowName} (Area ${areaGroupIndex + 1})` + : `${baseFlowName} (${basePageName})`; + } else { + firstPageGroupIndexForName.set(baseFlowName, pageGroupIndex); + } + let uniqueFlowName = flowName; + let dedupCounter = 2; + while (usedFlowNames.has(uniqueFlowName)) { + uniqueFlowName = `${flowName} (${dedupCounter})`; + dedupCounter++; } + flowName = uniqueFlowName; usedFlowNames.add(flowName); flowNamesByAreaGroupIndex.push(flowName); From daa7a3da63f29882b77fa5741760c2dac422c170 Mon Sep 17 00:00:00 2001 From: "d.svitak" Date: Tue, 18 Aug 2026 15:07:21 +0200 Subject: [PATCH 14/14] MIG-584 Base template deployment to Flex - changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 91bc4adb..31a2f09f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) ### Added +- DeployBaseTemplates task that deploys base templates with pages, areas, interactive flows and other options. + ### Changed ### Fixed