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

View File

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

View File

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

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