mirror of https://github.com/halo-dev/halo
feat: add scheduled post publishing feature (#5940)
#### What type of PR is this? /kind feature /area core /milestone 2.16.x #### What this PR does / why we need it: 新增文章定时发布功能 #### Which issue(s) this PR fixes: Fixes #4602 #### Does this PR introduce a user-facing change? ```release-note 新增文章定时发布功能 ```pull/5984/head
parent
de85156067
commit
c1e8bdb568
|
@ -3109,6 +3109,13 @@
|
|||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"in": "query",
|
||||
"name": "async",
|
||||
"schema": {
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
|
|
|
@ -47,6 +47,12 @@ public class Post extends AbstractExtension {
|
|||
|
||||
public static final String STATS_ANNO = "content.halo.run/stats";
|
||||
|
||||
/**
|
||||
* <p>The key of the label that indicates that the post is scheduled to be published.</p>
|
||||
* <p>Can be used to query posts that are scheduled to be published.</p>
|
||||
*/
|
||||
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";
|
||||
|
|
|
@ -4,13 +4,16 @@ import static org.springdoc.core.fn.builders.apiresponse.Builder.responseBuilder
|
|||
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 class PostEndpoint implements CustomEndpoint {
|
|||
.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 @@ public class PostEndpoint implements CustomEndpoint {
|
|||
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 @@ public class PostEndpoint implements CustomEndpoint {
|
|||
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 @@ public class PostEndpoint implements CustomEndpoint {
|
|||
}
|
||||
|
||||
private Mono<Post> awaitPostPublished(String postName) {
|
||||
Predicate<Post> 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"))))
|
||||
|
|
|
@ -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.Ref;
|
|||
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,27 +102,14 @@ public class PostReconciler implements Reconciler<Reconciler.Request> {
|
|||
events.forEach(eventPublisher::publishEvent);
|
||||
return;
|
||||
}
|
||||
|
||||
addFinalizers(post.getMetadata(), Set.of(FINALIZER_NAME));
|
||||
|
||||
populateLabels(post);
|
||||
|
||||
schedulePublishIfNecessary(post);
|
||||
|
||||
subscribeNewCommentNotification(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);
|
||||
}
|
||||
|
||||
if (!annotations.containsKey(Post.PUBLISHED_LABEL)) {
|
||||
labels.put(Post.PUBLISHED_LABEL, BooleanUtils.FALSE);
|
||||
}
|
||||
|
||||
var status = post.getStatus();
|
||||
if (status == null) {
|
||||
status = new Post.PostStatus();
|
||||
|
@ -131,6 +124,7 @@ public class PostReconciler implements Reconciler<Reconciler.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 class PostReconciler implements Reconciler<Reconciler.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 class PostReconciler implements Reconciler<Reconciler.Request> {
|
|||
status.setExcerpt(excerpt.getRaw());
|
||||
}
|
||||
|
||||
|
||||
var ref = Ref.of(post);
|
||||
// handle contributors
|
||||
var headSnapshot = post.getSpec().getHeadSnapshot();
|
||||
|
@ -227,19 +195,75 @@ public class PostReconciler implements Reconciler<Reconciler.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());
|
||||
|
|
|
@ -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`,
|
||||
},
|
||||
]"
|
||||
/>
|
||||
|
|
|
@ -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<ListedPost>(
|
|||
{
|
||||
priority: 50,
|
||||
position: "end",
|
||||
component: markRaw(VEntityField),
|
||||
component: markRaw(PublishTimeField),
|
||||
props: {
|
||||
description: formatDatetime(props.post.post.spec.publishTime),
|
||||
post: props.post,
|
||||
},
|
||||
},
|
||||
])
|
||||
|
|
|
@ -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"
|
||||
></FormKit>
|
||||
<FormKit
|
||||
v-model="formState.spec.template"
|
||||
|
@ -450,7 +467,11 @@ const { handleGenerateSlug } = useSlugify(
|
|||
type="secondary"
|
||||
@click="handlePublishClick()"
|
||||
>
|
||||
{{ $t("core.common.buttons.publish") }}
|
||||
{{
|
||||
isScheduledPublish
|
||||
? $t("core.common.buttons.schedule_publish")
|
||||
: $t("core.common.buttons.publish")
|
||||
}}
|
||||
</VButton>
|
||||
<VButton
|
||||
v-else
|
||||
|
|
|
@ -24,7 +24,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)
|
||||
);
|
||||
});
|
||||
|
|
|
@ -0,0 +1,33 @@
|
|||
<script lang="ts" setup>
|
||||
import { postLabels } from "@/constants/labels";
|
||||
import { formatDatetime } from "@/utils/date";
|
||||
import { IconTimerLine, VEntityField } from "@halo-dev/components";
|
||||
import type { ListedPost } from "packages/api-client/dist";
|
||||
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
post: ListedPost;
|
||||
}>(),
|
||||
{}
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VEntityField>
|
||||
<template #description>
|
||||
<div class="inline-flex items-center space-x-2">
|
||||
<span class="entity-field-description">
|
||||
{{ formatDatetime(post.post.spec.publishTime) }}
|
||||
</span>
|
||||
<IconTimerLine
|
||||
v-if="
|
||||
post.post.metadata.labels?.[postLabels.SCHEDULING_PUBLISH] ===
|
||||
'true'
|
||||
"
|
||||
v-tooltip="$t('core.post.list.fields.schedule_publish.tooltip')"
|
||||
class="text-sm"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</VEntityField>
|
||||
</template>
|
|
@ -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<RequestArgs> => {
|
||||
publishPost: async (name: string, headSnapshot?: string, async?: boolean, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {
|
||||
// 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<Post>> {
|
||||
const localVarAxiosArgs = await localVarAxiosParamCreator.publishPost(name, headSnapshot, options);
|
||||
async publishPost(name: string, headSnapshot?: string, async?: boolean, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Post>> {
|
||||
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<Post> {
|
||||
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));
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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": 否
|
||||
|
|
|
@ -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": 否
|
||||
|
|
|
@ -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)
|
||||
);
|
||||
});
|
||||
|
|
|
@ -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
|
||||
/>
|
||||
<VEntityField
|
||||
v-if="post.post.spec.publishTime"
|
||||
:description="formatDatetime(post.post.spec.publishTime)"
|
||||
></VEntityField>
|
||||
<VEntityField v-if="post.post.spec.publishTime">
|
||||
<template #description>
|
||||
<div class="inline-flex items-center space-x-2">
|
||||
<span class="entity-field-description">
|
||||
{{ formatDatetime(post.post.spec.publishTime) }}
|
||||
</span>
|
||||
<IconTimerLine
|
||||
v-if="
|
||||
post.post.metadata.labels?.[postLabels.SCHEDULING_PUBLISH] ===
|
||||
'true'
|
||||
"
|
||||
v-tooltip="$t('core.post.list.fields.schedule_publish.tooltip')"
|
||||
class="text-sm"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</VEntityField>
|
||||
</template>
|
||||
<template #dropdownItems>
|
||||
<VDropdownItem
|
||||
|
|
|
@ -1,12 +1,15 @@
|
|||
<script lang="ts" setup>
|
||||
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),
|
||||
})
|
||||
: "";
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
@ -189,6 +205,7 @@ const { handleGenerateSlug } = useSlugify(
|
|||
type="datetime-local"
|
||||
min="0000-01-01T00:00"
|
||||
max="9999-12-31T23:59"
|
||||
:help="publishTimeHelp"
|
||||
></FormKit>
|
||||
<HasPermission :permissions="['system:attachments:view']">
|
||||
<FormKit
|
||||
|
|
Loading…
Reference in New Issue