Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add method to find path of a specified node in a category tree #6135

Merged
merged 1 commit into from
Jun 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,6 @@ public interface CategoryFinder {
Flux<CategoryTreeVo> listAsTree(String name);

Mono<CategoryVo> getParentByName(String name);

Flux<CategoryVo> getBreadcrumbs(String name);
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
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;
Expand All @@ -16,6 +17,7 @@
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;
Expand Down Expand Up @@ -101,8 +103,17 @@ public Flux<CategoryVo> listAll() {
.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()
Expand Down Expand Up @@ -153,6 +164,7 @@ private CategoryTreeVo dummyVirtualRoot(List<CategoryTreeVo> treeNodes) {
Category.CategorySpec categorySpec = new Category.CategorySpec();
categorySpec.setSlug("/");
return CategoryTreeVo.builder()
.metadata(new Metadata())
.spec(categorySpec)
.postCount(0)
.children(treeNodes)
Expand Down Expand Up @@ -214,6 +226,48 @@ public Mono<CategoryVo> getParentByName(String name) {
.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);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,19 @@ public static CategoryTreeVo from(CategoryVo category) {
.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(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
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;
Expand Down Expand Up @@ -55,6 +56,7 @@ class CategoryFinderImplTest {
@BeforeEach
void setUp() {
categoryFinder = new CategoryFinderImpl(client, categoryService);
lenient().when(categoryService.isCategoryHidden(any())).thenReturn(Mono.just(false));
}

@Test
Expand Down Expand Up @@ -227,6 +229,44 @@ void computePostCountFromTree() {
└── 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() {
Expand Down