Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Nested Groups in sideNav #180

Merged
merged 10 commits into from
Oct 13, 2023
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,7 @@ architecture model:
| `generatr.search.language` | Indexing/stemming language for the search index. See [Lunr language support](https://github.com/olivernn/lunr-languages) | `en` | `nl` |
| `generatr.markdown.flexmark.extensions` | Additional extensions to the markdown generator to add new markdown capabilities. [More Details](https://avisi-cloud.github.io/structurizr-site-generatr/main/extended-markdown-features/) | Tables | `Tables,Admonition` |
| `generatr.svglink.target` | Specifies the link target for element links in the exported svg | `_top` | `_self` |
| `generatr.site.nestGroups` | Will show software systems in the left side navigator in collapsable groups | `false` | `true` |


See the included example for usage of some those properties in the
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ fun copySiteWideAssets(exportDir: File) {
copySiteWideAsset(exportDir, "/css/admonition.css")
copySiteWideAsset(exportDir, "/js/admonition.js")
copySiteWideAsset(exportDir, "/js/reformat-mermaid.js")
copySiteWideAsset(exportDir, "/css/treeview.css")
copySiteWideAsset(exportDir, "/js/treeview.js")
}

private fun copySiteWideAsset(exportDir: File, asset: String) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package nl.avisi.structurizr.site.generatr.site.model

data class MenuNodeViewModel(val name: String, val children: List<MenuNodeViewModel>)
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,34 @@ class MenuViewModel(generatorContext: GeneratorContext, private val pageViewMode

private fun createMenuItem(title: String, href: String, exact: Boolean = true) =
LinkViewModel(pageViewModel, title, href, exact)

fun softwareSystemNodes(): MenuNodeViewModel {
data class MutableMenuNode(val name: String, val children: MutableList<MutableMenuNode>) {
fun toMenuNode(): MenuNodeViewModel = MenuNodeViewModel(name, children.map { it.toMenuNode() })
}

val rootNode = MutableMenuNode("", mutableListOf())

softwareSystemPaths.forEach { path ->
var currentNode = rootNode
path.split(delimiter).forEach { part ->
val existingNode = currentNode.children.find { it.name == part }
currentNode = if (existingNode == null) {
val newNode = MutableMenuNode(part, mutableListOf())
currentNode.children.add(newNode)
newNode
} else {
existingNode
}
}
}

return rootNode.toMenuNode()
}

private val softwareSystemPaths = generatorContext.workspace.model.includedSoftwareSystems
.map { it.group + "/" + it.name }
.sortedBy { it.lowercase() }

private val delimiter = '/'
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ abstract class PageViewModel(protected val generatorContext: GeneratorContext) {
val includeAdmonition = flexmarkConfig.selectedExtensionMap.containsKey("Admonition")
val includeKatex = flexmarkConfig.selectedExtensionMap.containsKey("GitLab")
val configuration = generatorContext.workspace.views.configuration.properties
val includeTreeview = configuration.getOrDefault("generatr.site.nestGroups", "false").toBoolean()

abstract val url: String
abstract val pageSubTitle: String
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
package nl.avisi.structurizr.site.generatr.site.views

import kotlinx.html.*
import nl.avisi.structurizr.site.generatr.site.GeneratorContext
import nl.avisi.structurizr.site.generatr.site.model.LinkViewModel
import nl.avisi.structurizr.site.generatr.site.model.MenuNodeViewModel
import nl.avisi.structurizr.site.generatr.site.model.MenuViewModel

fun DIV.menu(viewModel: MenuViewModel) {
fun DIV.menu(viewModel: MenuViewModel, nestGroups:Boolean) {
aside(classes = "menu p-3") {
generalSection(viewModel.generalItems)
softwareSystemsSection(viewModel.softwareSystemItems)
softwareSystemsSection(viewModel, nestGroups)
}
}

Expand All @@ -16,9 +18,15 @@ private fun ASIDE.generalSection(items: List<LinkViewModel>) {
menuItemLinks(items)
}

private fun ASIDE.softwareSystemsSection(items: List<LinkViewModel>) {
private fun ASIDE.softwareSystemsSection(viewModel: MenuViewModel, nestGroups:Boolean) {
p(classes = "menu-label") { +"Software systems" }
menuItemLinks(items)
if(nestGroups){
ul(classes = "listree menu-list has-site-branding"){
buildHtmlTree(viewModel.softwareSystemNodes(), viewModel).invoke(this)
}
} else {
menuItemLinks(viewModel.softwareSystemItems)
}
}

private fun ASIDE.menuItemLinks(items: List<LinkViewModel>) {
Expand All @@ -30,3 +38,43 @@ private fun ASIDE.menuItemLinks(items: List<LinkViewModel>) {
}
}
}

private fun buildHtmlTree(node: MenuNodeViewModel, viewModel: MenuViewModel): UL.() -> Unit = {

qtzar marked this conversation as resolved.
Show resolved Hide resolved
if (node.name.isNotEmpty() && node.children.isEmpty()) {
val itemLink = viewModel.softwareSystemItems.find { it.title == node.name }
li {
if (itemLink != null) {
link(itemLink)
}
}
}

if (node.name.isNotEmpty() && node.children.isNotEmpty()) {
li {
if(node.name == "null"){
div(classes = "listree-submenu-heading") {
+"No Group"
}
} else {
div(classes = "listree-submenu-heading") {
+node.name
}
}

ul(classes = "listree-submenu-items") {
for (child in node.children) {
buildHtmlTree(child,viewModel).invoke(this)
}
}
}
}

if (node.name.isEmpty() && node.children.isNotEmpty()) {
qtzar marked this conversation as resolved.
Show resolved Hide resolved
ul(classes = "listree-submenu-items") {
for (child in node.children) {
buildHtmlTree(child, viewModel).invoke(this)
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ private fun HTML.headFragment(viewModel: PageViewModel) {
link(rel = "stylesheet", href = "../" + "/style.css".asUrlToFile(viewModel.url))
link(rel = "stylesheet", href = "./" + "/style-branding.css".asUrlToFile(viewModel.url))

if (viewModel.includeTreeview)
link(rel = "stylesheet", href = "../" + "/treeview.css".asUrlToFile(viewModel.url))

if (viewModel.includeAdmonition)
markdownAdmonitionStylesheet(viewModel)

Expand Down Expand Up @@ -51,7 +54,7 @@ private fun HTML.bodyFragment(viewModel: PageViewModel, block: DIV.() -> Unit) {

div(classes = "site-layout") {
id = "site"
menu(viewModel.menu)
menu(viewModel.menu, viewModel.includeTreeview)
div(classes = "container is-fluid has-background-white") {
block()
}
Expand All @@ -61,6 +64,12 @@ private fun HTML.bodyFragment(viewModel: PageViewModel, block: DIV.() -> Unit) {
updateSiteErrorHero()
if (viewModel.includeAdmonition)
markdownAdmonitionScript(viewModel)

mermaidScript(viewModel)

if (viewModel.includeTreeview){
script(type = ScriptType.textJavaScript,src = "../" + "/treeview.js".asUrlToFile(viewModel.url)) { }
script(type = ScriptType.textJavaScript) { unsafe { +"listree();" } }
}
}
}
36 changes: 36 additions & 0 deletions src/main/resources/assets/css/treeview.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
.listree-submenu-heading {
cursor: pointer;
padding: .5em .75em;
}

ul.listree {
list-style: none
}

ul.listree-submenu-items {
list-style: none;
white-space: nowrap;
padding-left: 10px
}

div.listree-submenu-heading.collapsed:before {
content: "\25B6";
margin-right: 4px
}

div.listree-submenu-heading.expanded:before {
content: "\25BC";
margin-right: 4px
}

.scrollable-menu {
height: auto;
max-width: 800px;
overflow-y: hidden
}

.menu-list li ul {
border-left: 0;
margin: .25em;
padding-left: .75em;
}
65 changes: 65 additions & 0 deletions src/main/resources/assets/js/treeview.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
function listree() {

const subMenuHeadingClass = "listree-submenu-heading";
const expandedClass = "expanded";
const collapsedClass = "collapsed";
const activeClass = "is-active";
const subMenuHeadings = document.getElementsByClassName(subMenuHeadingClass);

Array
.from(subMenuHeadings)
.forEach(function(subMenuHeading) {
// Collapse all the subMenuHeadings while searching for is-active class
let foundActive = false;
subMenuHeading.classList.add(collapsedClass);
subMenuHeading.nextElementSibling.style.display = "none";

// Check if this sub-menu heading is active
const liElements = subMenuHeading.nextElementSibling.children;
for (let i = 0; i < liElements.length; i++) {
if (liElements[i].hasChildNodes()) {
if (liElements[i].children[0].tagName === "A") {
if (liElements[i].children[0].classList.contains(activeClass)) {
foundActive = true;
break;
}
}
}
}

// Expand all parent sub-menus until root menu is reached
if (foundActive) {
subMenuHeading.classList.remove(collapsedClass);
subMenuHeading.classList.add(expandedClass);
subMenuHeading.nextElementSibling.style.display = "block";

let currentSubMenu = subMenuHeading.parentElement;
while (currentSubMenu !== null && !currentSubMenu.classList.contains("listree")) {
if(currentSubMenu.tagName === "UL"){
if(currentSubMenu.previousElementSibling != null) {
currentSubMenu.previousElementSibling.classList.remove(collapsedClass);
currentSubMenu.previousElementSibling.classList.add(expandedClass);
currentSubMenu.style.display = "block";
}
}
currentSubMenu = currentSubMenu.parentElement
}
}

// Add the eventlistener to all subMenuHeadings
subMenuHeading.addEventListener("click", function(event) {
event.preventDefault();
const subMenuList = event.target.nextElementSibling;
if (subMenuList.style.display === "none") {
subMenuHeading.classList.remove(collapsedClass);
subMenuHeading.classList.add(expandedClass);
subMenuHeading.nextElementSibling.style.display = "block";
} else {
subMenuHeading.classList.remove(expandedClass);
subMenuHeading.classList.add(collapsedClass);
subMenuHeading.nextElementSibling.style.display = "none";
}
event.stopPropagation();
});
});
}
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,26 @@ class MenuViewModelTest : ViewModelTest() {
}
}

@Test
fun `show nested groups in software systems list`() {
val generatorContext = generatorContext(branches = listOf("main", "branch-2"), currentBranch = "main")
generatorContext.workspace.views.configuration.addProperty("generatr.site.nestGroups","true")
generatorContext.workspace.model.addSoftwareSystem("System 1").group = "Group 1"
generatorContext.workspace.model.addSoftwareSystem("System 2").group = "Group 1"
generatorContext.workspace.model.addSoftwareSystem("System 3").group = "Group 2"
generatorContext.workspace.model.addSoftwareSystem("System 4").group = "Group 1/Group 3"

MenuViewModel(generatorContext, createPageViewModel(generatorContext, url = HomePageViewModel.url()))
.let {
assertThat(it.softwareSystemNodes().children).hasSize(2)
assertThat(it.softwareSystemNodes().children[0].name).isEqualTo("Group 1")
assertThat(it.softwareSystemNodes().children[0].children).hasSize(3)
assertThat(it.softwareSystemNodes().children[0].children[0].name).isEqualTo("Group 3")
assertThat(it.softwareSystemNodes().children[0].children[0].children[0].name).isEqualTo("System 4")
}

qtzar marked this conversation as resolved.
Show resolved Hide resolved
qtzar marked this conversation as resolved.
Show resolved Hide resolved
}

private fun createPageViewModel(generatorContext: GeneratorContext, url: String = "/master/page"): PageViewModel {
return object : PageViewModel(generatorContext) {
override val url = url
Expand Down