Merge pull request #6116 from guqing/feature/6093

feat: support hide categories and posts from the list
pull/6082/head
John Niang 2024-06-26 19:33:47 +08:00 committed by GitHub
commit 1a0e20b8ab
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
24 changed files with 318 additions and 31 deletions

View File

@ -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"
}, },

View File

@ -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

View File

@ -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;

View File

@ -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 {

View File

@ -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);

View File

@ -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);
} }

View File

@ -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);
} }

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 @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

View File

@ -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();
} }

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.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);
} }

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(); 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()

View File

@ -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) {

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.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);

View File

@ -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
} }
} }
""", """,

View File

@ -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="

View File

@ -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"
/> />

View File

@ -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}

View File

@ -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}

View File

@ -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}

View File

@ -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:

View File

@ -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:

View File

@ -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: