mirror of https://github.com/halo-dev/halo
feat: add method to find path of a specified node in a category tree (#6135)
#### What type of PR is this? /kind feature /area core /milestone 2.17.x #### What this PR does / why we need it: 为分类 Finder 提供获取指定节点的面包屑路径方法 #### Which issue(s) this PR fixes: Fixes #3374 #### Does this PR introduce a user-facing change? ```release-note 为分类 Finder 提供获取指定节点的面包屑路径方法 ```pull/6185/head
parent
bb0a3bc467
commit
0cdd043d1f
|
@ -30,4 +30,6 @@ public interface CategoryFinder {
|
|||
Flux<CategoryTreeVo> listAsTree(String name);
|
||||
|
||||
Mono<CategoryVo> getParentByName(String name);
|
||||
|
||||
Flux<CategoryVo> getBreadcrumbs(String name);
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@ package run.halo.app.theme.finders.impl;
|
|||
import static run.halo.app.extension.index.query.QueryFactory.notEqual;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
|
@ -16,6 +17,7 @@ import org.apache.commons.lang3.BooleanUtils;
|
|||
import org.apache.commons.lang3.ObjectUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.data.domain.Sort;
|
||||
import org.springframework.util.Assert;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
import run.halo.app.content.CategoryService;
|
||||
|
@ -101,8 +103,17 @@ public class CategoryFinderImpl implements CategoryFinder {
|
|||
.map(CategoryVo::from);
|
||||
}
|
||||
|
||||
Flux<CategoryVo> listAllFor(String parentName) {
|
||||
return categoryService.isCategoryHidden(parentName)
|
||||
.flatMapMany(
|
||||
isHidden -> client.listAll(Category.class, new ListOptions(), defaultSort())
|
||||
.filter(category -> isHidden || !category.getSpec().isHideFromList())
|
||||
.map(CategoryVo::from)
|
||||
);
|
||||
}
|
||||
|
||||
Flux<CategoryTreeVo> toCategoryTreeVoFlux(String name) {
|
||||
return listAll()
|
||||
return listAllFor(name)
|
||||
.collectList()
|
||||
.flatMapIterable(categoryVos -> {
|
||||
Map<String, CategoryTreeVo> nameIdentityMap = categoryVos.stream()
|
||||
|
@ -153,6 +164,7 @@ public class CategoryFinderImpl implements CategoryFinder {
|
|||
Category.CategorySpec categorySpec = new Category.CategorySpec();
|
||||
categorySpec.setSlug("/");
|
||||
return CategoryTreeVo.builder()
|
||||
.metadata(new Metadata())
|
||||
.spec(categorySpec)
|
||||
.postCount(0)
|
||||
.children(treeNodes)
|
||||
|
@ -214,6 +226,48 @@ public class CategoryFinderImpl implements CategoryFinder {
|
|||
.map(CategoryVo::from);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Flux<CategoryVo> getBreadcrumbs(String name) {
|
||||
return listAsTree()
|
||||
.collectList()
|
||||
.flatMapMany(treeNodes -> {
|
||||
var rootNode = dummyVirtualRoot(treeNodes);
|
||||
var paths = new ArrayList<CategoryVo>();
|
||||
findPathHelper(rootNode, name, paths);
|
||||
return Flux.fromIterable(paths);
|
||||
});
|
||||
}
|
||||
|
||||
private static boolean findPathHelper(CategoryTreeVo node, String targetName,
|
||||
List<CategoryVo> path) {
|
||||
Assert.notNull(targetName, "Target name must not be null");
|
||||
if (node == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// null name is just a virtual root
|
||||
if (node.getMetadata().getName() != null) {
|
||||
path.add(CategoryTreeVo.toCategoryVo(node));
|
||||
}
|
||||
|
||||
// node maybe a virtual root node so it may have null name
|
||||
if (targetName.equals(node.getMetadata().getName())) {
|
||||
return true;
|
||||
}
|
||||
|
||||
for (CategoryTreeVo child : node.getChildren()) {
|
||||
if (findPathHelper(child, targetName, path)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// if the target node is not in the current subtree, remove the current node to roll back
|
||||
if (!path.isEmpty()) {
|
||||
path.remove(path.size() - 1);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
int pageNullSafe(Integer page) {
|
||||
return ObjectUtils.defaultIfNull(page, 1);
|
||||
}
|
||||
|
|
|
@ -51,6 +51,19 @@ public class CategoryTreeVo implements VisualizableTreeNode<CategoryTreeVo>, Ext
|
|||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert {@link CategoryTreeVo} to {@link CategoryVo}.
|
||||
*/
|
||||
public static CategoryVo toCategoryVo(CategoryTreeVo categoryTreeVo) {
|
||||
Assert.notNull(categoryTreeVo, "The category tree vo must not be null");
|
||||
return CategoryVo.builder()
|
||||
.metadata(categoryTreeVo.getMetadata())
|
||||
.spec(categoryTreeVo.getSpec())
|
||||
.status(categoryTreeVo.getStatus())
|
||||
.postCount(categoryTreeVo.getPostCount())
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String nodeText() {
|
||||
return String.format("%s (%s)%s", getSpec().getDisplayName(), getPostCount(),
|
||||
|
|
|
@ -3,6 +3,7 @@ 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 static org.mockito.Mockito.lenient;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
|
@ -55,6 +56,7 @@ class CategoryFinderImplTest {
|
|||
@BeforeEach
|
||||
void setUp() {
|
||||
categoryFinder = new CategoryFinderImpl(client, categoryService);
|
||||
lenient().when(categoryService.isCategoryHidden(any())).thenReturn(Mono.just(false));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -227,6 +229,44 @@ class CategoryFinderImplTest {
|
|||
└── IndependentChild4 (3)
|
||||
""");
|
||||
}
|
||||
|
||||
@Test
|
||||
void getBreadcrumbsTest() {
|
||||
// first level
|
||||
var breadcrumbs = categoryFinder.getBreadcrumbs("全部").collectList().block();
|
||||
assertThat(toNames(breadcrumbs)).containsSequence("全部");
|
||||
|
||||
// second level
|
||||
breadcrumbs = categoryFinder.getBreadcrumbs("AnotherRootChild").collectList().block();
|
||||
assertThat(toNames(breadcrumbs)).containsSequence("全部", "AnotherRootChild");
|
||||
|
||||
// more levels
|
||||
breadcrumbs = categoryFinder.getBreadcrumbs("DeepNode5").collectList().block();
|
||||
assertThat(toNames(breadcrumbs)).containsSequence("全部", "AnotherRootChild", "Child1",
|
||||
"SubChild2", "DeepNode3", "DeepNode5");
|
||||
|
||||
breadcrumbs = categoryFinder.getBreadcrumbs("IndependentChild4").collectList().block();
|
||||
assertThat(toNames(breadcrumbs)).containsSequence("全部", "FIT2CLOUD",
|
||||
"IndependentNode",
|
||||
"IndependentChild4");
|
||||
|
||||
breadcrumbs = categoryFinder.getBreadcrumbs("SubNode4").collectList().block();
|
||||
assertThat(toNames(breadcrumbs)).containsSequence("全部", "AnotherRootChild", "Child2",
|
||||
"IndependentSubNode", "SubNode4");
|
||||
|
||||
// not exist
|
||||
breadcrumbs = categoryFinder.getBreadcrumbs("not-exist").collectList().block();
|
||||
assertThat(toNames(breadcrumbs)).isEmpty();
|
||||
}
|
||||
|
||||
static List<String> toNames(List<CategoryVo> categories) {
|
||||
if (categories == null) {
|
||||
return List.of();
|
||||
}
|
||||
return categories.stream()
|
||||
.map(category -> category.getMetadata().getName())
|
||||
.toList();
|
||||
}
|
||||
}
|
||||
|
||||
private List<Category> categoriesForTree() {
|
||||
|
|
Loading…
Reference in New Issue