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