diff --git a/api-docs/openapi/v3_0/aggregated.json b/api-docs/openapi/v3_0/aggregated.json
index f6621399c..80db1e8f7 100644
--- a/api-docs/openapi/v3_0/aggregated.json
+++ b/api-docs/openapi/v3_0/aggregated.json
@@ -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"
},
diff --git a/api/src/main/java/run/halo/app/core/extension/content/Category.java b/api/src/main/java/run/halo/app/core/extension/content/Category.java
index a2ddb04b1..245f681b4 100644
--- a/api/src/main/java/run/halo/app/core/extension/content/Category.java
+++ b/api/src/main/java/run/halo/app/core/extension/content/Category.java
@@ -27,6 +27,7 @@ import run.halo.app.extension.GroupVersionKind;
public class Category extends AbstractExtension {
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);
@@ -79,6 +80,15 @@ public class Category extends AbstractExtension {
* and B will be queried, but C and D will not be queried.
*/
private boolean preventParentPostCascadeQuery;
+
+ /**
+ * Whether to hide the category from the category list.
+ * 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.
+ * 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.
+ */
+ private boolean hideFromList;
}
@JsonIgnore
diff --git a/api/src/main/java/run/halo/app/core/extension/content/Post.java b/api/src/main/java/run/halo/app/core/extension/content/Post.java
index b9209688b..4ea16e74a 100644
--- a/api/src/main/java/run/halo/app/core/extension/content/Post.java
+++ b/api/src/main/java/run/halo/app/core/extension/content/Post.java
@@ -168,6 +168,11 @@ public class Post extends AbstractExtension {
private List contributors;
+ /**
+ * see {@link Category.CategorySpec#isHideFromList()}.
+ */
+ private Boolean hideFromList;
+
private Instant lastModifyTime;
private Long observedVersion;
diff --git a/api/src/main/java/run/halo/app/extension/index/query/NotEqual.java b/api/src/main/java/run/halo/app/extension/index/query/NotEqual.java
index 3ffa33ff0..af8f694dd 100644
--- a/api/src/main/java/run/halo/app/extension/index/query/NotEqual.java
+++ b/api/src/main/java/run/halo/app/extension/index/query/NotEqual.java
@@ -21,7 +21,7 @@ public class NotEqual extends SimpleQuery {
indexView.acquireReadLock();
try {
NavigableSet equalNames = equalQuery.matches(indexView);
- NavigableSet allNames = indexView.getIdsForField(fieldName);
+ NavigableSet allNames = indexView.getAllIds();
allNames.removeAll(equalNames);
return allNames;
} finally {
diff --git a/application/src/main/java/run/halo/app/content/AbstractEventReconciler.java b/application/src/main/java/run/halo/app/content/AbstractEventReconciler.java
index 28105aa78..1a3471921 100644
--- a/application/src/main/java/run/halo/app/content/AbstractEventReconciler.java
+++ b/application/src/main/java/run/halo/app/content/AbstractEventReconciler.java
@@ -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 implements Reconciler, 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);
diff --git a/application/src/main/java/run/halo/app/content/CategoryPostCountUpdater.java b/application/src/main/java/run/halo/app/content/CategoryPostCountUpdater.java
index 3c87fc328..0c7c71435 100644
--- a/application/src/main/java/run/halo/app/content/CategoryPostCountUpdater.java
+++ b/application/src/main/java/run/halo/app/content/CategoryPostCountUpdater.java
@@ -37,10 +37,12 @@ import run.halo.app.infra.utils.JsonUtils;
public class CategoryPostCountUpdater
extends AbstractEventReconciler {
+ 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);
}
diff --git a/application/src/main/java/run/halo/app/content/CategoryService.java b/application/src/main/java/run/halo/app/content/CategoryService.java
index 0b8a17ea1..36f14e5a5 100644
--- a/application/src/main/java/run/halo/app/content/CategoryService.java
+++ b/application/src/main/java/run/halo/app/content/CategoryService.java
@@ -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 listChildren(@NonNull String categoryName);
+
+ Mono getParentByName(@NonNull String categoryName);
+
+ Mono isCategoryHidden(@NonNull String categoryName);
}
diff --git a/application/src/main/java/run/halo/app/content/PostHideFromListStateUpdater.java b/application/src/main/java/run/halo/app/content/PostHideFromListStateUpdater.java
new file mode 100644
index 000000000..12bd88fe8
--- /dev/null
+++ b/application/src/main/java/run/halo/app/content/PostHideFromListStateUpdater.java
@@ -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 {
+ 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);
+ }
+}
diff --git a/application/src/main/java/run/halo/app/content/TagPostCountUpdater.java b/application/src/main/java/run/halo/app/content/TagPostCountUpdater.java
index a1e004367..481da4559 100644
--- a/application/src/main/java/run/halo/app/content/TagPostCountUpdater.java
+++ b/application/src/main/java/run/halo/app/content/TagPostCountUpdater.java
@@ -35,9 +35,11 @@ import run.halo.app.infra.utils.JsonUtils;
@Component
public class TagPostCountUpdater
extends AbstractEventReconciler {
+ private final ExtensionClient client;
public TagPostCountUpdater(ExtensionClient client) {
- super(TagPostCountUpdater.class.getName(), client);
+ super(TagPostCountUpdater.class.getName());
+ this.client = client;
}
@Override
diff --git a/application/src/main/java/run/halo/app/content/impl/CategoryServiceImpl.java b/application/src/main/java/run/halo/app/content/impl/CategoryServiceImpl.java
index 3ee561e61..b7328ab2a 100644
--- a/application/src/main/java/run/halo/app/content/impl/CategoryServiceImpl.java
+++ b/application/src/main/java/run/halo/app/content/impl/CategoryServiceImpl.java
@@ -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 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 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();
}
diff --git a/application/src/main/java/run/halo/app/core/extension/reconciler/CategoryReconciler.java b/application/src/main/java/run/halo/app/core/extension/reconciler/CategoryReconciler.java
index bfd32f007..498757e9e 100644
--- a/application/src/main/java/run/halo/app/core/extension/reconciler/CategoryReconciler.java
+++ b/application/src/main/java/run/halo/app/core/extension/reconciler/CategoryReconciler.java
@@ -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 {
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 {
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 {
}
void populatePermalinkPattern(Category category) {
- Map annotations = MetadataUtil.nullSafeAnnotations(category);
+ Map annotations = nullSafeAnnotations(category);
String newPattern = categoryPermalinkPolicy.pattern();
annotations.put(Constant.PERMALINK_PATTERN_ANNO, newPattern);
}
diff --git a/application/src/main/java/run/halo/app/event/post/CategoryHiddenStateChangeEvent.java b/application/src/main/java/run/halo/app/event/post/CategoryHiddenStateChangeEvent.java
new file mode 100644
index 000000000..92a2d23e5
--- /dev/null
+++ b/application/src/main/java/run/halo/app/event/post/CategoryHiddenStateChangeEvent.java
@@ -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;
+ }
+}
diff --git a/application/src/main/java/run/halo/app/infra/SchemeInitializer.java b/application/src/main/java/run/halo/app/infra/SchemeInitializer.java
index 3d8089393..072113429 100644
--- a/application/src/main/java/run/halo/app/infra/SchemeInitializer.java
+++ b/application/src/main/java/run/halo/app/infra/SchemeInitializer.java
@@ -192,6 +192,14 @@ public class SchemeInitializer implements ApplicationListener {
+ 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 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()
diff --git a/application/src/main/java/run/halo/app/theme/finders/impl/CategoryFinderImpl.java b/application/src/main/java/run/halo/app/theme/finders/impl/CategoryFinderImpl.java
index cbbe0ddc4..af479c116 100644
--- a/application/src/main/java/run/halo/app/theme/finders/impl/CategoryFinderImpl.java
+++ b/application/src/main/java/run/halo/app/theme/finders/impl/CategoryFinderImpl.java
@@ -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 getByName(String name) {
@@ -65,7 +67,11 @@ public class CategoryFinderImpl implements CategoryFinder {
@Override
public Mono> 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 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 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) {
diff --git a/application/src/main/java/run/halo/app/theme/finders/impl/PostFinderImpl.java b/application/src/main/java/run/halo/app/theme/finders/impl/PostFinderImpl.java
index 0651c5c6e..bb417d158 100644
--- a/application/src/main/java/run/halo/app/theme/finders/impl/PostFinderImpl.java
+++ b/application/src/main/java/run/halo/app/theme/finders/impl/PostFinderImpl.java
@@ -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> 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> 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);
diff --git a/application/src/test/java/run/halo/app/theme/finders/impl/CategoryFinderImplTest.java b/application/src/test/java/run/halo/app/theme/finders/impl/CategoryFinderImplTest.java
index fee3d0cb1..2156c20df 100644
--- a/application/src/test/java/run/halo/app/theme/finders/impl/CategoryFinderImplTest.java
+++ b/application/src/test/java/run/halo/app/theme/finders/impl/CategoryFinderImplTest.java
@@ -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
}
}
""",
diff --git a/ui/console-src/modules/contents/posts/categories/components/CategoryEditingModal.vue b/ui/console-src/modules/contents/posts/categories/components/CategoryEditingModal.vue
index b90c8c448..84394a1fd 100644
--- a/ui/console-src/modules/contents/posts/categories/components/CategoryEditingModal.vue
+++ b/ui/console-src/modules/contents/posts/categories/components/CategoryEditingModal.vue
@@ -26,10 +26,12 @@ const props = withDefaults(
defineProps<{
category?: Category;
parentCategory?: Category;
+ isChildLevelCategory: boolean;
}>(),
{
category: undefined,
parentCategory: undefined,
+ isChildLevelCategory: false,
}
);
@@ -266,6 +268,22 @@ const { handleGenerateSlug } = useSlugify(
:accepts="['image/*']"
validation="length:0,1024"
>
+
(), {});
+
const categories = defineModel({
type: Array as PropType,
default: [],
@@ -95,6 +98,7 @@ const handleDelete = async (category: CategoryTree) => {
>
{
/>
+
+
+
+
+
{
diff --git a/ui/packages/api-client/src/models/category-spec.ts b/ui/packages/api-client/src/models/category-spec.ts
index ba762b17b..b066b64bc 100644
--- a/ui/packages/api-client/src/models/category-spec.ts
+++ b/ui/packages/api-client/src/models/category-spec.ts
@@ -44,6 +44,12 @@ export interface CategorySpec {
* @memberof CategorySpec
*/
'displayName': string;
+ /**
+ *
+ * @type {boolean}
+ * @memberof CategorySpec
+ */
+ 'hideFromList'?: boolean;
/**
*
* @type {string}
diff --git a/ui/packages/api-client/src/models/post-status.ts b/ui/packages/api-client/src/models/post-status.ts
index 8a89e30a6..8d6b02274 100644
--- a/ui/packages/api-client/src/models/post-status.ts
+++ b/ui/packages/api-client/src/models/post-status.ts
@@ -47,6 +47,12 @@ export interface PostStatus {
* @memberof PostStatus
*/
'excerpt'?: string;
+ /**
+ *
+ * @type {boolean}
+ * @memberof PostStatus
+ */
+ 'hideFromList'?: boolean;
/**
*
* @type {boolean}
diff --git a/ui/packages/api-client/src/models/single-page-status.ts b/ui/packages/api-client/src/models/single-page-status.ts
index 210999e26..3329d3f59 100644
--- a/ui/packages/api-client/src/models/single-page-status.ts
+++ b/ui/packages/api-client/src/models/single-page-status.ts
@@ -47,6 +47,12 @@ export interface SinglePageStatus {
* @memberof SinglePageStatus
*/
'excerpt'?: string;
+ /**
+ *
+ * @type {boolean}
+ * @memberof SinglePageStatus
+ */
+ 'hideFromList'?: boolean;
/**
*
* @type {boolean}
diff --git a/ui/src/locales/en.yaml b/ui/src/locales/en.yaml
index 7b3aa29cb..1d5bb924d 100644
--- a/ui/src/locales/en.yaml
+++ b/ui/src/locales/en.yaml
@@ -416,11 +416,19 @@ core:
help: >-
Customize the rendering template of posts in the current category,
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:
fields:
prevent_parent_post_cascade_query: >-
Prevent parent category from including this category and its
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:
title: Pages
actions:
diff --git a/ui/src/locales/zh-CN.yaml b/ui/src/locales/zh-CN.yaml
index a6eb90b16..880797a37 100644
--- a/ui/src/locales/zh-CN.yaml
+++ b/ui/src/locales/zh-CN.yaml
@@ -406,9 +406,13 @@ core:
post_template:
label: 自定义文章模板
help: 自定义当前分类下文章的渲染模版,需要主题提供支持
+ hide_from_list:
+ label: 在列表中隐藏
+ help: 开启此选项后,此分类和其下子分类,以及其下文章将不会显示在前台的列表中,需要主动访问分类归档页面,此功能仅对第一级目录生效
list:
fields:
prevent_parent_post_cascade_query: 阻止父级分类在级联文章查询中包含此分类及其子分类
+ hide_from_list: 已隐藏,此分类和其下子分类,以及其下文章将不会显示在前台的列表中
page:
title: 页面
actions:
diff --git a/ui/src/locales/zh-TW.yaml b/ui/src/locales/zh-TW.yaml
index 2bd937f4a..162df9c24 100644
--- a/ui/src/locales/zh-TW.yaml
+++ b/ui/src/locales/zh-TW.yaml
@@ -386,9 +386,13 @@ core:
post_template:
label: 自定義文章模板
help: 自定義當前分類下文章的渲染模板,需要主題提供支持
+ hide_from_list:
+ label: 在列表中隱藏
+ help: 開啟此選項後,此分類和其下子分類,以及其下文章將不會顯示在前臺的列表中,需要主動訪問分類歸檔頁面,此功能僅對第一級目錄生效
list:
fields:
prevent_parent_post_cascade_query: 阻止父級分類在級聯文章查詢中包含此分類及其子分類
+ hide_from_list: 已隱藏,此分類和其下子分類,以及其下文章將不會顯示在前臺的列表中,需要主動訪問分類歸檔頁面
page:
title: 頁面
actions: