diff --git a/api-docs/openapi/v3_0/aggregated.json b/api-docs/openapi/v3_0/aggregated.json
index 82f97bcde5..c65f27f3a0 100644
--- a/api-docs/openapi/v3_0/aggregated.json
+++ b/api-docs/openapi/v3_0/aggregated.json
@@ -3109,6 +3109,13 @@
"schema": {
"type": "string"
}
+ },
+ {
+ "in": "query",
+ "name": "async",
+ "schema": {
+ "type": "boolean"
+ }
}
],
"responses": {
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 e606168df2..b9209688b8 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
@@ -47,6 +47,12 @@ public class Post extends AbstractExtension {
public static final String STATS_ANNO = "content.halo.run/stats";
+ /**
+ *
The key of the label that indicates that the post is scheduled to be published.
+ * Can be used to query posts that are scheduled to be published.
+ */
+ public static final String SCHEDULING_PUBLISH_LABEL = "content.halo.run/scheduling-publish";
+
public static final String DELETED_LABEL = "content.halo.run/deleted";
public static final String PUBLISHED_LABEL = "content.halo.run/published";
public static final String OWNER_LABEL = "content.halo.run/owner";
diff --git a/application/src/main/java/run/halo/app/core/extension/endpoint/PostEndpoint.java b/application/src/main/java/run/halo/app/core/extension/endpoint/PostEndpoint.java
index 42161266ac..51d48d2d3d 100644
--- a/application/src/main/java/run/halo/app/core/extension/endpoint/PostEndpoint.java
+++ b/application/src/main/java/run/halo/app/core/extension/endpoint/PostEndpoint.java
@@ -4,13 +4,16 @@
import static org.springdoc.core.fn.builders.content.Builder.contentBuilder;
import static org.springdoc.core.fn.builders.parameter.Builder.parameterBuilder;
import static org.springdoc.core.fn.builders.requestbody.Builder.requestBodyBuilder;
+import static run.halo.app.extension.MetadataUtil.nullSafeLabels;
import io.swagger.v3.oas.annotations.enums.ParameterIn;
import io.swagger.v3.oas.annotations.media.Schema;
import java.time.Duration;
import java.util.Objects;
+import java.util.function.Predicate;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.BooleanUtils;
import org.springdoc.core.fn.builders.schema.Builder;
import org.springdoc.webflux.core.fn.SpringdocRouteBuilder;
import org.springframework.dao.OptimisticLockingFailureException;
@@ -199,6 +202,11 @@ public RouterFunction endpoint() {
.description("Head snapshot name of content.")
.in(ParameterIn.QUERY)
.required(false))
+ .parameter(parameterBuilder()
+ .name("async")
+ .in(ParameterIn.QUERY)
+ .implementation(Boolean.class)
+ .required(false))
.response(responseBuilder()
.implementation(Post.class))
)
@@ -319,6 +327,7 @@ Mono publishPost(ServerRequest request) {
boolean asyncPublish = request.queryParam("async")
.map(Boolean::parseBoolean)
.orElse(false);
+
return Mono.defer(() -> client.get(Post.class, name)
.doOnNext(post -> {
var spec = post.getSpec();
@@ -327,7 +336,6 @@ Mono publishPost(ServerRequest request) {
if (spec.getHeadSnapshot() == null) {
spec.setHeadSnapshot(spec.getBaseSnapshot());
}
- // TODO Provide release snapshot query param to control
spec.setReleaseSnapshot(spec.getHeadSnapshot());
})
.flatMap(client::update)
@@ -342,12 +350,17 @@ Mono publishPost(ServerRequest request) {
}
private Mono awaitPostPublished(String postName) {
+ Predicate schedulePublish = post -> {
+ var labels = nullSafeLabels(post);
+ return BooleanUtils.TRUE.equals(labels.get(Post.SCHEDULING_PUBLISH_LABEL));
+ };
return Mono.defer(() -> client.get(Post.class, postName)
.filter(post -> {
var releasedSnapshot = MetadataUtil.nullSafeAnnotations(post)
.get(Post.LAST_RELEASED_SNAPSHOT_ANNO);
var expectReleaseSnapshot = post.getSpec().getReleaseSnapshot();
- return Objects.equals(releasedSnapshot, expectReleaseSnapshot);
+ return Objects.equals(releasedSnapshot, expectReleaseSnapshot)
+ || schedulePublish.test(post);
})
.switchIfEmpty(Mono.error(
() -> new RetryException("Retry to check post publish status"))))
diff --git a/application/src/main/java/run/halo/app/core/extension/reconciler/PostReconciler.java b/application/src/main/java/run/halo/app/core/extension/reconciler/PostReconciler.java
index 6b464d784f..3aa1970024 100644
--- a/application/src/main/java/run/halo/app/core/extension/reconciler/PostReconciler.java
+++ b/application/src/main/java/run/halo/app/core/extension/reconciler/PostReconciler.java
@@ -1,14 +1,19 @@
package run.halo.app.core.extension.reconciler;
import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.apache.commons.lang3.BooleanUtils.TRUE;
+import static org.apache.commons.lang3.BooleanUtils.isFalse;
+import static org.apache.commons.lang3.BooleanUtils.isTrue;
import static org.apache.commons.lang3.ObjectUtils.defaultIfNull;
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 static run.halo.app.extension.MetadataUtil.nullSafeLabels;
import static run.halo.app.extension.index.query.QueryFactory.equal;
import com.google.common.hash.Hashing;
+import java.time.Duration;
import java.time.Instant;
-import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
@@ -46,6 +51,7 @@
import run.halo.app.extension.controller.Controller;
import run.halo.app.extension.controller.ControllerBuilder;
import run.halo.app.extension.controller.Reconciler;
+import run.halo.app.extension.controller.RequeueException;
import run.halo.app.extension.index.query.QueryFactory;
import run.halo.app.extension.router.selector.FieldSelector;
import run.halo.app.infra.Condition;
@@ -96,26 +102,13 @@ public Result reconcile(Request request) {
events.forEach(eventPublisher::publishEvent);
return;
}
-
addFinalizers(post.getMetadata(), Set.of(FINALIZER_NAME));
- subscribeNewCommentNotification(post);
+ populateLabels(post);
- var labels = post.getMetadata().getLabels();
- if (labels == null) {
- labels = new HashMap<>();
- post.getMetadata().setLabels(labels);
- }
-
- var annotations = post.getMetadata().getAnnotations();
- if (annotations == null) {
- annotations = new HashMap<>();
- post.getMetadata().setAnnotations(annotations);
- }
+ schedulePublishIfNecessary(post);
- if (!annotations.containsKey(Post.PUBLISHED_LABEL)) {
- labels.put(Post.PUBLISHED_LABEL, BooleanUtils.FALSE);
- }
+ subscribeNewCommentNotification(post);
var status = post.getStatus();
if (status == null) {
@@ -131,6 +124,7 @@ public Result reconcile(Request request) {
var configSha256sum = Hashing.sha256().hashString(post.getSpec().toString(), UTF_8)
.toString();
+ var annotations = nullSafeAnnotations(post);
var oldConfigChecksum = annotations.get(Constant.CHECKSUM_CONFIG_ANNO);
if (!Objects.equals(oldConfigChecksum, configSha256sum)) {
// if the checksum doesn't match
@@ -138,37 +132,12 @@ public Result reconcile(Request request) {
annotations.put(Constant.CHECKSUM_CONFIG_ANNO, configSha256sum);
}
- var expectDelete = defaultIfNull(post.getSpec().getDeleted(), false);
- var expectPublish = defaultIfNull(post.getSpec().getPublish(), false);
-
- if (expectDelete || !expectPublish) {
+ if (shouldUnPublish(post)) {
unPublishPost(post, events);
} else {
publishPost(post, events);
}
- labels.put(Post.DELETED_LABEL, expectDelete.toString());
-
- var expectVisible = defaultIfNull(post.getSpec().getVisible(), VisibleEnum.PUBLIC);
- var oldVisible = VisibleEnum.from(labels.get(Post.VISIBLE_LABEL));
- if (!Objects.equals(oldVisible, expectVisible)) {
- eventPublisher.publishEvent(
- new PostVisibleChangedEvent(request.name(), oldVisible, expectVisible));
- }
- labels.put(Post.VISIBLE_LABEL, expectVisible.toString());
-
- var ownerName = post.getSpec().getOwner();
- if (StringUtils.isNotBlank(ownerName)) {
- labels.put(Post.OWNER_LABEL, ownerName);
- }
-
- var publishTime = post.getSpec().getPublishTime();
- if (publishTime != null) {
- labels.put(Post.ARCHIVE_YEAR_LABEL, HaloUtils.getYearText(publishTime));
- labels.put(Post.ARCHIVE_MONTH_LABEL, HaloUtils.getMonthText(publishTime));
- labels.put(Post.ARCHIVE_DAY_LABEL, HaloUtils.getDayText(publishTime));
- }
-
var permalinkPattern = postPermalinkPolicy.pattern();
annotations.put(Constant.PERMALINK_PATTERN_ANNO, permalinkPattern);
@@ -195,7 +164,6 @@ public Result reconcile(Request request) {
status.setExcerpt(excerpt.getRaw());
}
-
var ref = Ref.of(post);
// handle contributors
var headSnapshot = post.getSpec().getHeadSnapshot();
@@ -227,19 +195,75 @@ public Result reconcile(Request request) {
return Result.doNotRetry();
}
+ private void populateLabels(Post post) {
+ var labels = nullSafeLabels(post);
+ labels.put(Post.DELETED_LABEL, String.valueOf(isTrue(post.getSpec().getDeleted())));
+
+ var expectVisible = defaultIfNull(post.getSpec().getVisible(), VisibleEnum.PUBLIC);
+ var oldVisible = VisibleEnum.from(labels.get(Post.VISIBLE_LABEL));
+ if (!Objects.equals(oldVisible, expectVisible)) {
+ var postName = post.getMetadata().getName();
+ eventPublisher.publishEvent(
+ new PostVisibleChangedEvent(postName, oldVisible, expectVisible));
+ }
+ labels.put(Post.VISIBLE_LABEL, expectVisible.toString());
+
+ var ownerName = post.getSpec().getOwner();
+ if (StringUtils.isNotBlank(ownerName)) {
+ labels.put(Post.OWNER_LABEL, ownerName);
+ }
+
+ var publishTime = post.getSpec().getPublishTime();
+ if (publishTime != null) {
+ labels.put(Post.ARCHIVE_YEAR_LABEL, HaloUtils.getYearText(publishTime));
+ labels.put(Post.ARCHIVE_MONTH_LABEL, HaloUtils.getMonthText(publishTime));
+ labels.put(Post.ARCHIVE_DAY_LABEL, HaloUtils.getDayText(publishTime));
+ }
+
+ if (!labels.containsKey(Post.PUBLISHED_LABEL)) {
+ labels.put(Post.PUBLISHED_LABEL, BooleanUtils.FALSE);
+ }
+ }
+
+ private static boolean shouldUnPublish(Post post) {
+ return isTrue(post.getSpec().getDeleted()) || isFalse(post.getSpec().getPublish());
+ }
+
@Override
public Controller setupWith(ControllerBuilder builder) {
return builder
.extension(new Post())
.onAddMatcher(DefaultExtensionMatcher.builder(client, Post.GVK)
.fieldSelector(FieldSelector.of(
- equal(Post.REQUIRE_SYNC_ON_STARTUP_INDEX_NAME, BooleanUtils.TRUE))
+ equal(Post.REQUIRE_SYNC_ON_STARTUP_INDEX_NAME, TRUE))
)
.build()
)
.build();
}
+ void schedulePublishIfNecessary(Post post) {
+ var labels = nullSafeLabels(post);
+ // ensure the label is removed
+ labels.remove(Post.SCHEDULING_PUBLISH_LABEL);
+
+ final var now = Instant.now();
+ var publishTime = post.getSpec().getPublishTime();
+ if (post.isPublished() || publishTime == null) {
+ return;
+ }
+
+ // expect to publish in the future
+ if (isTrue(post.getSpec().getPublish()) && publishTime.isAfter(now)) {
+ labels.put(Post.SCHEDULING_PUBLISH_LABEL, TRUE);
+ // update post changes before requeue
+ client.update(post);
+
+ throw new RequeueException(Result.requeue(Duration.between(now, publishTime)),
+ "Requeue for scheduled publish.");
+ }
+ }
+
void subscribeNewCommentNotification(Post post) {
var subscriber = new Subscription.Subscriber();
subscriber.setName(post.getSpec().getOwner());
diff --git a/ui/console-src/modules/contents/posts/PostList.vue b/ui/console-src/modules/contents/posts/PostList.vue
index 1f804e1f97..76aa65c25c 100644
--- a/ui/console-src/modules/contents/posts/PostList.vue
+++ b/ui/console-src/modules/contents/posts/PostList.vue
@@ -133,9 +133,7 @@ const {
}
if (selectedPublishStatus.value !== undefined) {
- labelSelector.push(
- `${postLabels.PUBLISHED}=${selectedPublishStatus.value}`
- );
+ labelSelector.push(selectedPublishStatus.value);
}
const { data } = await apiClient.post.listPosts({
@@ -158,7 +156,9 @@ const {
const { spec, metadata, status } = post.post;
return (
spec.deleted ||
- (spec.publish && metadata.labels?.[postLabels.PUBLISHED] !== "true") ||
+ (spec.publish &&
+ metadata.labels?.[postLabels.PUBLISHED] !== "true" &&
+ metadata.labels?.[postLabels.SCHEDULING_PUBLISH] !== "true") ||
(spec.releaseSnapshot === spec.headSnapshot && status?.inProgress)
);
});
@@ -357,11 +357,15 @@ watch(selectedPostNames, (newValue) => {
},
{
label: t('core.post.filters.status.items.published'),
- value: 'true',
+ value: `${postLabels.PUBLISHED}=true`,
},
{
label: t('core.post.filters.status.items.draft'),
- value: 'false',
+ value: `${postLabels.PUBLISHED}=false`,
+ },
+ {
+ label: t('core.post.filters.status.items.scheduling'),
+ value: `${postLabels.SCHEDULING_PUBLISH}=true`,
},
]"
/>
diff --git a/ui/console-src/modules/contents/posts/components/PostListItem.vue b/ui/console-src/modules/contents/posts/components/PostListItem.vue
index 55acc68e74..a6d2bf9e51 100644
--- a/ui/console-src/modules/contents/posts/components/PostListItem.vue
+++ b/ui/console-src/modules/contents/posts/components/PostListItem.vue
@@ -5,9 +5,7 @@ import {
VDropdownDivider,
VDropdownItem,
VEntity,
- VEntityField,
} from "@halo-dev/components";
-import { formatDatetime } from "@/utils/date";
import type { ListedPost, Post } from "@halo-dev/api-client";
import { useI18n } from "vue-i18n";
import { usePermission } from "@/utils/permission";
@@ -26,6 +24,7 @@ import ContributorsField from "./entity-fields/ContributorsField.vue";
import PublishStatusField from "./entity-fields/PublishStatusField.vue";
import VisibleField from "./entity-fields/VisibleField.vue";
import StatusDotField from "@/components/entity-fields/StatusDotField.vue";
+import PublishTimeField from "./entity-fields/PublishTimeField.vue";
const { currentUserHasPermission } = usePermission();
const { t } = useI18n();
@@ -160,9 +159,9 @@ const { startFields, endFields } = useEntityFieldItemExtensionPoint(
{
priority: 50,
position: "end",
- component: markRaw(VEntityField),
+ component: markRaw(PublishTimeField),
props: {
- description: formatDatetime(props.post.post.spec.publishTime),
+ post: props.post,
},
},
])
diff --git a/ui/console-src/modules/contents/posts/components/PostSettingModal.vue b/ui/console-src/modules/contents/posts/components/PostSettingModal.vue
index 294d7aece2..c846b7e6af 100644
--- a/ui/console-src/modules/contents/posts/components/PostSettingModal.vue
+++ b/ui/console-src/modules/contents/posts/components/PostSettingModal.vue
@@ -13,7 +13,7 @@ import { apiClient } from "@/utils/api-client";
import { useThemeCustomTemplates } from "@console/modules/interface/themes/composables/use-theme";
import { postLabels } from "@/constants/labels";
import { randomUUID } from "@/utils/id";
-import { toDatetimeLocal, toISOString } from "@/utils/date";
+import { formatDatetime, toDatetimeLocal, toISOString } from "@/utils/date";
import AnnotationsForm from "@/components/form/AnnotationsForm.vue";
import { submitForm } from "@formkit/core";
import useSlugify from "@console/composables/use-slugify";
@@ -209,6 +209,7 @@ const handleUnpublish = async () => {
}
};
+// publish time
watch(
() => props.post,
(value) => {
@@ -229,6 +230,21 @@ watch(
}
);
+const isScheduledPublish = computed(() => {
+ return (
+ formState.value.spec.publishTime &&
+ new Date(formState.value.spec.publishTime) > new Date()
+ );
+});
+
+const publishTimeHelp = computed(() => {
+ return isScheduledPublish.value
+ ? t("core.post.settings.fields.publish_time.help.schedule_publish", {
+ datetime: formatDatetime(publishTime.value),
+ })
+ : "";
+});
+
// custom templates
const { templates } = useThemeCustomTemplates("post");
@@ -397,6 +413,7 @@ const { handleGenerateSlug } = useSlugify(
type="datetime-local"
min="0000-01-01T00:00"
max="9999-12-31T23:59"
+ :help="publishTimeHelp"
>
- {{ $t("core.common.buttons.publish") }}
+ {{
+ isScheduledPublish
+ ? $t("core.common.buttons.schedule_publish")
+ : $t("core.common.buttons.publish")
+ }}
{
const isPublishing = computed(() => {
const { spec, status, metadata } = props.post.post;
return (
- (spec.publish && metadata.labels?.[postLabels.PUBLISHED] !== "true") ||
+ (spec.publish &&
+ metadata.labels?.[postLabels.PUBLISHED] !== "true" &&
+ metadata.labels?.[postLabels.SCHEDULING_PUBLISH] !== "true") ||
(spec.releaseSnapshot === spec.headSnapshot && status?.inProgress)
);
});
diff --git a/ui/console-src/modules/contents/posts/components/entity-fields/PublishTimeField.vue b/ui/console-src/modules/contents/posts/components/entity-fields/PublishTimeField.vue
new file mode 100644
index 0000000000..c0ec8aaab7
--- /dev/null
+++ b/ui/console-src/modules/contents/posts/components/entity-fields/PublishTimeField.vue
@@ -0,0 +1,33 @@
+
+
+
+
+
+
+
+ {{ formatDatetime(post.post.spec.publishTime) }}
+
+
+
+
+
+
diff --git a/ui/packages/api-client/src/api/api-console-halo-run-v1alpha1-post-api.ts b/ui/packages/api-client/src/api/api-console-halo-run-v1alpha1-post-api.ts
index c5e00c97f9..2d357d75ae 100644
--- a/ui/packages/api-client/src/api/api-console-halo-run-v1alpha1-post-api.ts
+++ b/ui/packages/api-client/src/api/api-console-halo-run-v1alpha1-post-api.ts
@@ -379,10 +379,11 @@ export const ApiConsoleHaloRunV1alpha1PostApiAxiosParamCreator = function (confi
* Publish a post.
* @param {string} name
* @param {string} [headSnapshot] Head snapshot name of content.
+ * @param {boolean} [async]
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
- publishPost: async (name: string, headSnapshot?: string, options: RawAxiosRequestConfig = {}): Promise => {
+ publishPost: async (name: string, headSnapshot?: string, async?: boolean, options: RawAxiosRequestConfig = {}): Promise => {
// verify required parameter 'name' is not null or undefined
assertParamExists('publishPost', 'name', name)
const localVarPath = `/apis/api.console.halo.run/v1alpha1/posts/{name}/publish`
@@ -410,6 +411,10 @@ export const ApiConsoleHaloRunV1alpha1PostApiAxiosParamCreator = function (confi
localVarQueryParameter['headSnapshot'] = headSnapshot;
}
+ if (async !== undefined) {
+ localVarQueryParameter['async'] = async;
+ }
+
setSearchParams(localVarUrlObj, localVarQueryParameter);
@@ -750,11 +755,12 @@ export const ApiConsoleHaloRunV1alpha1PostApiFp = function(configuration?: Confi
* Publish a post.
* @param {string} name
* @param {string} [headSnapshot] Head snapshot name of content.
+ * @param {boolean} [async]
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
- async publishPost(name: string, headSnapshot?: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> {
- const localVarAxiosArgs = await localVarAxiosParamCreator.publishPost(name, headSnapshot, options);
+ async publishPost(name: string, headSnapshot?: string, async?: boolean, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> {
+ const localVarAxiosArgs = await localVarAxiosParamCreator.publishPost(name, headSnapshot, async, options);
const localVarOperationServerIndex = configuration?.serverIndex ?? 0;
const localVarOperationServerBasePath = operationServerMap['ApiConsoleHaloRunV1alpha1PostApi.publishPost']?.[localVarOperationServerIndex]?.url;
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);
@@ -902,7 +908,7 @@ export const ApiConsoleHaloRunV1alpha1PostApiFactory = function (configuration?:
* @throws {RequiredError}
*/
publishPost(requestParameters: ApiConsoleHaloRunV1alpha1PostApiPublishPostRequest, options?: RawAxiosRequestConfig): AxiosPromise {
- return localVarFp.publishPost(requestParameters.name, requestParameters.headSnapshot, options).then((request) => request(axios, basePath));
+ return localVarFp.publishPost(requestParameters.name, requestParameters.headSnapshot, requestParameters.async, options).then((request) => request(axios, basePath));
},
/**
* Recycle a post.
@@ -1125,6 +1131,13 @@ export interface ApiConsoleHaloRunV1alpha1PostApiPublishPostRequest {
* @memberof ApiConsoleHaloRunV1alpha1PostApiPublishPost
*/
readonly headSnapshot?: string
+
+ /**
+ *
+ * @type {boolean}
+ * @memberof ApiConsoleHaloRunV1alpha1PostApiPublishPost
+ */
+ readonly async?: boolean
}
/**
@@ -1310,7 +1323,7 @@ export class ApiConsoleHaloRunV1alpha1PostApi extends BaseAPI {
* @memberof ApiConsoleHaloRunV1alpha1PostApi
*/
public publishPost(requestParameters: ApiConsoleHaloRunV1alpha1PostApiPublishPostRequest, options?: RawAxiosRequestConfig) {
- return ApiConsoleHaloRunV1alpha1PostApiFp(this.configuration).publishPost(requestParameters.name, requestParameters.headSnapshot, options).then((request) => request(this.axios, this.basePath));
+ return ApiConsoleHaloRunV1alpha1PostApiFp(this.configuration).publishPost(requestParameters.name, requestParameters.headSnapshot, requestParameters.async, options).then((request) => request(this.axios, this.basePath));
}
/**
diff --git a/ui/packages/components/src/icons/icons.ts b/ui/packages/components/src/icons/icons.ts
index 26ca5df919..5500094056 100644
--- a/ui/packages/components/src/icons/icons.ts
+++ b/ui/packages/components/src/icons/icons.ts
@@ -73,6 +73,7 @@ import IconSettings3Line from "~icons/ri/settings-3-line";
import IconImageAddLine from "~icons/ri/image-add-line";
import IconToolsFill from "~icons/ri/tools-fill";
import IconHistoryLine from "~icons/ri/history-line";
+import IconTimerLine from "~icons/ri/timer-line";
export {
IconDashboard,
@@ -150,4 +151,5 @@ export {
IconImageAddLine,
IconToolsFill,
IconHistoryLine,
+ IconTimerLine,
};
diff --git a/ui/src/constants/annotations.ts b/ui/src/constants/annotations.ts
index c09f1b01c6..abdfa85cbd 100644
--- a/ui/src/constants/annotations.ts
+++ b/ui/src/constants/annotations.ts
@@ -22,6 +22,7 @@ export enum contentAnnotations {
PATCHED_CONTENT = "content.halo.run/patched-content",
PATCHED_RAW = "content.halo.run/patched-raw",
CONTENT_JSON = "content.halo.run/content-json",
+ SCHEDULED_PUBLISH_AT = "content.halo.run/scheduled-publish-at",
}
// pat
diff --git a/ui/src/constants/labels.ts b/ui/src/constants/labels.ts
index 63939483ca..906942f4f3 100644
--- a/ui/src/constants/labels.ts
+++ b/ui/src/constants/labels.ts
@@ -17,6 +17,7 @@ export enum postLabels {
OWNER = "content.halo.run/owner",
VISIBLE = "content.halo.run/visible",
PHASE = "content.halo.run/phase",
+ SCHEDULING_PUBLISH = "content.halo.run/scheduling-publish",
}
// singlePage
diff --git a/ui/src/locales/en.yaml b/ui/src/locales/en.yaml
index 2fc98e5575..71a685b5b3 100644
--- a/ui/src/locales/en.yaml
+++ b/ui/src/locales/en.yaml
@@ -196,6 +196,7 @@ core:
items:
published: Published
draft: Draft
+ scheduling: Scheduling publish
visible:
label: Visible
result: "Visible: {visible}"
@@ -227,6 +228,8 @@ core:
visits: "{visits} Visits"
comments: "{comments} Comments"
pinned: Pinned
+ schedule_publish:
+ tooltip: Schedule publish
settings:
title: Settings
groups:
@@ -256,6 +259,8 @@ core:
label: Visible
publish_time:
label: Publish Time
+ help:
+ schedule_publish: Schedule a timed task and publish it at {datetime}
template:
label: Template
cover:
@@ -1575,6 +1580,7 @@ core:
verify: Verify
modify: Modify
access: Access
+ schedule_publish: Schedule publish
radio:
"yes": "Yes"
"no": "No"
diff --git a/ui/src/locales/zh-CN.yaml b/ui/src/locales/zh-CN.yaml
index 253e43cef2..97d516cc1e 100644
--- a/ui/src/locales/zh-CN.yaml
+++ b/ui/src/locales/zh-CN.yaml
@@ -188,6 +188,7 @@ core:
items:
published: 已发布
draft: 未发布
+ scheduling: 定时发布
visible:
label: 可见性
result: 可见性:{visible}
@@ -219,6 +220,8 @@ core:
visits: 访问量 {visits}
comments: 评论 {comments}
pinned: 已置顶
+ schedule_publish:
+ tooltip: 定时发布
settings:
title: 文章设置
groups:
@@ -248,6 +251,8 @@ core:
label: 可见性
publish_time:
label: 发表时间
+ help:
+ schedule_publish: 将设置定时任务,并于 {datetime} 发布
template:
label: 自定义模板
cover:
@@ -1519,6 +1524,7 @@ core:
verify: 验证
modify: 修改
access: 访问
+ schedule_publish: 定时发布
radio:
"yes": 是
"no": 否
diff --git a/ui/src/locales/zh-TW.yaml b/ui/src/locales/zh-TW.yaml
index 9869df7266..086234b764 100644
--- a/ui/src/locales/zh-TW.yaml
+++ b/ui/src/locales/zh-TW.yaml
@@ -188,6 +188,7 @@ core:
items:
published: 已發布
draft: 未發布
+ scheduling: 定時發佈
visible:
label: 可見性
result: 可見性:{visible}
@@ -219,6 +220,8 @@ core:
visits: 訪問量 {visits}
comments: 留言 {comments}
pinned: 已置頂
+ schedule_publish:
+ tooltip: 定時發佈
settings:
title: 文章設置
groups:
@@ -248,6 +251,8 @@ core:
label: 可見性
publish_time:
label: 發表時間
+ help:
+ schedule_publish: 將設定定時任務,並於 {datetime} 發佈
template:
label: 自定義模板
cover:
@@ -1477,6 +1482,7 @@ core:
modify: 修改
verify: 驗證
access: 訪問
+ schedule_publish: 定時發佈
radio:
"yes": 是
"no": 否
diff --git a/ui/uc-src/modules/contents/posts/PostList.vue b/ui/uc-src/modules/contents/posts/PostList.vue
index f9c9bf9268..9436933a7c 100644
--- a/ui/uc-src/modules/contents/posts/PostList.vue
+++ b/ui/uc-src/modules/contents/posts/PostList.vue
@@ -73,7 +73,8 @@ const {
const { spec, metadata, status } = post.post;
return (
spec.deleted ||
- metadata.labels?.[postLabels.PUBLISHED] !== spec.publish + "" ||
+ (metadata.labels?.[postLabels.PUBLISHED] !== spec.publish + "" &&
+ metadata.labels?.[postLabels.SCHEDULING_PUBLISH] !== "true") ||
(spec.releaseSnapshot === spec.headSnapshot && status?.inProgress)
);
});
diff --git a/ui/uc-src/modules/contents/posts/components/PostListItem.vue b/ui/uc-src/modules/contents/posts/components/PostListItem.vue
index 0388ecc4df..d7348a2bb3 100644
--- a/ui/uc-src/modules/contents/posts/components/PostListItem.vue
+++ b/ui/uc-src/modules/contents/posts/components/PostListItem.vue
@@ -4,6 +4,7 @@ import {
IconExternalLinkLine,
IconEye,
IconEyeOff,
+ IconTimerLine,
Toast,
VAvatar,
VDropdownItem,
@@ -51,7 +52,9 @@ const publishStatus = computed(() => {
const isPublishing = computed(() => {
const { spec, status, metadata } = props.post.post;
return (
- (spec.publish && metadata.labels?.[postLabels.PUBLISHED] !== "true") ||
+ (spec.publish &&
+ metadata.labels?.[postLabels.PUBLISHED] !== "true" &&
+ metadata.labels?.[postLabels.SCHEDULING_PUBLISH] !== "true") ||
(spec.releaseSnapshot === spec.headSnapshot && status?.inProgress)
);
});
@@ -204,10 +207,23 @@ function handleUnpublish() {
state="warning"
animate
/>
-
+
+
+
+
+ {{ formatDatetime(post.post.spec.publishTime) }}
+
+
+
+
+
import { IconRefreshLine } from "@halo-dev/components";
import type { PostFormState } from "../types";
-import { toISOString } from "@/utils/date";
+import { formatDatetime, toISOString } from "@/utils/date";
import { computed } from "vue";
import useSlugify from "@console/composables/use-slugify";
import { FormType } from "@/types/slug";
import { ref } from "vue";
import HasPermission from "@/components/permission/HasPermission.vue";
+import { useI18n } from "vue-i18n";
+
+const { t } = useI18n();
const props = withDefaults(
defineProps<{
@@ -60,6 +63,19 @@ const { handleGenerateSlug } = useSlugify(
computed(() => !props.updateMode),
FormType.POST
);
+
+const isScheduledPublish = computed(() => {
+ const { publishTime } = internalFormState.value;
+ return publishTime && new Date(publishTime) > new Date();
+});
+
+const publishTimeHelp = computed(() => {
+ return isScheduledPublish.value
+ ? t("core.post.settings.fields.publish_time.help.schedule_publish", {
+ datetime: formatDatetime(internalFormState.value.publishTime),
+ })
+ : "";
+});
@@ -189,6 +205,7 @@ const { handleGenerateSlug } = useSlugify(
type="datetime-local"
min="0000-01-01T00:00"
max="9999-12-31T23:59"
+ :help="publishTimeHelp"
>