mirror of https://github.com/halo-dev/halo
fix: list as tree in category finder (#2537)
#### What type of PR is this? /kind bug /area core /milestone 2.0 #### What this PR does / why we need it: 修复分类树查询 #### Which issue(s) this PR fixes: Fixes #2532 #### Special notes for your reviewer: how to test it? 1. 新建分类并,并拖动构建一个树形 2. 在主题端通过 `categoryFinder.listAsTree()` 查看结果是否正确 /cc @halo-dev/sig-halo #### Does this PR introduce a user-facing change? ```release-note 修复分类树状数据查询 ```pull/2604/head
parent
638ceac5a3
commit
9f1eafddc5
|
@ -3,14 +3,13 @@ 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.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.ObjectUtils;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import run.halo.app.core.extension.Category;
|
||||
import run.halo.app.extension.ListResult;
|
||||
import run.halo.app.extension.ReactiveExtensionClient;
|
||||
|
@ -25,6 +24,7 @@ import run.halo.app.theme.finders.vo.CategoryVo;
|
|||
* @author guqing
|
||||
* @since 2.0.0
|
||||
*/
|
||||
@Slf4j
|
||||
@Finder("categoryFinder")
|
||||
public class CategoryFinderImpl implements CategoryFinder {
|
||||
private final ReactiveExtensionClient client;
|
||||
|
@ -75,40 +75,55 @@ public class CategoryFinderImpl implements CategoryFinder {
|
|||
@Override
|
||||
public List<CategoryTreeVo> listAsTree() {
|
||||
List<CategoryVo> categoryVos = listAll();
|
||||
Map<String, CategoryVo> nameIdentityMap = categoryVos.stream()
|
||||
Map<String, CategoryTreeVo> nameIdentityMap = categoryVos.stream()
|
||||
.map(CategoryTreeVo::from)
|
||||
.collect(Collectors.toMap(categoryVo -> categoryVo.getMetadata().getName(),
|
||||
Function.identity()));
|
||||
|
||||
Map<String, CategoryTreeVo> treeVoMap = new HashMap<>();
|
||||
// populate parentName
|
||||
categoryVos.forEach(categoryVo -> {
|
||||
final String parentName = categoryVo.getMetadata().getName();
|
||||
treeVoMap.putIfAbsent(parentName, CategoryTreeVo.from(categoryVo));
|
||||
List<String> children = categoryVo.getSpec().getChildren();
|
||||
if (CollectionUtils.isEmpty(children)) {
|
||||
return;
|
||||
nameIdentityMap.forEach((name, value) -> {
|
||||
List<String> children = value.getSpec().getChildren();
|
||||
if (children != null) {
|
||||
for (String child : children) {
|
||||
CategoryTreeVo childNode = nameIdentityMap.get(child);
|
||||
childNode.setParentName(name);
|
||||
}
|
||||
}
|
||||
children.forEach(childrenName -> {
|
||||
CategoryVo childrenVo = nameIdentityMap.get(childrenName);
|
||||
CategoryTreeVo treeVo = CategoryTreeVo.from(childrenVo);
|
||||
treeVo.setParentName(parentName);
|
||||
treeVoMap.putIfAbsent(treeVo.getMetadata().getName(), treeVo);
|
||||
});
|
||||
});
|
||||
nameIdentityMap.clear();
|
||||
return listToTree(treeVoMap.values());
|
||||
return listToTree(nameIdentityMap.values());
|
||||
}
|
||||
|
||||
static List<CategoryTreeVo> listToTree(Collection<CategoryTreeVo> list) {
|
||||
Map<String, List<CategoryTreeVo>> nameIdentityMap = list.stream()
|
||||
.filter(item -> item.getParentName() != null)
|
||||
Map<String, List<CategoryTreeVo>> parentNameIdentityMap = list.stream()
|
||||
.filter(categoryTreeVo -> categoryTreeVo.getParentName() != null)
|
||||
.collect(Collectors.groupingBy(CategoryTreeVo::getParentName));
|
||||
list.forEach(node -> node.setChildren(nameIdentityMap.get(node.getMetadata().getName())));
|
||||
|
||||
list.forEach(node -> {
|
||||
// sort children
|
||||
List<CategoryTreeVo> children =
|
||||
parentNameIdentityMap.getOrDefault(node.getMetadata().getName(), List.of())
|
||||
.stream()
|
||||
.sorted(defaultTreeNodeComparator())
|
||||
.toList();
|
||||
node.setChildren(children);
|
||||
});
|
||||
return list.stream()
|
||||
.filter(v -> v.getParentName() == null)
|
||||
.sorted(defaultTreeNodeComparator())
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
static Comparator<CategoryTreeVo> defaultTreeNodeComparator() {
|
||||
Function<CategoryTreeVo, Integer> priority =
|
||||
category -> Objects.requireNonNullElse(category.getSpec().getPriority(), 0);
|
||||
Function<CategoryTreeVo, Instant> creationTimestamp =
|
||||
category -> category.getMetadata().getCreationTimestamp();
|
||||
Function<CategoryTreeVo, String> name =
|
||||
category -> category.getMetadata().getName();
|
||||
return Comparator.comparing(priority)
|
||||
.thenComparing(creationTimestamp)
|
||||
.thenComparing(name);
|
||||
}
|
||||
|
||||
static Comparator<Category> defaultComparator() {
|
||||
Function<Category, Integer> priority =
|
||||
category -> Objects.requireNonNullElse(category.getSpec().getPriority(), 0);
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package run.halo.app.theme.finders.vo;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
@ -19,7 +20,7 @@ import run.halo.app.extension.MetadataOperator;
|
|||
@Builder
|
||||
@ToString
|
||||
@EqualsAndHashCode
|
||||
public class CategoryTreeVo {
|
||||
public class CategoryTreeVo implements VisualizableTreeNode<CategoryTreeVo> {
|
||||
|
||||
private MetadataOperator metadata;
|
||||
|
||||
|
@ -46,7 +47,12 @@ public class CategoryTreeVo {
|
|||
.spec(category.getSpec())
|
||||
.status(category.getStatus())
|
||||
.children(List.of())
|
||||
.postCount(category.getPostCount())
|
||||
.postCount(Objects.requireNonNullElse(category.getPostCount(), 0))
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String nodeText() {
|
||||
return String.format("%s (%s)", getSpec().getDisplayName(), getPostCount());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,9 +6,11 @@ import static org.mockito.ArgumentMatchers.anyInt;
|
|||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import org.json.JSONException;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
@ -99,6 +101,28 @@ class CategoryFinderImplTest {
|
|||
assertThat(treeVos).hasSize(1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test for {@link CategoryFinderImpl#listAsTree()}.
|
||||
*
|
||||
* @see <a href="https://github.com/halo-dev/halo/issues/2532">Fix #2532</a>
|
||||
*/
|
||||
@Test
|
||||
void listAsTreeMore() {
|
||||
when(client.list(eq(Category.class), eq(null), any()))
|
||||
.thenReturn(Flux.fromIterable(moreCategories()));
|
||||
List<CategoryTreeVo> treeVos = categoryFinder.listAsTree();
|
||||
String s = visualizeTree(treeVos);
|
||||
assertThat(s).isEqualTo("""
|
||||
全部 (5)
|
||||
├── FIT2CLOUD (2)
|
||||
│ ├── DataEase (0)
|
||||
│ ├── Halo (2)
|
||||
│ ├── MeterSphere (0)
|
||||
│ └── JumpServer (0)
|
||||
└── 默认分类 (3)
|
||||
""");
|
||||
}
|
||||
|
||||
private List<Category> categoriesForTree() {
|
||||
/*
|
||||
* D
|
||||
|
@ -144,6 +168,30 @@ class CategoryFinderImplTest {
|
|||
return List.of(d, e, a, b, c, g, f, h);
|
||||
}
|
||||
|
||||
/**
|
||||
* Visualize a tree.
|
||||
*/
|
||||
String visualizeTree(List<CategoryTreeVo> categoryTreeVos) {
|
||||
Category.CategorySpec categorySpec = new Category.CategorySpec();
|
||||
categorySpec.setSlug("/");
|
||||
categorySpec.setDisplayName("全部");
|
||||
Integer postCount = categoryTreeVos.stream()
|
||||
.map(CategoryTreeVo::getPostCount)
|
||||
.filter(Objects::nonNull)
|
||||
.reduce(Integer::sum)
|
||||
.orElse(0);
|
||||
CategoryTreeVo root = CategoryTreeVo.builder()
|
||||
.spec(categorySpec)
|
||||
.postCount(postCount)
|
||||
.children(categoryTreeVos)
|
||||
.metadata(new Metadata())
|
||||
.build();
|
||||
StringBuilder stringBuilder = new StringBuilder();
|
||||
root.print(stringBuilder, "", "");
|
||||
return stringBuilder.toString();
|
||||
}
|
||||
|
||||
|
||||
private List<Category> categories() {
|
||||
Category category2 = JsonUtils.deepCopy(category());
|
||||
category2.getMetadata().setName("c2");
|
||||
|
@ -176,4 +224,176 @@ class CategoryFinderImplTest {
|
|||
category.setSpec(categorySpec);
|
||||
return category;
|
||||
}
|
||||
|
||||
private List<Category> moreCategories() {
|
||||
String s = """
|
||||
[
|
||||
{
|
||||
"spec":{
|
||||
"displayName":"默认分类",
|
||||
"slug":"default",
|
||||
"description":"这是你的默认分类,如不需要,删除即可。",
|
||||
"cover":"",
|
||||
"template":"",
|
||||
"priority":1,
|
||||
"children":[
|
||||
]
|
||||
},
|
||||
"status":{
|
||||
"permalink":"/categories/default",
|
||||
"postCount":3,
|
||||
"visiblePostCount":3
|
||||
},
|
||||
"apiVersion":"content.halo.run/v1alpha1",
|
||||
"kind":"Category",
|
||||
"metadata":{
|
||||
"name":"76514a40-6ef1-4ed9-b58a-e26945bde3ca",
|
||||
"version":16,
|
||||
"creationTimestamp":"2022-10-08T06:17:47.589181Z"
|
||||
}
|
||||
},
|
||||
{
|
||||
"spec":{
|
||||
"displayName":"MeterSphere",
|
||||
"slug":"metersphere",
|
||||
"description":"",
|
||||
"cover":"",
|
||||
"template":"",
|
||||
"priority":2,
|
||||
"children":[
|
||||
]
|
||||
},
|
||||
"status":{
|
||||
"permalink":"/categories/metersphere",
|
||||
"postCount":0,
|
||||
"visiblePostCount":0
|
||||
},
|
||||
"apiVersion":"content.halo.run/v1alpha1",
|
||||
"kind":"Category",
|
||||
"metadata":{
|
||||
"finalizers":[
|
||||
"category-protection"
|
||||
],
|
||||
"name":"acf09686-d5a7-4227-ba8c-3aeff063f12f",
|
||||
"version":13,
|
||||
"creationTimestamp":"2022-10-08T06:32:36.650974Z"
|
||||
}
|
||||
},
|
||||
{
|
||||
"spec":{
|
||||
"displayName":"DataEase",
|
||||
"slug":"dataease",
|
||||
"description":"",
|
||||
"cover":"",
|
||||
"template":"",
|
||||
"priority":0,
|
||||
"children":[
|
||||
]
|
||||
},
|
||||
"status":{
|
||||
"permalink":"/categories/dataease",
|
||||
"postCount":0,
|
||||
"visiblePostCount":0
|
||||
},
|
||||
"apiVersion":"content.halo.run/v1alpha1",
|
||||
"kind":"Category",
|
||||
"metadata":{
|
||||
"finalizers":[
|
||||
"category-protection"
|
||||
],
|
||||
"name":"bd95f914-22fc-4de5-afcc-a9ffba2f6401",
|
||||
"version":13,
|
||||
"creationTimestamp":"2022-10-08T06:32:53.353838Z"
|
||||
}
|
||||
},
|
||||
{
|
||||
"spec":{
|
||||
"displayName":"FIT2CLOUD",
|
||||
"slug":"fit2cloud",
|
||||
"description":"",
|
||||
"cover":"",
|
||||
"template":"",
|
||||
"priority":0,
|
||||
"children":[
|
||||
"bd95f914-22fc-4de5-afcc-a9ffba2f6401",
|
||||
"e1150fd9-4512-453c-9186-f8de9c156c3d",
|
||||
"acf09686-d5a7-4227-ba8c-3aeff063f12f",
|
||||
"ed064d5e-2b6f-4123-8114-78d0c6f2c4e2"
|
||||
]
|
||||
},
|
||||
"status":{
|
||||
"permalink":"/categories/fit2cloud",
|
||||
"postCount":2,
|
||||
"visiblePostCount":2
|
||||
},
|
||||
"apiVersion":"content.halo.run/v1alpha1",
|
||||
"kind":"Category",
|
||||
"metadata":{
|
||||
"finalizers":[
|
||||
"category-protection"
|
||||
],
|
||||
"name":"c25c17ae-4a7b-43c5-a424-76950b9622cd",
|
||||
"version":14,
|
||||
"creationTimestamp":"2022-10-08T06:32:27.802025Z"
|
||||
}
|
||||
},
|
||||
{
|
||||
"spec":{
|
||||
"displayName":"Halo",
|
||||
"slug":"halo",
|
||||
"description":"",
|
||||
"cover":"",
|
||||
"template":"",
|
||||
"priority":1,
|
||||
"children":[
|
||||
]
|
||||
},
|
||||
"status":{
|
||||
"permalink":"/categories/halo",
|
||||
"postCount":2,
|
||||
"visiblePostCount":2
|
||||
},
|
||||
"apiVersion":"content.halo.run/v1alpha1",
|
||||
"kind":"Category",
|
||||
"metadata":{
|
||||
"finalizers":[
|
||||
"category-protection"
|
||||
],
|
||||
"name":"e1150fd9-4512-453c-9186-f8de9c156c3d",
|
||||
"version":15,
|
||||
"creationTimestamp":"2022-10-08T06:32:42.991788Z"
|
||||
}
|
||||
},
|
||||
{
|
||||
"spec":{
|
||||
"displayName":"JumpServer",
|
||||
"slug":"jumpserver",
|
||||
"description":"",
|
||||
"cover":"",
|
||||
"template":"",
|
||||
"priority":3,
|
||||
"children":[
|
||||
]
|
||||
},
|
||||
"status":{
|
||||
"permalink":"/categories/jumpserver",
|
||||
"postCount":0,
|
||||
"visiblePostCount":0
|
||||
},
|
||||
"apiVersion":"content.halo.run/v1alpha1",
|
||||
"kind":"Category",
|
||||
"metadata":{
|
||||
"finalizers":[
|
||||
"category-protection"
|
||||
],
|
||||
"name":"ed064d5e-2b6f-4123-8114-78d0c6f2c4e2",
|
||||
"version":13,
|
||||
"creationTimestamp":"2022-10-08T06:33:00.557435Z"
|
||||
}
|
||||
}
|
||||
]
|
||||
""";
|
||||
return JsonUtils.jsonToObject(s, new TypeReference<>() {
|
||||
});
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue