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;
|
package run.halo.app.theme.finders.impl;
|
||||||
|
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.Collection;
|
||||||
import java.util.Comparator;
|
import java.util.Comparator;
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.LinkedHashSet;
|
import java.util.LinkedHashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
@ -9,6 +10,7 @@ import java.util.Objects;
|
||||||
import java.util.function.Function;
|
import java.util.function.Function;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
import org.springframework.util.CollectionUtils;
|
import org.springframework.util.CollectionUtils;
|
||||||
|
import org.springframework.util.comparator.Comparators;
|
||||||
import run.halo.app.core.extension.Menu;
|
import run.halo.app.core.extension.Menu;
|
||||||
import run.halo.app.core.extension.MenuItem;
|
import run.halo.app.core.extension.MenuItem;
|
||||||
import run.halo.app.extension.ReactiveExtensionClient;
|
import run.halo.app.extension.ReactiveExtensionClient;
|
||||||
|
@ -51,7 +53,6 @@ public class MenuFinderImpl implements MenuFinder {
|
||||||
return menuVos.get(0);
|
return menuVos.get(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
List<MenuVo> listAll() {
|
List<MenuVo> listAll() {
|
||||||
return client.list(Menu.class, null, null)
|
return client.list(Menu.class, null, null)
|
||||||
.map(MenuVo::from)
|
.map(MenuVo::from)
|
||||||
|
@ -60,7 +61,7 @@ public class MenuFinderImpl implements MenuFinder {
|
||||||
}
|
}
|
||||||
|
|
||||||
List<MenuVo> listAsTree() {
|
List<MenuVo> listAsTree() {
|
||||||
List<MenuItemVo> menuItemVos = populateParentName(listAllMenuItem());
|
Collection<MenuItemVo> menuItemVos = populateParentName(listAllMenuItem());
|
||||||
List<MenuItemVo> treeList = listToTree(menuItemVos);
|
List<MenuItemVo> treeList = listToTree(menuItemVos);
|
||||||
Map<String, MenuItemVo> nameItemRootNodeMap = treeList.stream()
|
Map<String, MenuItemVo> nameItemRootNodeMap = treeList.stream()
|
||||||
.collect(Collectors.toMap(item -> item.getMetadata().getName(), Function.identity()));
|
.collect(Collectors.toMap(item -> item.getMetadata().getName(), Function.identity()));
|
||||||
|
@ -74,62 +75,65 @@ public class MenuFinderImpl implements MenuFinder {
|
||||||
List<MenuItemVo> menuItems = menuItemNames.stream()
|
List<MenuItemVo> menuItems = menuItemNames.stream()
|
||||||
.map(nameItemRootNodeMap::get)
|
.map(nameItemRootNodeMap::get)
|
||||||
.filter(Objects::nonNull)
|
.filter(Objects::nonNull)
|
||||||
|
.sorted(defaultTreeNodeComparator())
|
||||||
.toList();
|
.toList();
|
||||||
return menuVo.withMenuItems(menuItems);
|
return menuVo.withMenuItems(menuItems);
|
||||||
})
|
})
|
||||||
.toList();
|
.toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
static List<MenuItemVo> listToTree(List<MenuItemVo> list) {
|
static List<MenuItemVo> listToTree(Collection<MenuItemVo> list) {
|
||||||
Map<String, List<MenuItemVo>> nameIdentityMap = list.stream()
|
Map<String, List<MenuItemVo>> parentNameIdentityMap = list.stream()
|
||||||
.filter(item -> item.getParentName() != null)
|
.filter(menuItemVo -> menuItemVo.getParentName() != null)
|
||||||
.collect(Collectors.groupingBy(MenuItemVo::getParentName));
|
.collect(Collectors.groupingBy(MenuItemVo::getParentName));
|
||||||
list.forEach(node -> node.setChildren(nameIdentityMap.get(node.getMetadata().getName())));
|
|
||||||
// clear map to release memory
|
list.forEach(node -> {
|
||||||
nameIdentityMap.clear();
|
// sort children
|
||||||
|
List<MenuItemVo> children =
|
||||||
|
parentNameIdentityMap.getOrDefault(node.getMetadata().getName(), List.of())
|
||||||
|
.stream()
|
||||||
|
.sorted(defaultTreeNodeComparator())
|
||||||
|
.toList();
|
||||||
|
node.setChildren(children);
|
||||||
|
});
|
||||||
|
|
||||||
return list.stream()
|
return list.stream()
|
||||||
.filter(v -> v.getParentName() == null)
|
.filter(v -> v.getParentName() == null)
|
||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
}
|
}
|
||||||
|
|
||||||
List<MenuItemVo> listAllMenuItem() {
|
List<MenuItemVo> listAllMenuItem() {
|
||||||
Function<MenuItem, Integer> priority = menuItem -> menuItem.getSpec().getPriority();
|
return client.list(MenuItem.class, null, null)
|
||||||
Function<MenuItem, String> name = menuItem -> menuItem.getMetadata().getName();
|
|
||||||
|
|
||||||
return client.list(MenuItem.class, null,
|
|
||||||
Comparator.comparing(priority).thenComparing(name).reversed()
|
|
||||||
)
|
|
||||||
.map(MenuItemVo::from)
|
.map(MenuItemVo::from)
|
||||||
.collectList()
|
.collectList()
|
||||||
.block();
|
.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()
|
Map<String, MenuItemVo> nameIdentityMap = menuItemVos.stream()
|
||||||
.collect(Collectors.toMap(menuItem -> menuItem.getMetadata().getName(),
|
.collect(Collectors.toMap(menuItem -> menuItem.getMetadata().getName(),
|
||||||
Function.identity()));
|
Function.identity()));
|
||||||
|
|
||||||
Map<String, MenuItemVo> treeVoMap = new HashMap<>();
|
nameIdentityMap.forEach((name, value) -> {
|
||||||
// populate parentName
|
LinkedHashSet<String> children = value.getSpec().getChildren();
|
||||||
menuItemVos.forEach(menuItemVo -> {
|
if (children != null) {
|
||||||
final String parentName = menuItemVo.getMetadata().getName();
|
for (String child : children) {
|
||||||
treeVoMap.putIfAbsent(parentName, menuItemVo);
|
MenuItemVo childNode = nameIdentityMap.get(child);
|
||||||
LinkedHashSet<String> children = menuItemVo.getSpec().getChildren();
|
childNode.setParentName(name);
|
||||||
if (CollectionUtils.isEmpty(children)) {
|
}
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
children.forEach(childrenName -> {
|
|
||||||
MenuItemVo childrenVo = nameIdentityMap.get(childrenName);
|
|
||||||
childrenVo.setParentName(parentName);
|
|
||||||
treeVoMap.putIfAbsent(childrenVo.getMetadata().getName(), childrenVo);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
// clear map to release memory
|
return nameIdentityMap.values();
|
||||||
nameIdentityMap.clear();
|
|
||||||
Function<MenuItemVo, Integer> priorityCmp = menuItem -> menuItem.getSpec().getPriority();
|
|
||||||
return treeVoMap.values()
|
|
||||||
.stream()
|
|
||||||
.sorted(Comparator.comparing(priorityCmp))
|
|
||||||
.toList();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,6 +4,7 @@ import java.util.List;
|
||||||
import lombok.Builder;
|
import lombok.Builder;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import lombok.ToString;
|
import lombok.ToString;
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import run.halo.app.core.extension.MenuItem;
|
import run.halo.app.core.extension.MenuItem;
|
||||||
import run.halo.app.extension.MetadataOperator;
|
import run.halo.app.extension.MetadataOperator;
|
||||||
|
|
||||||
|
@ -16,7 +17,7 @@ import run.halo.app.extension.MetadataOperator;
|
||||||
@Data
|
@Data
|
||||||
@ToString
|
@ToString
|
||||||
@Builder
|
@Builder
|
||||||
public class MenuItemVo {
|
public class MenuItemVo implements VisualizableTreeNode<MenuItemVo> {
|
||||||
|
|
||||||
MetadataOperator metadata;
|
MetadataOperator metadata;
|
||||||
|
|
||||||
|
@ -28,6 +29,16 @@ public class MenuItemVo {
|
||||||
|
|
||||||
String parentName;
|
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}.
|
* Convert {@link MenuItem} to {@link MenuItemVo}.
|
||||||
*
|
*
|
||||||
|
@ -43,4 +54,9 @@ public class MenuItemVo {
|
||||||
.children(List.of())
|
.children(List.of())
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String nodeText() {
|
||||||
|
return getDisplayName();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
package run.halo.app.theme.finders.vo;
|
package run.halo.app.theme.finders.vo;
|
||||||
|
|
||||||
|
import java.util.Iterator;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import lombok.Builder;
|
import lombok.Builder;
|
||||||
import lombok.ToString;
|
import lombok.ToString;
|
||||||
|
@ -39,4 +40,20 @@ public class MenuVo {
|
||||||
.menuItems(List.of())
|
.menuItems(List.of())
|
||||||
.build();
|
.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;
|
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.any;
|
||||||
import static org.mockito.ArgumentMatchers.eq;
|
import static org.mockito.ArgumentMatchers.eq;
|
||||||
|
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.LinkedHashSet;
|
import java.util.LinkedHashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import org.json.JSONException;
|
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.junit.jupiter.api.extension.ExtendWith;
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
import org.mockito.Mock;
|
import org.mockito.Mock;
|
||||||
import org.mockito.Mockito;
|
import org.mockito.Mockito;
|
||||||
import org.mockito.junit.jupiter.MockitoExtension;
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
import org.skyscreamer.jsonassert.JSONAssert;
|
|
||||||
import reactor.core.publisher.Flux;
|
import reactor.core.publisher.Flux;
|
||||||
import reactor.util.function.Tuple2;
|
import reactor.util.function.Tuple2;
|
||||||
import reactor.util.function.Tuples;
|
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.core.extension.MenuItem;
|
||||||
import run.halo.app.extension.Metadata;
|
import run.halo.app.extension.Metadata;
|
||||||
import run.halo.app.extension.ReactiveExtensionClient;
|
import run.halo.app.extension.ReactiveExtensionClient;
|
||||||
import run.halo.app.infra.utils.JsonUtils;
|
|
||||||
import run.halo.app.theme.finders.vo.MenuVo;
|
import run.halo.app.theme.finders.vo.MenuVo;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -44,7 +42,7 @@ class MenuFinderImplTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void listAsTree() throws JSONException {
|
void listAsTree() {
|
||||||
Tuple2<List<Menu>, List<MenuItem>> tuple = testTree();
|
Tuple2<List<Menu>, List<MenuItem>> tuple = testTree();
|
||||||
Mockito.when(client.list(eq(Menu.class), eq(null), eq(null)))
|
Mockito.when(client.list(eq(Menu.class), eq(null), eq(null)))
|
||||||
.thenReturn(Flux.fromIterable(tuple.getT1()));
|
.thenReturn(Flux.fromIterable(tuple.getT1()));
|
||||||
|
@ -52,141 +50,29 @@ class MenuFinderImplTest {
|
||||||
.thenReturn(Flux.fromIterable(tuple.getT2()));
|
.thenReturn(Flux.fromIterable(tuple.getT2()));
|
||||||
|
|
||||||
List<MenuVo> menuVos = menuFinder.listAsTree();
|
List<MenuVo> menuVos = menuFinder.listAsTree();
|
||||||
JSONAssert.assertEquals("""
|
assertThat(visualizeTree(menuVos)).isEqualTo("""
|
||||||
[
|
D
|
||||||
{
|
└── E
|
||||||
"metadata": {
|
├── A
|
||||||
"name": "D"
|
│ └── B
|
||||||
},
|
└── C
|
||||||
"spec": {
|
X
|
||||||
"displayName": "D",
|
└── G
|
||||||
"menuItems": [
|
Y
|
||||||
"E"
|
└── F
|
||||||
]
|
└── H
|
||||||
},
|
""");
|
||||||
"menuItems": [
|
}
|
||||||
{
|
|
||||||
"metadata": {
|
/**
|
||||||
"name": "E"
|
* Visualize a tree.
|
||||||
},
|
*/
|
||||||
"spec": {
|
String visualizeTree(List<MenuVo> menuVos) {
|
||||||
"displayName": "E",
|
StringBuilder stringBuilder = new StringBuilder();
|
||||||
"priority": 0,
|
for (MenuVo menuVo : menuVos) {
|
||||||
"children": [
|
menuVo.print(stringBuilder);
|
||||||
"A",
|
}
|
||||||
"C"
|
return stringBuilder.toString();
|
||||||
]
|
|
||||||
},
|
|
||||||
"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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Tuple2<List<Menu>, List<MenuItem>> testTree() {
|
Tuple2<List<Menu>, List<MenuItem>> testTree() {
|
||||||
|
|
Loading…
Reference in New Issue