mirror of https://github.com/halo-dev/halo
refactor: get default menu (#2587)
#### What type of PR is this? /kind improvement /area core /milestone 2.0 #### What this PR does / why we need it: 优化主题端菜单项查询 #### Which issue(s) this PR fixes: Fixes #2564 #### Special notes for your reviewer: how to test it? 使用 menuFinder.getDefault() 方法测试多级菜单项的排序及新增 /cc @halo-dev/sig-halo #### Does this PR introduce a user-facing change? ```release-note 修复主题端菜单查询数据错误问题 ```pull/2537/head
parent
a3448adee2
commit
3973768a7a
|
@ -1,7 +1,8 @@
|
|||
package run.halo.app.theme.finders.impl;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.Collection;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
@ -9,6 +10,7 @@ import java.util.Objects;
|
|||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import org.springframework.util.comparator.Comparators;
|
||||
import run.halo.app.core.extension.Menu;
|
||||
import run.halo.app.core.extension.MenuItem;
|
||||
import run.halo.app.extension.ReactiveExtensionClient;
|
||||
|
@ -51,7 +53,6 @@ public class MenuFinderImpl implements MenuFinder {
|
|||
return menuVos.get(0);
|
||||
}
|
||||
|
||||
|
||||
List<MenuVo> listAll() {
|
||||
return client.list(Menu.class, null, null)
|
||||
.map(MenuVo::from)
|
||||
|
@ -60,7 +61,7 @@ public class MenuFinderImpl implements MenuFinder {
|
|||
}
|
||||
|
||||
List<MenuVo> listAsTree() {
|
||||
List<MenuItemVo> menuItemVos = populateParentName(listAllMenuItem());
|
||||
Collection<MenuItemVo> menuItemVos = populateParentName(listAllMenuItem());
|
||||
List<MenuItemVo> treeList = listToTree(menuItemVos);
|
||||
Map<String, MenuItemVo> nameItemRootNodeMap = treeList.stream()
|
||||
.collect(Collectors.toMap(item -> item.getMetadata().getName(), Function.identity()));
|
||||
|
@ -74,62 +75,65 @@ public class MenuFinderImpl implements MenuFinder {
|
|||
List<MenuItemVo> menuItems = menuItemNames.stream()
|
||||
.map(nameItemRootNodeMap::get)
|
||||
.filter(Objects::nonNull)
|
||||
.sorted(defaultTreeNodeComparator())
|
||||
.toList();
|
||||
return menuVo.withMenuItems(menuItems);
|
||||
})
|
||||
.toList();
|
||||
}
|
||||
|
||||
static List<MenuItemVo> listToTree(List<MenuItemVo> list) {
|
||||
Map<String, List<MenuItemVo>> nameIdentityMap = list.stream()
|
||||
.filter(item -> item.getParentName() != null)
|
||||
static List<MenuItemVo> listToTree(Collection<MenuItemVo> list) {
|
||||
Map<String, List<MenuItemVo>> parentNameIdentityMap = list.stream()
|
||||
.filter(menuItemVo -> menuItemVo.getParentName() != null)
|
||||
.collect(Collectors.groupingBy(MenuItemVo::getParentName));
|
||||
list.forEach(node -> node.setChildren(nameIdentityMap.get(node.getMetadata().getName())));
|
||||
// clear map to release memory
|
||||
nameIdentityMap.clear();
|
||||
|
||||
list.forEach(node -> {
|
||||
// sort children
|
||||
List<MenuItemVo> children =
|
||||
parentNameIdentityMap.getOrDefault(node.getMetadata().getName(), List.of())
|
||||
.stream()
|
||||
.sorted(defaultTreeNodeComparator())
|
||||
.toList();
|
||||
node.setChildren(children);
|
||||
});
|
||||
|
||||
return list.stream()
|
||||
.filter(v -> v.getParentName() == null)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
List<MenuItemVo> listAllMenuItem() {
|
||||
Function<MenuItem, Integer> priority = menuItem -> menuItem.getSpec().getPriority();
|
||||
Function<MenuItem, String> name = menuItem -> menuItem.getMetadata().getName();
|
||||
|
||||
return client.list(MenuItem.class, null,
|
||||
Comparator.comparing(priority).thenComparing(name).reversed()
|
||||
)
|
||||
return client.list(MenuItem.class, null, null)
|
||||
.map(MenuItemVo::from)
|
||||
.collectList()
|
||||
.block();
|
||||
}
|
||||
|
||||
static List<MenuItemVo> populateParentName(List<MenuItemVo> menuItemVos) {
|
||||
static Comparator<MenuItemVo> defaultTreeNodeComparator() {
|
||||
Function<MenuItemVo, Integer> priority = menuItem -> menuItem.getSpec().getPriority();
|
||||
Function<MenuItemVo, Instant> createTime = menuItem -> menuItem.getMetadata()
|
||||
.getCreationTimestamp();
|
||||
Function<MenuItemVo, String> name = menuItem -> menuItem.getMetadata().getName();
|
||||
|
||||
return Comparator.comparing(priority)
|
||||
.thenComparing(createTime, Comparators.nullsLow())
|
||||
.thenComparing(name);
|
||||
}
|
||||
|
||||
static Collection<MenuItemVo> populateParentName(List<MenuItemVo> menuItemVos) {
|
||||
Map<String, MenuItemVo> nameIdentityMap = menuItemVos.stream()
|
||||
.collect(Collectors.toMap(menuItem -> menuItem.getMetadata().getName(),
|
||||
Function.identity()));
|
||||
|
||||
Map<String, MenuItemVo> treeVoMap = new HashMap<>();
|
||||
// populate parentName
|
||||
menuItemVos.forEach(menuItemVo -> {
|
||||
final String parentName = menuItemVo.getMetadata().getName();
|
||||
treeVoMap.putIfAbsent(parentName, menuItemVo);
|
||||
LinkedHashSet<String> children = menuItemVo.getSpec().getChildren();
|
||||
if (CollectionUtils.isEmpty(children)) {
|
||||
return;
|
||||
nameIdentityMap.forEach((name, value) -> {
|
||||
LinkedHashSet<String> children = value.getSpec().getChildren();
|
||||
if (children != null) {
|
||||
for (String child : children) {
|
||||
MenuItemVo childNode = nameIdentityMap.get(child);
|
||||
childNode.setParentName(name);
|
||||
}
|
||||
}
|
||||
children.forEach(childrenName -> {
|
||||
MenuItemVo childrenVo = nameIdentityMap.get(childrenName);
|
||||
childrenVo.setParentName(parentName);
|
||||
treeVoMap.putIfAbsent(childrenVo.getMetadata().getName(), childrenVo);
|
||||
});
|
||||
});
|
||||
// clear map to release memory
|
||||
nameIdentityMap.clear();
|
||||
Function<MenuItemVo, Integer> priorityCmp = menuItem -> menuItem.getSpec().getPriority();
|
||||
return treeVoMap.values()
|
||||
.stream()
|
||||
.sorted(Comparator.comparing(priorityCmp))
|
||||
.toList();
|
||||
return nameIdentityMap.values();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@ import java.util.List;
|
|||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.ToString;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import run.halo.app.core.extension.MenuItem;
|
||||
import run.halo.app.extension.MetadataOperator;
|
||||
|
||||
|
@ -16,7 +17,7 @@ import run.halo.app.extension.MetadataOperator;
|
|||
@Data
|
||||
@ToString
|
||||
@Builder
|
||||
public class MenuItemVo {
|
||||
public class MenuItemVo implements VisualizableTreeNode<MenuItemVo> {
|
||||
|
||||
MetadataOperator metadata;
|
||||
|
||||
|
@ -28,6 +29,16 @@ public class MenuItemVo {
|
|||
|
||||
String parentName;
|
||||
|
||||
/**
|
||||
* Gets menu item's display name.
|
||||
*/
|
||||
public String getDisplayName() {
|
||||
if (status != null && StringUtils.isNotBlank(status.getDisplayName())) {
|
||||
return status.getDisplayName();
|
||||
}
|
||||
return spec.getDisplayName();
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert {@link MenuItem} to {@link MenuItemVo}.
|
||||
*
|
||||
|
@ -43,4 +54,9 @@ public class MenuItemVo {
|
|||
.children(List.of())
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String nodeText() {
|
||||
return getDisplayName();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package run.halo.app.theme.finders.vo;
|
||||
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import lombok.Builder;
|
||||
import lombok.ToString;
|
||||
|
@ -39,4 +40,20 @@ public class MenuVo {
|
|||
.menuItems(List.of())
|
||||
.build();
|
||||
}
|
||||
|
||||
public void print(StringBuilder buffer) {
|
||||
buffer.append(getSpec().getDisplayName());
|
||||
buffer.append('\n');
|
||||
if (menuItems == null) {
|
||||
return;
|
||||
}
|
||||
for (Iterator<MenuItemVo> it = menuItems.iterator(); it.hasNext(); ) {
|
||||
MenuItemVo next = it.next();
|
||||
if (it.hasNext()) {
|
||||
next.print(buffer, "├── ", "│ ");
|
||||
} else {
|
||||
next.print(buffer, "└── ", " ");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,37 @@
|
|||
package run.halo.app.theme.finders.vo;
|
||||
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Show Tree Hierarchy.
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.0.0
|
||||
*/
|
||||
public interface VisualizableTreeNode<T extends VisualizableTreeNode<T>> {
|
||||
|
||||
/**
|
||||
* Visualize tree node.
|
||||
*/
|
||||
default void print(StringBuilder buffer, String prefix, String childrenPrefix) {
|
||||
buffer.append(prefix);
|
||||
buffer.append(nodeText());
|
||||
buffer.append('\n');
|
||||
if (getChildren() == null) {
|
||||
return;
|
||||
}
|
||||
for (Iterator<T> it = getChildren().iterator(); it.hasNext(); ) {
|
||||
T next = it.next();
|
||||
if (it.hasNext()) {
|
||||
next.print(buffer, childrenPrefix + "├── ", childrenPrefix + "│ ");
|
||||
} else {
|
||||
next.print(buffer, childrenPrefix + "└── ", childrenPrefix + " ");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
String nodeText();
|
||||
|
||||
List<T> getChildren();
|
||||
}
|
|
@ -1,19 +1,18 @@
|
|||
package run.halo.app.theme.finders.impl;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import org.json.JSONException;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.Mockito;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.skyscreamer.jsonassert.JSONAssert;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.util.function.Tuple2;
|
||||
import reactor.util.function.Tuples;
|
||||
|
@ -21,7 +20,6 @@ import run.halo.app.core.extension.Menu;
|
|||
import run.halo.app.core.extension.MenuItem;
|
||||
import run.halo.app.extension.Metadata;
|
||||
import run.halo.app.extension.ReactiveExtensionClient;
|
||||
import run.halo.app.infra.utils.JsonUtils;
|
||||
import run.halo.app.theme.finders.vo.MenuVo;
|
||||
|
||||
/**
|
||||
|
@ -44,7 +42,7 @@ class MenuFinderImplTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
void listAsTree() throws JSONException {
|
||||
void listAsTree() {
|
||||
Tuple2<List<Menu>, List<MenuItem>> tuple = testTree();
|
||||
Mockito.when(client.list(eq(Menu.class), eq(null), eq(null)))
|
||||
.thenReturn(Flux.fromIterable(tuple.getT1()));
|
||||
|
@ -52,141 +50,29 @@ class MenuFinderImplTest {
|
|||
.thenReturn(Flux.fromIterable(tuple.getT2()));
|
||||
|
||||
List<MenuVo> menuVos = menuFinder.listAsTree();
|
||||
JSONAssert.assertEquals("""
|
||||
[
|
||||
{
|
||||
"metadata": {
|
||||
"name": "D"
|
||||
},
|
||||
"spec": {
|
||||
"displayName": "D",
|
||||
"menuItems": [
|
||||
"E"
|
||||
]
|
||||
},
|
||||
"menuItems": [
|
||||
{
|
||||
"metadata": {
|
||||
"name": "E"
|
||||
},
|
||||
"spec": {
|
||||
"displayName": "E",
|
||||
"priority": 0,
|
||||
"children": [
|
||||
"A",
|
||||
"C"
|
||||
]
|
||||
},
|
||||
"status": {},
|
||||
"children": [
|
||||
{
|
||||
"metadata": {
|
||||
"name": "A"
|
||||
},
|
||||
"spec": {
|
||||
"displayName": "A",
|
||||
"priority": 0,
|
||||
"children": [
|
||||
"B"
|
||||
]
|
||||
},
|
||||
"status": {},
|
||||
"children": [
|
||||
{
|
||||
"metadata": {
|
||||
"name": "B"
|
||||
},
|
||||
"spec": {
|
||||
"displayName": "B",
|
||||
"priority": 0
|
||||
},
|
||||
"status": {},
|
||||
"parentName": "A"
|
||||
}
|
||||
],
|
||||
"parentName": "E"
|
||||
},
|
||||
{
|
||||
"metadata": {
|
||||
"name": "C"
|
||||
},
|
||||
"spec": {
|
||||
"displayName": "C",
|
||||
"priority": 0
|
||||
},
|
||||
"status": {},
|
||||
"parentName": "E"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"metadata": {
|
||||
"name": "X"
|
||||
},
|
||||
"spec": {
|
||||
"displayName": "X",
|
||||
"menuItems": [
|
||||
"G"
|
||||
]
|
||||
},
|
||||
"menuItems": [
|
||||
{
|
||||
"metadata": {
|
||||
"name": "G"
|
||||
},
|
||||
"spec": {
|
||||
"displayName": "G",
|
||||
"priority": 0
|
||||
},
|
||||
"status": {}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"metadata": {
|
||||
"name": "Y"
|
||||
},
|
||||
"spec": {
|
||||
"displayName": "Y",
|
||||
"menuItems": [
|
||||
"F"
|
||||
]
|
||||
},
|
||||
"menuItems": [
|
||||
{
|
||||
"metadata": {
|
||||
"name": "F"
|
||||
},
|
||||
"spec": {
|
||||
"displayName": "F",
|
||||
"priority": 0,
|
||||
"children": [
|
||||
"H"
|
||||
]
|
||||
},
|
||||
"status": {},
|
||||
"children": [
|
||||
{
|
||||
"metadata": {
|
||||
"name": "H"
|
||||
},
|
||||
"spec": {
|
||||
"displayName": "H",
|
||||
"priority": 0
|
||||
},
|
||||
"status": {},
|
||||
"parentName": "F"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
""",
|
||||
JsonUtils.objectToJson(menuVos),
|
||||
true);
|
||||
assertThat(visualizeTree(menuVos)).isEqualTo("""
|
||||
D
|
||||
└── E
|
||||
├── A
|
||||
│ └── B
|
||||
└── C
|
||||
X
|
||||
└── G
|
||||
Y
|
||||
└── F
|
||||
└── H
|
||||
""");
|
||||
}
|
||||
|
||||
/**
|
||||
* Visualize a tree.
|
||||
*/
|
||||
String visualizeTree(List<MenuVo> menuVos) {
|
||||
StringBuilder stringBuilder = new StringBuilder();
|
||||
for (MenuVo menuVo : menuVos) {
|
||||
menuVo.print(stringBuilder);
|
||||
}
|
||||
return stringBuilder.toString();
|
||||
}
|
||||
|
||||
Tuple2<List<Menu>, List<MenuItem>> testTree() {
|
||||
|
|
Loading…
Reference in New Issue