From f32f8916c6b9ea07ec2aaf61428ea59bd847d872 Mon Sep 17 00:00:00 2001 From: LunarN0va Date: Thu, 25 Apr 2024 13:59:47 +0200 Subject: [PATCH 1/3] Optimize imports --- .../nl/avisi/structurizr/site/generatr/StructurizrUtilities.kt | 1 - .../avisi/structurizr/site/generatr/site/PlantUmlExporterTest.kt | 1 - .../model/SoftwareSystemContainerSectionsPageViewModelTest.kt | 1 - 3 files changed, 3 deletions(-) diff --git a/src/main/kotlin/nl/avisi/structurizr/site/generatr/StructurizrUtilities.kt b/src/main/kotlin/nl/avisi/structurizr/site/generatr/StructurizrUtilities.kt index c529f861..49b389fe 100644 --- a/src/main/kotlin/nl/avisi/structurizr/site/generatr/StructurizrUtilities.kt +++ b/src/main/kotlin/nl/avisi/structurizr/site/generatr/StructurizrUtilities.kt @@ -2,7 +2,6 @@ package nl.avisi.structurizr.site.generatr import com.structurizr.Workspace import com.structurizr.model.Container -import com.structurizr.model.Location import com.structurizr.model.SoftwareSystem import com.structurizr.view.ViewSet diff --git a/src/test/kotlin/nl/avisi/structurizr/site/generatr/site/PlantUmlExporterTest.kt b/src/test/kotlin/nl/avisi/structurizr/site/generatr/site/PlantUmlExporterTest.kt index d54f40dc..a7c3f7f8 100644 --- a/src/test/kotlin/nl/avisi/structurizr/site/generatr/site/PlantUmlExporterTest.kt +++ b/src/test/kotlin/nl/avisi/structurizr/site/generatr/site/PlantUmlExporterTest.kt @@ -4,7 +4,6 @@ import assertk.assertThat import assertk.assertions.contains import assertk.assertions.isEqualTo import com.structurizr.Workspace -import com.structurizr.model.Location import org.junit.jupiter.api.DynamicTest import org.junit.jupiter.api.TestFactory import kotlin.test.Test diff --git a/src/test/kotlin/nl/avisi/structurizr/site/generatr/site/model/SoftwareSystemContainerSectionsPageViewModelTest.kt b/src/test/kotlin/nl/avisi/structurizr/site/generatr/site/model/SoftwareSystemContainerSectionsPageViewModelTest.kt index 18812c8f..57241b02 100644 --- a/src/test/kotlin/nl/avisi/structurizr/site/generatr/site/model/SoftwareSystemContainerSectionsPageViewModelTest.kt +++ b/src/test/kotlin/nl/avisi/structurizr/site/generatr/site/model/SoftwareSystemContainerSectionsPageViewModelTest.kt @@ -6,7 +6,6 @@ import assertk.assertions.hasSize import assertk.assertions.index import assertk.assertions.isEqualTo import assertk.assertions.isFalse -import nl.avisi.structurizr.site.generatr.normalize import kotlin.test.Test class SoftwareSystemContainerSectionsPageViewModelTest : ViewModelTest() { From ad91115b9fb0284a63678af5d91ffa123c475084 Mon Sep 17 00:00:00 2001 From: LunarN0va Date: Thu, 25 Apr 2024 14:20:44 +0200 Subject: [PATCH 2/3] Refactor exact bool to Match Enum --- .../site/model/DecisionTabViewModel.kt | 2 +- .../site/generatr/site/model/LinkViewModel.kt | 15 +++++++-- .../site/generatr/site/model/MenuViewModel.kt | 8 ++--- .../site/model/SectionTabViewModel.kt | 2 +- .../site/model/SoftwareSystemPageViewModel.kt | 8 ++--- .../generatr/site/model/LinkViewModelTest.kt | 32 ++++++++++++++++--- .../generatr/site/model/MenuViewModelTest.kt | 6 ++-- ...stemContainerDecisionsPageViewModelTest.kt | 3 +- ...ystemContainerSectionsPageViewModelTest.kt | 3 +- 9 files changed, 56 insertions(+), 23 deletions(-) diff --git a/src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/DecisionTabViewModel.kt b/src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/DecisionTabViewModel.kt index 3cff81f6..611b0211 100644 --- a/src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/DecisionTabViewModel.kt +++ b/src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/DecisionTabViewModel.kt @@ -1,5 +1,5 @@ package nl.avisi.structurizr.site.generatr.site.model data class DecisionTabViewModel(val pageViewModel: SoftwareSystemPageViewModel, val title: String, val url: String) { - val link = LinkViewModel(pageViewModel, title, url, true) + val link = LinkViewModel(pageViewModel, title, url) } diff --git a/src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/LinkViewModel.kt b/src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/LinkViewModel.kt index 238822af..dcb1c63f 100644 --- a/src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/LinkViewModel.kt +++ b/src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/LinkViewModel.kt @@ -6,11 +6,22 @@ data class LinkViewModel( private val pageViewModel: PageViewModel, val title: String, val href: String, - val exact: Boolean = true + val match: Match = Match.EXACT ) { val relativeHref get() = href.asUrlToDirectory(pageViewModel.url) - val active get() = if (exact) isHrefOfContainingPage else isChildHrefOfContainingPage + val active get() = + when (match) { + Match.EXACT -> isHrefOfContainingPage + Match.CHILD -> isChildHrefOfContainingPage + Match.SIBLING -> isSiblingHrefOfContainingPage + } private val isHrefOfContainingPage get() = href == pageViewModel.url private val isChildHrefOfContainingPage get() = pageViewModel.url == href || pageViewModel.url.startsWith("$href/") + private val isSiblingHrefOfContainingPage get() = pageViewModel.url.trimEnd('/').dropLastWhile { it != '/' } == href.trimEnd('/').dropLastWhile { it != '/' } +} +enum class Match { + EXACT, + CHILD, + SIBLING } diff --git a/src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/MenuViewModel.kt b/src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/MenuViewModel.kt index 0e7f0d2d..d73e033f 100644 --- a/src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/MenuViewModel.kt +++ b/src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/MenuViewModel.kt @@ -7,7 +7,7 @@ class MenuViewModel(generatorContext: GeneratorContext, private val pageViewMode yield(createMenuItem("Home", HomePageViewModel.url())) if (generatorContext.workspace.documentation.decisions.isNotEmpty()) - yield(createMenuItem("Decisions", WorkspaceDecisionsPageViewModel.url(), false)) + yield(createMenuItem("Decisions", WorkspaceDecisionsPageViewModel.url(), Match.CHILD)) if (generatorContext.workspace.model.softwareSystems.isNotEmpty()) yield(createMenuItem("Software Systems", SoftwareSystemsPageViewModel.url())) @@ -21,7 +21,7 @@ class MenuViewModel(generatorContext: GeneratorContext, private val pageViewMode val softwareSystemItems = pageViewModel.includedSoftwareSystems .sortedBy { it.name.lowercase() } .map { - createMenuItem(it.name, SoftwareSystemPageViewModel.url(it, SoftwareSystemPageViewModel.Tab.HOME), false) + createMenuItem(it.name, SoftwareSystemPageViewModel.url(it, SoftwareSystemPageViewModel.Tab.HOME), Match.CHILD) } private val groupSeparator = generatorContext.workspace.model.properties["structurizr.groupSeparator"] ?: "/" @@ -30,8 +30,8 @@ class MenuViewModel(generatorContext: GeneratorContext, private val pageViewMode .map { "${it.group ?: ""}$groupSeparator${it.name}" } .sortedBy { it.lowercase() } - private fun createMenuItem(title: String, href: String, exact: Boolean = true) = - LinkViewModel(pageViewModel, title, href, exact) + private fun createMenuItem(title: String, href: String, match: Match = Match.EXACT) = + LinkViewModel(pageViewModel, title, href, match) fun softwareSystemNodes(): MenuNodeViewModel { data class MutableMenuNode(val name: String, val children: MutableList) { diff --git a/src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/SectionTabViewModel.kt b/src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/SectionTabViewModel.kt index 24a0035a..0fddfb0c 100644 --- a/src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/SectionTabViewModel.kt +++ b/src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/SectionTabViewModel.kt @@ -1,5 +1,5 @@ package nl.avisi.structurizr.site.generatr.site.model data class SectionTabViewModel(val pageViewModel: SoftwareSystemPageViewModel, val title: String, val url: String) { - val link = LinkViewModel(pageViewModel, title, url, true) + val link = LinkViewModel(pageViewModel, title, url) } diff --git a/src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/SoftwareSystemPageViewModel.kt b/src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/SoftwareSystemPageViewModel.kt index 2212328f..80b2aa59 100644 --- a/src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/SoftwareSystemPageViewModel.kt +++ b/src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/SoftwareSystemPageViewModel.kt @@ -11,8 +11,8 @@ open class SoftwareSystemPageViewModel( ) : PageViewModel(generatorContext) { enum class Tab { HOME, SYSTEM_CONTEXT, CONTAINER, COMPONENT, CODE, DYNAMIC, DEPLOYMENT, DEPENDENCIES, DECISIONS, SECTIONS } - inner class TabViewModel(val tab: Tab, exactLink: Boolean = true) { - val link = LinkViewModel(this@SoftwareSystemPageViewModel, title, url(softwareSystem, tab), exactLink) + inner class TabViewModel(val tab: Tab, match: Match = Match.EXACT) { + val link = LinkViewModel(this@SoftwareSystemPageViewModel, title, url(softwareSystem, tab), match) private val title get() = when (tab) { @@ -55,8 +55,8 @@ open class SoftwareSystemPageViewModel( TabViewModel(Tab.DYNAMIC), TabViewModel(Tab.DEPLOYMENT), TabViewModel(Tab.DEPENDENCIES), - TabViewModel(Tab.DECISIONS, exactLink = false), - TabViewModel(Tab.SECTIONS, exactLink = false) + TabViewModel(Tab.DECISIONS, Match.CHILD), + TabViewModel(Tab.SECTIONS, Match.CHILD) ) val description: String = softwareSystem.description diff --git a/src/test/kotlin/nl/avisi/structurizr/site/generatr/site/model/LinkViewModelTest.kt b/src/test/kotlin/nl/avisi/structurizr/site/generatr/site/model/LinkViewModelTest.kt index 609723e9..3a9d6097 100644 --- a/src/test/kotlin/nl/avisi/structurizr/site/generatr/site/model/LinkViewModelTest.kt +++ b/src/test/kotlin/nl/avisi/structurizr/site/generatr/site/model/LinkViewModelTest.kt @@ -28,7 +28,7 @@ class LinkViewModelTest : ViewModelTest() { @ValueSource(strings = ["/some-page", "/some-page/1", "/some-page/subpage/subsubpage"]) fun `non-exact links are active when the page url matches partially`(pageHref: String) { val pageViewModel = pageViewModel(pageHref) - val viewModel = LinkViewModel(pageViewModel, "Some page", "/some-page", false) + val viewModel = LinkViewModel(pageViewModel, "Some page", "/some-page", Match.CHILD) assertThat(viewModel.active).isTrue() } @@ -36,14 +36,14 @@ class LinkViewModelTest : ViewModelTest() { @ValueSource(strings = ["/", "/some-other-page/1", "/some-other-page/subpage/subsubpage"]) fun `non-exact links are not active when the page url doesn't match partially`(pageHref: String) { val pageViewModel = pageViewModel(pageHref) - val viewModel = LinkViewModel(pageViewModel, "Some page", "/some-page", false) + val viewModel = LinkViewModel(pageViewModel, "Some page", "/some-page", Match.CHILD) assertThat(viewModel.active).isFalse() } @Test fun `non-exact links are only active when page url is a subdirectory of the link`() { - val expectInactivePartialMatch = LinkViewModel(pageViewModel("/page-two"), "Some page", "/page", false) - val expectActivePartialMatch = LinkViewModel(pageViewModel("/page/two"), "Some page", "/page", false) + val expectInactivePartialMatch = LinkViewModel(pageViewModel("/page-two"), "Some page", "/page", Match.CHILD) + val expectActivePartialMatch = LinkViewModel(pageViewModel("/page/two"), "Some page", "/page", Match.CHILD) assertThat(expectInactivePartialMatch.active).isFalse() assertThat(expectActivePartialMatch.active).isTrue() } @@ -54,4 +54,28 @@ class LinkViewModelTest : ViewModelTest() { val viewModel = LinkViewModel(pageViewModel, "Some other page", "/some-other-page") assertThat(viewModel.relativeHref).isEqualTo("../../some-other-page/") } + + @ParameterizedTest + @ValueSource(strings = ["/some-page/sibling-page-1/", "/some-page/sibling-page-2/"]) + fun `sibling links are active when the previous url path matches`(pageHref: String) { + val pageViewModel = pageViewModel(pageHref) + val viewModel = LinkViewModel(pageViewModel, "Some page", "/some-page/sibling-page-3/", Match.SIBLING) + assertThat(viewModel.active).isTrue() + } + + @ParameterizedTest + @ValueSource(strings = ["/some-other-page/sibling-page-1/", "/some-other-page/sibling-page-2/"]) + fun `sibling links are not active when the previous url path doesn't match`(pageHref: String) { + val pageViewModel = pageViewModel(pageHref) + val viewModel = LinkViewModel(pageViewModel, "Some page", "/some-page/sibling-page-1", Match.SIBLING) + assertThat(viewModel.active).isFalse() + } + + @Test + fun `sibling links are only active when previous page url path matches previous href path url`() { + val expectInactiveSiblingMatch = LinkViewModel(pageViewModel("/page/two/"), "Some page", "/some-page/", Match.SIBLING) + val expectActiveSiblingMatch = LinkViewModel(pageViewModel("/page/two"), "Some page", "/page/one", Match.SIBLING) + assertThat(expectInactiveSiblingMatch.active).isFalse() + assertThat(expectActiveSiblingMatch.active).isTrue() + } } diff --git a/src/test/kotlin/nl/avisi/structurizr/site/generatr/site/model/MenuViewModelTest.kt b/src/test/kotlin/nl/avisi/structurizr/site/generatr/site/model/MenuViewModelTest.kt index 1667754b..76937228 100644 --- a/src/test/kotlin/nl/avisi/structurizr/site/generatr/site/model/MenuViewModelTest.kt +++ b/src/test/kotlin/nl/avisi/structurizr/site/generatr/site/model/MenuViewModelTest.kt @@ -56,7 +56,7 @@ class MenuViewModelTest : ViewModelTest() { val viewModel = MenuViewModel(generatorContext, pageViewModel) assertThat(viewModel.generalItems[1]).isEqualTo( - LinkViewModel(pageViewModel, "Decisions", WorkspaceDecisionsPageViewModel.url(), false) + LinkViewModel(pageViewModel, "Decisions", WorkspaceDecisionsPageViewModel.url(), Match.CHILD) ) } @@ -92,13 +92,13 @@ class MenuViewModelTest : ViewModelTest() { pageViewModel, "system 1", SoftwareSystemPageViewModel.url(system1, SoftwareSystemPageViewModel.Tab.HOME), - false + Match.CHILD ), LinkViewModel( pageViewModel, "System 2", SoftwareSystemPageViewModel.url(system2, SoftwareSystemPageViewModel.Tab.HOME), - false + Match.CHILD ) ) } diff --git a/src/test/kotlin/nl/avisi/structurizr/site/generatr/site/model/SoftwareSystemContainerDecisionsPageViewModelTest.kt b/src/test/kotlin/nl/avisi/structurizr/site/generatr/site/model/SoftwareSystemContainerDecisionsPageViewModelTest.kt index bb3a209c..4bdfabf1 100644 --- a/src/test/kotlin/nl/avisi/structurizr/site/generatr/site/model/SoftwareSystemContainerDecisionsPageViewModelTest.kt +++ b/src/test/kotlin/nl/avisi/structurizr/site/generatr/site/model/SoftwareSystemContainerDecisionsPageViewModelTest.kt @@ -35,8 +35,7 @@ class SoftwareSystemContainerDecisionsPageViewModelTest : ViewModelTest() { LinkViewModel( viewModel, "Decision 1", - "/software-system/decisions/api-application/1", - true + "/software-system/decisions/api-application/1" ) ) } diff --git a/src/test/kotlin/nl/avisi/structurizr/site/generatr/site/model/SoftwareSystemContainerSectionsPageViewModelTest.kt b/src/test/kotlin/nl/avisi/structurizr/site/generatr/site/model/SoftwareSystemContainerSectionsPageViewModelTest.kt index 57241b02..1b622bf3 100644 --- a/src/test/kotlin/nl/avisi/structurizr/site/generatr/site/model/SoftwareSystemContainerSectionsPageViewModelTest.kt +++ b/src/test/kotlin/nl/avisi/structurizr/site/generatr/site/model/SoftwareSystemContainerSectionsPageViewModelTest.kt @@ -35,8 +35,7 @@ class SoftwareSystemContainerSectionsPageViewModelTest : ViewModelTest() { LinkViewModel( viewModel, "Content", - "/software-system/sections/api-application/1", - true + "/software-system/sections/api-application/1" ) ) } From f4f2d34095b003478326153609494b6dbfcee9df Mon Sep 17 00:00:00 2001 From: LunarN0va Date: Thu, 25 Apr 2024 14:26:19 +0200 Subject: [PATCH 3/3] Add container tabs on component view --- .../site/generatr/StructurizrUtilities.kt | 9 ++ .../site/generatr/site/PlantUmlExporter.kt | 2 +- .../site/generatr/site/SiteGenerator.kt | 10 +- .../site/model/ContainerTabViewModel.kt | 5 + .../site/model/ContainersTabViewModel.kt | 25 ++++ .../SoftwareSystemComponentPageViewModel.kt | 18 --- ...eSystemContainerComponentsPageViewModel.kt | 24 ++++ .../site/model/SoftwareSystemPageViewModel.kt | 12 +- .../site/views/SoftwareSystemComponentPage.kt | 15 -- .../SoftwareSystemContainerComponentsPage.kt | 27 ++++ .../generatr/site/PlantUmlExporterTest.kt | 4 +- ...oftwareSystemComponentPageViewModelTest.kt | 77 ---------- ...temContainerComponentsPageViewModelTest.kt | 135 ++++++++++++++++++ .../model/SoftwareSystemPageViewModelTest.kt | 28 ++-- .../site/generatr/site/model/ViewModelTest.kt | 2 +- 15 files changed, 262 insertions(+), 131 deletions(-) create mode 100644 src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/ContainerTabViewModel.kt create mode 100644 src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/ContainersTabViewModel.kt delete mode 100644 src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/SoftwareSystemComponentPageViewModel.kt create mode 100644 src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/SoftwareSystemContainerComponentsPageViewModel.kt delete mode 100644 src/main/kotlin/nl/avisi/structurizr/site/generatr/site/views/SoftwareSystemComponentPage.kt create mode 100644 src/main/kotlin/nl/avisi/structurizr/site/generatr/site/views/SoftwareSystemContainerComponentsPage.kt delete mode 100644 src/test/kotlin/nl/avisi/structurizr/site/generatr/site/model/SoftwareSystemComponentPageViewModelTest.kt create mode 100644 src/test/kotlin/nl/avisi/structurizr/site/generatr/site/model/SoftwareSystemContainerComponentsPageViewModelTest.kt diff --git a/src/main/kotlin/nl/avisi/structurizr/site/generatr/StructurizrUtilities.kt b/src/main/kotlin/nl/avisi/structurizr/site/generatr/StructurizrUtilities.kt index 49b389fe..cef8d7dd 100644 --- a/src/main/kotlin/nl/avisi/structurizr/site/generatr/StructurizrUtilities.kt +++ b/src/main/kotlin/nl/avisi/structurizr/site/generatr/StructurizrUtilities.kt @@ -4,6 +4,7 @@ import com.structurizr.Workspace import com.structurizr.model.Container import com.structurizr.model.SoftwareSystem import com.structurizr.view.ViewSet +import nl.avisi.structurizr.site.generatr.site.GeneratorContext val Workspace.includedSoftwareSystems: List get() = model.softwareSystems.filter { @@ -13,6 +14,8 @@ val Workspace.includedSoftwareSystems: List fun Workspace.hasImageViews(id: String) = views.imageViews.any { it.elementId == id } +fun Workspace.hasComponentDiagrams(container: Container) = views.componentViews.any { it.container == container} + val SoftwareSystem.hasContainers get() = this.containers.isNotEmpty() @@ -22,6 +25,12 @@ val SoftwareSystem.includedProperties val Container.hasComponents get() = this.components.isNotEmpty() +fun SoftwareSystem.firstContainerName(generatorContext: GeneratorContext) = containers + .firstOrNull { container -> + generatorContext.workspace.hasComponentDiagrams(container) or + generatorContext.workspace.hasImageViews(container.id) } + ?.name?.normalize() + fun SoftwareSystem.hasDecisions() = documentation.decisions.isNotEmpty() fun SoftwareSystem.hasContainerDecisions() = containers.any { it.hasDecisions() } diff --git a/src/main/kotlin/nl/avisi/structurizr/site/generatr/site/PlantUmlExporter.kt b/src/main/kotlin/nl/avisi/structurizr/site/generatr/site/PlantUmlExporter.kt index 27b05473..520b4f4c 100644 --- a/src/main/kotlin/nl/avisi/structurizr/site/generatr/site/PlantUmlExporter.kt +++ b/src/main/kotlin/nl/avisi/structurizr/site/generatr/site/PlantUmlExporter.kt @@ -108,7 +108,7 @@ private class WriterWithElementLinks( private fun getUrlToElement(element: Element?, page: String? = null): String { val path = when (element) { is SoftwareSystem -> "/${element.name?.normalize()}/${page?.let { page } ?: "container"}/".asUrlToDirectory(url) - is Container -> "/${element.parent?.name?.normalize()}/${page?.let { page } ?: "component"}/".asUrlToDirectory(url) + is Container -> "/${element.parent?.name?.normalize()}/${page?.let { page } ?: "component"}/${element.name?.normalize()}".asUrlToDirectory(url) is Component -> "/${element.parent?.parent?.name?.normalize()}/${page?.let { page } ?: "code"}/".asUrlToDirectory(url) else -> throw IllegalStateException("Not supported element") } diff --git a/src/main/kotlin/nl/avisi/structurizr/site/generatr/site/SiteGenerator.kt b/src/main/kotlin/nl/avisi/structurizr/site/generatr/site/SiteGenerator.kt index 9ea65311..8c8fa88e 100644 --- a/src/main/kotlin/nl/avisi/structurizr/site/generatr/site/SiteGenerator.kt +++ b/src/main/kotlin/nl/avisi/structurizr/site/generatr/site/SiteGenerator.kt @@ -144,7 +144,6 @@ private fun generateHtmlFiles(context: GeneratorContext, branchDir: File) { add { writeHtmlFile(branchDir, SoftwareSystemHomePageViewModel(context, it)) } add { writeHtmlFile(branchDir, SoftwareSystemContextPageViewModel(context, it)) } add { writeHtmlFile(branchDir, SoftwareSystemContainerPageViewModel(context, it)) } - add { writeHtmlFile(branchDir, SoftwareSystemComponentPageViewModel(context, it)) } add { writeHtmlFile(branchDir, SoftwareSystemCodePageViewModel(context, it)) } add { writeHtmlFile(branchDir, SoftwareSystemDynamicPageViewModel(context, it)) } add { writeHtmlFile(branchDir, SoftwareSystemDeploymentPageViewModel(context, it)) } @@ -174,6 +173,13 @@ private fun generateHtmlFiles(context: GeneratorContext, branchDir: File) { } } + it.containers + .filter { container -> + context.workspace.views.componentViews.any { containerView -> containerView.container == container } or + context.workspace.views.imageViews.any { imageView -> imageView.elementId in container.id } } + .forEach { container -> + add { writeHtmlFile(branchDir, SoftwareSystemContainerComponentsPageViewModel(context, container)) } } + it.documentation.sections.filter { section -> section.order != 1 }.forEach { section -> add { writeHtmlFile(branchDir, SoftwareSystemSectionPageViewModel(context, it, section)) } } @@ -198,7 +204,7 @@ private fun writeHtmlFile(exportDir: File, viewModel: PageViewModel) { is SoftwareSystemContainerDecisionsPageViewModel -> softwareSystemContainerDecisionsPage(viewModel) is SoftwareSystemContainerSectionPageViewModel -> softwareSystemContainerSectionPage(viewModel) is SoftwareSystemContainerSectionsPageViewModel -> softwareSystemContainerSectionsPage(viewModel) - is SoftwareSystemComponentPageViewModel -> softwareSystemComponentPage(viewModel) + is SoftwareSystemContainerComponentsPageViewModel -> softwareSystemContainerComponentsPage(viewModel) is SoftwareSystemCodePageViewModel -> softwareSystemCodePage(viewModel) is SoftwareSystemDynamicPageViewModel -> softwareSystemDynamicPage(viewModel) is SoftwareSystemDeploymentPageViewModel -> softwareSystemDeploymentPage(viewModel) diff --git a/src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/ContainerTabViewModel.kt b/src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/ContainerTabViewModel.kt new file mode 100644 index 00000000..02d75c04 --- /dev/null +++ b/src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/ContainerTabViewModel.kt @@ -0,0 +1,5 @@ +package nl.avisi.structurizr.site.generatr.site.model + +data class ContainerTabViewModel(val pageViewModel: SoftwareSystemPageViewModel, val title: String, val url: String) { + val link = LinkViewModel(pageViewModel, title, url) +} diff --git a/src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/ContainersTabViewModel.kt b/src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/ContainersTabViewModel.kt new file mode 100644 index 00000000..2e8b4c89 --- /dev/null +++ b/src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/ContainersTabViewModel.kt @@ -0,0 +1,25 @@ +package nl.avisi.structurizr.site.generatr.site.model + +import com.structurizr.model.SoftwareSystem +import nl.avisi.structurizr.site.generatr.hasComponentDiagrams +import nl.avisi.structurizr.site.generatr.hasImageViews +import nl.avisi.structurizr.site.generatr.site.GeneratorContext + +fun SoftwareSystemPageViewModel.createContainersTabViewModel( + generatorContext: GeneratorContext, + softwareSystem: SoftwareSystem, +) = buildList { + softwareSystem + .containers + .filter { container -> + generatorContext.workspace.hasComponentDiagrams(container) or + generatorContext.workspace.hasImageViews(container.id) } + .map { + ContainerTabViewModel( + this@createContainersTabViewModel, + it.name, + SoftwareSystemContainerComponentsPageViewModel.url(it) + ) + } + .forEach { add(it) } +} diff --git a/src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/SoftwareSystemComponentPageViewModel.kt b/src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/SoftwareSystemComponentPageViewModel.kt deleted file mode 100644 index d5460450..00000000 --- a/src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/SoftwareSystemComponentPageViewModel.kt +++ /dev/null @@ -1,18 +0,0 @@ -package nl.avisi.structurizr.site.generatr.site.model - -import com.structurizr.model.SoftwareSystem -import nl.avisi.structurizr.site.generatr.hasComponentViews -import nl.avisi.structurizr.site.generatr.site.GeneratorContext - -class SoftwareSystemComponentPageViewModel(generatorContext: GeneratorContext, softwareSystem: SoftwareSystem) : - SoftwareSystemPageViewModel(generatorContext, softwareSystem, Tab.COMPONENT) { - val diagrams = generatorContext.workspace.views.componentViews - .filter { it.softwareSystem == softwareSystem } - .sortedBy { it.key } - .map { DiagramViewModel.forView(this, it, generatorContext.svgFactory) } - val images = generatorContext.workspace.views.imageViews - .filter { it.elementId in softwareSystem.containers.map { c -> c.id } } - .sortedBy { it.key } - .map { ImageViewViewModel(it) } - val visible = generatorContext.workspace.views.hasComponentViews(generatorContext.workspace, softwareSystem) || images.isNotEmpty() -} diff --git a/src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/SoftwareSystemContainerComponentsPageViewModel.kt b/src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/SoftwareSystemContainerComponentsPageViewModel.kt new file mode 100644 index 00000000..747faf4d --- /dev/null +++ b/src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/SoftwareSystemContainerComponentsPageViewModel.kt @@ -0,0 +1,24 @@ +package nl.avisi.structurizr.site.generatr.site.model + +import com.structurizr.model.Container +import nl.avisi.structurizr.site.generatr.normalize +import nl.avisi.structurizr.site.generatr.site.GeneratorContext + +class SoftwareSystemContainerComponentsPageViewModel(generatorContext: GeneratorContext, container: Container) : + SoftwareSystemPageViewModel(generatorContext, container.softwareSystem, Tab.COMPONENT) { + override val url = url(container) + val diagrams = generatorContext.workspace.views.componentViews + .filter { it.container == container } + .sortedBy { it.key } + .map { DiagramViewModel.forView(this, it, generatorContext.svgFactory) } + val images = generatorContext.workspace.views.imageViews + .filter { it.elementId in container.id } + .sortedBy { it.key } + .map { ImageViewViewModel(it) } + + val visible = diagrams.isNotEmpty() or images.isNotEmpty() + val containerTabs = createContainersTabViewModel(generatorContext, container.softwareSystem) + companion object { + fun url(container: Container) = "${url(container.softwareSystem, Tab.COMPONENT)}/${container.name.normalize()}" + } +} diff --git a/src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/SoftwareSystemPageViewModel.kt b/src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/SoftwareSystemPageViewModel.kt index 80b2aa59..ec340d93 100644 --- a/src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/SoftwareSystemPageViewModel.kt +++ b/src/main/kotlin/nl/avisi/structurizr/site/generatr/site/model/SoftwareSystemPageViewModel.kt @@ -12,7 +12,15 @@ open class SoftwareSystemPageViewModel( enum class Tab { HOME, SYSTEM_CONTEXT, CONTAINER, COMPONENT, CODE, DYNAMIC, DEPLOYMENT, DEPENDENCIES, DECISIONS, SECTIONS } inner class TabViewModel(val tab: Tab, match: Match = Match.EXACT) { - val link = LinkViewModel(this@SoftwareSystemPageViewModel, title, url(softwareSystem, tab), match) + val link = when (tab) { + Tab.COMPONENT -> LinkViewModel( + this@SoftwareSystemPageViewModel, + title, + "${url(softwareSystem, tab)}/${softwareSystem.firstContainerName(generatorContext)}", + match + ) + else -> LinkViewModel(this@SoftwareSystemPageViewModel, title, url(softwareSystem, tab), match) + } private val title get() = when (tab) { @@ -50,7 +58,7 @@ open class SoftwareSystemPageViewModel( TabViewModel(Tab.HOME), TabViewModel(Tab.SYSTEM_CONTEXT), TabViewModel(Tab.CONTAINER), - TabViewModel(Tab.COMPONENT), + TabViewModel(Tab.COMPONENT, Match.SIBLING), TabViewModel(Tab.CODE), TabViewModel(Tab.DYNAMIC), TabViewModel(Tab.DEPLOYMENT), diff --git a/src/main/kotlin/nl/avisi/structurizr/site/generatr/site/views/SoftwareSystemComponentPage.kt b/src/main/kotlin/nl/avisi/structurizr/site/generatr/site/views/SoftwareSystemComponentPage.kt deleted file mode 100644 index 1ce4efb0..00000000 --- a/src/main/kotlin/nl/avisi/structurizr/site/generatr/site/views/SoftwareSystemComponentPage.kt +++ /dev/null @@ -1,15 +0,0 @@ -package nl.avisi.structurizr.site.generatr.site.views - -import kotlinx.html.HTML -import nl.avisi.structurizr.site.generatr.site.model.SoftwareSystemComponentPageViewModel - -fun HTML.softwareSystemComponentPage(viewModel: SoftwareSystemComponentPageViewModel) { - if (viewModel.visible) - softwareSystemPage(viewModel) { - // TODO: group by containers - viewModel.diagrams.forEach { diagram(it) } - viewModel.images.forEach { image(it) } - } - else - redirectUpPage() -} diff --git a/src/main/kotlin/nl/avisi/structurizr/site/generatr/site/views/SoftwareSystemContainerComponentsPage.kt b/src/main/kotlin/nl/avisi/structurizr/site/generatr/site/views/SoftwareSystemContainerComponentsPage.kt new file mode 100644 index 00000000..f6cedb91 --- /dev/null +++ b/src/main/kotlin/nl/avisi/structurizr/site/generatr/site/views/SoftwareSystemContainerComponentsPage.kt @@ -0,0 +1,27 @@ +package nl.avisi.structurizr.site.generatr.site.views + +import kotlinx.html.HTML +import kotlinx.html.div +import kotlinx.html.li +import kotlinx.html.ul +import nl.avisi.structurizr.site.generatr.site.model.SoftwareSystemContainerComponentsPageViewModel + +fun HTML.softwareSystemContainerComponentsPage(viewModel: SoftwareSystemContainerComponentsPageViewModel) { + if (viewModel.visible) { + softwareSystemPage(viewModel) { + div(classes = "tabs") { + ul(classes = "m-0 is-flex-wrap-wrap is flex-shrink-1 is flex-grow-0") { + viewModel.containerTabs + .forEach { + li(classes = if (it.link.active) "is-active" else null) { + link(it.link) + } + } + } + } + viewModel.diagrams.forEach { diagram(it) } + viewModel.images.forEach { image(it) } + } + } else + redirectUpPage() +} diff --git a/src/test/kotlin/nl/avisi/structurizr/site/generatr/site/PlantUmlExporterTest.kt b/src/test/kotlin/nl/avisi/structurizr/site/generatr/site/PlantUmlExporterTest.kt index a7c3f7f8..ab1f7063 100644 --- a/src/test/kotlin/nl/avisi/structurizr/site/generatr/site/PlantUmlExporterTest.kt +++ b/src/test/kotlin/nl/avisi/structurizr/site/generatr/site/PlantUmlExporterTest.kt @@ -141,7 +141,7 @@ class PlantUmlExporterTest { assertThat(diagram.definition.withoutC4HeaderAndFooter()).isEqualTo( """ System_Boundary("System1_boundary", "System 1", ${'$'}tags="") { - Container(System1.Container1, "Container 1", ${'$'}techn="", ${'$'}descr="", ${'$'}tags="", ${'$'}link="../system-1/component/") + Container(System1.Container1, "Container 1", ${'$'}techn="", ${'$'}descr="", ${'$'}tags="", ${'$'}link="../system-1/component/container-1/") } """.trimIndent() ) @@ -157,7 +157,7 @@ class PlantUmlExporterTest { assertThat(diagram.definition.withoutStructurizrHeaderAndFooter()).isEqualTo( """ rectangle "System 1\n[Software System]" <> { - rectangle "==Container 1\n[Container]" <> as System1.Container1 [[../system-1/component/]] + rectangle "==Container 1\n[Container]" <> as System1.Container1 [[../system-1/component/container-1/]] } """.trimIndent() ) diff --git a/src/test/kotlin/nl/avisi/structurizr/site/generatr/site/model/SoftwareSystemComponentPageViewModelTest.kt b/src/test/kotlin/nl/avisi/structurizr/site/generatr/site/model/SoftwareSystemComponentPageViewModelTest.kt deleted file mode 100644 index 8a40c10b..00000000 --- a/src/test/kotlin/nl/avisi/structurizr/site/generatr/site/model/SoftwareSystemComponentPageViewModelTest.kt +++ /dev/null @@ -1,77 +0,0 @@ -package nl.avisi.structurizr.site.generatr.site.model - -import assertk.assertThat -import assertk.assertions.containsExactly -import assertk.assertions.hasSize -import assertk.assertions.isEqualTo -import assertk.assertions.isFalse -import assertk.assertions.isTrue -import com.structurizr.model.SoftwareSystem -import kotlin.test.Test - -class SoftwareSystemComponentPageViewModelTest : ViewModelTest() { - private val generatorContext = generatorContext() - private val softwareSystem: SoftwareSystem = generatorContext.workspace.model - .addSoftwareSystem("Software system").also { - val backend = it.addContainer("Backend") - generatorContext.workspace.views.createComponentView(backend, "component-1", "Component view 1") - generatorContext.workspace.views.createComponentView(backend, "component-2", "Component view 2") - } - private val imageView = createImageView(generatorContext.workspace, softwareSystem.containers.single()) - - @Test - fun `active tab`() { - val viewModel = SoftwareSystemComponentPageViewModel(generatorContext, softwareSystem) - - assertThat(viewModel.tabs.single { it.link.active }.tab) - .isEqualTo(SoftwareSystemPageViewModel.Tab.COMPONENT) - } - - @Test - fun `diagrams sorted by key`() { - val viewModel = SoftwareSystemComponentPageViewModel(generatorContext, softwareSystem) - - assertThat(viewModel.visible).isTrue() - assertThat(viewModel.diagrams).containsExactly( - DiagramViewModel( - "component-1", - "Software system - Backend - Components", - "Component view 1", - """""", - 800, - ImageViewModel(viewModel, "/svg/component-1.svg"), - ImageViewModel(viewModel, "/png/component-1.png"), - ImageViewModel(viewModel, "/puml/component-1.puml") - ), - DiagramViewModel( - "component-2", - "Software system - Backend - Components", - "Component view 2", - """""", - 800, - ImageViewModel(viewModel, "/svg/component-2.svg"), - ImageViewModel(viewModel, "/png/component-2.png"), - ImageViewModel(viewModel, "/puml/component-2.puml") - ) - ) - } - - @Test - fun `has image`() { - val viewModel = SoftwareSystemComponentPageViewModel(generatorContext, softwareSystem) - - assertThat(viewModel.visible).isTrue() - assertThat(viewModel.images).hasSize(1) - assertThat(viewModel.images.single().imageView).isEqualTo(imageView) - } - - @Test - fun `hidden view`() { - val viewModel = SoftwareSystemComponentPageViewModel( - generatorContext, - generatorContext.workspace.model.addSoftwareSystem("Software system 2") - ) - - assertThat(viewModel.visible).isFalse() - } -} diff --git a/src/test/kotlin/nl/avisi/structurizr/site/generatr/site/model/SoftwareSystemContainerComponentsPageViewModelTest.kt b/src/test/kotlin/nl/avisi/structurizr/site/generatr/site/model/SoftwareSystemContainerComponentsPageViewModelTest.kt new file mode 100644 index 00000000..fd72961d --- /dev/null +++ b/src/test/kotlin/nl/avisi/structurizr/site/generatr/site/model/SoftwareSystemContainerComponentsPageViewModelTest.kt @@ -0,0 +1,135 @@ +package nl.avisi.structurizr.site.generatr.site.model + +import assertk.assertThat +import assertk.assertions.containsExactly +import assertk.assertions.hasSize +import assertk.assertions.isEqualTo +import assertk.assertions.isFalse +import assertk.assertions.isTrue +import com.structurizr.model.Container +import com.structurizr.model.SoftwareSystem +import nl.avisi.structurizr.site.generatr.normalize +import kotlin.test.Test + +class SoftwareSystemContainerComponentsPageViewModelTest : ViewModelTest() { + private val generatorContext = generatorContext() + private val softwareSystem: SoftwareSystem = generatorContext.workspace.model + .addSoftwareSystem("Software system").also { + val backend = it.addContainer("Backend") + val frontend = it.addContainer("Frontend") + generatorContext.workspace.views.createComponentView(backend, "component-1-backend", "Component view 1 - Backend") + generatorContext.workspace.views.createComponentView(backend, "component-2-backend", "Component view 2 - Backend") + + generatorContext.workspace.views.createComponentView(frontend, "component-1-frontend", "Component view 1 - Frontend") + generatorContext.workspace.views.createComponentView(frontend, "component-2-frontend", "Component view 2 - Frontend") + } + + private val backendContainer: Container = softwareSystem.containers.elementAt(0) + private val frontendContainer: Container = softwareSystem.containers.elementAt(1) + private val backendImageView = createImageView(generatorContext.workspace, backendContainer) + private val frontendImageView = createImageView(generatorContext.workspace, frontendContainer) + + @Test + fun `active tab`() { + val viewModel = SoftwareSystemContainerComponentsPageViewModel(generatorContext, backendContainer) + assertThat(viewModel.tabs.single { it.link.active }.tab) + .isEqualTo(SoftwareSystemPageViewModel.Tab.COMPONENT) + } + + @Test + fun `container tabs`() { + val viewModel = SoftwareSystemContainerComponentsPageViewModel(generatorContext, backendContainer) + val componentTabList = listOf( + ContainerTabViewModel(viewModel, "Backend", "/software-system/component/backend"), + ContainerTabViewModel(viewModel, "Frontend", "/software-system/component/frontend")) + assertThat(viewModel.containerTabs.elementAtOrNull(0)).isEqualTo(componentTabList.elementAt(0)) + assertThat(viewModel.containerTabs.elementAtOrNull(1)).isEqualTo(componentTabList.elementAt(1)) + } + + @Test + fun url() { + var viewModel = SoftwareSystemContainerComponentsPageViewModel(generatorContext, backendContainer) + assertThat(SoftwareSystemContainerComponentsPageViewModel.url(backendContainer)) + .isEqualTo("/${softwareSystem.name.normalize()}/component/${backendContainer.name.normalize()}") + assertThat(viewModel.url) + .isEqualTo(SoftwareSystemContainerComponentsPageViewModel.url(backendContainer)) + + viewModel = SoftwareSystemContainerComponentsPageViewModel(generatorContext, frontendContainer) + assertThat(SoftwareSystemContainerComponentsPageViewModel.url(frontendContainer)) + .isEqualTo("/${softwareSystem.name.normalize()}/component/${frontendContainer.name.normalize()}") + assertThat(viewModel.url) + .isEqualTo(SoftwareSystemContainerComponentsPageViewModel.url(frontendContainer)) + } + + @Test + fun `diagrams correctly mapped to containers`() { + var viewModel = SoftwareSystemContainerComponentsPageViewModel(generatorContext, backendContainer) + assertThat(viewModel.visible).isTrue() + assertThat(viewModel.diagrams).containsExactly( + DiagramViewModel( + "component-1-backend", + "Software system - Backend - Components", + "Component view 1 - Backend", + """""", + 800, + ImageViewModel(viewModel, "/svg/component-1-backend.svg"), + ImageViewModel(viewModel, "/png/component-1-backend.png"), + ImageViewModel(viewModel, "/puml/component-1-backend.puml") + ), + DiagramViewModel( + "component-2-backend", + "Software system - Backend - Components", + "Component view 2 - Backend", + """""", + 800, + ImageViewModel(viewModel, "/svg/component-2-backend.svg"), + ImageViewModel(viewModel, "/png/component-2-backend.png"), + ImageViewModel(viewModel, "/puml/component-2-backend.puml") + ) + ) + + viewModel = SoftwareSystemContainerComponentsPageViewModel(generatorContext, frontendContainer) + assertThat(viewModel.visible).isTrue() + assertThat(viewModel.diagrams).containsExactly( + DiagramViewModel( + "component-1-frontend", + "Software system - Frontend - Components", + "Component view 1 - Frontend", + """""", + 800, + ImageViewModel(viewModel, "/svg/component-1-frontend.svg"), + ImageViewModel(viewModel, "/png/component-1-frontend.png"), + ImageViewModel(viewModel, "/puml/component-1-frontend.puml") + ), + DiagramViewModel( + "component-2-frontend", + "Software system - Frontend - Components", + "Component view 2 - Frontend", + """""", + 800, + ImageViewModel(viewModel, "/svg/component-2-frontend.svg"), + ImageViewModel(viewModel, "/png/component-2-frontend.png"), + ImageViewModel(viewModel, "/puml/component-2-frontend.puml") + ) + ) + } + + @Test + fun `has image`() { + var viewModel = SoftwareSystemContainerComponentsPageViewModel(generatorContext, backendContainer) + assertThat(viewModel.visible).isTrue() + assertThat(viewModel.images).hasSize(1) + assertThat(viewModel.images.single().imageView).isEqualTo(backendImageView) + + viewModel = SoftwareSystemContainerComponentsPageViewModel(generatorContext, frontendContainer) + assertThat(viewModel.visible).isTrue() + assertThat(viewModel.images).hasSize(1) + assertThat(viewModel.images.single().imageView).isEqualTo(frontendImageView) + } + + @Test + fun `hidden view`() { + val viewModel = SoftwareSystemContainerComponentsPageViewModel(generatorContext, softwareSystem.addContainer("Container")) + assertThat(viewModel.visible).isFalse() + } +} diff --git a/src/test/kotlin/nl/avisi/structurizr/site/generatr/site/model/SoftwareSystemPageViewModelTest.kt b/src/test/kotlin/nl/avisi/structurizr/site/generatr/site/model/SoftwareSystemPageViewModelTest.kt index 15378f57..73db47d5 100644 --- a/src/test/kotlin/nl/avisi/structurizr/site/generatr/site/model/SoftwareSystemPageViewModelTest.kt +++ b/src/test/kotlin/nl/avisi/structurizr/site/generatr/site/model/SoftwareSystemPageViewModelTest.kt @@ -30,7 +30,7 @@ class SoftwareSystemPageViewModelTest : ViewModelTest() { } @TestFactory - fun subtitle() = Tab.values().map { tab -> + fun subtitle() = Tab.entries.map { tab -> DynamicTest.dynamicTest("subtitle - $tab") { val generatorContext = generatorContext() val softwareSystem = generatorContext.workspace.model.addSoftwareSystem("Some software system") @@ -84,19 +84,21 @@ class SoftwareSystemPageViewModelTest : ViewModelTest() { } @TestFactory - fun `active tab`() = Tab.values().map { tab -> - DynamicTest.dynamicTest("active tab - $tab") { - val generatorContext = generatorContext() - val softwareSystem = generatorContext.workspace.model.addSoftwareSystem("Some software system") - val viewModel = SoftwareSystemPageViewModel(generatorContext, softwareSystem, tab) - - assertThat( - viewModel.tabs - .single { it.link.active } - .tab - ).isEqualTo(tab) + fun `active tab`() = Tab.entries + .filter { it != Tab.COMPONENT } // Component link is dynamic + .map { tab -> + DynamicTest.dynamicTest("active tab - $tab") { + val generatorContext = generatorContext() + val softwareSystem = generatorContext.workspace.model.addSoftwareSystem("Some software system") + val viewModel = SoftwareSystemPageViewModel(generatorContext, softwareSystem, tab) + + assertThat( + viewModel.tabs + .single { it.link.active } + .tab + ).isEqualTo(tab) + } } - } @Test fun `home tab is visible`() { diff --git a/src/test/kotlin/nl/avisi/structurizr/site/generatr/site/model/ViewModelTest.kt b/src/test/kotlin/nl/avisi/structurizr/site/generatr/site/model/ViewModelTest.kt index a520e768..d67b4765 100644 --- a/src/test/kotlin/nl/avisi/structurizr/site/generatr/site/model/ViewModelTest.kt +++ b/src/test/kotlin/nl/avisi/structurizr/site/generatr/site/model/ViewModelTest.kt @@ -41,7 +41,7 @@ abstract class ViewModelTest { protected fun createSection(content: String = "# Content") = Section(Format.Markdown, content) - protected fun createImageView(workspace: Workspace, element: Element): ImageView = workspace.views.createImageView(element, "imageview-001").also { + protected fun createImageView(workspace: Workspace, element: Element): ImageView = workspace.views.createImageView(element, "imageview-${element.id}").also { it.description = "Image View Description" it.title = "Image View Title" it.contentType = "image/png"