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
guqing 2022-10-18 12:14:13 +08:00 committed by GitHub
parent a3448adee2
commit 3973768a7a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 136 additions and 176 deletions

View File

@ -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();
}
}

View File

@ -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();
}
}

View File

@ -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, "└── ", " ");
}
}
}
}

View File

@ -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();
}

View File

@ -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() {