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": {
|
"schema": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"in": "query",
|
||||||
|
"name": "async",
|
||||||
|
"schema": {
|
||||||
|
"type": "boolean"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"responses": {
|
"responses": {
|
||||||
|
|
|
@ -47,6 +47,12 @@ public class Post extends AbstractExtension {
|
||||||
|
|
||||||
public static final String STATS_ANNO = "content.halo.run/stats";
|
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 DELETED_LABEL = "content.halo.run/deleted";
|
||||||
public static final String PUBLISHED_LABEL = "content.halo.run/published";
|
public static final String PUBLISHED_LABEL = "content.halo.run/published";
|
||||||
public static final String OWNER_LABEL = "content.halo.run/owner";
|
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.content.Builder.contentBuilder;
|
||||||
import static org.springdoc.core.fn.builders.parameter.Builder.parameterBuilder;
|
import static org.springdoc.core.fn.builders.parameter.Builder.parameterBuilder;
|
||||||
import static org.springdoc.core.fn.builders.requestbody.Builder.requestBodyBuilder;
|
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.enums.ParameterIn;
|
||||||
import io.swagger.v3.oas.annotations.media.Schema;
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
import java.util.function.Predicate;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.apache.commons.lang3.BooleanUtils;
|
||||||
import org.springdoc.core.fn.builders.schema.Builder;
|
import org.springdoc.core.fn.builders.schema.Builder;
|
||||||
import org.springdoc.webflux.core.fn.SpringdocRouteBuilder;
|
import org.springdoc.webflux.core.fn.SpringdocRouteBuilder;
|
||||||
import org.springframework.dao.OptimisticLockingFailureException;
|
import org.springframework.dao.OptimisticLockingFailureException;
|
||||||
|
@ -199,6 +202,11 @@ public class PostEndpoint implements CustomEndpoint {
|
||||||
.description("Head snapshot name of content.")
|
.description("Head snapshot name of content.")
|
||||||
.in(ParameterIn.QUERY)
|
.in(ParameterIn.QUERY)
|
||||||
.required(false))
|
.required(false))
|
||||||
|
.parameter(parameterBuilder()
|
||||||
|
.name("async")
|
||||||
|
.in(ParameterIn.QUERY)
|
||||||
|
.implementation(Boolean.class)
|
||||||
|
.required(false))
|
||||||
.response(responseBuilder()
|
.response(responseBuilder()
|
||||||
.implementation(Post.class))
|
.implementation(Post.class))
|
||||||
)
|
)
|
||||||
|
@ -319,6 +327,7 @@ public class PostEndpoint implements CustomEndpoint {
|
||||||
boolean asyncPublish = request.queryParam("async")
|
boolean asyncPublish = request.queryParam("async")
|
||||||
.map(Boolean::parseBoolean)
|
.map(Boolean::parseBoolean)
|
||||||
.orElse(false);
|
.orElse(false);
|
||||||
|
|
||||||
return Mono.defer(() -> client.get(Post.class, name)
|
return Mono.defer(() -> client.get(Post.class, name)
|
||||||
.doOnNext(post -> {
|
.doOnNext(post -> {
|
||||||
var spec = post.getSpec();
|
var spec = post.getSpec();
|
||||||
|
@ -327,7 +336,6 @@ public class PostEndpoint implements CustomEndpoint {
|
||||||
if (spec.getHeadSnapshot() == null) {
|
if (spec.getHeadSnapshot() == null) {
|
||||||
spec.setHeadSnapshot(spec.getBaseSnapshot());
|
spec.setHeadSnapshot(spec.getBaseSnapshot());
|
||||||
}
|
}
|
||||||
// TODO Provide release snapshot query param to control
|
|
||||||
spec.setReleaseSnapshot(spec.getHeadSnapshot());
|
spec.setReleaseSnapshot(spec.getHeadSnapshot());
|
||||||
})
|
})
|
||||||
.flatMap(client::update)
|
.flatMap(client::update)
|
||||||
|
@ -342,12 +350,17 @@ public class PostEndpoint implements CustomEndpoint {
|
||||||
}
|
}
|
||||||
|
|
||||||
private Mono<Post> awaitPostPublished(String postName) {
|
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)
|
return Mono.defer(() -> client.get(Post.class, postName)
|
||||||
.filter(post -> {
|
.filter(post -> {
|
||||||
var releasedSnapshot = MetadataUtil.nullSafeAnnotations(post)
|
var releasedSnapshot = MetadataUtil.nullSafeAnnotations(post)
|
||||||
.get(Post.LAST_RELEASED_SNAPSHOT_ANNO);
|
.get(Post.LAST_RELEASED_SNAPSHOT_ANNO);
|
||||||
var expectReleaseSnapshot = post.getSpec().getReleaseSnapshot();
|
var expectReleaseSnapshot = post.getSpec().getReleaseSnapshot();
|
||||||
return Objects.equals(releasedSnapshot, expectReleaseSnapshot);
|
return Objects.equals(releasedSnapshot, expectReleaseSnapshot)
|
||||||
|
|| schedulePublish.test(post);
|
||||||
})
|
})
|
||||||
.switchIfEmpty(Mono.error(
|
.switchIfEmpty(Mono.error(
|
||||||
() -> new RetryException("Retry to check post publish status"))))
|
() -> new RetryException("Retry to check post publish status"))))
|
||||||
|
|
|
@ -1,14 +1,19 @@
|
||||||
package run.halo.app.core.extension.reconciler;
|
package run.halo.app.core.extension.reconciler;
|
||||||
|
|
||||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
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 org.apache.commons.lang3.ObjectUtils.defaultIfNull;
|
||||||
import static run.halo.app.extension.ExtensionUtil.addFinalizers;
|
import static run.halo.app.extension.ExtensionUtil.addFinalizers;
|
||||||
import static run.halo.app.extension.ExtensionUtil.removeFinalizers;
|
import static run.halo.app.extension.ExtensionUtil.removeFinalizers;
|
||||||
|
import static run.halo.app.extension.MetadataUtil.nullSafeAnnotations;
|
||||||
|
import static run.halo.app.extension.MetadataUtil.nullSafeLabels;
|
||||||
import static run.halo.app.extension.index.query.QueryFactory.equal;
|
import static run.halo.app.extension.index.query.QueryFactory.equal;
|
||||||
|
|
||||||
import com.google.common.hash.Hashing;
|
import com.google.common.hash.Hashing;
|
||||||
|
import java.time.Duration;
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Objects;
|
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.Controller;
|
||||||
import run.halo.app.extension.controller.ControllerBuilder;
|
import run.halo.app.extension.controller.ControllerBuilder;
|
||||||
import run.halo.app.extension.controller.Reconciler;
|
import run.halo.app.extension.controller.Reconciler;
|
||||||
|
import run.halo.app.extension.controller.RequeueException;
|
||||||
import run.halo.app.extension.index.query.QueryFactory;
|
import run.halo.app.extension.index.query.QueryFactory;
|
||||||
import run.halo.app.extension.router.selector.FieldSelector;
|
import run.halo.app.extension.router.selector.FieldSelector;
|
||||||
import run.halo.app.infra.Condition;
|
import run.halo.app.infra.Condition;
|
||||||
|
@ -96,27 +102,14 @@ public class PostReconciler implements Reconciler<Reconciler.Request> {
|
||||||
events.forEach(eventPublisher::publishEvent);
|
events.forEach(eventPublisher::publishEvent);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
addFinalizers(post.getMetadata(), Set.of(FINALIZER_NAME));
|
addFinalizers(post.getMetadata(), Set.of(FINALIZER_NAME));
|
||||||
|
|
||||||
|
populateLabels(post);
|
||||||
|
|
||||||
|
schedulePublishIfNecessary(post);
|
||||||
|
|
||||||
subscribeNewCommentNotification(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();
|
var status = post.getStatus();
|
||||||
if (status == null) {
|
if (status == null) {
|
||||||
status = new Post.PostStatus();
|
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)
|
var configSha256sum = Hashing.sha256().hashString(post.getSpec().toString(), UTF_8)
|
||||||
.toString();
|
.toString();
|
||||||
|
|
||||||
|
var annotations = nullSafeAnnotations(post);
|
||||||
var oldConfigChecksum = annotations.get(Constant.CHECKSUM_CONFIG_ANNO);
|
var oldConfigChecksum = annotations.get(Constant.CHECKSUM_CONFIG_ANNO);
|
||||||
if (!Objects.equals(oldConfigChecksum, configSha256sum)) {
|
if (!Objects.equals(oldConfigChecksum, configSha256sum)) {
|
||||||
// if the checksum doesn't match
|
// if the checksum doesn't match
|
||||||
|
@ -138,37 +132,12 @@ public class PostReconciler implements Reconciler<Reconciler.Request> {
|
||||||
annotations.put(Constant.CHECKSUM_CONFIG_ANNO, configSha256sum);
|
annotations.put(Constant.CHECKSUM_CONFIG_ANNO, configSha256sum);
|
||||||
}
|
}
|
||||||
|
|
||||||
var expectDelete = defaultIfNull(post.getSpec().getDeleted(), false);
|
if (shouldUnPublish(post)) {
|
||||||
var expectPublish = defaultIfNull(post.getSpec().getPublish(), false);
|
|
||||||
|
|
||||||
if (expectDelete || !expectPublish) {
|
|
||||||
unPublishPost(post, events);
|
unPublishPost(post, events);
|
||||||
} else {
|
} else {
|
||||||
publishPost(post, events);
|
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();
|
var permalinkPattern = postPermalinkPolicy.pattern();
|
||||||
annotations.put(Constant.PERMALINK_PATTERN_ANNO, permalinkPattern);
|
annotations.put(Constant.PERMALINK_PATTERN_ANNO, permalinkPattern);
|
||||||
|
|
||||||
|
@ -195,7 +164,6 @@ public class PostReconciler implements Reconciler<Reconciler.Request> {
|
||||||
status.setExcerpt(excerpt.getRaw());
|
status.setExcerpt(excerpt.getRaw());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
var ref = Ref.of(post);
|
var ref = Ref.of(post);
|
||||||
// handle contributors
|
// handle contributors
|
||||||
var headSnapshot = post.getSpec().getHeadSnapshot();
|
var headSnapshot = post.getSpec().getHeadSnapshot();
|
||||||
|
@ -227,19 +195,75 @@ public class PostReconciler implements Reconciler<Reconciler.Request> {
|
||||||
return Result.doNotRetry();
|
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
|
@Override
|
||||||
public Controller setupWith(ControllerBuilder builder) {
|
public Controller setupWith(ControllerBuilder builder) {
|
||||||
return builder
|
return builder
|
||||||
.extension(new Post())
|
.extension(new Post())
|
||||||
.onAddMatcher(DefaultExtensionMatcher.builder(client, Post.GVK)
|
.onAddMatcher(DefaultExtensionMatcher.builder(client, Post.GVK)
|
||||||
.fieldSelector(FieldSelector.of(
|
.fieldSelector(FieldSelector.of(
|
||||||
equal(Post.REQUIRE_SYNC_ON_STARTUP_INDEX_NAME, BooleanUtils.TRUE))
|
equal(Post.REQUIRE_SYNC_ON_STARTUP_INDEX_NAME, TRUE))
|
||||||
)
|
)
|
||||||
.build()
|
.build()
|
||||||
)
|
)
|
||||||
.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) {
|
void subscribeNewCommentNotification(Post post) {
|
||||||
var subscriber = new Subscription.Subscriber();
|
var subscriber = new Subscription.Subscriber();
|
||||||
subscriber.setName(post.getSpec().getOwner());
|
subscriber.setName(post.getSpec().getOwner());
|
||||||
|
|
|
@ -133,9 +133,7 @@ const {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (selectedPublishStatus.value !== undefined) {
|
if (selectedPublishStatus.value !== undefined) {
|
||||||
labelSelector.push(
|
labelSelector.push(selectedPublishStatus.value);
|
||||||
`${postLabels.PUBLISHED}=${selectedPublishStatus.value}`
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const { data } = await apiClient.post.listPosts({
|
const { data } = await apiClient.post.listPosts({
|
||||||
|
@ -158,7 +156,9 @@ const {
|
||||||
const { spec, metadata, status } = post.post;
|
const { spec, metadata, status } = post.post;
|
||||||
return (
|
return (
|
||||||
spec.deleted ||
|
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)
|
(spec.releaseSnapshot === spec.headSnapshot && status?.inProgress)
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -357,11 +357,15 @@ watch(selectedPostNames, (newValue) => {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: t('core.post.filters.status.items.published'),
|
label: t('core.post.filters.status.items.published'),
|
||||||
value: 'true',
|
value: `${postLabels.PUBLISHED}=true`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: t('core.post.filters.status.items.draft'),
|
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,
|
VDropdownDivider,
|
||||||
VDropdownItem,
|
VDropdownItem,
|
||||||
VEntity,
|
VEntity,
|
||||||
VEntityField,
|
|
||||||
} from "@halo-dev/components";
|
} from "@halo-dev/components";
|
||||||
import { formatDatetime } from "@/utils/date";
|
|
||||||
import type { ListedPost, Post } from "@halo-dev/api-client";
|
import type { ListedPost, Post } from "@halo-dev/api-client";
|
||||||
import { useI18n } from "vue-i18n";
|
import { useI18n } from "vue-i18n";
|
||||||
import { usePermission } from "@/utils/permission";
|
import { usePermission } from "@/utils/permission";
|
||||||
|
@ -26,6 +24,7 @@ import ContributorsField from "./entity-fields/ContributorsField.vue";
|
||||||
import PublishStatusField from "./entity-fields/PublishStatusField.vue";
|
import PublishStatusField from "./entity-fields/PublishStatusField.vue";
|
||||||
import VisibleField from "./entity-fields/VisibleField.vue";
|
import VisibleField from "./entity-fields/VisibleField.vue";
|
||||||
import StatusDotField from "@/components/entity-fields/StatusDotField.vue";
|
import StatusDotField from "@/components/entity-fields/StatusDotField.vue";
|
||||||
|
import PublishTimeField from "./entity-fields/PublishTimeField.vue";
|
||||||
|
|
||||||
const { currentUserHasPermission } = usePermission();
|
const { currentUserHasPermission } = usePermission();
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
@ -160,9 +159,9 @@ const { startFields, endFields } = useEntityFieldItemExtensionPoint<ListedPost>(
|
||||||
{
|
{
|
||||||
priority: 50,
|
priority: 50,
|
||||||
position: "end",
|
position: "end",
|
||||||
component: markRaw(VEntityField),
|
component: markRaw(PublishTimeField),
|
||||||
props: {
|
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 { useThemeCustomTemplates } from "@console/modules/interface/themes/composables/use-theme";
|
||||||
import { postLabels } from "@/constants/labels";
|
import { postLabels } from "@/constants/labels";
|
||||||
import { randomUUID } from "@/utils/id";
|
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 AnnotationsForm from "@/components/form/AnnotationsForm.vue";
|
||||||
import { submitForm } from "@formkit/core";
|
import { submitForm } from "@formkit/core";
|
||||||
import useSlugify from "@console/composables/use-slugify";
|
import useSlugify from "@console/composables/use-slugify";
|
||||||
|
@ -209,6 +209,7 @@ const handleUnpublish = async () => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// publish time
|
||||||
watch(
|
watch(
|
||||||
() => props.post,
|
() => props.post,
|
||||||
(value) => {
|
(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
|
// custom templates
|
||||||
const { templates } = useThemeCustomTemplates("post");
|
const { templates } = useThemeCustomTemplates("post");
|
||||||
|
|
||||||
|
@ -397,6 +413,7 @@ const { handleGenerateSlug } = useSlugify(
|
||||||
type="datetime-local"
|
type="datetime-local"
|
||||||
min="0000-01-01T00:00"
|
min="0000-01-01T00:00"
|
||||||
max="9999-12-31T23:59"
|
max="9999-12-31T23:59"
|
||||||
|
:help="publishTimeHelp"
|
||||||
></FormKit>
|
></FormKit>
|
||||||
<FormKit
|
<FormKit
|
||||||
v-model="formState.spec.template"
|
v-model="formState.spec.template"
|
||||||
|
@ -450,7 +467,11 @@ const { handleGenerateSlug } = useSlugify(
|
||||||
type="secondary"
|
type="secondary"
|
||||||
@click="handlePublishClick()"
|
@click="handlePublishClick()"
|
||||||
>
|
>
|
||||||
{{ $t("core.common.buttons.publish") }}
|
{{
|
||||||
|
isScheduledPublish
|
||||||
|
? $t("core.common.buttons.schedule_publish")
|
||||||
|
: $t("core.common.buttons.publish")
|
||||||
|
}}
|
||||||
</VButton>
|
</VButton>
|
||||||
<VButton
|
<VButton
|
||||||
v-else
|
v-else
|
||||||
|
|
|
@ -24,7 +24,9 @@ const publishStatus = computed(() => {
|
||||||
const isPublishing = computed(() => {
|
const isPublishing = computed(() => {
|
||||||
const { spec, status, metadata } = props.post.post;
|
const { spec, status, metadata } = props.post.post;
|
||||||
return (
|
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)
|
(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.
|
* Publish a post.
|
||||||
* @param {string} name
|
* @param {string} name
|
||||||
* @param {string} [headSnapshot] Head snapshot name of content.
|
* @param {string} [headSnapshot] Head snapshot name of content.
|
||||||
|
* @param {boolean} [async]
|
||||||
* @param {*} [options] Override http request option.
|
* @param {*} [options] Override http request option.
|
||||||
* @throws {RequiredError}
|
* @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
|
// verify required parameter 'name' is not null or undefined
|
||||||
assertParamExists('publishPost', 'name', name)
|
assertParamExists('publishPost', 'name', name)
|
||||||
const localVarPath = `/apis/api.console.halo.run/v1alpha1/posts/{name}/publish`
|
const localVarPath = `/apis/api.console.halo.run/v1alpha1/posts/{name}/publish`
|
||||||
|
@ -410,6 +411,10 @@ export const ApiConsoleHaloRunV1alpha1PostApiAxiosParamCreator = function (confi
|
||||||
localVarQueryParameter['headSnapshot'] = headSnapshot;
|
localVarQueryParameter['headSnapshot'] = headSnapshot;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (async !== undefined) {
|
||||||
|
localVarQueryParameter['async'] = async;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
setSearchParams(localVarUrlObj, localVarQueryParameter);
|
setSearchParams(localVarUrlObj, localVarQueryParameter);
|
||||||
|
@ -750,11 +755,12 @@ export const ApiConsoleHaloRunV1alpha1PostApiFp = function(configuration?: Confi
|
||||||
* Publish a post.
|
* Publish a post.
|
||||||
* @param {string} name
|
* @param {string} name
|
||||||
* @param {string} [headSnapshot] Head snapshot name of content.
|
* @param {string} [headSnapshot] Head snapshot name of content.
|
||||||
|
* @param {boolean} [async]
|
||||||
* @param {*} [options] Override http request option.
|
* @param {*} [options] Override http request option.
|
||||||
* @throws {RequiredError}
|
* @throws {RequiredError}
|
||||||
*/
|
*/
|
||||||
async publishPost(name: string, headSnapshot?: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Post>> {
|
async publishPost(name: string, headSnapshot?: string, async?: boolean, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Post>> {
|
||||||
const localVarAxiosArgs = await localVarAxiosParamCreator.publishPost(name, headSnapshot, options);
|
const localVarAxiosArgs = await localVarAxiosParamCreator.publishPost(name, headSnapshot, async, options);
|
||||||
const localVarOperationServerIndex = configuration?.serverIndex ?? 0;
|
const localVarOperationServerIndex = configuration?.serverIndex ?? 0;
|
||||||
const localVarOperationServerBasePath = operationServerMap['ApiConsoleHaloRunV1alpha1PostApi.publishPost']?.[localVarOperationServerIndex]?.url;
|
const localVarOperationServerBasePath = operationServerMap['ApiConsoleHaloRunV1alpha1PostApi.publishPost']?.[localVarOperationServerIndex]?.url;
|
||||||
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);
|
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);
|
||||||
|
@ -902,7 +908,7 @@ export const ApiConsoleHaloRunV1alpha1PostApiFactory = function (configuration?:
|
||||||
* @throws {RequiredError}
|
* @throws {RequiredError}
|
||||||
*/
|
*/
|
||||||
publishPost(requestParameters: ApiConsoleHaloRunV1alpha1PostApiPublishPostRequest, options?: RawAxiosRequestConfig): AxiosPromise<Post> {
|
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.
|
* Recycle a post.
|
||||||
|
@ -1125,6 +1131,13 @@ export interface ApiConsoleHaloRunV1alpha1PostApiPublishPostRequest {
|
||||||
* @memberof ApiConsoleHaloRunV1alpha1PostApiPublishPost
|
* @memberof ApiConsoleHaloRunV1alpha1PostApiPublishPost
|
||||||
*/
|
*/
|
||||||
readonly headSnapshot?: string
|
readonly headSnapshot?: string
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {boolean}
|
||||||
|
* @memberof ApiConsoleHaloRunV1alpha1PostApiPublishPost
|
||||||
|
*/
|
||||||
|
readonly async?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -1310,7 +1323,7 @@ export class ApiConsoleHaloRunV1alpha1PostApi extends BaseAPI {
|
||||||
* @memberof ApiConsoleHaloRunV1alpha1PostApi
|
* @memberof ApiConsoleHaloRunV1alpha1PostApi
|
||||||
*/
|
*/
|
||||||
public publishPost(requestParameters: ApiConsoleHaloRunV1alpha1PostApiPublishPostRequest, options?: RawAxiosRequestConfig) {
|
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 IconImageAddLine from "~icons/ri/image-add-line";
|
||||||
import IconToolsFill from "~icons/ri/tools-fill";
|
import IconToolsFill from "~icons/ri/tools-fill";
|
||||||
import IconHistoryLine from "~icons/ri/history-line";
|
import IconHistoryLine from "~icons/ri/history-line";
|
||||||
|
import IconTimerLine from "~icons/ri/timer-line";
|
||||||
|
|
||||||
export {
|
export {
|
||||||
IconDashboard,
|
IconDashboard,
|
||||||
|
@ -150,4 +151,5 @@ export {
|
||||||
IconImageAddLine,
|
IconImageAddLine,
|
||||||
IconToolsFill,
|
IconToolsFill,
|
||||||
IconHistoryLine,
|
IconHistoryLine,
|
||||||
|
IconTimerLine,
|
||||||
};
|
};
|
||||||
|
|
|
@ -22,6 +22,7 @@ export enum contentAnnotations {
|
||||||
PATCHED_CONTENT = "content.halo.run/patched-content",
|
PATCHED_CONTENT = "content.halo.run/patched-content",
|
||||||
PATCHED_RAW = "content.halo.run/patched-raw",
|
PATCHED_RAW = "content.halo.run/patched-raw",
|
||||||
CONTENT_JSON = "content.halo.run/content-json",
|
CONTENT_JSON = "content.halo.run/content-json",
|
||||||
|
SCHEDULED_PUBLISH_AT = "content.halo.run/scheduled-publish-at",
|
||||||
}
|
}
|
||||||
|
|
||||||
// pat
|
// pat
|
||||||
|
|
|
@ -17,6 +17,7 @@ export enum postLabels {
|
||||||
OWNER = "content.halo.run/owner",
|
OWNER = "content.halo.run/owner",
|
||||||
VISIBLE = "content.halo.run/visible",
|
VISIBLE = "content.halo.run/visible",
|
||||||
PHASE = "content.halo.run/phase",
|
PHASE = "content.halo.run/phase",
|
||||||
|
SCHEDULING_PUBLISH = "content.halo.run/scheduling-publish",
|
||||||
}
|
}
|
||||||
|
|
||||||
// singlePage
|
// singlePage
|
||||||
|
|
|
@ -196,6 +196,7 @@ core:
|
||||||
items:
|
items:
|
||||||
published: Published
|
published: Published
|
||||||
draft: Draft
|
draft: Draft
|
||||||
|
scheduling: Scheduling publish
|
||||||
visible:
|
visible:
|
||||||
label: Visible
|
label: Visible
|
||||||
result: "Visible: {visible}"
|
result: "Visible: {visible}"
|
||||||
|
@ -227,6 +228,8 @@ core:
|
||||||
visits: "{visits} Visits"
|
visits: "{visits} Visits"
|
||||||
comments: "{comments} Comments"
|
comments: "{comments} Comments"
|
||||||
pinned: Pinned
|
pinned: Pinned
|
||||||
|
schedule_publish:
|
||||||
|
tooltip: Schedule publish
|
||||||
settings:
|
settings:
|
||||||
title: Settings
|
title: Settings
|
||||||
groups:
|
groups:
|
||||||
|
@ -256,6 +259,8 @@ core:
|
||||||
label: Visible
|
label: Visible
|
||||||
publish_time:
|
publish_time:
|
||||||
label: Publish Time
|
label: Publish Time
|
||||||
|
help:
|
||||||
|
schedule_publish: Schedule a timed task and publish it at {datetime}
|
||||||
template:
|
template:
|
||||||
label: Template
|
label: Template
|
||||||
cover:
|
cover:
|
||||||
|
@ -1575,6 +1580,7 @@ core:
|
||||||
verify: Verify
|
verify: Verify
|
||||||
modify: Modify
|
modify: Modify
|
||||||
access: Access
|
access: Access
|
||||||
|
schedule_publish: Schedule publish
|
||||||
radio:
|
radio:
|
||||||
"yes": "Yes"
|
"yes": "Yes"
|
||||||
"no": "No"
|
"no": "No"
|
||||||
|
|
|
@ -188,6 +188,7 @@ core:
|
||||||
items:
|
items:
|
||||||
published: 已发布
|
published: 已发布
|
||||||
draft: 未发布
|
draft: 未发布
|
||||||
|
scheduling: 定时发布
|
||||||
visible:
|
visible:
|
||||||
label: 可见性
|
label: 可见性
|
||||||
result: 可见性:{visible}
|
result: 可见性:{visible}
|
||||||
|
@ -219,6 +220,8 @@ core:
|
||||||
visits: 访问量 {visits}
|
visits: 访问量 {visits}
|
||||||
comments: 评论 {comments}
|
comments: 评论 {comments}
|
||||||
pinned: 已置顶
|
pinned: 已置顶
|
||||||
|
schedule_publish:
|
||||||
|
tooltip: 定时发布
|
||||||
settings:
|
settings:
|
||||||
title: 文章设置
|
title: 文章设置
|
||||||
groups:
|
groups:
|
||||||
|
@ -248,6 +251,8 @@ core:
|
||||||
label: 可见性
|
label: 可见性
|
||||||
publish_time:
|
publish_time:
|
||||||
label: 发表时间
|
label: 发表时间
|
||||||
|
help:
|
||||||
|
schedule_publish: 将设置定时任务,并于 {datetime} 发布
|
||||||
template:
|
template:
|
||||||
label: 自定义模板
|
label: 自定义模板
|
||||||
cover:
|
cover:
|
||||||
|
@ -1519,6 +1524,7 @@ core:
|
||||||
verify: 验证
|
verify: 验证
|
||||||
modify: 修改
|
modify: 修改
|
||||||
access: 访问
|
access: 访问
|
||||||
|
schedule_publish: 定时发布
|
||||||
radio:
|
radio:
|
||||||
"yes": 是
|
"yes": 是
|
||||||
"no": 否
|
"no": 否
|
||||||
|
|
|
@ -188,6 +188,7 @@ core:
|
||||||
items:
|
items:
|
||||||
published: 已發布
|
published: 已發布
|
||||||
draft: 未發布
|
draft: 未發布
|
||||||
|
scheduling: 定時發佈
|
||||||
visible:
|
visible:
|
||||||
label: 可見性
|
label: 可見性
|
||||||
result: 可見性:{visible}
|
result: 可見性:{visible}
|
||||||
|
@ -219,6 +220,8 @@ core:
|
||||||
visits: 訪問量 {visits}
|
visits: 訪問量 {visits}
|
||||||
comments: 留言 {comments}
|
comments: 留言 {comments}
|
||||||
pinned: 已置頂
|
pinned: 已置頂
|
||||||
|
schedule_publish:
|
||||||
|
tooltip: 定時發佈
|
||||||
settings:
|
settings:
|
||||||
title: 文章設置
|
title: 文章設置
|
||||||
groups:
|
groups:
|
||||||
|
@ -248,6 +251,8 @@ core:
|
||||||
label: 可見性
|
label: 可見性
|
||||||
publish_time:
|
publish_time:
|
||||||
label: 發表時間
|
label: 發表時間
|
||||||
|
help:
|
||||||
|
schedule_publish: 將設定定時任務,並於 {datetime} 發佈
|
||||||
template:
|
template:
|
||||||
label: 自定義模板
|
label: 自定義模板
|
||||||
cover:
|
cover:
|
||||||
|
@ -1477,6 +1482,7 @@ core:
|
||||||
modify: 修改
|
modify: 修改
|
||||||
verify: 驗證
|
verify: 驗證
|
||||||
access: 訪問
|
access: 訪問
|
||||||
|
schedule_publish: 定時發佈
|
||||||
radio:
|
radio:
|
||||||
"yes": 是
|
"yes": 是
|
||||||
"no": 否
|
"no": 否
|
||||||
|
|
|
@ -73,7 +73,8 @@ const {
|
||||||
const { spec, metadata, status } = post.post;
|
const { spec, metadata, status } = post.post;
|
||||||
return (
|
return (
|
||||||
spec.deleted ||
|
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)
|
(spec.releaseSnapshot === spec.headSnapshot && status?.inProgress)
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
@ -4,6 +4,7 @@ import {
|
||||||
IconExternalLinkLine,
|
IconExternalLinkLine,
|
||||||
IconEye,
|
IconEye,
|
||||||
IconEyeOff,
|
IconEyeOff,
|
||||||
|
IconTimerLine,
|
||||||
Toast,
|
Toast,
|
||||||
VAvatar,
|
VAvatar,
|
||||||
VDropdownItem,
|
VDropdownItem,
|
||||||
|
@ -51,7 +52,9 @@ const publishStatus = computed(() => {
|
||||||
const isPublishing = computed(() => {
|
const isPublishing = computed(() => {
|
||||||
const { spec, status, metadata } = props.post.post;
|
const { spec, status, metadata } = props.post.post;
|
||||||
return (
|
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)
|
(spec.releaseSnapshot === spec.headSnapshot && status?.inProgress)
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -204,10 +207,23 @@ function handleUnpublish() {
|
||||||
state="warning"
|
state="warning"
|
||||||
animate
|
animate
|
||||||
/>
|
/>
|
||||||
<VEntityField
|
<VEntityField v-if="post.post.spec.publishTime">
|
||||||
v-if="post.post.spec.publishTime"
|
<template #description>
|
||||||
:description="formatDatetime(post.post.spec.publishTime)"
|
<div class="inline-flex items-center space-x-2">
|
||||||
></VEntityField>
|
<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>
|
||||||
<template #dropdownItems>
|
<template #dropdownItems>
|
||||||
<VDropdownItem
|
<VDropdownItem
|
||||||
|
|
|
@ -1,12 +1,15 @@
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { IconRefreshLine } from "@halo-dev/components";
|
import { IconRefreshLine } from "@halo-dev/components";
|
||||||
import type { PostFormState } from "../types";
|
import type { PostFormState } from "../types";
|
||||||
import { toISOString } from "@/utils/date";
|
import { formatDatetime, toISOString } from "@/utils/date";
|
||||||
import { computed } from "vue";
|
import { computed } from "vue";
|
||||||
import useSlugify from "@console/composables/use-slugify";
|
import useSlugify from "@console/composables/use-slugify";
|
||||||
import { FormType } from "@/types/slug";
|
import { FormType } from "@/types/slug";
|
||||||
import { ref } from "vue";
|
import { ref } from "vue";
|
||||||
import HasPermission from "@/components/permission/HasPermission.vue";
|
import HasPermission from "@/components/permission/HasPermission.vue";
|
||||||
|
import { useI18n } from "vue-i18n";
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
|
|
||||||
const props = withDefaults(
|
const props = withDefaults(
|
||||||
defineProps<{
|
defineProps<{
|
||||||
|
@ -60,6 +63,19 @@ const { handleGenerateSlug } = useSlugify(
|
||||||
computed(() => !props.updateMode),
|
computed(() => !props.updateMode),
|
||||||
FormType.POST
|
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>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
@ -189,6 +205,7 @@ const { handleGenerateSlug } = useSlugify(
|
||||||
type="datetime-local"
|
type="datetime-local"
|
||||||
min="0000-01-01T00:00"
|
min="0000-01-01T00:00"
|
||||||
max="9999-12-31T23:59"
|
max="9999-12-31T23:59"
|
||||||
|
:help="publishTimeHelp"
|
||||||
></FormKit>
|
></FormKit>
|
||||||
<HasPermission :permissions="['system:attachments:view']">
|
<HasPermission :permissions="['system:attachments:view']">
|
||||||
<FormKit
|
<FormKit
|
||||||
|
|
Loading…
Reference in New Issue