mirror of https://github.com/halo-dev/halo
Merge pull request #6116 from guqing/feature/6093
feat: support hide categories and posts from the listpull/6082/head
commit
1a0e20b8ab
|
@ -15506,6 +15506,9 @@
|
||||||
"minLength": 1,
|
"minLength": 1,
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
"hideFromList": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
"postTemplate": {
|
"postTemplate": {
|
||||||
"maxLength": 255,
|
"maxLength": 255,
|
||||||
"type": "string"
|
"type": "string"
|
||||||
|
@ -19659,6 +19662,9 @@
|
||||||
"excerpt": {
|
"excerpt": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
"hideFromList": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
"inProgress": {
|
"inProgress": {
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
},
|
},
|
||||||
|
@ -21468,6 +21474,9 @@
|
||||||
"excerpt": {
|
"excerpt": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
"hideFromList": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
"inProgress": {
|
"inProgress": {
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
},
|
},
|
||||||
|
|
|
@ -27,6 +27,7 @@ import run.halo.app.extension.GroupVersionKind;
|
||||||
public class Category extends AbstractExtension {
|
public class Category extends AbstractExtension {
|
||||||
|
|
||||||
public static final String KIND = "Category";
|
public static final String KIND = "Category";
|
||||||
|
public static final String LAST_HIDDEN_STATE_ANNO = "content.halo.run/last-hidden-state";
|
||||||
|
|
||||||
public static final GroupVersionKind GVK = GroupVersionKind.fromExtension(Category.class);
|
public static final GroupVersionKind GVK = GroupVersionKind.fromExtension(Category.class);
|
||||||
|
|
||||||
|
@ -79,6 +80,15 @@ public class Category extends AbstractExtension {
|
||||||
* and B will be queried, but C and D will not be queried.</p>
|
* and B will be queried, but C and D will not be queried.</p>
|
||||||
*/
|
*/
|
||||||
private boolean preventParentPostCascadeQuery;
|
private boolean preventParentPostCascadeQuery;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <p>Whether to hide the category from the category list.</p>
|
||||||
|
* <p>When set to true, the category including its subcategories and related posts will
|
||||||
|
* not be displayed in the category list, but it can still be accessed by permalink.</p>
|
||||||
|
* <p>Limitation: It only takes effect on the theme-side categorized list and it only
|
||||||
|
* allows to be set to true on the first level(root node) of categories.</p>
|
||||||
|
*/
|
||||||
|
private boolean hideFromList;
|
||||||
}
|
}
|
||||||
|
|
||||||
@JsonIgnore
|
@JsonIgnore
|
||||||
|
|
|
@ -168,6 +168,11 @@ public class Post extends AbstractExtension {
|
||||||
|
|
||||||
private List<String> contributors;
|
private List<String> contributors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* see {@link Category.CategorySpec#isHideFromList()}.
|
||||||
|
*/
|
||||||
|
private Boolean hideFromList;
|
||||||
|
|
||||||
private Instant lastModifyTime;
|
private Instant lastModifyTime;
|
||||||
|
|
||||||
private Long observedVersion;
|
private Long observedVersion;
|
||||||
|
|
|
@ -21,7 +21,7 @@ public class NotEqual extends SimpleQuery {
|
||||||
indexView.acquireReadLock();
|
indexView.acquireReadLock();
|
||||||
try {
|
try {
|
||||||
NavigableSet<String> equalNames = equalQuery.matches(indexView);
|
NavigableSet<String> equalNames = equalQuery.matches(indexView);
|
||||||
NavigableSet<String> allNames = indexView.getIdsForField(fieldName);
|
NavigableSet<String> allNames = indexView.getAllIds();
|
||||||
allNames.removeAll(equalNames);
|
allNames.removeAll(equalNames);
|
||||||
return allNames;
|
return allNames;
|
||||||
} finally {
|
} finally {
|
||||||
|
|
|
@ -3,7 +3,6 @@ package run.halo.app.content;
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
import org.springframework.context.SmartLifecycle;
|
import org.springframework.context.SmartLifecycle;
|
||||||
import run.halo.app.extension.ExtensionClient;
|
|
||||||
import run.halo.app.extension.controller.Controller;
|
import run.halo.app.extension.controller.Controller;
|
||||||
import run.halo.app.extension.controller.ControllerBuilder;
|
import run.halo.app.extension.controller.ControllerBuilder;
|
||||||
import run.halo.app.extension.controller.DefaultController;
|
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 volatile boolean running = false;
|
||||||
|
|
||||||
protected final ExtensionClient client;
|
|
||||||
|
|
||||||
private final String controllerName;
|
private final String controllerName;
|
||||||
|
|
||||||
protected AbstractEventReconciler(String controllerName, ExtensionClient client) {
|
protected AbstractEventReconciler(String controllerName) {
|
||||||
this.client = client;
|
|
||||||
this.controllerName = controllerName;
|
this.controllerName = controllerName;
|
||||||
this.queue = new DefaultQueue<>(Instant::now);
|
this.queue = new DefaultQueue<>(Instant::now);
|
||||||
this.controller = this.setupWith(null);
|
this.controller = this.setupWith(null);
|
||||||
|
|
|
@ -37,10 +37,12 @@ import run.halo.app.infra.utils.JsonUtils;
|
||||||
public class CategoryPostCountUpdater
|
public class CategoryPostCountUpdater
|
||||||
extends AbstractEventReconciler<CategoryPostCountUpdater.PostRelatedCategories> {
|
extends AbstractEventReconciler<CategoryPostCountUpdater.PostRelatedCategories> {
|
||||||
|
|
||||||
|
protected final ExtensionClient client;
|
||||||
private final CategoryPostCountService categoryPostCountService;
|
private final CategoryPostCountService categoryPostCountService;
|
||||||
|
|
||||||
public CategoryPostCountUpdater(ExtensionClient client) {
|
public CategoryPostCountUpdater(ExtensionClient client) {
|
||||||
super(CategoryPostCountUpdater.class.getName(), client);
|
super(CategoryPostCountUpdater.class.getName());
|
||||||
|
this.client = client;
|
||||||
this.categoryPostCountService = new CategoryPostCountService(client);
|
this.categoryPostCountService = new CategoryPostCountService(client);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,9 +2,14 @@ package run.halo.app.content;
|
||||||
|
|
||||||
import org.springframework.lang.NonNull;
|
import org.springframework.lang.NonNull;
|
||||||
import reactor.core.publisher.Flux;
|
import reactor.core.publisher.Flux;
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
import run.halo.app.core.extension.content.Category;
|
import run.halo.app.core.extension.content.Category;
|
||||||
|
|
||||||
public interface CategoryService {
|
public interface CategoryService {
|
||||||
|
|
||||||
Flux<Category> listChildren(@NonNull String categoryName);
|
Flux<Category> listChildren(@NonNull String categoryName);
|
||||||
|
|
||||||
|
Mono<Category> getParentByName(@NonNull String categoryName);
|
||||||
|
|
||||||
|
Mono<Boolean> isCategoryHidden(@NonNull String categoryName);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -35,9 +35,11 @@ import run.halo.app.infra.utils.JsonUtils;
|
||||||
@Component
|
@Component
|
||||||
public class TagPostCountUpdater
|
public class TagPostCountUpdater
|
||||||
extends AbstractEventReconciler<TagPostCountUpdater.PostRelatedTags> {
|
extends AbstractEventReconciler<TagPostCountUpdater.PostRelatedTags> {
|
||||||
|
private final ExtensionClient client;
|
||||||
|
|
||||||
public TagPostCountUpdater(ExtensionClient client) {
|
public TagPostCountUpdater(ExtensionClient client) {
|
||||||
super(TagPostCountUpdater.class.getName(), client);
|
super(TagPostCountUpdater.class.getName());
|
||||||
|
this.client = client;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -1,13 +1,21 @@
|
||||||
package run.halo.app.content.impl;
|
package run.halo.app.content.impl;
|
||||||
|
|
||||||
|
import static run.halo.app.extension.index.query.QueryFactory.equal;
|
||||||
|
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
import org.springframework.data.domain.Sort;
|
||||||
import org.springframework.lang.NonNull;
|
import org.springframework.lang.NonNull;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
import reactor.core.publisher.Flux;
|
import reactor.core.publisher.Flux;
|
||||||
import reactor.core.publisher.Mono;
|
import reactor.core.publisher.Mono;
|
||||||
import run.halo.app.content.CategoryService;
|
import run.halo.app.content.CategoryService;
|
||||||
import run.halo.app.core.extension.content.Category;
|
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.ReactiveExtensionClient;
|
||||||
|
import run.halo.app.extension.router.selector.FieldSelector;
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
@RequiredArgsConstructor
|
@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) {
|
private boolean isNotIndependent(Category category) {
|
||||||
return !category.getSpec().isPreventParentPostCascadeQuery();
|
return !category.getSpec().isPreventParentPostCascadeQuery();
|
||||||
}
|
}
|
||||||
|
|
|
@ -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.addFinalizers;
|
||||||
import static run.halo.app.extension.ExtensionUtil.removeFinalizers;
|
import static run.halo.app.extension.ExtensionUtil.removeFinalizers;
|
||||||
|
import static run.halo.app.extension.MetadataUtil.nullSafeAnnotations;
|
||||||
|
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
|
import org.springframework.context.ApplicationEventPublisher;
|
||||||
import org.springframework.stereotype.Component;
|
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.content.permalinks.CategoryPermalinkPolicy;
|
||||||
import run.halo.app.core.extension.content.Category;
|
import run.halo.app.core.extension.content.Category;
|
||||||
import run.halo.app.core.extension.content.Constant;
|
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.ExtensionClient;
|
||||||
import run.halo.app.extension.ExtensionUtil;
|
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.Controller;
|
||||||
import run.halo.app.extension.controller.ControllerBuilder;
|
import run.halo.app.extension.controller.ControllerBuilder;
|
||||||
import run.halo.app.extension.controller.Reconciler;
|
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";
|
static final String FINALIZER_NAME = "category-protection";
|
||||||
private final ExtensionClient client;
|
private final ExtensionClient client;
|
||||||
private final CategoryPermalinkPolicy categoryPermalinkPolicy;
|
private final CategoryPermalinkPolicy categoryPermalinkPolicy;
|
||||||
|
private final CategoryService categoryService;
|
||||||
|
private final ApplicationEventPublisher eventPublisher;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Result reconcile(Request request) {
|
public Result reconcile(Request request) {
|
||||||
|
@ -44,12 +50,52 @@ public class CategoryReconciler implements Reconciler<Reconciler.Request> {
|
||||||
|
|
||||||
populatePermalinkPattern(category);
|
populatePermalinkPattern(category);
|
||||||
populatePermalink(category);
|
populatePermalink(category);
|
||||||
|
checkHideFromListState(category);
|
||||||
|
|
||||||
client.update(category);
|
client.update(category);
|
||||||
});
|
});
|
||||||
return Result.doNotRetry();
|
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
|
@Override
|
||||||
public Controller setupWith(ControllerBuilder builder) {
|
public Controller setupWith(ControllerBuilder builder) {
|
||||||
return builder
|
return builder
|
||||||
|
@ -58,7 +104,7 @@ public class CategoryReconciler implements Reconciler<Reconciler.Request> {
|
||||||
}
|
}
|
||||||
|
|
||||||
void populatePermalinkPattern(Category category) {
|
void populatePermalinkPattern(Category category) {
|
||||||
Map<String, String> annotations = MetadataUtil.nullSafeAnnotations(category);
|
Map<String, String> annotations = nullSafeAnnotations(category);
|
||||||
String newPattern = categoryPermalinkPolicy.pattern();
|
String newPattern = categoryPermalinkPolicy.pattern();
|
||||||
annotations.put(Constant.PERMALINK_PATTERN_ANNO, newPattern);
|
annotations.put(Constant.PERMALINK_PATTERN_ANNO, newPattern);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -192,6 +192,14 @@ public class SchemeInitializer implements ApplicationListener<ApplicationContext
|
||||||
var lastModifyTime = post.getStatus().getLastModifyTime();
|
var lastModifyTime = post.getStatus().getLastModifyTime();
|
||||||
return lastModifyTime == null ? null : lastModifyTime.toString();
|
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()
|
indexSpecs.add(new IndexSpec()
|
||||||
.setName(Post.REQUIRE_SYNC_ON_STARTUP_INDEX_NAME)
|
.setName(Post.REQUIRE_SYNC_ON_STARTUP_INDEX_NAME)
|
||||||
.setIndexFunc(simpleAttribute(Post.class, post -> {
|
.setIndexFunc(simpleAttribute(Post.class, post -> {
|
||||||
|
@ -238,6 +246,19 @@ public class SchemeInitializer implements ApplicationListener<ApplicationContext
|
||||||
.setName("spec.priority")
|
.setName("spec.priority")
|
||||||
.setIndexFunc(simpleAttribute(Category.class,
|
.setIndexFunc(simpleAttribute(Category.class,
|
||||||
category -> defaultIfNull(category.getSpec().getPriority(), 0).toString())));
|
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 -> {
|
schemeManager.register(Tag.class, indexSpecs -> {
|
||||||
indexSpecs.add(new IndexSpec()
|
indexSpecs.add(new IndexSpec()
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
package run.halo.app.theme.finders.impl;
|
package run.halo.app.theme.finders.impl;
|
||||||
|
|
||||||
|
import static run.halo.app.extension.index.query.QueryFactory.notEqual;
|
||||||
|
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.Comparator;
|
import java.util.Comparator;
|
||||||
|
@ -8,19 +10,21 @@ import java.util.Map;
|
||||||
import java.util.Objects;
|
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 lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.apache.commons.lang3.BooleanUtils;
|
||||||
import org.apache.commons.lang3.ObjectUtils;
|
import org.apache.commons.lang3.ObjectUtils;
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.springframework.data.domain.Sort;
|
import org.springframework.data.domain.Sort;
|
||||||
import reactor.core.publisher.Flux;
|
import reactor.core.publisher.Flux;
|
||||||
import reactor.core.publisher.Mono;
|
import reactor.core.publisher.Mono;
|
||||||
|
import run.halo.app.content.CategoryService;
|
||||||
import run.halo.app.core.extension.content.Category;
|
import run.halo.app.core.extension.content.Category;
|
||||||
import run.halo.app.extension.ListOptions;
|
import run.halo.app.extension.ListOptions;
|
||||||
import run.halo.app.extension.ListResult;
|
import run.halo.app.extension.ListResult;
|
||||||
import run.halo.app.extension.Metadata;
|
import run.halo.app.extension.Metadata;
|
||||||
import run.halo.app.extension.PageRequestImpl;
|
import run.halo.app.extension.PageRequestImpl;
|
||||||
import run.halo.app.extension.ReactiveExtensionClient;
|
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.extension.router.selector.FieldSelector;
|
||||||
import run.halo.app.theme.finders.CategoryFinder;
|
import run.halo.app.theme.finders.CategoryFinder;
|
||||||
import run.halo.app.theme.finders.Finder;
|
import run.halo.app.theme.finders.Finder;
|
||||||
|
@ -35,12 +39,10 @@ import run.halo.app.theme.finders.vo.CategoryVo;
|
||||||
*/
|
*/
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@Finder("categoryFinder")
|
@Finder("categoryFinder")
|
||||||
|
@RequiredArgsConstructor
|
||||||
public class CategoryFinderImpl implements CategoryFinder {
|
public class CategoryFinderImpl implements CategoryFinder {
|
||||||
private final ReactiveExtensionClient client;
|
private final ReactiveExtensionClient client;
|
||||||
|
private final CategoryService categoryService;
|
||||||
public CategoryFinderImpl(ReactiveExtensionClient client) {
|
|
||||||
this.client = client;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Mono<CategoryVo> getByName(String name) {
|
public Mono<CategoryVo> getByName(String name) {
|
||||||
|
@ -65,7 +67,11 @@ public class CategoryFinderImpl implements CategoryFinder {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Mono<ListResult<CategoryVo>> list(Integer page, Integer size) {
|
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())
|
PageRequestImpl.of(pageNullSafe(page), sizeNullSafe(size), defaultSort())
|
||||||
)
|
)
|
||||||
.map(list -> {
|
.map(list -> {
|
||||||
|
@ -91,6 +97,7 @@ public class CategoryFinderImpl implements CategoryFinder {
|
||||||
@Override
|
@Override
|
||||||
public Flux<CategoryVo> listAll() {
|
public Flux<CategoryVo> listAll() {
|
||||||
return client.listAll(Category.class, new ListOptions(), defaultSort())
|
return client.listAll(Category.class, new ListOptions(), defaultSort())
|
||||||
|
.filter(category -> !category.getSpec().isHideFromList())
|
||||||
.map(CategoryVo::from);
|
.map(CategoryVo::from);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -203,18 +210,8 @@ public class CategoryFinderImpl implements CategoryFinder {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Mono<CategoryVo> getParentByName(String name) {
|
public Mono<CategoryVo> getParentByName(String name) {
|
||||||
if (StringUtils.isBlank(name)) {
|
return categoryService.getParentByName(name)
|
||||||
return Mono.empty();
|
.map(CategoryVo::from);
|
||||||
}
|
|
||||||
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));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
int pageNullSafe(Integer page) {
|
int pageNullSafe(Integer page) {
|
||||||
|
|
|
@ -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.and;
|
||||||
import static run.halo.app.extension.index.query.QueryFactory.equal;
|
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.in;
|
||||||
|
import static run.halo.app.extension.index.query.QueryFactory.notEqual;
|
||||||
|
|
||||||
import java.util.Comparator;
|
import java.util.Comparator;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
|
import org.apache.commons.lang3.BooleanUtils;
|
||||||
import org.apache.commons.lang3.ObjectUtils;
|
import org.apache.commons.lang3.ObjectUtils;
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.springframework.data.domain.Sort;
|
import org.springframework.data.domain.Sort;
|
||||||
|
@ -136,7 +138,10 @@ public class PostFinderImpl implements PostFinder {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Mono<ListResult<ListedPostVo>> list(Integer page, Integer size) {
|
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) {
|
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,
|
public Mono<ListResult<PostArchiveVo>> archives(Integer page, Integer size, String year,
|
||||||
String month) {
|
String month) {
|
||||||
var listOptions = new ListOptions();
|
var listOptions = new ListOptions();
|
||||||
|
listOptions.setFieldSelector(FieldSelector.of(
|
||||||
|
notEqual("status.hideFromList", BooleanUtils.TRUE))
|
||||||
|
);
|
||||||
var labelSelectorBuilder = LabelSelector.builder();
|
var labelSelectorBuilder = LabelSelector.builder();
|
||||||
if (StringUtils.isNotBlank(year)) {
|
if (StringUtils.isNotBlank(year)) {
|
||||||
labelSelectorBuilder.eq(Post.ARCHIVE_YEAR_LABEL, year);
|
labelSelectorBuilder.eq(Post.ARCHIVE_YEAR_LABEL, year);
|
||||||
|
|
|
@ -24,6 +24,7 @@ import org.springframework.data.domain.Sort;
|
||||||
import org.springframework.util.ResourceUtils;
|
import org.springframework.util.ResourceUtils;
|
||||||
import reactor.core.publisher.Flux;
|
import reactor.core.publisher.Flux;
|
||||||
import reactor.core.publisher.Mono;
|
import reactor.core.publisher.Mono;
|
||||||
|
import run.halo.app.content.CategoryService;
|
||||||
import run.halo.app.core.extension.content.Category;
|
import run.halo.app.core.extension.content.Category;
|
||||||
import run.halo.app.extension.ListOptions;
|
import run.halo.app.extension.ListOptions;
|
||||||
import run.halo.app.extension.ListResult;
|
import run.halo.app.extension.ListResult;
|
||||||
|
@ -46,11 +47,14 @@ class CategoryFinderImplTest {
|
||||||
@Mock
|
@Mock
|
||||||
private ReactiveExtensionClient client;
|
private ReactiveExtensionClient client;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private CategoryService categoryService;
|
||||||
|
|
||||||
private CategoryFinderImpl categoryFinder;
|
private CategoryFinderImpl categoryFinder;
|
||||||
|
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
void setUp() {
|
void setUp() {
|
||||||
categoryFinder = new CategoryFinderImpl(client);
|
categoryFinder = new CategoryFinderImpl(client, categoryService);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -78,7 +82,8 @@ class CategoryFinderImplTest {
|
||||||
"C1",
|
"C1",
|
||||||
"C2"
|
"C2"
|
||||||
],
|
],
|
||||||
"preventParentPostCascadeQuery": false
|
"preventParentPostCascadeQuery": false,
|
||||||
|
"hideFromList": false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
""",
|
""",
|
||||||
|
|
|
@ -26,10 +26,12 @@ const props = withDefaults(
|
||||||
defineProps<{
|
defineProps<{
|
||||||
category?: Category;
|
category?: Category;
|
||||||
parentCategory?: Category;
|
parentCategory?: Category;
|
||||||
|
isChildLevelCategory: boolean;
|
||||||
}>(),
|
}>(),
|
||||||
{
|
{
|
||||||
category: undefined,
|
category: undefined,
|
||||||
parentCategory: undefined,
|
parentCategory: undefined,
|
||||||
|
isChildLevelCategory: false,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -266,6 +268,22 @@ const { handleGenerateSlug } = useSlugify(
|
||||||
:accepts="['image/*']"
|
:accepts="['image/*']"
|
||||||
validation="length:0,1024"
|
validation="length:0,1024"
|
||||||
></FormKit>
|
></FormKit>
|
||||||
|
<FormKit
|
||||||
|
v-model="formState.spec.hideFromList"
|
||||||
|
:disabled="isChildLevelCategory"
|
||||||
|
:label="
|
||||||
|
$t(
|
||||||
|
'core.post_category.editing_modal.fields.hide_from_list.label'
|
||||||
|
)
|
||||||
|
"
|
||||||
|
:help="
|
||||||
|
$t(
|
||||||
|
'core.post_category.editing_modal.fields.hide_from_list.help'
|
||||||
|
)
|
||||||
|
"
|
||||||
|
type="checkbox"
|
||||||
|
name="hideFromList"
|
||||||
|
></FormKit>
|
||||||
<FormKit
|
<FormKit
|
||||||
v-model="formState.spec.preventParentPostCascadeQuery"
|
v-model="formState.spec.preventParentPostCascadeQuery"
|
||||||
:label="
|
:label="
|
||||||
|
|
|
@ -5,6 +5,7 @@ import type { Category } from "@halo-dev/api-client";
|
||||||
import { coreApiClient } from "@halo-dev/api-client";
|
import { coreApiClient } from "@halo-dev/api-client";
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
|
IconEyeOff,
|
||||||
IconList,
|
IconList,
|
||||||
Toast,
|
Toast,
|
||||||
VDropdownItem,
|
VDropdownItem,
|
||||||
|
@ -23,6 +24,8 @@ import CategoryEditingModal from "./CategoryEditingModal.vue";
|
||||||
|
|
||||||
const { currentUserHasPermission } = usePermission();
|
const { currentUserHasPermission } = usePermission();
|
||||||
|
|
||||||
|
withDefaults(defineProps<{ isChildLevel?: boolean }>(), {});
|
||||||
|
|
||||||
const categories = defineModel({
|
const categories = defineModel({
|
||||||
type: Array as PropType<CategoryTree[]>,
|
type: Array as PropType<CategoryTree[]>,
|
||||||
default: [],
|
default: [],
|
||||||
|
@ -95,6 +98,7 @@ const handleDelete = async (category: CategoryTree) => {
|
||||||
>
|
>
|
||||||
<CategoryEditingModal
|
<CategoryEditingModal
|
||||||
v-if="editingModal"
|
v-if="editingModal"
|
||||||
|
:is-child-level-category="isChildLevel"
|
||||||
:category="selectedCategory"
|
:category="selectedCategory"
|
||||||
:parent-category="selectedParentCategory"
|
:parent-category="selectedParentCategory"
|
||||||
@close="onEditingModalClose"
|
@close="onEditingModalClose"
|
||||||
|
@ -134,6 +138,14 @@ const handleDelete = async (category: CategoryTree) => {
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
</VEntityField>
|
</VEntityField>
|
||||||
|
<VEntityField v-if="category.spec.hideFromList">
|
||||||
|
<template #description>
|
||||||
|
<IconEyeOff
|
||||||
|
v-tooltip="$t('core.post_category.list.fields.hide_from_list')"
|
||||||
|
class="cursor-pointer text-sm transition-all hover:text-blue-600"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</VEntityField>
|
||||||
<VEntityField v-if="category.spec.preventParentPostCascadeQuery">
|
<VEntityField v-if="category.spec.preventParentPostCascadeQuery">
|
||||||
<template #description>
|
<template #description>
|
||||||
<GridiconsLinkBreak
|
<GridiconsLinkBreak
|
||||||
|
@ -185,6 +197,7 @@ const handleDelete = async (category: CategoryTree) => {
|
||||||
</VEntity>
|
</VEntity>
|
||||||
<CategoryListItem
|
<CategoryListItem
|
||||||
v-model="category.spec.children"
|
v-model="category.spec.children"
|
||||||
|
is-child-level
|
||||||
class="pl-10 transition-all duration-300"
|
class="pl-10 transition-all duration-300"
|
||||||
@change="onChange"
|
@change="onChange"
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -44,6 +44,12 @@ export interface CategorySpec {
|
||||||
* @memberof CategorySpec
|
* @memberof CategorySpec
|
||||||
*/
|
*/
|
||||||
'displayName': string;
|
'displayName': string;
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {boolean}
|
||||||
|
* @memberof CategorySpec
|
||||||
|
*/
|
||||||
|
'hideFromList'?: boolean;
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @type {string}
|
* @type {string}
|
||||||
|
|
|
@ -47,6 +47,12 @@ export interface PostStatus {
|
||||||
* @memberof PostStatus
|
* @memberof PostStatus
|
||||||
*/
|
*/
|
||||||
'excerpt'?: string;
|
'excerpt'?: string;
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {boolean}
|
||||||
|
* @memberof PostStatus
|
||||||
|
*/
|
||||||
|
'hideFromList'?: boolean;
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @type {boolean}
|
* @type {boolean}
|
||||||
|
|
|
@ -47,6 +47,12 @@ export interface SinglePageStatus {
|
||||||
* @memberof SinglePageStatus
|
* @memberof SinglePageStatus
|
||||||
*/
|
*/
|
||||||
'excerpt'?: string;
|
'excerpt'?: string;
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {boolean}
|
||||||
|
* @memberof SinglePageStatus
|
||||||
|
*/
|
||||||
|
'hideFromList'?: boolean;
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @type {boolean}
|
* @type {boolean}
|
||||||
|
|
|
@ -416,11 +416,19 @@ core:
|
||||||
help: >-
|
help: >-
|
||||||
Customize the rendering template of posts in the current category,
|
Customize the rendering template of posts in the current category,
|
||||||
which requires support from the theme
|
which requires support from the theme
|
||||||
|
hide_from_list:
|
||||||
|
label: Hide from list
|
||||||
|
help: >-
|
||||||
|
After turning on this option, this category and its subcategories,
|
||||||
|
as well as its posts, will not be displayed in the front-end list.
|
||||||
|
You need to actively visit the category archive page. This feature
|
||||||
|
is only effective for the first-level directory.
|
||||||
list:
|
list:
|
||||||
fields:
|
fields:
|
||||||
prevent_parent_post_cascade_query: >-
|
prevent_parent_post_cascade_query: >-
|
||||||
Prevent parent category from including this category and its
|
Prevent parent category from including this category and its
|
||||||
subcategories in cascade post queries
|
subcategories in cascade post queries
|
||||||
|
hide_from_list: This category is hidden, This category and its subcategories, as well as its posts, will not be displayed in the front-end list. You need to actively visit the category archive page
|
||||||
page:
|
page:
|
||||||
title: Pages
|
title: Pages
|
||||||
actions:
|
actions:
|
||||||
|
|
|
@ -406,9 +406,13 @@ core:
|
||||||
post_template:
|
post_template:
|
||||||
label: 自定义文章模板
|
label: 自定义文章模板
|
||||||
help: 自定义当前分类下文章的渲染模版,需要主题提供支持
|
help: 自定义当前分类下文章的渲染模版,需要主题提供支持
|
||||||
|
hide_from_list:
|
||||||
|
label: 在列表中隐藏
|
||||||
|
help: 开启此选项后,此分类和其下子分类,以及其下文章将不会显示在前台的列表中,需要主动访问分类归档页面,此功能仅对第一级目录生效
|
||||||
list:
|
list:
|
||||||
fields:
|
fields:
|
||||||
prevent_parent_post_cascade_query: 阻止父级分类在级联文章查询中包含此分类及其子分类
|
prevent_parent_post_cascade_query: 阻止父级分类在级联文章查询中包含此分类及其子分类
|
||||||
|
hide_from_list: 已隐藏,此分类和其下子分类,以及其下文章将不会显示在前台的列表中
|
||||||
page:
|
page:
|
||||||
title: 页面
|
title: 页面
|
||||||
actions:
|
actions:
|
||||||
|
|
|
@ -386,9 +386,13 @@ core:
|
||||||
post_template:
|
post_template:
|
||||||
label: 自定義文章模板
|
label: 自定義文章模板
|
||||||
help: 自定義當前分類下文章的渲染模板,需要主題提供支持
|
help: 自定義當前分類下文章的渲染模板,需要主題提供支持
|
||||||
|
hide_from_list:
|
||||||
|
label: 在列表中隱藏
|
||||||
|
help: 開啟此選項後,此分類和其下子分類,以及其下文章將不會顯示在前臺的列表中,需要主動訪問分類歸檔頁面,此功能僅對第一級目錄生效
|
||||||
list:
|
list:
|
||||||
fields:
|
fields:
|
||||||
prevent_parent_post_cascade_query: 阻止父級分類在級聯文章查詢中包含此分類及其子分類
|
prevent_parent_post_cascade_query: 阻止父級分類在級聯文章查詢中包含此分類及其子分類
|
||||||
|
hide_from_list: 已隱藏,此分類和其下子分類,以及其下文章將不會顯示在前臺的列表中,需要主動訪問分類歸檔頁面
|
||||||
page:
|
page:
|
||||||
title: 頁面
|
title: 頁面
|
||||||
actions:
|
actions:
|
||||||
|
|
Loading…
Reference in New Issue