feat: support hide categories and posts from the list

pull/6116/head
guqing 2024-06-26 19:24:47 +08:00
parent 68d428aa29
commit 0196315228
13 changed files with 237 additions and 30 deletions

View File

@ -15506,6 +15506,9 @@
"minLength": 1,
"type": "string"
},
"hideFromList": {
"type": "boolean"
},
"postTemplate": {
"maxLength": 255,
"type": "string"
@ -19659,6 +19662,9 @@
"excerpt": {
"type": "string"
},
"hideFromList": {
"type": "boolean"
},
"inProgress": {
"type": "boolean"
},
@ -21468,6 +21474,9 @@
"excerpt": {
"type": "string"
},
"hideFromList": {
"type": "boolean"
},
"inProgress": {
"type": "boolean"
},

View File

@ -3,7 +3,6 @@ package run.halo.app.content;
import java.time.Duration;
import java.time.Instant;
import org.springframework.context.SmartLifecycle;
import run.halo.app.extension.ExtensionClient;
import run.halo.app.extension.controller.Controller;
import run.halo.app.extension.controller.ControllerBuilder;
import run.halo.app.extension.controller.DefaultController;
@ -24,12 +23,9 @@ public abstract class AbstractEventReconciler<E> implements Reconciler<E>, Smart
protected volatile boolean running = false;
protected final ExtensionClient client;
private final String controllerName;
protected AbstractEventReconciler(String controllerName, ExtensionClient client) {
this.client = client;
protected AbstractEventReconciler(String controllerName) {
this.controllerName = controllerName;
this.queue = new DefaultQueue<>(Instant::now);
this.controller = this.setupWith(null);

View File

@ -37,10 +37,12 @@ import run.halo.app.infra.utils.JsonUtils;
public class CategoryPostCountUpdater
extends AbstractEventReconciler<CategoryPostCountUpdater.PostRelatedCategories> {
protected final ExtensionClient client;
private final CategoryPostCountService categoryPostCountService;
public CategoryPostCountUpdater(ExtensionClient client) {
super(CategoryPostCountUpdater.class.getName(), client);
super(CategoryPostCountUpdater.class.getName());
this.client = client;
this.categoryPostCountService = new CategoryPostCountService(client);
}

View File

@ -2,9 +2,14 @@ package run.halo.app.content;
import org.springframework.lang.NonNull;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import run.halo.app.core.extension.content.Category;
public interface CategoryService {
Flux<Category> listChildren(@NonNull String categoryName);
Mono<Category> getParentByName(@NonNull String categoryName);
Mono<Boolean> isCategoryHidden(@NonNull String categoryName);
}

View File

@ -0,0 +1,55 @@
package run.halo.app.content;
import static run.halo.app.extension.index.query.QueryFactory.equal;
import org.springframework.context.event.EventListener;
import org.springframework.lang.NonNull;
import org.springframework.stereotype.Component;
import run.halo.app.core.extension.content.Post;
import run.halo.app.event.post.CategoryHiddenStateChangeEvent;
import run.halo.app.extension.ListOptions;
import run.halo.app.extension.ReactiveExtensionClient;
import run.halo.app.extension.router.selector.FieldSelector;
import run.halo.app.infra.ReactiveExtensionPaginatedOperator;
/**
* Synchronize the {@link Post.PostStatus#getHideFromList()} state of the post with the category.
*
* @author guqing
* @since 2.17.0
*/
@Component
public class PostHideFromListStateUpdater
extends AbstractEventReconciler<CategoryHiddenStateChangeEvent> {
private final ReactiveExtensionPaginatedOperator reactiveExtensionPaginatedOperator;
private final ReactiveExtensionClient client;
protected PostHideFromListStateUpdater(ReactiveExtensionClient client,
ReactiveExtensionPaginatedOperator reactiveExtensionPaginatedOperator) {
super(PostHideFromListStateUpdater.class.getName());
this.reactiveExtensionPaginatedOperator = reactiveExtensionPaginatedOperator;
this.client = client;
}
@Override
public Result reconcile(CategoryHiddenStateChangeEvent request) {
var listOptions = new ListOptions();
listOptions.setFieldSelector(FieldSelector.of(
equal("spec.categories", request.getCategoryName())
));
reactiveExtensionPaginatedOperator.list(Post.class, listOptions)
.flatMap(post -> {
post.getStatusOrDefault().setHideFromList(request.isHidden());
return client.update(post);
})
.then()
.block();
return Result.doNotRetry();
}
@EventListener(CategoryHiddenStateChangeEvent.class)
public void onApplicationEvent(@NonNull CategoryHiddenStateChangeEvent event) {
this.queue.addImmediately(event);
}
}

View File

@ -35,9 +35,11 @@ import run.halo.app.infra.utils.JsonUtils;
@Component
public class TagPostCountUpdater
extends AbstractEventReconciler<TagPostCountUpdater.PostRelatedTags> {
private final ExtensionClient client;
public TagPostCountUpdater(ExtensionClient client) {
super(TagPostCountUpdater.class.getName(), client);
super(TagPostCountUpdater.class.getName());
this.client = client;
}
@Override

View File

@ -1,13 +1,21 @@
package run.halo.app.content.impl;
import static run.halo.app.extension.index.query.QueryFactory.equal;
import lombok.RequiredArgsConstructor;
import org.apache.commons.lang3.StringUtils;
import org.springframework.data.domain.Sort;
import org.springframework.lang.NonNull;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import run.halo.app.content.CategoryService;
import run.halo.app.core.extension.content.Category;
import run.halo.app.extension.ListOptions;
import run.halo.app.extension.ListResult;
import run.halo.app.extension.PageRequestImpl;
import run.halo.app.extension.ReactiveExtensionClient;
import run.halo.app.extension.router.selector.FieldSelector;
@Component
@RequiredArgsConstructor
@ -28,6 +36,35 @@ public class CategoryServiceImpl implements CategoryService {
});
}
@Override
public Mono<Category> getParentByName(@NonNull String name) {
if (StringUtils.isBlank(name)) {
return Mono.empty();
}
var listOptions = new ListOptions();
listOptions.setFieldSelector(FieldSelector.of(
equal("spec.children", name)
));
return client.listBy(Category.class, listOptions,
PageRequestImpl.of(1, 1, defaultSort())
)
.flatMap(result -> Mono.justOrEmpty(ListResult.first(result)));
}
@Override
public Mono<Boolean> isCategoryHidden(@NonNull String categoryName) {
return client.fetch(Category.class, categoryName)
.expand(category -> getParentByName(category.getMetadata().getName()))
.filter(category -> category.getSpec().isHideFromList())
.hasElements();
}
static Sort defaultSort() {
return Sort.by(Sort.Order.desc("spec.priority"),
Sort.Order.desc("metadata.creationTimestamp"),
Sort.Order.desc("metadata.name"));
}
private boolean isNotIndependent(Category category) {
return !category.getSpec().isPreventParentPostCascadeQuery();
}

View File

@ -2,17 +2,21 @@ package run.halo.app.core.extension.reconciler;
import static run.halo.app.extension.ExtensionUtil.addFinalizers;
import static run.halo.app.extension.ExtensionUtil.removeFinalizers;
import static run.halo.app.extension.MetadataUtil.nullSafeAnnotations;
import java.util.Map;
import java.util.Set;
import lombok.AllArgsConstructor;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import run.halo.app.content.CategoryService;
import run.halo.app.content.permalinks.CategoryPermalinkPolicy;
import run.halo.app.core.extension.content.Category;
import run.halo.app.core.extension.content.Constant;
import run.halo.app.event.post.CategoryHiddenStateChangeEvent;
import run.halo.app.extension.ExtensionClient;
import run.halo.app.extension.ExtensionUtil;
import run.halo.app.extension.MetadataUtil;
import run.halo.app.extension.controller.Controller;
import run.halo.app.extension.controller.ControllerBuilder;
import run.halo.app.extension.controller.Reconciler;
@ -29,6 +33,8 @@ public class CategoryReconciler implements Reconciler<Reconciler.Request> {
static final String FINALIZER_NAME = "category-protection";
private final ExtensionClient client;
private final CategoryPermalinkPolicy categoryPermalinkPolicy;
private final CategoryService categoryService;
private final ApplicationEventPublisher eventPublisher;
@Override
public Result reconcile(Request request) {
@ -44,12 +50,52 @@ public class CategoryReconciler implements Reconciler<Reconciler.Request> {
populatePermalinkPattern(category);
populatePermalink(category);
checkHideFromListState(category);
client.update(category);
});
return Result.doNotRetry();
}
/**
* TODO move this logic to before-create/update hook in the future see {@code gh-4343}.
*/
private void checkHideFromListState(Category category) {
final boolean hidden = categoryService.isCategoryHidden(category.getMetadata().getName())
.blockOptional()
.orElse(false);
category.getSpec().setHideFromList(hidden);
if (isHiddenStateChanged(category)) {
publishHiddenStateChangeEvent(category);
}
var children = category.getSpec().getChildren();
if (CollectionUtils.isEmpty(children)) {
return;
}
for (String childName : children) {
client.fetch(Category.class, childName)
.ifPresent(child -> {
child.getSpec().setHideFromList(hidden);
if (isHiddenStateChanged(child)) {
publishHiddenStateChangeEvent(child);
}
client.update(child);
});
}
}
private void publishHiddenStateChangeEvent(Category category) {
var hidden = category.getSpec().isHideFromList();
nullSafeAnnotations(category).put(Category.LAST_HIDDEN_STATE_ANNO, String.valueOf(hidden));
eventPublisher.publishEvent(new CategoryHiddenStateChangeEvent(this,
category.getMetadata().getName(), hidden));
}
boolean isHiddenStateChanged(Category category) {
var lastHiddenState = nullSafeAnnotations(category).get(Category.LAST_HIDDEN_STATE_ANNO);
return !String.valueOf(category.getSpec().isHideFromList()).equals(lastHiddenState);
}
@Override
public Controller setupWith(ControllerBuilder builder) {
return builder
@ -58,7 +104,7 @@ public class CategoryReconciler implements Reconciler<Reconciler.Request> {
}
void populatePermalinkPattern(Category category) {
Map<String, String> annotations = MetadataUtil.nullSafeAnnotations(category);
Map<String, String> annotations = nullSafeAnnotations(category);
String newPattern = categoryPermalinkPolicy.pattern();
annotations.put(Constant.PERMALINK_PATTERN_ANNO, newPattern);
}

View File

@ -0,0 +1,24 @@
package run.halo.app.event.post;
import lombok.Getter;
import org.springframework.context.ApplicationEvent;
import run.halo.app.core.extension.content.Category;
/**
* When the category {@link Category.CategorySpec#isHideFromList()} state changes, this event is
* triggered.
*
* @author guqing
* @since 2.17.0
*/
@Getter
public class CategoryHiddenStateChangeEvent extends ApplicationEvent {
private final String categoryName;
private final boolean hidden;
public CategoryHiddenStateChangeEvent(Object source, String categoryName, boolean hidden) {
super(source);
this.categoryName = categoryName;
this.hidden = hidden;
}
}

View File

@ -192,6 +192,14 @@ public class SchemeInitializer implements ApplicationListener<ApplicationContext
var lastModifyTime = post.getStatus().getLastModifyTime();
return lastModifyTime == null ? null : lastModifyTime.toString();
})));
indexSpecs.add(new IndexSpec()
.setName("status.hideFromList")
.setIndexFunc(simpleAttribute(Post.class, post -> {
var hidden = post.getStatus().getHideFromList();
// only index when hidden is true
return (hidden == null || !hidden) ? null : BooleanUtils.TRUE;
}))
);
indexSpecs.add(new IndexSpec()
.setName(Post.REQUIRE_SYNC_ON_STARTUP_INDEX_NAME)
.setIndexFunc(simpleAttribute(Post.class, post -> {
@ -238,6 +246,19 @@ public class SchemeInitializer implements ApplicationListener<ApplicationContext
.setName("spec.priority")
.setIndexFunc(simpleAttribute(Category.class,
category -> defaultIfNull(category.getSpec().getPriority(), 0).toString())));
indexSpecs.add(new IndexSpec()
.setName("spec.children")
.setIndexFunc(multiValueAttribute(Category.class, category -> {
var children = category.getSpec().getChildren();
return children == null ? Set.of() : Set.copyOf(children);
}))
);
indexSpecs.add(new IndexSpec()
.setName("spec.hideFromList")
.setIndexFunc(simpleAttribute(Category.class,
category -> toStringTrueFalse(isTrue(category.getSpec().isHideFromList()))
))
);
});
schemeManager.register(Tag.class, indexSpecs -> {
indexSpecs.add(new IndexSpec()

View File

@ -1,5 +1,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.Collection;
import java.util.Comparator;
@ -8,19 +10,21 @@ import java.util.Map;
import java.util.Objects;
import java.util.function.Function;
import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
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 reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import run.halo.app.content.CategoryService;
import run.halo.app.core.extension.content.Category;
import run.halo.app.extension.ListOptions;
import run.halo.app.extension.ListResult;
import run.halo.app.extension.Metadata;
import run.halo.app.extension.PageRequestImpl;
import run.halo.app.extension.ReactiveExtensionClient;
import run.halo.app.extension.index.query.QueryFactory;
import run.halo.app.extension.router.selector.FieldSelector;
import run.halo.app.theme.finders.CategoryFinder;
import run.halo.app.theme.finders.Finder;
@ -35,12 +39,10 @@ import run.halo.app.theme.finders.vo.CategoryVo;
*/
@Slf4j
@Finder("categoryFinder")
@RequiredArgsConstructor
public class CategoryFinderImpl implements CategoryFinder {
private final ReactiveExtensionClient client;
public CategoryFinderImpl(ReactiveExtensionClient client) {
this.client = client;
}
private final CategoryService categoryService;
@Override
public Mono<CategoryVo> getByName(String name) {
@ -65,7 +67,11 @@ public class CategoryFinderImpl implements CategoryFinder {
@Override
public Mono<ListResult<CategoryVo>> list(Integer page, Integer size) {
return client.listBy(Category.class, new ListOptions(),
var listOptions = new ListOptions();
listOptions.setFieldSelector(FieldSelector.of(
notEqual("spec.hideFromList", BooleanUtils.TRUE)
));
return client.listBy(Category.class, listOptions,
PageRequestImpl.of(pageNullSafe(page), sizeNullSafe(size), defaultSort())
)
.map(list -> {
@ -91,6 +97,7 @@ public class CategoryFinderImpl implements CategoryFinder {
@Override
public Flux<CategoryVo> listAll() {
return client.listAll(Category.class, new ListOptions(), defaultSort())
.filter(category -> !category.getSpec().isHideFromList())
.map(CategoryVo::from);
}
@ -203,18 +210,8 @@ public class CategoryFinderImpl implements CategoryFinder {
@Override
public Mono<CategoryVo> getParentByName(String name) {
if (StringUtils.isBlank(name)) {
return Mono.empty();
}
var listOptions = new ListOptions();
listOptions.setFieldSelector(FieldSelector.of(
QueryFactory.equal("spec.children", name)
));
return client.listBy(Category.class, listOptions,
PageRequestImpl.of(1, 1, defaultSort())
)
.map(ListResult::first)
.mapNotNull(item -> item.map(CategoryVo::from).orElse(null));
return categoryService.getParentByName(name)
.map(CategoryVo::from);
}
int pageNullSafe(Integer page) {

View File

@ -3,12 +3,14 @@ package run.halo.app.theme.finders.impl;
import static run.halo.app.extension.index.query.QueryFactory.and;
import static run.halo.app.extension.index.query.QueryFactory.equal;
import static run.halo.app.extension.index.query.QueryFactory.in;
import static run.halo.app.extension.index.query.QueryFactory.notEqual;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import lombok.AllArgsConstructor;
import org.apache.commons.lang3.BooleanUtils;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.data.domain.Sort;
@ -136,7 +138,10 @@ public class PostFinderImpl implements PostFinder {
@Override
public Mono<ListResult<ListedPostVo>> list(Integer page, Integer size) {
return postPublicQueryService.list(new ListOptions(), getPageRequest(page, size));
var listOptions = ListOptions.builder()
.fieldQuery(notEqual("status.hideFromList", BooleanUtils.TRUE))
.build();
return postPublicQueryService.list(listOptions, getPageRequest(page, size));
}
private PageRequestImpl getPageRequest(Integer page, Integer size) {
@ -202,6 +207,9 @@ public class PostFinderImpl implements PostFinder {
public Mono<ListResult<PostArchiveVo>> archives(Integer page, Integer size, String year,
String month) {
var listOptions = new ListOptions();
listOptions.setFieldSelector(FieldSelector.of(
notEqual("status.hideFromList", BooleanUtils.TRUE))
);
var labelSelectorBuilder = LabelSelector.builder();
if (StringUtils.isNotBlank(year)) {
labelSelectorBuilder.eq(Post.ARCHIVE_YEAR_LABEL, year);

View File

@ -24,6 +24,7 @@ import org.springframework.data.domain.Sort;
import org.springframework.util.ResourceUtils;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import run.halo.app.content.CategoryService;
import run.halo.app.core.extension.content.Category;
import run.halo.app.extension.ListOptions;
import run.halo.app.extension.ListResult;
@ -46,11 +47,14 @@ class CategoryFinderImplTest {
@Mock
private ReactiveExtensionClient client;
@Mock
private CategoryService categoryService;
private CategoryFinderImpl categoryFinder;
@BeforeEach
void setUp() {
categoryFinder = new CategoryFinderImpl(client);
categoryFinder = new CategoryFinderImpl(client, categoryService);
}
@Test
@ -78,7 +82,8 @@ class CategoryFinderImplTest {
"C1",
"C2"
],
"preventParentPostCascadeQuery": false
"preventParentPostCascadeQuery": false,
"hideFromList": false
}
}
""",