mirror of https://github.com/halo-dev/halo
Support managing posts in the user center (#4866)
* Support managing posts in user center Signed-off-by: John Niang <johnniang@foxmail.com> * Adapt post management in user center Signed-off-by: Ryan Wang <i@ryanc.cc> --------- Signed-off-by: John Niang <johnniang@foxmail.com> Signed-off-by: Ryan Wang <i@ryanc.cc> Co-authored-by: Ryan Wang <i@ryanc.cc>pull/4942/head^2
parent
f659a3279e
commit
b2b096c544
|
@ -16,7 +16,7 @@ import reactor.core.publisher.Mono;
|
|||
* @param content is binary data of the attachment file.
|
||||
* @param mediaType is media type of the attachment file.
|
||||
*/
|
||||
record SimpleFilePart(
|
||||
public record SimpleFilePart(
|
||||
String filename,
|
||||
Flux<DataBuffer> content,
|
||||
MediaType mediaType
|
||||
|
|
|
@ -9,6 +9,7 @@ import java.util.Set;
|
|||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.ToString;
|
||||
import org.springframework.lang.NonNull;
|
||||
import org.springframework.util.Assert;
|
||||
import run.halo.app.extension.AbstractExtension;
|
||||
import run.halo.app.extension.GVK;
|
||||
|
@ -27,6 +28,8 @@ import run.halo.app.extension.Ref;
|
|||
public class Snapshot extends AbstractExtension {
|
||||
public static final String KIND = "Snapshot";
|
||||
public static final String KEEP_RAW_ANNO = "content.halo.run/keep-raw";
|
||||
public static final String PATCHED_CONTENT_ANNO = "content.halo.run/patched-content";
|
||||
public static final String PATCHED_RAW_ANNO = "content.halo.run/patched-raw";
|
||||
|
||||
@Schema(requiredMode = REQUIRED)
|
||||
private SnapShotSpec spec;
|
||||
|
@ -67,4 +70,18 @@ public class Snapshot extends AbstractExtension {
|
|||
contributors.add(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the given snapshot is a base snapshot.
|
||||
*
|
||||
* @param snapshot must not be null.
|
||||
* @return true if the given snapshot is a base snapshot; false otherwise.
|
||||
*/
|
||||
public static boolean isBaseSnapshot(@NonNull Snapshot snapshot) {
|
||||
var annotations = snapshot.getMetadata().getAnnotations();
|
||||
if (annotations == null) {
|
||||
return false;
|
||||
}
|
||||
return Boolean.parseBoolean(annotations.get(Snapshot.KEEP_RAW_ANNO));
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -2,8 +2,10 @@ package run.halo.app.core.extension.service;
|
|||
|
||||
import java.net.URI;
|
||||
import java.time.Duration;
|
||||
import java.util.function.Consumer;
|
||||
import org.springframework.core.io.buffer.DataBuffer;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.codec.multipart.FilePart;
|
||||
import org.springframework.lang.NonNull;
|
||||
import org.springframework.lang.Nullable;
|
||||
import reactor.core.publisher.Flux;
|
||||
|
@ -18,6 +20,25 @@ import run.halo.app.core.extension.attachment.Attachment;
|
|||
*/
|
||||
public interface AttachmentService {
|
||||
|
||||
/**
|
||||
* Uploads the given attachment to specific storage using handlers in plugins.
|
||||
* <p>
|
||||
* If no handler can be found to upload the given attachment, ServerError exception will be
|
||||
* thrown.
|
||||
*
|
||||
* @param policyName is attachment policy name.
|
||||
* @param groupName is group name the attachment belongs.
|
||||
* @param filePart contains filename, content and media type.
|
||||
* @param beforeCreating is an attachment modifier before creating.
|
||||
* @return attachment.
|
||||
*/
|
||||
Mono<Attachment> upload(
|
||||
@NonNull String username,
|
||||
@NonNull String policyName,
|
||||
@Nullable String groupName,
|
||||
@NonNull FilePart filePart,
|
||||
@Nullable Consumer<Attachment> beforeCreating);
|
||||
|
||||
/**
|
||||
* Uploads the given attachment to specific storage using handlers in plugins. Please note
|
||||
* that we will make sure the request is authenticated, or an unauthorized exception throws.
|
||||
|
|
|
@ -5,6 +5,7 @@ import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;
|
|||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import java.util.Objects;
|
||||
import lombok.Data;
|
||||
import org.springframework.lang.NonNull;
|
||||
|
||||
@Data
|
||||
@Schema(description = "Extension reference object. The name is mandatory")
|
||||
|
@ -59,4 +60,18 @@ public class Ref {
|
|||
return Objects.equals(ref.getGroup(), gvk.group())
|
||||
&& Objects.equals(ref.getKind(), gvk.kind());
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the extension is equal to the ref.
|
||||
*
|
||||
* @param ref must not be null.
|
||||
* @param extension must not be null.
|
||||
* @return true if they are equal; false otherwise.
|
||||
*/
|
||||
public static boolean equals(@NonNull Ref ref, @NonNull ExtensionOperator extension) {
|
||||
var gvk = extension.groupVersionKind();
|
||||
var name = extension.getMetadata().getName();
|
||||
return groupKindEquals(ref, gvk) && Objects.equals(ref.getName(), name);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -80,6 +80,9 @@ public class SystemSetting {
|
|||
Integer tagPageSize;
|
||||
Boolean review;
|
||||
String slugGenerationStrategy;
|
||||
|
||||
String attachmentPolicyName;
|
||||
String attachmentGroupName;
|
||||
}
|
||||
|
||||
@Data
|
||||
|
|
|
@ -75,6 +75,15 @@ public class SwaggerConfig {
|
|||
.build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
GroupedOpenApi userCenterApi() {
|
||||
return GroupedOpenApi.builder()
|
||||
.group("uc.api")
|
||||
.displayName("User center APIs.")
|
||||
.pathsToMatch("/apis/uc.api.*/**")
|
||||
.build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
GroupedOpenApi allApi() {
|
||||
return GroupedOpenApi.builder()
|
||||
|
|
|
@ -51,9 +51,7 @@ public abstract class AbstractContentService {
|
|||
|
||||
protected void checkBaseSnapshot(Snapshot snapshot) {
|
||||
Assert.notNull(snapshot, "The snapshot must not be null.");
|
||||
String keepRawAnno =
|
||||
MetadataUtil.nullSafeAnnotations(snapshot).get(Snapshot.KEEP_RAW_ANNO);
|
||||
if (!StringUtils.equals(Boolean.TRUE.toString(), keepRawAnno)) {
|
||||
if (!Snapshot.isBaseSnapshot(snapshot)) {
|
||||
throw new IllegalArgumentException(
|
||||
String.format("The snapshot [%s] is not a base snapshot.",
|
||||
snapshot.getMetadata().getName()));
|
||||
|
@ -68,7 +66,7 @@ public abstract class AbstractContentService {
|
|||
snapshot.getSpec().setParentSnapshotName(parentSnapshotName);
|
||||
|
||||
final String baseSnapshotNameToUse =
|
||||
StringUtils.defaultString(baseSnapshotName, snapshot.getMetadata().getName());
|
||||
StringUtils.defaultIfBlank(baseSnapshotName, snapshot.getMetadata().getName());
|
||||
return client.fetch(Snapshot.class, baseSnapshotName)
|
||||
.doOnNext(this::checkBaseSnapshot)
|
||||
.defaultIfEmpty(snapshot)
|
||||
|
@ -119,7 +117,8 @@ public abstract class AbstractContentService {
|
|||
.map(baseSnapshot -> ContentWrapper.patchSnapshot(headSnapshot, baseSnapshot));
|
||||
}
|
||||
|
||||
protected Snapshot determineRawAndContentPatch(Snapshot snapshotToUse, Snapshot baseSnapshot,
|
||||
protected Snapshot determineRawAndContentPatch(Snapshot snapshotToUse,
|
||||
Snapshot baseSnapshot,
|
||||
ContentRequest contentRequest) {
|
||||
Assert.notNull(baseSnapshot, "The baseSnapshot must not be null.");
|
||||
Assert.notNull(contentRequest, "The contentRequest must not be null.");
|
||||
|
@ -130,7 +129,7 @@ public abstract class AbstractContentService {
|
|||
|
||||
snapshotToUse.getSpec().setLastModifyTime(Instant.now());
|
||||
// it is the v1 snapshot, set the content directly
|
||||
if (org.thymeleaf.util.StringUtils.equals(baseSnapshotName,
|
||||
if (StringUtils.equals(baseSnapshotName,
|
||||
snapshotToUse.getMetadata().getName())) {
|
||||
snapshotToUse.getSpec().setRawPatch(contentRequest.raw());
|
||||
snapshotToUse.getSpec().setContentPatch(contentRequest.content());
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
package run.halo.app.content;
|
||||
|
||||
public record Content(String raw, String content, String rawType) {
|
||||
}
|
|
@ -3,17 +3,20 @@ package run.halo.app.content;
|
|||
import static java.util.Comparator.comparing;
|
||||
import static run.halo.app.extension.router.selector.SelectorUtil.labelAndFieldSelectorToPredicate;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import io.swagger.v3.oas.annotations.media.ArraySchema;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.function.Predicate;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.data.domain.Sort;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import org.springframework.web.reactive.function.server.ServerRequest;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
import run.halo.app.core.extension.content.Post;
|
||||
|
@ -31,9 +34,22 @@ public class PostQuery extends IListRequest.QueryListRequest {
|
|||
|
||||
private final ServerWebExchange exchange;
|
||||
|
||||
private final String username;
|
||||
|
||||
public PostQuery(ServerRequest request) {
|
||||
this(request, null);
|
||||
}
|
||||
|
||||
public PostQuery(ServerRequest request, @Nullable String username) {
|
||||
super(request.queryParams());
|
||||
this.exchange = request.exchange();
|
||||
this.username = username;
|
||||
}
|
||||
|
||||
@Schema(hidden = true)
|
||||
@JsonIgnore
|
||||
public String getUsername() {
|
||||
return username;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
|
@ -131,14 +147,27 @@ public class PostQuery extends IListRequest.QueryListRequest {
|
|||
* @return a predicate
|
||||
*/
|
||||
public Predicate<Post> toPredicate() {
|
||||
Predicate<Post> paramPredicate = post ->
|
||||
contains(getCategories(), post.getSpec().getCategories())
|
||||
&& contains(getTags(), post.getSpec().getTags())
|
||||
&& contains(getContributors(), post.getStatusOrDefault().getContributors());
|
||||
Predicate<Post> predicate = labelAndFieldSelectorToPredicate(getLabelSelector(),
|
||||
getFieldSelector());
|
||||
|
||||
if (!CollectionUtils.isEmpty(getCategories())) {
|
||||
predicate =
|
||||
predicate.and(post -> contains(getCategories(), post.getSpec().getCategories()));
|
||||
}
|
||||
if (!CollectionUtils.isEmpty(getTags())) {
|
||||
predicate = predicate.and(post -> contains(getTags(), post.getSpec().getTags()));
|
||||
}
|
||||
if (!CollectionUtils.isEmpty(getContributors())) {
|
||||
Predicate<Post> hasStatus = post -> post.getStatus() != null;
|
||||
var containsContributors = hasStatus.and(
|
||||
post -> contains(getContributors(), post.getStatus().getContributors())
|
||||
);
|
||||
predicate = predicate.and(containsContributors);
|
||||
}
|
||||
|
||||
String keyword = getKeyword();
|
||||
if (keyword != null) {
|
||||
paramPredicate = paramPredicate.and(post -> {
|
||||
predicate = predicate.and(post -> {
|
||||
String excerpt = post.getStatusOrDefault().getExcerpt();
|
||||
return StringUtils.containsIgnoreCase(excerpt, keyword)
|
||||
|| StringUtils.containsIgnoreCase(post.getSpec().getSlug(), keyword)
|
||||
|
@ -148,7 +177,7 @@ public class PostQuery extends IListRequest.QueryListRequest {
|
|||
|
||||
Post.PostPhase publishPhase = getPublishPhase();
|
||||
if (publishPhase != null) {
|
||||
paramPredicate = paramPredicate.and(post -> {
|
||||
predicate = predicate.and(post -> {
|
||||
if (Post.PostPhase.PENDING_APPROVAL.equals(publishPhase)) {
|
||||
return !post.isPublished()
|
||||
&& Post.PostPhase.PENDING_APPROVAL.name()
|
||||
|
@ -165,13 +194,15 @@ public class PostQuery extends IListRequest.QueryListRequest {
|
|||
|
||||
Post.VisibleEnum visible = getVisible();
|
||||
if (visible != null) {
|
||||
paramPredicate =
|
||||
paramPredicate.and(post -> visible.equals(post.getSpec().getVisible()));
|
||||
predicate =
|
||||
predicate.and(post -> visible.equals(post.getSpec().getVisible()));
|
||||
}
|
||||
|
||||
Predicate<Post> predicate = labelAndFieldSelectorToPredicate(getLabelSelector(),
|
||||
getFieldSelector());
|
||||
return predicate.and(paramPredicate);
|
||||
if (StringUtils.isNotBlank(username)) {
|
||||
Predicate<Post> isOwner = post -> Objects.equals(username, post.getSpec().getOwner());
|
||||
predicate = predicate.and(isOwner);
|
||||
}
|
||||
return predicate;
|
||||
}
|
||||
|
||||
boolean contains(Collection<String> left, List<String> right) {
|
||||
|
|
|
@ -3,22 +3,23 @@ package run.halo.app.content;
|
|||
import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import org.springframework.lang.NonNull;
|
||||
import run.halo.app.core.extension.content.Post;
|
||||
import run.halo.app.extension.Ref;
|
||||
|
||||
/**
|
||||
* Post and content data for creating and updating post.
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.0.0
|
||||
*/
|
||||
public record PostRequest(@Schema(requiredMode = REQUIRED) Post post,
|
||||
@Schema(requiredMode = REQUIRED) Content content) {
|
||||
public record PostRequest(@Schema(requiredMode = REQUIRED) @NonNull Post post,
|
||||
Content content) {
|
||||
|
||||
public ContentRequest contentRequest() {
|
||||
Ref subjectRef = Ref.of(post);
|
||||
return new ContentRequest(subjectRef, post.getSpec().getHeadSnapshot(), content.raw,
|
||||
content.content, content.rawType);
|
||||
return new ContentRequest(subjectRef, post.getSpec().getHeadSnapshot(), content.raw(),
|
||||
content.content(), content.rawType());
|
||||
}
|
||||
|
||||
public record Content(String raw, String content, String rawType) {
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package run.halo.app.content;
|
||||
|
||||
import org.springframework.lang.NonNull;
|
||||
import reactor.core.publisher.Mono;
|
||||
import run.halo.app.core.extension.content.Post;
|
||||
import run.halo.app.extension.ListResult;
|
||||
|
@ -18,9 +19,28 @@ public interface PostService {
|
|||
|
||||
Mono<Post> updatePost(PostRequest postRequest);
|
||||
|
||||
Mono<Post> updateBy(@NonNull Post post);
|
||||
|
||||
Mono<ContentWrapper> getHeadContent(String postName);
|
||||
|
||||
Mono<ContentWrapper> getHeadContent(Post post);
|
||||
|
||||
Mono<ContentWrapper> getReleaseContent(String postName);
|
||||
|
||||
Mono<ContentWrapper> getReleaseContent(Post post);
|
||||
|
||||
Mono<ContentWrapper> getContent(String snapshotName, String baseSnapshotName);
|
||||
|
||||
Mono<Post> publish(Post post);
|
||||
|
||||
Mono<Post> unpublish(Post post);
|
||||
|
||||
/**
|
||||
* Get post by username.
|
||||
*
|
||||
* @param postName is post name.
|
||||
* @param username is username.
|
||||
* @return full post data or empty.
|
||||
*/
|
||||
Mono<Post> getByUsername(String postName, String username);
|
||||
}
|
||||
|
|
|
@ -17,10 +17,8 @@ public record SinglePageRequest(@Schema(requiredMode = REQUIRED) SinglePage page
|
|||
|
||||
public ContentRequest contentRequest() {
|
||||
Ref subjectRef = Ref.of(page);
|
||||
return new ContentRequest(subjectRef, page.getSpec().getHeadSnapshot(), content.raw,
|
||||
content.content, content.rawType);
|
||||
return new ContentRequest(subjectRef, page.getSpec().getHeadSnapshot(), content.raw(),
|
||||
content.content(), content.rawType());
|
||||
}
|
||||
|
||||
public record Content(String raw, String content, String rawType) {
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
package run.halo.app.content;
|
||||
|
||||
import org.springframework.lang.NonNull;
|
||||
import org.springframework.lang.Nullable;
|
||||
import reactor.core.publisher.Mono;
|
||||
import run.halo.app.core.extension.content.Snapshot;
|
||||
|
||||
public interface SnapshotService {
|
||||
|
||||
Mono<Snapshot> getBy(String snapshotName);
|
||||
|
||||
Mono<Snapshot> getPatchedBy(String snapshotName, String baseSnapshotName);
|
||||
|
||||
Mono<Snapshot> patchAndCreate(@NonNull Snapshot snapshot,
|
||||
@Nullable Snapshot baseSnapshot,
|
||||
@NonNull Content content);
|
||||
|
||||
Mono<Snapshot> patchAndUpdate(@NonNull Snapshot snapshot,
|
||||
@NonNull Snapshot baseSnapshot,
|
||||
@NonNull Content content);
|
||||
|
||||
}
|
|
@ -8,6 +8,7 @@ import java.util.function.Function;
|
|||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.dao.OptimisticLockingFailureException;
|
||||
import org.springframework.lang.NonNull;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.util.Assert;
|
||||
import reactor.core.publisher.Flux;
|
||||
|
@ -185,6 +186,9 @@ public class PostServiceImpl extends AbstractContentService implements PostServi
|
|||
)
|
||||
.flatMap(client::create)
|
||||
.flatMap(post -> {
|
||||
if (postRequest.content() == null) {
|
||||
return Mono.just(post);
|
||||
}
|
||||
var contentRequest =
|
||||
new ContentRequest(Ref.of(post), post.getSpec().getHeadSnapshot(),
|
||||
postRequest.content().raw(), postRequest.content().content(),
|
||||
|
@ -248,21 +252,59 @@ public class PostServiceImpl extends AbstractContentService implements PostServi
|
|||
.filter(throwable -> throwable instanceof OptimisticLockingFailureException));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Post> updateBy(@NonNull Post post) {
|
||||
return client.update(post);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<ContentWrapper> getHeadContent(String postName) {
|
||||
return client.get(Post.class, postName)
|
||||
.flatMap(post -> {
|
||||
String headSnapshot = post.getSpec().getHeadSnapshot();
|
||||
.flatMap(this::getHeadContent);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<ContentWrapper> getHeadContent(Post post) {
|
||||
var headSnapshot = post.getSpec().getHeadSnapshot();
|
||||
return getContent(headSnapshot, post.getSpec().getBaseSnapshot());
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<ContentWrapper> getReleaseContent(String postName) {
|
||||
return client.get(Post.class, postName)
|
||||
.flatMap(post -> {
|
||||
String releaseSnapshot = post.getSpec().getReleaseSnapshot();
|
||||
.flatMap(this::getReleaseContent);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<ContentWrapper> getReleaseContent(Post post) {
|
||||
var releaseSnapshot = post.getSpec().getReleaseSnapshot();
|
||||
return getContent(releaseSnapshot, post.getSpec().getBaseSnapshot());
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Post> publish(Post post) {
|
||||
return Mono.just(post)
|
||||
.doOnNext(p -> {
|
||||
var spec = post.getSpec();
|
||||
spec.setPublish(true);
|
||||
if (spec.getHeadSnapshot() == null) {
|
||||
spec.setHeadSnapshot(spec.getBaseSnapshot());
|
||||
}
|
||||
spec.setReleaseSnapshot(spec.getHeadSnapshot());
|
||||
}).flatMap(client::update);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Post> unpublish(Post post) {
|
||||
return Mono.just(post)
|
||||
.doOnNext(p -> p.getSpec().setPublish(false))
|
||||
.flatMap(client::update);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Post> getByUsername(String postName, String username) {
|
||||
return client.get(Post.class, postName)
|
||||
.filter(post -> post.getSpec() != null)
|
||||
.filter(post -> Objects.equals(username, post.getSpec().getOwner()));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,125 @@
|
|||
package run.halo.app.content.impl;
|
||||
|
||||
import java.time.Clock;
|
||||
import java.util.HashMap;
|
||||
import java.util.Objects;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.lang.NonNull;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.stereotype.Service;
|
||||
import reactor.core.publisher.Mono;
|
||||
import run.halo.app.content.Content;
|
||||
import run.halo.app.content.PatchUtils;
|
||||
import run.halo.app.content.SnapshotService;
|
||||
import run.halo.app.core.extension.content.Snapshot;
|
||||
import run.halo.app.extension.ReactiveExtensionClient;
|
||||
|
||||
@Service
|
||||
public class SnapshotServiceImpl implements SnapshotService {
|
||||
|
||||
private final ReactiveExtensionClient client;
|
||||
|
||||
private Clock clock;
|
||||
|
||||
public SnapshotServiceImpl(ReactiveExtensionClient client) {
|
||||
this.client = client;
|
||||
this.clock = Clock.systemDefaultZone();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Snapshot> getBy(String snapshotName) {
|
||||
return client.get(Snapshot.class, snapshotName);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Snapshot> getPatchedBy(String snapshotName, String baseSnapshotName) {
|
||||
if (StringUtils.isBlank(snapshotName) || StringUtils.isBlank(baseSnapshotName)) {
|
||||
return Mono.empty();
|
||||
}
|
||||
|
||||
return client.fetch(Snapshot.class, baseSnapshotName)
|
||||
.filter(Snapshot::isBaseSnapshot)
|
||||
.switchIfEmpty(Mono.error(() -> new IllegalArgumentException(
|
||||
"The snapshot " + baseSnapshotName + " is not a base snapshot.")))
|
||||
.flatMap(baseSnapshot ->
|
||||
Mono.defer(() -> {
|
||||
if (Objects.equals(snapshotName, baseSnapshotName)) {
|
||||
return Mono.just(baseSnapshot);
|
||||
}
|
||||
return client.fetch(Snapshot.class, snapshotName);
|
||||
}).doOnNext(snapshot -> {
|
||||
var baseRaw = baseSnapshot.getSpec().getRawPatch();
|
||||
var baseContent = baseSnapshot.getSpec().getContentPatch();
|
||||
|
||||
var rawPatch = snapshot.getSpec().getRawPatch();
|
||||
var contentPatch = snapshot.getSpec().getContentPatch();
|
||||
|
||||
var annotations = snapshot.getMetadata().getAnnotations();
|
||||
if (annotations == null) {
|
||||
annotations = new HashMap<>();
|
||||
snapshot.getMetadata().setAnnotations(annotations);
|
||||
}
|
||||
|
||||
String patchedContent = baseContent;
|
||||
String patchedRaw = baseRaw;
|
||||
if (!Objects.equals(snapshot, baseSnapshot)) {
|
||||
patchedContent = PatchUtils.applyPatch(baseContent, contentPatch);
|
||||
patchedRaw = PatchUtils.applyPatch(baseRaw, rawPatch);
|
||||
}
|
||||
|
||||
annotations.put(Snapshot.PATCHED_CONTENT_ANNO, patchedContent);
|
||||
annotations.put(Snapshot.PATCHED_RAW_ANNO, patchedRaw);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Snapshot> patchAndCreate(@NonNull Snapshot snapshot,
|
||||
@Nullable Snapshot baseSnapshot,
|
||||
@NonNull Content content) {
|
||||
return Mono.just(snapshot)
|
||||
.doOnNext(s -> this.patch(s, baseSnapshot, content))
|
||||
.flatMap(client::create);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Snapshot> patchAndUpdate(@NonNull Snapshot snapshot,
|
||||
@NonNull Snapshot baseSnapshot,
|
||||
@NonNull Content content) {
|
||||
return Mono.just(snapshot)
|
||||
.doOnNext(s -> this.patch(s, baseSnapshot, content))
|
||||
.flatMap(client::update);
|
||||
}
|
||||
|
||||
private void patch(@NonNull Snapshot snapshot,
|
||||
@Nullable Snapshot baseSnapshot,
|
||||
@NonNull Content content) {
|
||||
var annotations = snapshot.getMetadata().getAnnotations();
|
||||
if (annotations != null) {
|
||||
annotations.remove(Snapshot.PATCHED_CONTENT_ANNO);
|
||||
annotations.remove(Snapshot.PATCHED_RAW_ANNO);
|
||||
}
|
||||
var spec = snapshot.getSpec();
|
||||
if (spec == null) {
|
||||
spec = new Snapshot.SnapShotSpec();
|
||||
}
|
||||
spec.setRawType(content.rawType());
|
||||
if (baseSnapshot == null || Objects.equals(snapshot, baseSnapshot)) {
|
||||
// indicate the snapshot is a base snapshot
|
||||
// update raw and content directly
|
||||
spec.setRawPatch(content.raw());
|
||||
spec.setContentPatch(content.content());
|
||||
} else {
|
||||
// apply the patch and set the raw and content
|
||||
var baseSpec = baseSnapshot.getSpec();
|
||||
var baseContent = baseSpec.getContentPatch();
|
||||
var baseRaw = baseSpec.getRawPatch();
|
||||
|
||||
var rawPatch = PatchUtils.diffToJsonPatch(baseRaw, content.raw());
|
||||
var contentPatch = PatchUtils.diffToJsonPatch(baseContent, content.content());
|
||||
spec.setRawPatch(rawPatch);
|
||||
spec.setContentPatch(contentPatch);
|
||||
}
|
||||
spec.setLastModifyTime(clock.instant());
|
||||
}
|
||||
}
|
|
@ -23,6 +23,7 @@ import org.springframework.web.server.ServerErrorException;
|
|||
import reactor.core.Exceptions;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.util.retry.Retry;
|
||||
import run.halo.app.content.Content;
|
||||
import run.halo.app.content.ContentWrapper;
|
||||
import run.halo.app.content.ListedPost;
|
||||
import run.halo.app.content.PostQuery;
|
||||
|
@ -132,7 +133,7 @@ public class PostEndpoint implements CustomEndpoint {
|
|||
.content(contentBuilder()
|
||||
.mediaType(MediaType.APPLICATION_JSON_VALUE)
|
||||
.schema(Builder.schemaBuilder()
|
||||
.implementation(PostRequest.Content.class))
|
||||
.implementation(Content.class))
|
||||
))
|
||||
.response(responseBuilder()
|
||||
.implementation(Post.class))
|
||||
|
@ -191,7 +192,7 @@ public class PostEndpoint implements CustomEndpoint {
|
|||
|
||||
Mono<ServerResponse> updateContent(ServerRequest request) {
|
||||
String postName = request.pathVariable("name");
|
||||
return request.bodyToMono(PostRequest.Content.class)
|
||||
return request.bodyToMono(Content.class)
|
||||
.flatMap(content -> client.fetch(Post.class, postName)
|
||||
.flatMap(post -> {
|
||||
PostRequest postRequest = new PostRequest(post, content);
|
||||
|
|
|
@ -21,6 +21,7 @@ import org.springframework.web.reactive.function.server.ServerResponse;
|
|||
import org.thymeleaf.util.StringUtils;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.util.retry.Retry;
|
||||
import run.halo.app.content.Content;
|
||||
import run.halo.app.content.ContentWrapper;
|
||||
import run.halo.app.content.ListedSinglePage;
|
||||
import run.halo.app.content.SinglePageQuery;
|
||||
|
@ -130,7 +131,7 @@ public class SinglePageEndpoint implements CustomEndpoint {
|
|||
.content(contentBuilder()
|
||||
.mediaType(MediaType.APPLICATION_JSON_VALUE)
|
||||
.schema(Builder.schemaBuilder()
|
||||
.implementation(SinglePageRequest.Content.class))
|
||||
.implementation(Content.class))
|
||||
))
|
||||
.response(responseBuilder()
|
||||
.implementation(Post.class))
|
||||
|
@ -169,7 +170,7 @@ public class SinglePageEndpoint implements CustomEndpoint {
|
|||
|
||||
Mono<ServerResponse> updateContent(ServerRequest request) {
|
||||
String pageName = request.pathVariable("name");
|
||||
return request.bodyToMono(SinglePageRequest.Content.class)
|
||||
return request.bodyToMono(Content.class)
|
||||
.flatMap(content -> client.fetch(SinglePage.class, pageName)
|
||||
.flatMap(page -> {
|
||||
SinglePageRequest pageRequest = new SinglePageRequest(page, content);
|
||||
|
|
|
@ -66,11 +66,9 @@ public class DefaultRoleService implements RoleService {
|
|||
// search all permissions
|
||||
return extensionClient.list(Role.class,
|
||||
shouldFilterHidden(true),
|
||||
compareCreationTimestamp(true))
|
||||
.filter(DefaultRoleService::isRoleTemplate);
|
||||
compareCreationTimestamp(true));
|
||||
}
|
||||
return listDependencies(names, shouldFilterHidden(true))
|
||||
.filter(DefaultRoleService::isRoleTemplate);
|
||||
return listDependencies(names, shouldFilterHidden(true));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -2,10 +2,12 @@ package run.halo.app.core.extension.service.impl;
|
|||
|
||||
import java.net.URI;
|
||||
import java.time.Duration;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
import org.springframework.core.io.buffer.DataBuffer;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.codec.multipart.FilePart;
|
||||
import org.springframework.lang.NonNull;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.security.core.Authentication;
|
||||
|
@ -22,6 +24,7 @@ import run.halo.app.core.extension.attachment.Attachment;
|
|||
import run.halo.app.core.extension.attachment.Policy;
|
||||
import run.halo.app.core.extension.attachment.endpoint.AttachmentHandler;
|
||||
import run.halo.app.core.extension.attachment.endpoint.DeleteOption;
|
||||
import run.halo.app.core.extension.attachment.endpoint.SimpleFilePart;
|
||||
import run.halo.app.core.extension.attachment.endpoint.UploadOption;
|
||||
import run.halo.app.core.extension.service.AttachmentService;
|
||||
import run.halo.app.extension.ConfigMap;
|
||||
|
@ -42,12 +45,13 @@ public class DefaultAttachmentService implements AttachmentService {
|
|||
}
|
||||
|
||||
@Override
|
||||
public Mono<Attachment> upload(@NonNull String policyName,
|
||||
public Mono<Attachment> upload(
|
||||
@NonNull String username,
|
||||
@NonNull String policyName,
|
||||
@Nullable String groupName,
|
||||
@NonNull String filename,
|
||||
@NonNull Flux<DataBuffer> content,
|
||||
@Nullable MediaType mediaType) {
|
||||
return authenticationConsumer(authentication -> client.get(Policy.class, policyName)
|
||||
@NonNull FilePart filePart,
|
||||
@Nullable Consumer<Attachment> beforeCreating) {
|
||||
return client.get(Policy.class, policyName)
|
||||
.flatMap(policy -> {
|
||||
var configMapName = policy.getSpec().getConfigMapName();
|
||||
if (!StringUtils.hasText(configMapName)) {
|
||||
|
@ -55,11 +59,7 @@ public class DefaultAttachmentService implements AttachmentService {
|
|||
"ConfigMap name not found in Policy " + policyName));
|
||||
}
|
||||
return client.get(ConfigMap.class, configMapName)
|
||||
.map(configMap -> UploadOption.from(filename,
|
||||
content,
|
||||
mediaType,
|
||||
policy,
|
||||
configMap));
|
||||
.map(configMap -> new UploadOption(filePart, policy, configMap));
|
||||
})
|
||||
.flatMap(uploadContext -> {
|
||||
var handlers = extensionComponentsFinder.getExtensions(AttachmentHandler.class);
|
||||
|
@ -75,13 +75,29 @@ public class DefaultAttachmentService implements AttachmentService {
|
|||
spec = new Attachment.AttachmentSpec();
|
||||
attachment.setSpec(spec);
|
||||
}
|
||||
spec.setOwnerName(authentication.getName());
|
||||
spec.setOwnerName(username);
|
||||
if (StringUtils.hasText(groupName)) {
|
||||
spec.setGroupName(groupName);
|
||||
}
|
||||
spec.setPolicyName(policyName);
|
||||
})
|
||||
.flatMap(client::create));
|
||||
.doOnNext(attachment -> {
|
||||
if (beforeCreating != null) {
|
||||
beforeCreating.accept(attachment);
|
||||
}
|
||||
})
|
||||
.flatMap(client::create);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Attachment> upload(@NonNull String policyName,
|
||||
@Nullable String groupName,
|
||||
@NonNull String filename,
|
||||
@NonNull Flux<DataBuffer> content,
|
||||
@Nullable MediaType mediaType) {
|
||||
var file = new SimpleFilePart(filename, content, mediaType);
|
||||
return authenticationConsumer(
|
||||
authentication -> upload(authentication.getName(), policyName, groupName, file, null));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -0,0 +1,208 @@
|
|||
package run.halo.app.endpoint.uc.content;
|
||||
|
||||
import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.NOT_REQUIRED;
|
||||
import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;
|
||||
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 org.springdoc.core.fn.builders.schema.Builder.schemaBuilder;
|
||||
import static org.springdoc.webflux.core.fn.SpringdocRouteBuilder.route;
|
||||
|
||||
import io.swagger.v3.oas.annotations.enums.ParameterIn;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import java.util.HashMap;
|
||||
import java.util.function.Consumer;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.codec.multipart.FilePart;
|
||||
import org.springframework.http.codec.multipart.FormFieldPart;
|
||||
import org.springframework.http.codec.multipart.Part;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.context.ReactiveSecurityContextHolder;
|
||||
import org.springframework.security.core.context.SecurityContext;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
import org.springframework.web.reactive.function.BodyExtractors;
|
||||
import org.springframework.web.reactive.function.server.RouterFunction;
|
||||
import org.springframework.web.reactive.function.server.ServerRequest;
|
||||
import org.springframework.web.reactive.function.server.ServerResponse;
|
||||
import org.springframework.web.server.ServerWebInputException;
|
||||
import reactor.core.publisher.Mono;
|
||||
import run.halo.app.content.PostService;
|
||||
import run.halo.app.core.extension.attachment.Attachment;
|
||||
import run.halo.app.core.extension.content.Post;
|
||||
import run.halo.app.core.extension.endpoint.CustomEndpoint;
|
||||
import run.halo.app.core.extension.service.AttachmentService;
|
||||
import run.halo.app.extension.GroupVersion;
|
||||
import run.halo.app.infra.SystemConfigurableEnvironmentFetcher;
|
||||
import run.halo.app.infra.SystemSetting;
|
||||
import run.halo.app.infra.exception.NotFoundException;
|
||||
|
||||
@Component
|
||||
public class UcPostAttachmentEndpoint implements CustomEndpoint {
|
||||
|
||||
public static final String POST_NAME_LABEL = "content.halo.run/post-name";
|
||||
public static final String SINGLE_PAGE_NAME_LABEL = "content.halo.run/single-page-name";
|
||||
|
||||
private final AttachmentService attachmentService;
|
||||
|
||||
private final PostService postService;
|
||||
|
||||
private final SystemConfigurableEnvironmentFetcher systemSettingFetcher;
|
||||
|
||||
public UcPostAttachmentEndpoint(AttachmentService attachmentService, PostService postService,
|
||||
SystemConfigurableEnvironmentFetcher systemSettingFetcher) {
|
||||
this.attachmentService = attachmentService;
|
||||
this.postService = postService;
|
||||
this.systemSettingFetcher = systemSettingFetcher;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RouterFunction<ServerResponse> endpoint() {
|
||||
var tag = groupVersion() + "/Attachment";
|
||||
return route()
|
||||
.POST("/attachments",
|
||||
this::createAttachmentForPost,
|
||||
builder -> builder.operationId("CreateAttachmentForPost").tag(tag)
|
||||
.description("Create attachment for the given post.")
|
||||
.parameter(parameterBuilder()
|
||||
.name("waitForPermalink")
|
||||
.description("Wait for permalink.")
|
||||
.in(ParameterIn.QUERY)
|
||||
.required(false)
|
||||
.implementation(boolean.class))
|
||||
.requestBody(requestBodyBuilder()
|
||||
.content(contentBuilder()
|
||||
.mediaType(MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||
.schema(schemaBuilder().implementation(PostAttachmentRequest.class)))
|
||||
)
|
||||
.response(responseBuilder().implementation(Attachment.class))
|
||||
)
|
||||
.build();
|
||||
}
|
||||
|
||||
private Mono<ServerResponse> createAttachmentForPost(ServerRequest request) {
|
||||
var postAttachmentRequestMono = request.body(BodyExtractors.toMultipartData())
|
||||
.map(PostAttachmentRequest::from)
|
||||
.cache();
|
||||
|
||||
var postSettingMono = systemSettingFetcher.fetchPost()
|
||||
.<SystemSetting.Post>handle((postSetting, sink) -> {
|
||||
var attachmentPolicyName = postSetting.getAttachmentPolicyName();
|
||||
if (StringUtils.isBlank(attachmentPolicyName)) {
|
||||
sink.error(new ServerWebInputException(
|
||||
"Please configure storage policy for post attachment first."));
|
||||
return;
|
||||
}
|
||||
sink.next(postSetting);
|
||||
});
|
||||
|
||||
// get settings
|
||||
var createdAttachment = postSettingMono.flatMap(postSetting -> postAttachmentRequestMono
|
||||
.flatMap(postAttachmentRequest -> getCurrentUser().flatMap(
|
||||
username -> attachmentService.upload(username,
|
||||
postSetting.getAttachmentPolicyName(),
|
||||
postSetting.getAttachmentGroupName(),
|
||||
postAttachmentRequest.file(),
|
||||
linkWith(postAttachmentRequest)))));
|
||||
|
||||
var waitForPermalink = request.queryParam("waitForPermalink")
|
||||
.map(Boolean::valueOf)
|
||||
.orElse(false);
|
||||
if (waitForPermalink) {
|
||||
createdAttachment = createdAttachment.flatMap(attachment ->
|
||||
attachmentService.getPermalink(attachment)
|
||||
.doOnNext(permalink -> {
|
||||
var status = attachment.getStatus();
|
||||
if (status == null) {
|
||||
status = new Attachment.AttachmentStatus();
|
||||
attachment.setStatus(status);
|
||||
}
|
||||
status.setPermalink(permalink.toString());
|
||||
})
|
||||
.thenReturn(attachment));
|
||||
}
|
||||
return ServerResponse.ok().body(createdAttachment, Attachment.class);
|
||||
}
|
||||
|
||||
private Consumer<Attachment> linkWith(PostAttachmentRequest request) {
|
||||
return attachment -> {
|
||||
var labels = attachment.getMetadata().getLabels();
|
||||
if (labels == null) {
|
||||
labels = new HashMap<>();
|
||||
attachment.getMetadata().setLabels(labels);
|
||||
}
|
||||
if (StringUtils.isNotBlank(request.postName())) {
|
||||
labels.put(POST_NAME_LABEL, request.postName());
|
||||
}
|
||||
if (StringUtils.isNotBlank(request.singlePageName())) {
|
||||
labels.put(SINGLE_PAGE_NAME_LABEL, request.singlePageName());
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private Mono<Void> checkPostOwnership(Mono<PostAttachmentRequest> postAttachmentRequest) {
|
||||
// check the post
|
||||
var postNotFoundError = Mono.<Post>error(
|
||||
() -> new NotFoundException("The post was not found or deleted.")
|
||||
);
|
||||
return postAttachmentRequest.map(PostAttachmentRequest::postName)
|
||||
.flatMap(postName -> getCurrentUser()
|
||||
.flatMap(username -> postService.getByUsername(postName, username)
|
||||
.switchIfEmpty(postNotFoundError)))
|
||||
.then();
|
||||
}
|
||||
|
||||
private Mono<String> getCurrentUser() {
|
||||
return ReactiveSecurityContextHolder.getContext()
|
||||
.map(SecurityContext::getAuthentication)
|
||||
.map(Authentication::getName);
|
||||
}
|
||||
|
||||
@Override
|
||||
public GroupVersion groupVersion() {
|
||||
return GroupVersion.parseAPIVersion("uc.api.content.halo.run/v1alpha1");
|
||||
}
|
||||
|
||||
@Schema(types = "object")
|
||||
public record PostAttachmentRequest(
|
||||
@Schema(requiredMode = REQUIRED, description = "Attachment data.")
|
||||
FilePart file,
|
||||
|
||||
@Schema(requiredMode = NOT_REQUIRED, description = "Post name.")
|
||||
String postName,
|
||||
|
||||
@Schema(requiredMode = NOT_REQUIRED, description = "Single page name.")
|
||||
String singlePageName
|
||||
) {
|
||||
|
||||
/**
|
||||
* Convert multipart data into PostAttachmentRequest.
|
||||
*
|
||||
* @param multipartData is multipart data from request.
|
||||
* @return post attachment request data.
|
||||
*/
|
||||
public static PostAttachmentRequest from(MultiValueMap<String, Part> multipartData) {
|
||||
var part = multipartData.getFirst("postName");
|
||||
String postName = null;
|
||||
if (part instanceof FormFieldPart formFieldPart) {
|
||||
postName = formFieldPart.value();
|
||||
}
|
||||
|
||||
part = multipartData.getFirst("singlePageName");
|
||||
String singlePageName = null;
|
||||
if (part instanceof FormFieldPart formFieldPart) {
|
||||
singlePageName = formFieldPart.value();
|
||||
}
|
||||
|
||||
part = multipartData.getFirst("file");
|
||||
if (!(part instanceof FilePart file)) {
|
||||
throw new ServerWebInputException("Invalid type of parameter 'file'.");
|
||||
}
|
||||
|
||||
return new PostAttachmentRequest(file, postName, singlePageName);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
|
@ -0,0 +1,333 @@
|
|||
package run.halo.app.endpoint.uc.content;
|
||||
|
||||
import static org.springdoc.core.fn.builders.apiresponse.Builder.responseBuilder;
|
||||
import static org.springdoc.core.fn.builders.parameter.Builder.parameterBuilder;
|
||||
import static org.springdoc.core.fn.builders.requestbody.Builder.requestBodyBuilder;
|
||||
import static org.springdoc.webflux.core.fn.SpringdocRouteBuilder.route;
|
||||
import static org.springframework.web.reactive.function.server.RequestPredicates.path;
|
||||
import static run.halo.app.extension.router.QueryParamBuildUtil.buildParametersFromType;
|
||||
|
||||
import io.swagger.v3.oas.annotations.enums.ParameterIn;
|
||||
import java.util.Objects;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.context.ReactiveSecurityContextHolder;
|
||||
import org.springframework.security.core.context.SecurityContext;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.reactive.function.server.RouterFunction;
|
||||
import org.springframework.web.reactive.function.server.ServerRequest;
|
||||
import org.springframework.web.reactive.function.server.ServerResponse;
|
||||
import org.springframework.web.server.ServerWebInputException;
|
||||
import reactor.core.publisher.Mono;
|
||||
import run.halo.app.content.Content;
|
||||
import run.halo.app.content.ListedPost;
|
||||
import run.halo.app.content.PostQuery;
|
||||
import run.halo.app.content.PostRequest;
|
||||
import run.halo.app.content.PostService;
|
||||
import run.halo.app.content.SnapshotService;
|
||||
import run.halo.app.core.extension.content.Post;
|
||||
import run.halo.app.core.extension.content.Snapshot;
|
||||
import run.halo.app.core.extension.endpoint.CustomEndpoint;
|
||||
import run.halo.app.extension.GroupVersion;
|
||||
import run.halo.app.extension.ListResult;
|
||||
import run.halo.app.extension.Metadata;
|
||||
import run.halo.app.extension.Ref;
|
||||
import run.halo.app.infra.exception.NotFoundException;
|
||||
import run.halo.app.infra.utils.JsonUtils;
|
||||
|
||||
@Component
|
||||
public class UcPostEndpoint implements CustomEndpoint {
|
||||
|
||||
private static final String CONTENT_JSON_ANNO = "content.halo.run/content-json";
|
||||
|
||||
private final PostService postService;
|
||||
|
||||
private final SnapshotService snapshotService;
|
||||
|
||||
public UcPostEndpoint(PostService postService, SnapshotService snapshotService) {
|
||||
this.postService = postService;
|
||||
this.snapshotService = snapshotService;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RouterFunction<ServerResponse> endpoint() {
|
||||
var tag = groupVersion() + "/Post";
|
||||
var namePathParam = parameterBuilder().name("name")
|
||||
.description("Post name")
|
||||
.in(ParameterIn.PATH)
|
||||
.required(true);
|
||||
return route().nest(
|
||||
path("/posts"),
|
||||
() -> route()
|
||||
.GET(this::listMyPost, builder -> {
|
||||
builder.operationId("ListMyPosts")
|
||||
.description("List posts owned by the current user.")
|
||||
.tag(tag)
|
||||
.response(responseBuilder().implementation(
|
||||
ListResult.generateGenericClass(ListedPost.class)));
|
||||
buildParametersFromType(builder, PostQuery.class);
|
||||
}
|
||||
)
|
||||
.POST(this::createMyPost, builder -> builder.operationId("CreateMyPost")
|
||||
.tag(tag)
|
||||
.description("""
|
||||
Create my post. If you want to create a post with content, please set
|
||||
annotation: "content.halo.run/content-json" into annotations and refer
|
||||
to Content for corresponding data type.
|
||||
""")
|
||||
.requestBody(requestBodyBuilder().implementation(Post.class))
|
||||
.response(responseBuilder().implementation(Post.class))
|
||||
)
|
||||
.GET("/{name}", this::getMyPost, builder -> builder.operationId("GetMyPost")
|
||||
.tag(tag)
|
||||
.parameter(namePathParam)
|
||||
.description("Get post that belongs to the current user.")
|
||||
.response(responseBuilder().implementation(Post.class))
|
||||
)
|
||||
.PUT("/{name}", this::updateMyPost, builder ->
|
||||
builder.operationId("UpdateMyPost")
|
||||
.tag(tag)
|
||||
.parameter(namePathParam)
|
||||
.description("Update my post.")
|
||||
.requestBody(requestBodyBuilder().implementation(Post.class))
|
||||
.response(responseBuilder().implementation(Post.class))
|
||||
)
|
||||
.GET("/{name}/draft", this::getMyPostDraft, builder -> builder.tag(tag)
|
||||
.operationId("GetMyPostDraft")
|
||||
.description("Get my post draft.")
|
||||
.parameter(namePathParam)
|
||||
.parameter(parameterBuilder()
|
||||
.name("patched")
|
||||
.in(ParameterIn.QUERY)
|
||||
.required(false)
|
||||
.implementation(Boolean.class)
|
||||
.description("Should include patched content and raw or not.")
|
||||
)
|
||||
.response(responseBuilder().implementation(Snapshot.class))
|
||||
)
|
||||
.PUT("/{name}/draft", this::updateMyPostDraft, builder -> builder.tag(tag)
|
||||
.operationId("UpdateMyPostDraft")
|
||||
.description("""
|
||||
Update draft of my post. Please make sure set annotation:
|
||||
"content.halo.run/content-json" into annotations and refer to
|
||||
Content for corresponding data type.
|
||||
""")
|
||||
.parameter(namePathParam)
|
||||
.requestBody(requestBodyBuilder().implementation(Snapshot.class))
|
||||
.response(responseBuilder().implementation(Snapshot.class)))
|
||||
.PUT("/{name}/publish", this::publishMyPost, builder -> builder.tag(tag)
|
||||
.operationId("PublishMyPost")
|
||||
.description("Publish my post.")
|
||||
.parameter(namePathParam)
|
||||
.response(responseBuilder().implementation(Post.class)))
|
||||
.PUT("/{name}/unpublish", this::unpublishMyPost, builder -> builder.tag(tag)
|
||||
.operationId("UnpublishMyPost")
|
||||
.description("Unpublish my post.")
|
||||
.parameter(namePathParam)
|
||||
.response(responseBuilder().implementation(Post.class)))
|
||||
.build(),
|
||||
builder -> {
|
||||
})
|
||||
.build();
|
||||
}
|
||||
|
||||
private Mono<ServerResponse> getMyPostDraft(ServerRequest request) {
|
||||
var name = request.pathVariable("name");
|
||||
var patched = request.queryParam("patched").map(Boolean::valueOf).orElse(false);
|
||||
var draft = getMyPost(name)
|
||||
.flatMap(post -> {
|
||||
var headSnapshotName = post.getSpec().getHeadSnapshot();
|
||||
var baseSnapshotName = post.getSpec().getBaseSnapshot();
|
||||
if (StringUtils.isBlank(headSnapshotName)) {
|
||||
headSnapshotName = baseSnapshotName;
|
||||
}
|
||||
if (patched) {
|
||||
return snapshotService.getPatchedBy(headSnapshotName, baseSnapshotName);
|
||||
}
|
||||
return snapshotService.getBy(headSnapshotName);
|
||||
});
|
||||
return ServerResponse.ok().body(draft, Snapshot.class);
|
||||
}
|
||||
|
||||
private Mono<ServerResponse> unpublishMyPost(ServerRequest request) {
|
||||
var name = request.pathVariable("name");
|
||||
var postMono = getCurrentUser()
|
||||
.flatMap(username -> postService.getByUsername(name, username));
|
||||
var unpublishedPost = postMono.flatMap(postService::unpublish);
|
||||
return ServerResponse.ok().body(unpublishedPost, Post.class);
|
||||
}
|
||||
|
||||
private Mono<ServerResponse> publishMyPost(ServerRequest request) {
|
||||
var name = request.pathVariable("name");
|
||||
var postMono = getCurrentUser()
|
||||
.flatMap(username -> postService.getByUsername(name, username));
|
||||
|
||||
var publishedPost = postMono.flatMap(postService::publish);
|
||||
return ServerResponse.ok().body(publishedPost, Post.class);
|
||||
}
|
||||
|
||||
private Mono<ServerResponse> updateMyPostDraft(ServerRequest request) {
|
||||
var name = request.pathVariable("name");
|
||||
var postMono = getMyPost(name).cache();
|
||||
var snapshotMono = request.bodyToMono(Snapshot.class).cache();
|
||||
|
||||
var contentMono = snapshotMono
|
||||
.map(Snapshot::getMetadata)
|
||||
.filter(metadata -> {
|
||||
var annotations = metadata.getAnnotations();
|
||||
return annotations != null && annotations.containsKey(CONTENT_JSON_ANNO);
|
||||
})
|
||||
.map(metadata -> {
|
||||
var contentJson = metadata.getAnnotations().remove(CONTENT_JSON_ANNO);
|
||||
return JsonUtils.jsonToObject(contentJson, Content.class);
|
||||
})
|
||||
.cache();
|
||||
|
||||
// check the snapshot belongs to the post.
|
||||
var checkSnapshot = postMono.flatMap(post -> snapshotMono.filter(
|
||||
snapshot -> Ref.equals(snapshot.getSpec().getSubjectRef(), post)
|
||||
).switchIfEmpty(Mono.error(() ->
|
||||
new ServerWebInputException("The snapshot does not belong to the given post."))
|
||||
).filter(snapshot -> {
|
||||
var snapshotName = snapshot.getMetadata().getName();
|
||||
var headSnapshotName = post.getSpec().getHeadSnapshot();
|
||||
return Objects.equals(snapshotName, headSnapshotName);
|
||||
}).switchIfEmpty(Mono.error(() ->
|
||||
new ServerWebInputException("The snapshot was not the head snapshot of the post.")))
|
||||
).then();
|
||||
|
||||
var setContributor = getCurrentUser().flatMap(username ->
|
||||
snapshotMono.doOnNext(snapshot -> Snapshot.addContributor(snapshot, username)));
|
||||
|
||||
var getBaseSnapshot = postMono.map(post -> post.getSpec().getBaseSnapshot())
|
||||
.flatMap(snapshotService::getBy);
|
||||
|
||||
var updatedSnapshot = getBaseSnapshot.flatMap(
|
||||
baseSnapshot -> contentMono.flatMap(content -> postMono.flatMap(post -> {
|
||||
var postName = post.getMetadata().getName();
|
||||
var headSnapshotName = post.getSpec().getHeadSnapshot();
|
||||
var releaseSnapshotName = post.getSpec().getReleaseSnapshot();
|
||||
if (!Objects.equals(headSnapshotName, releaseSnapshotName)) {
|
||||
// patch and update
|
||||
return snapshotMono.flatMap(
|
||||
s -> snapshotService.patchAndUpdate(s, baseSnapshot, content));
|
||||
}
|
||||
// patch and create
|
||||
return getCurrentUser().map(
|
||||
username -> {
|
||||
var metadata = new Metadata();
|
||||
metadata.setGenerateName(postName + "-snapshot-");
|
||||
var spec = new Snapshot.SnapShotSpec();
|
||||
spec.setParentSnapshotName(headSnapshotName);
|
||||
spec.setOwner(username);
|
||||
spec.setSubjectRef(Ref.of(post));
|
||||
|
||||
var snapshot = new Snapshot();
|
||||
snapshot.setMetadata(metadata);
|
||||
snapshot.setSpec(spec);
|
||||
Snapshot.addContributor(snapshot, username);
|
||||
return snapshot;
|
||||
})
|
||||
.flatMap(s -> snapshotService.patchAndCreate(s, baseSnapshot, content))
|
||||
.flatMap(createdSnapshot -> {
|
||||
post.getSpec().setHeadSnapshot(createdSnapshot.getMetadata().getName());
|
||||
return postService.updateBy(post).thenReturn(createdSnapshot);
|
||||
});
|
||||
})));
|
||||
|
||||
return ServerResponse.ok()
|
||||
.body(checkSnapshot.and(setContributor).then(updatedSnapshot), Snapshot.class);
|
||||
}
|
||||
|
||||
private Mono<ServerResponse> updateMyPost(ServerRequest request) {
|
||||
var name = request.pathVariable("name");
|
||||
|
||||
var postBody = request.bodyToMono(Post.class)
|
||||
.doOnNext(post -> {
|
||||
var annotations = post.getMetadata().getAnnotations();
|
||||
if (annotations != null) {
|
||||
// we don't support updating content while updating post.
|
||||
annotations.remove(CONTENT_JSON_ANNO);
|
||||
}
|
||||
})
|
||||
.switchIfEmpty(Mono.error(() -> new ServerWebInputException("Request body required.")));
|
||||
|
||||
var updatedPost = getMyPost(name).flatMap(oldPost ->
|
||||
postBody.doOnNext(post -> {
|
||||
var oldSpec = oldPost.getSpec();
|
||||
// restrict fields of post.spec.
|
||||
var spec = post.getSpec();
|
||||
spec.setOwner(oldSpec.getOwner());
|
||||
spec.setPublish(oldSpec.getPublish());
|
||||
spec.setPublishTime(oldSpec.getPublishTime());
|
||||
spec.setHeadSnapshot(oldSpec.getHeadSnapshot());
|
||||
spec.setBaseSnapshot(oldSpec.getBaseSnapshot());
|
||||
spec.setReleaseSnapshot(oldSpec.getReleaseSnapshot());
|
||||
spec.setDeleted(oldSpec.getDeleted());
|
||||
post.getMetadata().setName(oldPost.getMetadata().getName());
|
||||
}))
|
||||
.flatMap(postService::updateBy);
|
||||
return ServerResponse.ok().body(updatedPost, Post.class);
|
||||
}
|
||||
|
||||
private Mono<ServerResponse> createMyPost(ServerRequest request) {
|
||||
var postFromRequest = request.bodyToMono(Post.class)
|
||||
.switchIfEmpty(Mono.error(() -> new ServerWebInputException("Request body required.")));
|
||||
|
||||
var createdPost = getCurrentUser()
|
||||
.flatMap(username -> postFromRequest
|
||||
.doOnNext(post -> {
|
||||
if (post.getSpec() == null) {
|
||||
post.setSpec(new Post.PostSpec());
|
||||
}
|
||||
post.getSpec().setOwner(username);
|
||||
}))
|
||||
.map(post -> new PostRequest(post, getContent(post)))
|
||||
.flatMap(postService::draftPost);
|
||||
return ServerResponse.ok().body(createdPost, Post.class);
|
||||
}
|
||||
|
||||
private Content getContent(Post post) {
|
||||
Content content = null;
|
||||
var annotations = post.getMetadata().getAnnotations();
|
||||
if (annotations != null && annotations.containsKey(CONTENT_JSON_ANNO)) {
|
||||
var contentJson = annotations.remove(CONTENT_JSON_ANNO);
|
||||
content = JsonUtils.jsonToObject(contentJson, Content.class);
|
||||
}
|
||||
return content;
|
||||
}
|
||||
|
||||
private Mono<ServerResponse> listMyPost(ServerRequest request) {
|
||||
var posts = getCurrentUser()
|
||||
.map(username -> new PostQuery(request, username))
|
||||
.flatMap(postService::listPost);
|
||||
return ServerResponse.ok().body(posts, ListedPost.class);
|
||||
}
|
||||
|
||||
private Mono<ServerResponse> getMyPost(ServerRequest request) {
|
||||
var postName = request.pathVariable("name");
|
||||
var post = getMyPost(postName);
|
||||
return ServerResponse.ok().body(post, Post.class);
|
||||
}
|
||||
|
||||
private Mono<Post> getMyPost(String postName) {
|
||||
return getCurrentUser()
|
||||
.flatMap(username -> postService.getByUsername(postName, username)
|
||||
.switchIfEmpty(
|
||||
Mono.error(() -> new NotFoundException("The post was not found or deleted"))
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
private Mono<String> getCurrentUser() {
|
||||
return ReactiveSecurityContextHolder.getContext()
|
||||
.map(SecurityContext::getAuthentication)
|
||||
.map(Authentication::getName);
|
||||
}
|
||||
|
||||
@Override
|
||||
public GroupVersion groupVersion() {
|
||||
return GroupVersion.parseAPIVersion("uc.api.content.halo.run/v1alpha1");
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,124 @@
|
|||
package run.halo.app.endpoint.uc.content;
|
||||
|
||||
import static org.springdoc.core.fn.builders.apiresponse.Builder.responseBuilder;
|
||||
import static org.springdoc.core.fn.builders.parameter.Builder.parameterBuilder;
|
||||
import static org.springdoc.webflux.core.fn.SpringdocRouteBuilder.route;
|
||||
import static org.springframework.web.reactive.function.server.RequestPredicates.path;
|
||||
|
||||
import io.swagger.v3.oas.annotations.enums.ParameterIn;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.context.ReactiveSecurityContextHolder;
|
||||
import org.springframework.security.core.context.SecurityContext;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.reactive.function.server.RouterFunction;
|
||||
import org.springframework.web.reactive.function.server.ServerRequest;
|
||||
import org.springframework.web.reactive.function.server.ServerResponse;
|
||||
import org.springframework.web.server.ServerWebInputException;
|
||||
import reactor.core.publisher.Mono;
|
||||
import run.halo.app.content.PostService;
|
||||
import run.halo.app.content.SnapshotService;
|
||||
import run.halo.app.core.extension.content.Post;
|
||||
import run.halo.app.core.extension.content.Snapshot;
|
||||
import run.halo.app.core.extension.endpoint.CustomEndpoint;
|
||||
import run.halo.app.extension.GroupVersion;
|
||||
import run.halo.app.extension.Ref;
|
||||
import run.halo.app.infra.exception.NotFoundException;
|
||||
|
||||
@Component
|
||||
public class UcSnapshotEndpoint implements CustomEndpoint {
|
||||
|
||||
private final PostService postService;
|
||||
|
||||
private final SnapshotService snapshotService;
|
||||
|
||||
public UcSnapshotEndpoint(PostService postService, SnapshotService snapshotService) {
|
||||
this.postService = postService;
|
||||
this.snapshotService = snapshotService;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RouterFunction<ServerResponse> endpoint() {
|
||||
var tag = groupVersion() + "/Snapshot";
|
||||
|
||||
return route().nest(path("/snapshots"),
|
||||
() -> route()
|
||||
.GET("/{name}",
|
||||
this::getSnapshot,
|
||||
builder -> builder.operationId("GetSnapshotForPost")
|
||||
.description("Get snapshot for one post.")
|
||||
.parameter(parameterBuilder()
|
||||
.name("name")
|
||||
.in(ParameterIn.PATH)
|
||||
.required(true)
|
||||
.description("Snapshot name.")
|
||||
)
|
||||
.parameter(parameterBuilder()
|
||||
.name("postName")
|
||||
.in(ParameterIn.QUERY)
|
||||
.required(true)
|
||||
.description("Post name.")
|
||||
)
|
||||
.parameter(parameterBuilder()
|
||||
.name("patched")
|
||||
.in(ParameterIn.QUERY)
|
||||
.required(false)
|
||||
.implementation(Boolean.class)
|
||||
.description("Should include patched content and raw or not.")
|
||||
)
|
||||
.response(responseBuilder().implementation(Snapshot.class))
|
||||
.tag(tag))
|
||||
.build(),
|
||||
builder -> {
|
||||
})
|
||||
.build();
|
||||
}
|
||||
|
||||
private Mono<ServerResponse> getSnapshot(ServerRequest request) {
|
||||
var snapshotName = request.pathVariable("name");
|
||||
var postName = request.queryParam("postName")
|
||||
.orElseThrow(() -> new ServerWebInputException("Query parameter postName is required"));
|
||||
var patched = request.queryParam("patched").map(Boolean::valueOf).orElse(false);
|
||||
|
||||
var postNotFoundError = Mono.<Post>error(
|
||||
() -> new NotFoundException("The post was not found or deleted.")
|
||||
);
|
||||
var snapshotNotFoundError = Mono.<Snapshot>error(
|
||||
() -> new NotFoundException("The snapshot was not found or deleted.")
|
||||
);
|
||||
|
||||
var postMono = getCurrentUser().flatMap(username ->
|
||||
postService.getByUsername(postName, username).switchIfEmpty(postNotFoundError)
|
||||
);
|
||||
|
||||
// check the post belongs to the current user.
|
||||
var snapshotMono = postMono.flatMap(post -> Mono.defer(
|
||||
() -> {
|
||||
if (patched) {
|
||||
var baseSnapshotName = post.getSpec().getBaseSnapshot();
|
||||
return snapshotService.getPatchedBy(snapshotName, baseSnapshotName);
|
||||
}
|
||||
return snapshotService.getBy(snapshotName);
|
||||
})
|
||||
.filter(snapshot -> {
|
||||
var subjectRef = snapshot.getSpec().getSubjectRef();
|
||||
return Ref.equals(subjectRef, post);
|
||||
})
|
||||
.switchIfEmpty(snapshotNotFoundError)
|
||||
);
|
||||
|
||||
return ServerResponse.ok().body(snapshotMono, Snapshot.class);
|
||||
}
|
||||
|
||||
private Mono<String> getCurrentUser() {
|
||||
return ReactiveSecurityContextHolder
|
||||
.getContext()
|
||||
.map(SecurityContext::getAuthentication)
|
||||
.map(Authentication::getName);
|
||||
}
|
||||
|
||||
@Override
|
||||
public GroupVersion groupVersion() {
|
||||
return GroupVersion.parseAPIVersion("uc.api.content.halo.run/v1alpha1");
|
||||
}
|
||||
|
||||
}
|
|
@ -7,6 +7,8 @@ metadata:
|
|||
halo.run/hidden: "true"
|
||||
annotations:
|
||||
rbac.authorization.halo.run/dependencies: "[ \"role-template-view-categories\" ]"
|
||||
rbac.authorization.halo.run/ui-permissions: |
|
||||
[ "system:categories:manage", "uc:categories:manage" ]
|
||||
rules:
|
||||
- apiGroups: [ "content.halo.run" ]
|
||||
resources: [ "categories" ]
|
||||
|
@ -19,6 +21,9 @@ metadata:
|
|||
labels:
|
||||
halo.run/role-template: "true"
|
||||
halo.run/hidden: "true"
|
||||
annotations:
|
||||
rbac.authorization.halo.run/ui-permissions: |
|
||||
[ "system:categories:view", "uc:categories:view" ]
|
||||
rules:
|
||||
- apiGroups: [ "content.halo.run" ]
|
||||
resources: [ "categories" ]
|
||||
|
|
|
@ -0,0 +1,94 @@
|
|||
apiVersion: v1alpha1
|
||||
kind: "Role"
|
||||
metadata:
|
||||
name: post-editor
|
||||
annotations:
|
||||
rbac.authorization.halo.run/module: "Posts Management"
|
||||
rbac.authorization.halo.run/display-name: "Post Editor"
|
||||
rbac.authorization.halo.run/dependencies: |
|
||||
["role-template-manage-posts", "post-author"]
|
||||
rules: [ ]
|
||||
|
||||
---
|
||||
apiVersion: v1alpha1
|
||||
kind: "Role"
|
||||
metadata:
|
||||
name: post-author
|
||||
annotations:
|
||||
rbac.authorization.halo.run/module: "Posts Management"
|
||||
rbac.authorization.halo.run/display-name: "Post Author"
|
||||
rbac.authorization.halo.run/dependencies: |
|
||||
[ "post-contributor", "post-publisher" ]
|
||||
rules: [ ]
|
||||
|
||||
---
|
||||
apiVersion: v1alpha1
|
||||
kind: "Role"
|
||||
metadata:
|
||||
name: post-contributor
|
||||
annotations:
|
||||
rbac.authorization.halo.run/module: "Posts Management"
|
||||
rbac.authorization.halo.run/display-name: "Post Contributor"
|
||||
rbac.authorization.halo.run/dependencies: |
|
||||
[ "role-template-view-categories", "role-template-view-tags" ]
|
||||
rbac.authorization.halo.run/ui-permissions: |
|
||||
[ "uc:posts:manage" ]
|
||||
rules:
|
||||
- apiGroups: [ "uc.api.content.halo.run" ]
|
||||
resources: [ "posts" ]
|
||||
verbs: [ "get", "list", "create", "update", "delete" ]
|
||||
- apiGroups: [ "uc.api.content.halo.run" ]
|
||||
resources: [ "posts/draft" ]
|
||||
verbs: [ "update", "get" ]
|
||||
|
||||
---
|
||||
apiVersion: v1alpha1
|
||||
kind: Role
|
||||
metadata:
|
||||
name: post-publisher
|
||||
labels:
|
||||
halo.run/role-template: "true"
|
||||
annotations:
|
||||
rbac.authorization.halo.run/module: "Posts Management"
|
||||
rbac.authorization.halo.run/display-name: "Post Publisher"
|
||||
rbac.authorization.halo.run/ui-permissions: |
|
||||
[ "uc:posts:publish" ]
|
||||
rules:
|
||||
- apiGroups: [ "uc.api.content.halo.run" ]
|
||||
resources: [ "posts/publish", "posts/unpublish" ]
|
||||
verbs: [ "update" ]
|
||||
---
|
||||
apiVersion: v1alpha1
|
||||
kind: Role
|
||||
metadata:
|
||||
name: post-attachment-manager
|
||||
labels:
|
||||
halo.run/role-template: "true"
|
||||
annotations:
|
||||
rbac.authorization.halo.run/module: "Posts Management"
|
||||
rbac.authorization.halo.run/display-name: "Post Attachment Manager"
|
||||
rbac.authorization.halo.run/dependencies: |
|
||||
[ "role-template-post-attachment-viewer" ]
|
||||
rbac.authorization.halo.run/ui-permissions: |
|
||||
[ "uc:attachments:manage" ]
|
||||
rules:
|
||||
- apiGroups: [ "uc.api.content.halo.run" ]
|
||||
resources: [ "attachments" ]
|
||||
verbs: [ "create", "update", "delete" ]
|
||||
|
||||
---
|
||||
apiVersion: v1alpha1
|
||||
kind: Role
|
||||
metadata:
|
||||
name: post-attachment-viewer
|
||||
labels:
|
||||
halo.run/role-template: "true"
|
||||
annotations:
|
||||
rbac.authorization.halo.run/module: "Posts Management"
|
||||
rbac.authorization.halo.run/display-name: "Post Attachment Viewer"
|
||||
rbac.authorization.halo.run/ui-permissions: |
|
||||
[ "uc:attachments:view" ]
|
||||
rules:
|
||||
- apiGroups: [ "uc.api.content.halo.run" ]
|
||||
resources: [ "attachments" ]
|
||||
verbs: [ "get", "list" ]
|
|
@ -32,7 +32,8 @@ data:
|
|||
"archivePageSize": 10,
|
||||
"categoryPageSize": 10,
|
||||
"tagPageSize": 10,
|
||||
"slugGenerationStrategy": "generateByTitle"
|
||||
"slugGenerationStrategy": "generateByTitle",
|
||||
"attachmentPolicyName": "default-policy"
|
||||
}
|
||||
comment: |
|
||||
{
|
||||
|
|
|
@ -68,6 +68,14 @@ spec:
|
|||
value: 'shortUUID'
|
||||
- label: 'UUID'
|
||||
value: 'UUID'
|
||||
- $formkit: attachmentPolicySelect
|
||||
name: attachmentPolicyName
|
||||
label: "附件存储策略"
|
||||
value: "default-policy"
|
||||
- $formkit: attachmentGroupSelect
|
||||
name: attachmentGroupName
|
||||
label: "附件存储组"
|
||||
value: ""
|
||||
- group: seo
|
||||
label: SEO 设置
|
||||
formSchema:
|
||||
|
|
|
@ -23,6 +23,26 @@ import run.halo.app.core.extension.content.Post;
|
|||
@ExtendWith(MockitoExtension.class)
|
||||
class PostQueryTest {
|
||||
|
||||
@Test
|
||||
void userScopedQueryTest() {
|
||||
MultiValueMap<String, String> multiValueMap = new LinkedMultiValueMap<>();
|
||||
MockServerRequest request = MockServerRequest.builder()
|
||||
.queryParams(multiValueMap)
|
||||
.exchange(mock(ServerWebExchange.class))
|
||||
.build();
|
||||
|
||||
PostQuery postQuery = new PostQuery(request, "faker");
|
||||
var spec = new Post.PostSpec();
|
||||
var post = new Post();
|
||||
post.setSpec(spec);
|
||||
|
||||
spec.setOwner("another-faker");
|
||||
assertThat(postQuery.toPredicate().test(post)).isFalse();
|
||||
|
||||
spec.setOwner("faker");
|
||||
assertThat(postQuery.toPredicate().test(post)).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void toPredicate() {
|
||||
MultiValueMap<String, String> multiValueMap = new LinkedMultiValueMap<>();
|
||||
|
|
|
@ -18,6 +18,7 @@ import org.springframework.context.ApplicationEventPublisher;
|
|||
import org.springframework.dao.OptimisticLockingFailureException;
|
||||
import org.springframework.test.web.reactive.server.WebTestClient;
|
||||
import reactor.core.publisher.Mono;
|
||||
import run.halo.app.content.Content;
|
||||
import run.halo.app.content.PostRequest;
|
||||
import run.halo.app.content.PostService;
|
||||
import run.halo.app.content.TestPost;
|
||||
|
@ -176,6 +177,6 @@ class PostEndpointTest {
|
|||
}
|
||||
|
||||
PostRequest postRequest(Post post) {
|
||||
return new PostRequest(post, new PostRequest.Content("B", "<p>B</p>", "MARKDOWN"));
|
||||
return new PostRequest(post, new Content("B", "<p>B</p>", "MARKDOWN"));
|
||||
}
|
||||
}
|
|
@ -28,7 +28,7 @@ import cloneDeep from "lodash.clonedeep";
|
|||
import { useRouter } from "vue-router";
|
||||
import { randomUUID } from "@/utils/id";
|
||||
import { useContentCache } from "@console/composables/use-content-cache";
|
||||
import { useEditorExtensionPoints } from "@console/composables/use-editor-extension-points";
|
||||
import { useEditorExtensionPoints } from "@/composables/use-editor-extension-points";
|
||||
import type { EditorProvider } from "@halo-dev/console-shared";
|
||||
import { useLocalStorage } from "@vueuse/core";
|
||||
import EditorProviderSelector from "@/components/dropdown-selector/EditorProviderSelector.vue";
|
||||
|
@ -372,6 +372,20 @@ const handlePreview = async () => {
|
|||
};
|
||||
|
||||
useSaveKeybinding(handleSave);
|
||||
|
||||
// Upload image
|
||||
async function handleUploadImage(file: File) {
|
||||
if (!isUpdateMode.value) {
|
||||
await handleSave();
|
||||
}
|
||||
|
||||
const { data } = await apiClient.uc.attachment.createAttachmentForPost({
|
||||
file,
|
||||
singlePageName: formState.value.page.metadata.name,
|
||||
waitForPermalink: true,
|
||||
});
|
||||
return data;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
@ -450,6 +464,7 @@ useSaveKeybinding(handleSave);
|
|||
v-if="currentEditorProvider"
|
||||
v-model:raw="formState.content.raw"
|
||||
v-model:content="formState.content.content"
|
||||
:upload-image="handleUploadImage"
|
||||
class="h-full"
|
||||
@update="handleSetContentCache"
|
||||
/>
|
||||
|
|
|
@ -28,7 +28,7 @@ import { useRouteQuery } from "@vueuse/router";
|
|||
import { useRouter } from "vue-router";
|
||||
import { randomUUID } from "@/utils/id";
|
||||
import { useContentCache } from "@console/composables/use-content-cache";
|
||||
import { useEditorExtensionPoints } from "@console/composables/use-editor-extension-points";
|
||||
import { useEditorExtensionPoints } from "@/composables/use-editor-extension-points";
|
||||
import type { EditorProvider } from "@halo-dev/console-shared";
|
||||
import { useLocalStorage } from "@vueuse/core";
|
||||
import EditorProviderSelector from "@/components/dropdown-selector/EditorProviderSelector.vue";
|
||||
|
@ -66,8 +66,17 @@ const handleChangeEditorProvider = async (provider: EditorProvider) => {
|
|||
}
|
||||
};
|
||||
|
||||
// fixme: PostRequest type may be wrong
|
||||
interface PostRequestWithContent extends PostRequest {
|
||||
content: {
|
||||
raw: string;
|
||||
content: string;
|
||||
rawType: string;
|
||||
};
|
||||
}
|
||||
|
||||
// Post form
|
||||
const initialFormState: PostRequest = {
|
||||
const initialFormState: PostRequestWithContent = {
|
||||
post: {
|
||||
spec: {
|
||||
title: "",
|
||||
|
@ -103,7 +112,7 @@ const initialFormState: PostRequest = {
|
|||
},
|
||||
};
|
||||
|
||||
const formState = ref<PostRequest>(cloneDeep(initialFormState));
|
||||
const formState = ref<PostRequestWithContent>(cloneDeep(initialFormState));
|
||||
const settingModal = ref(false);
|
||||
const saving = ref(false);
|
||||
const publishing = ref(false);
|
||||
|
@ -388,6 +397,20 @@ const handlePreview = async () => {
|
|||
};
|
||||
|
||||
useSaveKeybinding(handleSave);
|
||||
|
||||
// Upload image
|
||||
async function handleUploadImage(file: File) {
|
||||
if (!isUpdateMode.value) {
|
||||
await handleSave();
|
||||
}
|
||||
|
||||
const { data } = await apiClient.uc.attachment.createAttachmentForPost({
|
||||
file,
|
||||
postName: formState.value.post.metadata.name,
|
||||
waitForPermalink: true,
|
||||
});
|
||||
return data;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
@ -466,6 +489,7 @@ useSaveKeybinding(handleSave);
|
|||
v-if="currentEditorProvider"
|
||||
v-model:raw="formState.content.raw"
|
||||
v-model:content="formState.content.content"
|
||||
:upload-image="handleUploadImage"
|
||||
class="h-full"
|
||||
@update="handleSetContentCache"
|
||||
/>
|
||||
|
|
|
@ -39,7 +39,6 @@ api/content-halo-run-v1alpha1-reply-api.ts
|
|||
api/content-halo-run-v1alpha1-single-page-api.ts
|
||||
api/content-halo-run-v1alpha1-snapshot-api.ts
|
||||
api/content-halo-run-v1alpha1-tag-api.ts
|
||||
api/doc-halo-run-v1alpha1-doc-tree-api.ts
|
||||
api/login-api.ts
|
||||
api/metrics-halo-run-v1alpha1-counter-api.ts
|
||||
api/migration-halo-run-v1alpha1-backup-api.ts
|
||||
|
@ -60,6 +59,9 @@ api/storage-halo-run-v1alpha1-group-api.ts
|
|||
api/storage-halo-run-v1alpha1-policy-api.ts
|
||||
api/storage-halo-run-v1alpha1-policy-template-api.ts
|
||||
api/theme-halo-run-v1alpha1-theme-api.ts
|
||||
api/uc-api-content-halo-run-v1alpha1-attachment-api.ts
|
||||
api/uc-api-content-halo-run-v1alpha1-post-api.ts
|
||||
api/uc-api-content-halo-run-v1alpha1-snapshot-api.ts
|
||||
api/v1alpha1-annotation-setting-api.ts
|
||||
api/v1alpha1-cache-api.ts
|
||||
api/v1alpha1-config-map-api.ts
|
||||
|
@ -124,9 +126,6 @@ models/create-user-request.ts
|
|||
models/custom-templates.ts
|
||||
models/dashboard-stats.ts
|
||||
models/detailed-user.ts
|
||||
models/doc-tree-list.ts
|
||||
models/doc-tree-status.ts
|
||||
models/doc-tree.ts
|
||||
models/email-verify-request.ts
|
||||
models/excerpt.ts
|
||||
models/extension-definition-list.ts
|
||||
|
@ -262,7 +261,6 @@ models/site-stats-vo.ts
|
|||
models/snap-shot-spec.ts
|
||||
models/snapshot-list.ts
|
||||
models/snapshot.ts
|
||||
models/spec.ts
|
||||
models/stats-vo.ts
|
||||
models/stats.ts
|
||||
models/subject.ts
|
||||
|
|
|
@ -70,6 +70,9 @@ export * from "./api/storage-halo-run-v1alpha1-group-api";
|
|||
export * from "./api/storage-halo-run-v1alpha1-policy-api";
|
||||
export * from "./api/storage-halo-run-v1alpha1-policy-template-api";
|
||||
export * from "./api/theme-halo-run-v1alpha1-theme-api";
|
||||
export * from "./api/uc-api-content-halo-run-v1alpha1-attachment-api";
|
||||
export * from "./api/uc-api-content-halo-run-v1alpha1-post-api";
|
||||
export * from "./api/uc-api-content-halo-run-v1alpha1-snapshot-api";
|
||||
export * from "./api/v1alpha1-annotation-setting-api";
|
||||
export * from "./api/v1alpha1-cache-api";
|
||||
export * from "./api/v1alpha1-config-map-api";
|
||||
|
|
|
@ -0,0 +1,273 @@
|
|||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
/**
|
||||
* Halo Next API
|
||||
* No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)
|
||||
*
|
||||
* The version of the OpenAPI document: 2.0.0
|
||||
*
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||
* https://openapi-generator.tech
|
||||
* Do not edit the class manually.
|
||||
*/
|
||||
|
||||
import type { Configuration } from "../configuration";
|
||||
import type { AxiosPromise, AxiosInstance, AxiosRequestConfig } from "axios";
|
||||
import globalAxios from "axios";
|
||||
// Some imports not used depending on template conditions
|
||||
// @ts-ignore
|
||||
import {
|
||||
DUMMY_BASE_URL,
|
||||
assertParamExists,
|
||||
setApiKeyToObject,
|
||||
setBasicAuthToObject,
|
||||
setBearerAuthToObject,
|
||||
setOAuthToObject,
|
||||
setSearchParams,
|
||||
serializeDataIfNeeded,
|
||||
toPathString,
|
||||
createRequestFunction,
|
||||
} from "../common";
|
||||
// @ts-ignore
|
||||
import {
|
||||
BASE_PATH,
|
||||
COLLECTION_FORMATS,
|
||||
RequestArgs,
|
||||
BaseAPI,
|
||||
RequiredError,
|
||||
} from "../base";
|
||||
// @ts-ignore
|
||||
import { Attachment } from "../models";
|
||||
/**
|
||||
* UcApiContentHaloRunV1alpha1AttachmentApi - axios parameter creator
|
||||
* @export
|
||||
*/
|
||||
export const UcApiContentHaloRunV1alpha1AttachmentApiAxiosParamCreator =
|
||||
function (configuration?: Configuration) {
|
||||
return {
|
||||
/**
|
||||
* Create attachment for the given post.
|
||||
* @param {File} file
|
||||
* @param {boolean} [waitForPermalink] Wait for permalink.
|
||||
* @param {string} [postName] Post name.
|
||||
* @param {string} [singlePageName] Single page name.
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
createAttachmentForPost: async (
|
||||
file: File,
|
||||
waitForPermalink?: boolean,
|
||||
postName?: string,
|
||||
singlePageName?: string,
|
||||
options: AxiosRequestConfig = {}
|
||||
): Promise<RequestArgs> => {
|
||||
// verify required parameter 'file' is not null or undefined
|
||||
assertParamExists("createAttachmentForPost", "file", file);
|
||||
const localVarPath = `/apis/uc.api.content.halo.run/v1alpha1/attachments`;
|
||||
// use dummy base URL string because the URL constructor only accepts absolute URLs.
|
||||
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
|
||||
let baseOptions;
|
||||
if (configuration) {
|
||||
baseOptions = configuration.baseOptions;
|
||||
}
|
||||
|
||||
const localVarRequestOptions = {
|
||||
method: "POST",
|
||||
...baseOptions,
|
||||
...options,
|
||||
};
|
||||
const localVarHeaderParameter = {} as any;
|
||||
const localVarQueryParameter = {} as any;
|
||||
const localVarFormParams = new ((configuration &&
|
||||
configuration.formDataCtor) ||
|
||||
FormData)();
|
||||
|
||||
// authentication BasicAuth required
|
||||
// http basic authentication required
|
||||
setBasicAuthToObject(localVarRequestOptions, configuration);
|
||||
|
||||
// authentication BearerAuth required
|
||||
// http bearer authentication required
|
||||
await setBearerAuthToObject(localVarHeaderParameter, configuration);
|
||||
|
||||
if (waitForPermalink !== undefined) {
|
||||
localVarQueryParameter["waitForPermalink"] = waitForPermalink;
|
||||
}
|
||||
|
||||
if (file !== undefined) {
|
||||
localVarFormParams.append("file", file as any);
|
||||
}
|
||||
|
||||
if (postName !== undefined) {
|
||||
localVarFormParams.append("postName", postName as any);
|
||||
}
|
||||
|
||||
if (singlePageName !== undefined) {
|
||||
localVarFormParams.append("singlePageName", singlePageName as any);
|
||||
}
|
||||
|
||||
localVarHeaderParameter["Content-Type"] = "multipart/form-data";
|
||||
|
||||
setSearchParams(localVarUrlObj, localVarQueryParameter);
|
||||
let headersFromBaseOptions =
|
||||
baseOptions && baseOptions.headers ? baseOptions.headers : {};
|
||||
localVarRequestOptions.headers = {
|
||||
...localVarHeaderParameter,
|
||||
...headersFromBaseOptions,
|
||||
...options.headers,
|
||||
};
|
||||
localVarRequestOptions.data = localVarFormParams;
|
||||
|
||||
return {
|
||||
url: toPathString(localVarUrlObj),
|
||||
options: localVarRequestOptions,
|
||||
};
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* UcApiContentHaloRunV1alpha1AttachmentApi - functional programming interface
|
||||
* @export
|
||||
*/
|
||||
export const UcApiContentHaloRunV1alpha1AttachmentApiFp = function (
|
||||
configuration?: Configuration
|
||||
) {
|
||||
const localVarAxiosParamCreator =
|
||||
UcApiContentHaloRunV1alpha1AttachmentApiAxiosParamCreator(configuration);
|
||||
return {
|
||||
/**
|
||||
* Create attachment for the given post.
|
||||
* @param {File} file
|
||||
* @param {boolean} [waitForPermalink] Wait for permalink.
|
||||
* @param {string} [postName] Post name.
|
||||
* @param {string} [singlePageName] Single page name.
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
async createAttachmentForPost(
|
||||
file: File,
|
||||
waitForPermalink?: boolean,
|
||||
postName?: string,
|
||||
singlePageName?: string,
|
||||
options?: AxiosRequestConfig
|
||||
): Promise<
|
||||
(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Attachment>
|
||||
> {
|
||||
const localVarAxiosArgs =
|
||||
await localVarAxiosParamCreator.createAttachmentForPost(
|
||||
file,
|
||||
waitForPermalink,
|
||||
postName,
|
||||
singlePageName,
|
||||
options
|
||||
);
|
||||
return createRequestFunction(
|
||||
localVarAxiosArgs,
|
||||
globalAxios,
|
||||
BASE_PATH,
|
||||
configuration
|
||||
);
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* UcApiContentHaloRunV1alpha1AttachmentApi - factory interface
|
||||
* @export
|
||||
*/
|
||||
export const UcApiContentHaloRunV1alpha1AttachmentApiFactory = function (
|
||||
configuration?: Configuration,
|
||||
basePath?: string,
|
||||
axios?: AxiosInstance
|
||||
) {
|
||||
const localVarFp = UcApiContentHaloRunV1alpha1AttachmentApiFp(configuration);
|
||||
return {
|
||||
/**
|
||||
* Create attachment for the given post.
|
||||
* @param {UcApiContentHaloRunV1alpha1AttachmentApiCreateAttachmentForPostRequest} requestParameters Request parameters.
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
createAttachmentForPost(
|
||||
requestParameters: UcApiContentHaloRunV1alpha1AttachmentApiCreateAttachmentForPostRequest,
|
||||
options?: AxiosRequestConfig
|
||||
): AxiosPromise<Attachment> {
|
||||
return localVarFp
|
||||
.createAttachmentForPost(
|
||||
requestParameters.file,
|
||||
requestParameters.waitForPermalink,
|
||||
requestParameters.postName,
|
||||
requestParameters.singlePageName,
|
||||
options
|
||||
)
|
||||
.then((request) => request(axios, basePath));
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Request parameters for createAttachmentForPost operation in UcApiContentHaloRunV1alpha1AttachmentApi.
|
||||
* @export
|
||||
* @interface UcApiContentHaloRunV1alpha1AttachmentApiCreateAttachmentForPostRequest
|
||||
*/
|
||||
export interface UcApiContentHaloRunV1alpha1AttachmentApiCreateAttachmentForPostRequest {
|
||||
/**
|
||||
*
|
||||
* @type {File}
|
||||
* @memberof UcApiContentHaloRunV1alpha1AttachmentApiCreateAttachmentForPost
|
||||
*/
|
||||
readonly file: File;
|
||||
|
||||
/**
|
||||
* Wait for permalink.
|
||||
* @type {boolean}
|
||||
* @memberof UcApiContentHaloRunV1alpha1AttachmentApiCreateAttachmentForPost
|
||||
*/
|
||||
readonly waitForPermalink?: boolean;
|
||||
|
||||
/**
|
||||
* Post name.
|
||||
* @type {string}
|
||||
* @memberof UcApiContentHaloRunV1alpha1AttachmentApiCreateAttachmentForPost
|
||||
*/
|
||||
readonly postName?: string;
|
||||
|
||||
/**
|
||||
* Single page name.
|
||||
* @type {string}
|
||||
* @memberof UcApiContentHaloRunV1alpha1AttachmentApiCreateAttachmentForPost
|
||||
*/
|
||||
readonly singlePageName?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* UcApiContentHaloRunV1alpha1AttachmentApi - object-oriented interface
|
||||
* @export
|
||||
* @class UcApiContentHaloRunV1alpha1AttachmentApi
|
||||
* @extends {BaseAPI}
|
||||
*/
|
||||
export class UcApiContentHaloRunV1alpha1AttachmentApi extends BaseAPI {
|
||||
/**
|
||||
* Create attachment for the given post.
|
||||
* @param {UcApiContentHaloRunV1alpha1AttachmentApiCreateAttachmentForPostRequest} requestParameters Request parameters.
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
* @memberof UcApiContentHaloRunV1alpha1AttachmentApi
|
||||
*/
|
||||
public createAttachmentForPost(
|
||||
requestParameters: UcApiContentHaloRunV1alpha1AttachmentApiCreateAttachmentForPostRequest,
|
||||
options?: AxiosRequestConfig
|
||||
) {
|
||||
return UcApiContentHaloRunV1alpha1AttachmentApiFp(this.configuration)
|
||||
.createAttachmentForPost(
|
||||
requestParameters.file,
|
||||
requestParameters.waitForPermalink,
|
||||
requestParameters.postName,
|
||||
requestParameters.singlePageName,
|
||||
options
|
||||
)
|
||||
.then((request) => request(this.axios, this.basePath));
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,251 @@
|
|||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
/**
|
||||
* Halo Next API
|
||||
* No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)
|
||||
*
|
||||
* The version of the OpenAPI document: 2.0.0
|
||||
*
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||
* https://openapi-generator.tech
|
||||
* Do not edit the class manually.
|
||||
*/
|
||||
|
||||
import type { Configuration } from "../configuration";
|
||||
import type { AxiosPromise, AxiosInstance, AxiosRequestConfig } from "axios";
|
||||
import globalAxios from "axios";
|
||||
// Some imports not used depending on template conditions
|
||||
// @ts-ignore
|
||||
import {
|
||||
DUMMY_BASE_URL,
|
||||
assertParamExists,
|
||||
setApiKeyToObject,
|
||||
setBasicAuthToObject,
|
||||
setBearerAuthToObject,
|
||||
setOAuthToObject,
|
||||
setSearchParams,
|
||||
serializeDataIfNeeded,
|
||||
toPathString,
|
||||
createRequestFunction,
|
||||
} from "../common";
|
||||
// @ts-ignore
|
||||
import {
|
||||
BASE_PATH,
|
||||
COLLECTION_FORMATS,
|
||||
RequestArgs,
|
||||
BaseAPI,
|
||||
RequiredError,
|
||||
} from "../base";
|
||||
// @ts-ignore
|
||||
import { Snapshot } from "../models";
|
||||
/**
|
||||
* UcApiContentHaloRunV1alpha1SnapshotApi - axios parameter creator
|
||||
* @export
|
||||
*/
|
||||
export const UcApiContentHaloRunV1alpha1SnapshotApiAxiosParamCreator =
|
||||
function (configuration?: Configuration) {
|
||||
return {
|
||||
/**
|
||||
* Get snapshot for one post.
|
||||
* @param {string} name Snapshot name.
|
||||
* @param {string} postName Post name.
|
||||
* @param {boolean} [patched] Should include patched content and raw or not.
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
getSnapshotForPost: async (
|
||||
name: string,
|
||||
postName: string,
|
||||
patched?: boolean,
|
||||
options: AxiosRequestConfig = {}
|
||||
): Promise<RequestArgs> => {
|
||||
// verify required parameter 'name' is not null or undefined
|
||||
assertParamExists("getSnapshotForPost", "name", name);
|
||||
// verify required parameter 'postName' is not null or undefined
|
||||
assertParamExists("getSnapshotForPost", "postName", postName);
|
||||
const localVarPath =
|
||||
`/apis/uc.api.content.halo.run/v1alpha1/snapshots/{name}`.replace(
|
||||
`{${"name"}}`,
|
||||
encodeURIComponent(String(name))
|
||||
);
|
||||
// use dummy base URL string because the URL constructor only accepts absolute URLs.
|
||||
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
|
||||
let baseOptions;
|
||||
if (configuration) {
|
||||
baseOptions = configuration.baseOptions;
|
||||
}
|
||||
|
||||
const localVarRequestOptions = {
|
||||
method: "GET",
|
||||
...baseOptions,
|
||||
...options,
|
||||
};
|
||||
const localVarHeaderParameter = {} as any;
|
||||
const localVarQueryParameter = {} as any;
|
||||
|
||||
// authentication BasicAuth required
|
||||
// http basic authentication required
|
||||
setBasicAuthToObject(localVarRequestOptions, configuration);
|
||||
|
||||
// authentication BearerAuth required
|
||||
// http bearer authentication required
|
||||
await setBearerAuthToObject(localVarHeaderParameter, configuration);
|
||||
|
||||
if (postName !== undefined) {
|
||||
localVarQueryParameter["postName"] = postName;
|
||||
}
|
||||
|
||||
if (patched !== undefined) {
|
||||
localVarQueryParameter["patched"] = patched;
|
||||
}
|
||||
|
||||
setSearchParams(localVarUrlObj, localVarQueryParameter);
|
||||
let headersFromBaseOptions =
|
||||
baseOptions && baseOptions.headers ? baseOptions.headers : {};
|
||||
localVarRequestOptions.headers = {
|
||||
...localVarHeaderParameter,
|
||||
...headersFromBaseOptions,
|
||||
...options.headers,
|
||||
};
|
||||
|
||||
return {
|
||||
url: toPathString(localVarUrlObj),
|
||||
options: localVarRequestOptions,
|
||||
};
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* UcApiContentHaloRunV1alpha1SnapshotApi - functional programming interface
|
||||
* @export
|
||||
*/
|
||||
export const UcApiContentHaloRunV1alpha1SnapshotApiFp = function (
|
||||
configuration?: Configuration
|
||||
) {
|
||||
const localVarAxiosParamCreator =
|
||||
UcApiContentHaloRunV1alpha1SnapshotApiAxiosParamCreator(configuration);
|
||||
return {
|
||||
/**
|
||||
* Get snapshot for one post.
|
||||
* @param {string} name Snapshot name.
|
||||
* @param {string} postName Post name.
|
||||
* @param {boolean} [patched] Should include patched content and raw or not.
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
async getSnapshotForPost(
|
||||
name: string,
|
||||
postName: string,
|
||||
patched?: boolean,
|
||||
options?: AxiosRequestConfig
|
||||
): Promise<
|
||||
(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Snapshot>
|
||||
> {
|
||||
const localVarAxiosArgs =
|
||||
await localVarAxiosParamCreator.getSnapshotForPost(
|
||||
name,
|
||||
postName,
|
||||
patched,
|
||||
options
|
||||
);
|
||||
return createRequestFunction(
|
||||
localVarAxiosArgs,
|
||||
globalAxios,
|
||||
BASE_PATH,
|
||||
configuration
|
||||
);
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* UcApiContentHaloRunV1alpha1SnapshotApi - factory interface
|
||||
* @export
|
||||
*/
|
||||
export const UcApiContentHaloRunV1alpha1SnapshotApiFactory = function (
|
||||
configuration?: Configuration,
|
||||
basePath?: string,
|
||||
axios?: AxiosInstance
|
||||
) {
|
||||
const localVarFp = UcApiContentHaloRunV1alpha1SnapshotApiFp(configuration);
|
||||
return {
|
||||
/**
|
||||
* Get snapshot for one post.
|
||||
* @param {UcApiContentHaloRunV1alpha1SnapshotApiGetSnapshotForPostRequest} requestParameters Request parameters.
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
getSnapshotForPost(
|
||||
requestParameters: UcApiContentHaloRunV1alpha1SnapshotApiGetSnapshotForPostRequest,
|
||||
options?: AxiosRequestConfig
|
||||
): AxiosPromise<Snapshot> {
|
||||
return localVarFp
|
||||
.getSnapshotForPost(
|
||||
requestParameters.name,
|
||||
requestParameters.postName,
|
||||
requestParameters.patched,
|
||||
options
|
||||
)
|
||||
.then((request) => request(axios, basePath));
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Request parameters for getSnapshotForPost operation in UcApiContentHaloRunV1alpha1SnapshotApi.
|
||||
* @export
|
||||
* @interface UcApiContentHaloRunV1alpha1SnapshotApiGetSnapshotForPostRequest
|
||||
*/
|
||||
export interface UcApiContentHaloRunV1alpha1SnapshotApiGetSnapshotForPostRequest {
|
||||
/**
|
||||
* Snapshot name.
|
||||
* @type {string}
|
||||
* @memberof UcApiContentHaloRunV1alpha1SnapshotApiGetSnapshotForPost
|
||||
*/
|
||||
readonly name: string;
|
||||
|
||||
/**
|
||||
* Post name.
|
||||
* @type {string}
|
||||
* @memberof UcApiContentHaloRunV1alpha1SnapshotApiGetSnapshotForPost
|
||||
*/
|
||||
readonly postName: string;
|
||||
|
||||
/**
|
||||
* Should include patched content and raw or not.
|
||||
* @type {boolean}
|
||||
* @memberof UcApiContentHaloRunV1alpha1SnapshotApiGetSnapshotForPost
|
||||
*/
|
||||
readonly patched?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* UcApiContentHaloRunV1alpha1SnapshotApi - object-oriented interface
|
||||
* @export
|
||||
* @class UcApiContentHaloRunV1alpha1SnapshotApi
|
||||
* @extends {BaseAPI}
|
||||
*/
|
||||
export class UcApiContentHaloRunV1alpha1SnapshotApi extends BaseAPI {
|
||||
/**
|
||||
* Get snapshot for one post.
|
||||
* @param {UcApiContentHaloRunV1alpha1SnapshotApiGetSnapshotForPostRequest} requestParameters Request parameters.
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
* @memberof UcApiContentHaloRunV1alpha1SnapshotApi
|
||||
*/
|
||||
public getSnapshotForPost(
|
||||
requestParameters: UcApiContentHaloRunV1alpha1SnapshotApiGetSnapshotForPostRequest,
|
||||
options?: AxiosRequestConfig
|
||||
) {
|
||||
return UcApiContentHaloRunV1alpha1SnapshotApiFp(this.configuration)
|
||||
.getSnapshotForPost(
|
||||
requestParameters.name,
|
||||
requestParameters.postName,
|
||||
requestParameters.patched,
|
||||
options
|
||||
)
|
||||
.then((request) => request(this.axios, this.basePath));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,655 @@
|
|||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
/**
|
||||
* Halo Next API
|
||||
* No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)
|
||||
*
|
||||
* The version of the OpenAPI document: 2.0.0
|
||||
*
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||
* https://openapi-generator.tech
|
||||
* Do not edit the class manually.
|
||||
*/
|
||||
|
||||
import type { Configuration } from "../configuration";
|
||||
import type { AxiosPromise, AxiosInstance, AxiosRequestConfig } from "axios";
|
||||
import globalAxios from "axios";
|
||||
// Some imports not used depending on template conditions
|
||||
// @ts-ignore
|
||||
import {
|
||||
DUMMY_BASE_URL,
|
||||
assertParamExists,
|
||||
setApiKeyToObject,
|
||||
setBasicAuthToObject,
|
||||
setBearerAuthToObject,
|
||||
setOAuthToObject,
|
||||
setSearchParams,
|
||||
serializeDataIfNeeded,
|
||||
toPathString,
|
||||
createRequestFunction,
|
||||
} from "../common";
|
||||
// @ts-ignore
|
||||
import {
|
||||
BASE_PATH,
|
||||
COLLECTION_FORMATS,
|
||||
RequestArgs,
|
||||
BaseAPI,
|
||||
RequiredError,
|
||||
} from "../base";
|
||||
// @ts-ignore
|
||||
import { ListedPostList } from "../models";
|
||||
// @ts-ignore
|
||||
import { Post } from "../models";
|
||||
/**
|
||||
* UcContentHaloRunV1alpha1PostApi - axios parameter creator
|
||||
* @export
|
||||
*/
|
||||
export const UcContentHaloRunV1alpha1PostApiAxiosParamCreator = function (
|
||||
configuration?: Configuration
|
||||
) {
|
||||
return {
|
||||
/**
|
||||
* Create my post. If you want to create a post with content, please set annotation: \"content.halo.run/content-json\" into annotations and refer to PostRequestContent for corresponding data type.
|
||||
* @param {Post} [post]
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
createMyPost: async (
|
||||
post?: Post,
|
||||
options: AxiosRequestConfig = {}
|
||||
): Promise<RequestArgs> => {
|
||||
const localVarPath = `/apis/uc.content.halo.run/v1alpha1/posts`;
|
||||
// use dummy base URL string because the URL constructor only accepts absolute URLs.
|
||||
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
|
||||
let baseOptions;
|
||||
if (configuration) {
|
||||
baseOptions = configuration.baseOptions;
|
||||
}
|
||||
|
||||
const localVarRequestOptions = {
|
||||
method: "POST",
|
||||
...baseOptions,
|
||||
...options,
|
||||
};
|
||||
const localVarHeaderParameter = {} as any;
|
||||
const localVarQueryParameter = {} as any;
|
||||
|
||||
// authentication BasicAuth required
|
||||
// http basic authentication required
|
||||
setBasicAuthToObject(localVarRequestOptions, configuration);
|
||||
|
||||
// authentication BearerAuth required
|
||||
// http bearer authentication required
|
||||
await setBearerAuthToObject(localVarHeaderParameter, configuration);
|
||||
|
||||
localVarHeaderParameter["Content-Type"] = "application/json";
|
||||
|
||||
setSearchParams(localVarUrlObj, localVarQueryParameter);
|
||||
let headersFromBaseOptions =
|
||||
baseOptions && baseOptions.headers ? baseOptions.headers : {};
|
||||
localVarRequestOptions.headers = {
|
||||
...localVarHeaderParameter,
|
||||
...headersFromBaseOptions,
|
||||
...options.headers,
|
||||
};
|
||||
localVarRequestOptions.data = serializeDataIfNeeded(
|
||||
post,
|
||||
localVarRequestOptions,
|
||||
configuration
|
||||
);
|
||||
|
||||
return {
|
||||
url: toPathString(localVarUrlObj),
|
||||
options: localVarRequestOptions,
|
||||
};
|
||||
},
|
||||
/**
|
||||
* List posts owned by the current user.
|
||||
* @param {Array<string>} [category]
|
||||
* @param {Array<string>} [contributor]
|
||||
* @param {Array<string>} [fieldSelector] Field selector for filtering.
|
||||
* @param {string} [keyword] Posts filtered by keyword.
|
||||
* @param {Array<string>} [labelSelector] Label selector for filtering.
|
||||
* @param {number} [page] The page number. Zero indicates no page.
|
||||
* @param {'DRAFT' | 'PENDING_APPROVAL' | 'PUBLISHED' | 'FAILED'} [publishPhase]
|
||||
* @param {number} [size] Size of one page. Zero indicates no limit.
|
||||
* @param {Array<string>} [sort] Sort property and direction of the list result. Supported fields: creationTimestamp,publishTime
|
||||
* @param {Array<string>} [tag]
|
||||
* @param {'PUBLIC' | 'INTERNAL' | 'PRIVATE'} [visible]
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
listMyPosts: async (
|
||||
category?: Array<string>,
|
||||
contributor?: Array<string>,
|
||||
fieldSelector?: Array<string>,
|
||||
keyword?: string,
|
||||
labelSelector?: Array<string>,
|
||||
page?: number,
|
||||
publishPhase?: "DRAFT" | "PENDING_APPROVAL" | "PUBLISHED" | "FAILED",
|
||||
size?: number,
|
||||
sort?: Array<string>,
|
||||
tag?: Array<string>,
|
||||
visible?: "PUBLIC" | "INTERNAL" | "PRIVATE",
|
||||
options: AxiosRequestConfig = {}
|
||||
): Promise<RequestArgs> => {
|
||||
const localVarPath = `/apis/uc.content.halo.run/v1alpha1/posts`;
|
||||
// use dummy base URL string because the URL constructor only accepts absolute URLs.
|
||||
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
|
||||
let baseOptions;
|
||||
if (configuration) {
|
||||
baseOptions = configuration.baseOptions;
|
||||
}
|
||||
|
||||
const localVarRequestOptions = {
|
||||
method: "GET",
|
||||
...baseOptions,
|
||||
...options,
|
||||
};
|
||||
const localVarHeaderParameter = {} as any;
|
||||
const localVarQueryParameter = {} as any;
|
||||
|
||||
// authentication BasicAuth required
|
||||
// http basic authentication required
|
||||
setBasicAuthToObject(localVarRequestOptions, configuration);
|
||||
|
||||
// authentication BearerAuth required
|
||||
// http bearer authentication required
|
||||
await setBearerAuthToObject(localVarHeaderParameter, configuration);
|
||||
|
||||
if (category) {
|
||||
localVarQueryParameter["category"] = Array.from(category);
|
||||
}
|
||||
|
||||
if (contributor) {
|
||||
localVarQueryParameter["contributor"] = Array.from(contributor);
|
||||
}
|
||||
|
||||
if (fieldSelector) {
|
||||
localVarQueryParameter["fieldSelector"] = fieldSelector;
|
||||
}
|
||||
|
||||
if (keyword !== undefined) {
|
||||
localVarQueryParameter["keyword"] = keyword;
|
||||
}
|
||||
|
||||
if (labelSelector) {
|
||||
localVarQueryParameter["labelSelector"] = labelSelector;
|
||||
}
|
||||
|
||||
if (page !== undefined) {
|
||||
localVarQueryParameter["page"] = page;
|
||||
}
|
||||
|
||||
if (publishPhase !== undefined) {
|
||||
localVarQueryParameter["publishPhase"] = publishPhase;
|
||||
}
|
||||
|
||||
if (size !== undefined) {
|
||||
localVarQueryParameter["size"] = size;
|
||||
}
|
||||
|
||||
if (sort) {
|
||||
localVarQueryParameter["sort"] = Array.from(sort);
|
||||
}
|
||||
|
||||
if (tag) {
|
||||
localVarQueryParameter["tag"] = Array.from(tag);
|
||||
}
|
||||
|
||||
if (visible !== undefined) {
|
||||
localVarQueryParameter["visible"] = visible;
|
||||
}
|
||||
|
||||
setSearchParams(localVarUrlObj, localVarQueryParameter);
|
||||
let headersFromBaseOptions =
|
||||
baseOptions && baseOptions.headers ? baseOptions.headers : {};
|
||||
localVarRequestOptions.headers = {
|
||||
...localVarHeaderParameter,
|
||||
...headersFromBaseOptions,
|
||||
...options.headers,
|
||||
};
|
||||
|
||||
return {
|
||||
url: toPathString(localVarUrlObj),
|
||||
options: localVarRequestOptions,
|
||||
};
|
||||
},
|
||||
/**
|
||||
* Update my post. If you want to update a post with content, please set annotation: \"content.halo.run/content-json\" into annotations and refer to PostRequestContent for corresponding data type.
|
||||
* @param {string} name Post name
|
||||
* @param {Post} [post]
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
updateMyPost: async (
|
||||
name: string,
|
||||
post?: Post,
|
||||
options: AxiosRequestConfig = {}
|
||||
): Promise<RequestArgs> => {
|
||||
// verify required parameter 'name' is not null or undefined
|
||||
assertParamExists("updateMyPost", "name", name);
|
||||
const localVarPath =
|
||||
`/apis/uc.content.halo.run/v1alpha1/posts/{name}`.replace(
|
||||
`{${"name"}}`,
|
||||
encodeURIComponent(String(name))
|
||||
);
|
||||
// use dummy base URL string because the URL constructor only accepts absolute URLs.
|
||||
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
|
||||
let baseOptions;
|
||||
if (configuration) {
|
||||
baseOptions = configuration.baseOptions;
|
||||
}
|
||||
|
||||
const localVarRequestOptions = {
|
||||
method: "PUT",
|
||||
...baseOptions,
|
||||
...options,
|
||||
};
|
||||
const localVarHeaderParameter = {} as any;
|
||||
const localVarQueryParameter = {} as any;
|
||||
|
||||
// authentication BasicAuth required
|
||||
// http basic authentication required
|
||||
setBasicAuthToObject(localVarRequestOptions, configuration);
|
||||
|
||||
// authentication BearerAuth required
|
||||
// http bearer authentication required
|
||||
await setBearerAuthToObject(localVarHeaderParameter, configuration);
|
||||
|
||||
localVarHeaderParameter["Content-Type"] = "application/json";
|
||||
|
||||
setSearchParams(localVarUrlObj, localVarQueryParameter);
|
||||
let headersFromBaseOptions =
|
||||
baseOptions && baseOptions.headers ? baseOptions.headers : {};
|
||||
localVarRequestOptions.headers = {
|
||||
...localVarHeaderParameter,
|
||||
...headersFromBaseOptions,
|
||||
...options.headers,
|
||||
};
|
||||
localVarRequestOptions.data = serializeDataIfNeeded(
|
||||
post,
|
||||
localVarRequestOptions,
|
||||
configuration
|
||||
);
|
||||
|
||||
return {
|
||||
url: toPathString(localVarUrlObj),
|
||||
options: localVarRequestOptions,
|
||||
};
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* UcContentHaloRunV1alpha1PostApi - functional programming interface
|
||||
* @export
|
||||
*/
|
||||
export const UcContentHaloRunV1alpha1PostApiFp = function (
|
||||
configuration?: Configuration
|
||||
) {
|
||||
const localVarAxiosParamCreator =
|
||||
UcContentHaloRunV1alpha1PostApiAxiosParamCreator(configuration);
|
||||
return {
|
||||
/**
|
||||
* Create my post. If you want to create a post with content, please set annotation: \"content.halo.run/content-json\" into annotations and refer to PostRequestContent for corresponding data type.
|
||||
* @param {Post} [post]
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
async createMyPost(
|
||||
post?: Post,
|
||||
options?: AxiosRequestConfig
|
||||
): Promise<
|
||||
(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Post>
|
||||
> {
|
||||
const localVarAxiosArgs = await localVarAxiosParamCreator.createMyPost(
|
||||
post,
|
||||
options
|
||||
);
|
||||
return createRequestFunction(
|
||||
localVarAxiosArgs,
|
||||
globalAxios,
|
||||
BASE_PATH,
|
||||
configuration
|
||||
);
|
||||
},
|
||||
/**
|
||||
* List posts owned by the current user.
|
||||
* @param {Array<string>} [category]
|
||||
* @param {Array<string>} [contributor]
|
||||
* @param {Array<string>} [fieldSelector] Field selector for filtering.
|
||||
* @param {string} [keyword] Posts filtered by keyword.
|
||||
* @param {Array<string>} [labelSelector] Label selector for filtering.
|
||||
* @param {number} [page] The page number. Zero indicates no page.
|
||||
* @param {'DRAFT' | 'PENDING_APPROVAL' | 'PUBLISHED' | 'FAILED'} [publishPhase]
|
||||
* @param {number} [size] Size of one page. Zero indicates no limit.
|
||||
* @param {Array<string>} [sort] Sort property and direction of the list result. Supported fields: creationTimestamp,publishTime
|
||||
* @param {Array<string>} [tag]
|
||||
* @param {'PUBLIC' | 'INTERNAL' | 'PRIVATE'} [visible]
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
async listMyPosts(
|
||||
category?: Array<string>,
|
||||
contributor?: Array<string>,
|
||||
fieldSelector?: Array<string>,
|
||||
keyword?: string,
|
||||
labelSelector?: Array<string>,
|
||||
page?: number,
|
||||
publishPhase?: "DRAFT" | "PENDING_APPROVAL" | "PUBLISHED" | "FAILED",
|
||||
size?: number,
|
||||
sort?: Array<string>,
|
||||
tag?: Array<string>,
|
||||
visible?: "PUBLIC" | "INTERNAL" | "PRIVATE",
|
||||
options?: AxiosRequestConfig
|
||||
): Promise<
|
||||
(axios?: AxiosInstance, basePath?: string) => AxiosPromise<ListedPostList>
|
||||
> {
|
||||
const localVarAxiosArgs = await localVarAxiosParamCreator.listMyPosts(
|
||||
category,
|
||||
contributor,
|
||||
fieldSelector,
|
||||
keyword,
|
||||
labelSelector,
|
||||
page,
|
||||
publishPhase,
|
||||
size,
|
||||
sort,
|
||||
tag,
|
||||
visible,
|
||||
options
|
||||
);
|
||||
return createRequestFunction(
|
||||
localVarAxiosArgs,
|
||||
globalAxios,
|
||||
BASE_PATH,
|
||||
configuration
|
||||
);
|
||||
},
|
||||
/**
|
||||
* Update my post. If you want to update a post with content, please set annotation: \"content.halo.run/content-json\" into annotations and refer to PostRequestContent for corresponding data type.
|
||||
* @param {string} name Post name
|
||||
* @param {Post} [post]
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
async updateMyPost(
|
||||
name: string,
|
||||
post?: Post,
|
||||
options?: AxiosRequestConfig
|
||||
): Promise<
|
||||
(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Post>
|
||||
> {
|
||||
const localVarAxiosArgs = await localVarAxiosParamCreator.updateMyPost(
|
||||
name,
|
||||
post,
|
||||
options
|
||||
);
|
||||
return createRequestFunction(
|
||||
localVarAxiosArgs,
|
||||
globalAxios,
|
||||
BASE_PATH,
|
||||
configuration
|
||||
);
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* UcContentHaloRunV1alpha1PostApi - factory interface
|
||||
* @export
|
||||
*/
|
||||
export const UcContentHaloRunV1alpha1PostApiFactory = function (
|
||||
configuration?: Configuration,
|
||||
basePath?: string,
|
||||
axios?: AxiosInstance
|
||||
) {
|
||||
const localVarFp = UcContentHaloRunV1alpha1PostApiFp(configuration);
|
||||
return {
|
||||
/**
|
||||
* Create my post. If you want to create a post with content, please set annotation: \"content.halo.run/content-json\" into annotations and refer to PostRequestContent for corresponding data type.
|
||||
* @param {UcContentHaloRunV1alpha1PostApiCreateMyPostRequest} requestParameters Request parameters.
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
createMyPost(
|
||||
requestParameters: UcContentHaloRunV1alpha1PostApiCreateMyPostRequest = {},
|
||||
options?: AxiosRequestConfig
|
||||
): AxiosPromise<Post> {
|
||||
return localVarFp
|
||||
.createMyPost(requestParameters.post, options)
|
||||
.then((request) => request(axios, basePath));
|
||||
},
|
||||
/**
|
||||
* List posts owned by the current user.
|
||||
* @param {UcContentHaloRunV1alpha1PostApiListMyPostsRequest} requestParameters Request parameters.
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
listMyPosts(
|
||||
requestParameters: UcContentHaloRunV1alpha1PostApiListMyPostsRequest = {},
|
||||
options?: AxiosRequestConfig
|
||||
): AxiosPromise<ListedPostList> {
|
||||
return localVarFp
|
||||
.listMyPosts(
|
||||
requestParameters.category,
|
||||
requestParameters.contributor,
|
||||
requestParameters.fieldSelector,
|
||||
requestParameters.keyword,
|
||||
requestParameters.labelSelector,
|
||||
requestParameters.page,
|
||||
requestParameters.publishPhase,
|
||||
requestParameters.size,
|
||||
requestParameters.sort,
|
||||
requestParameters.tag,
|
||||
requestParameters.visible,
|
||||
options
|
||||
)
|
||||
.then((request) => request(axios, basePath));
|
||||
},
|
||||
/**
|
||||
* Update my post. If you want to update a post with content, please set annotation: \"content.halo.run/content-json\" into annotations and refer to PostRequestContent for corresponding data type.
|
||||
* @param {UcContentHaloRunV1alpha1PostApiUpdateMyPostRequest} requestParameters Request parameters.
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
updateMyPost(
|
||||
requestParameters: UcContentHaloRunV1alpha1PostApiUpdateMyPostRequest,
|
||||
options?: AxiosRequestConfig
|
||||
): AxiosPromise<Post> {
|
||||
return localVarFp
|
||||
.updateMyPost(requestParameters.name, requestParameters.post, options)
|
||||
.then((request) => request(axios, basePath));
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Request parameters for createMyPost operation in UcContentHaloRunV1alpha1PostApi.
|
||||
* @export
|
||||
* @interface UcContentHaloRunV1alpha1PostApiCreateMyPostRequest
|
||||
*/
|
||||
export interface UcContentHaloRunV1alpha1PostApiCreateMyPostRequest {
|
||||
/**
|
||||
*
|
||||
* @type {Post}
|
||||
* @memberof UcContentHaloRunV1alpha1PostApiCreateMyPost
|
||||
*/
|
||||
readonly post?: Post;
|
||||
}
|
||||
|
||||
/**
|
||||
* Request parameters for listMyPosts operation in UcContentHaloRunV1alpha1PostApi.
|
||||
* @export
|
||||
* @interface UcContentHaloRunV1alpha1PostApiListMyPostsRequest
|
||||
*/
|
||||
export interface UcContentHaloRunV1alpha1PostApiListMyPostsRequest {
|
||||
/**
|
||||
*
|
||||
* @type {Array<string>}
|
||||
* @memberof UcContentHaloRunV1alpha1PostApiListMyPosts
|
||||
*/
|
||||
readonly category?: Array<string>;
|
||||
|
||||
/**
|
||||
*
|
||||
* @type {Array<string>}
|
||||
* @memberof UcContentHaloRunV1alpha1PostApiListMyPosts
|
||||
*/
|
||||
readonly contributor?: Array<string>;
|
||||
|
||||
/**
|
||||
* Field selector for filtering.
|
||||
* @type {Array<string>}
|
||||
* @memberof UcContentHaloRunV1alpha1PostApiListMyPosts
|
||||
*/
|
||||
readonly fieldSelector?: Array<string>;
|
||||
|
||||
/**
|
||||
* Posts filtered by keyword.
|
||||
* @type {string}
|
||||
* @memberof UcContentHaloRunV1alpha1PostApiListMyPosts
|
||||
*/
|
||||
readonly keyword?: string;
|
||||
|
||||
/**
|
||||
* Label selector for filtering.
|
||||
* @type {Array<string>}
|
||||
* @memberof UcContentHaloRunV1alpha1PostApiListMyPosts
|
||||
*/
|
||||
readonly labelSelector?: Array<string>;
|
||||
|
||||
/**
|
||||
* The page number. Zero indicates no page.
|
||||
* @type {number}
|
||||
* @memberof UcContentHaloRunV1alpha1PostApiListMyPosts
|
||||
*/
|
||||
readonly page?: number;
|
||||
|
||||
/**
|
||||
*
|
||||
* @type {'DRAFT' | 'PENDING_APPROVAL' | 'PUBLISHED' | 'FAILED'}
|
||||
* @memberof UcContentHaloRunV1alpha1PostApiListMyPosts
|
||||
*/
|
||||
readonly publishPhase?: "DRAFT" | "PENDING_APPROVAL" | "PUBLISHED" | "FAILED";
|
||||
|
||||
/**
|
||||
* Size of one page. Zero indicates no limit.
|
||||
* @type {number}
|
||||
* @memberof UcContentHaloRunV1alpha1PostApiListMyPosts
|
||||
*/
|
||||
readonly size?: number;
|
||||
|
||||
/**
|
||||
* Sort property and direction of the list result. Supported fields: creationTimestamp,publishTime
|
||||
* @type {Array<string>}
|
||||
* @memberof UcContentHaloRunV1alpha1PostApiListMyPosts
|
||||
*/
|
||||
readonly sort?: Array<string>;
|
||||
|
||||
/**
|
||||
*
|
||||
* @type {Array<string>}
|
||||
* @memberof UcContentHaloRunV1alpha1PostApiListMyPosts
|
||||
*/
|
||||
readonly tag?: Array<string>;
|
||||
|
||||
/**
|
||||
*
|
||||
* @type {'PUBLIC' | 'INTERNAL' | 'PRIVATE'}
|
||||
* @memberof UcContentHaloRunV1alpha1PostApiListMyPosts
|
||||
*/
|
||||
readonly visible?: "PUBLIC" | "INTERNAL" | "PRIVATE";
|
||||
}
|
||||
|
||||
/**
|
||||
* Request parameters for updateMyPost operation in UcContentHaloRunV1alpha1PostApi.
|
||||
* @export
|
||||
* @interface UcContentHaloRunV1alpha1PostApiUpdateMyPostRequest
|
||||
*/
|
||||
export interface UcContentHaloRunV1alpha1PostApiUpdateMyPostRequest {
|
||||
/**
|
||||
* Post name
|
||||
* @type {string}
|
||||
* @memberof UcContentHaloRunV1alpha1PostApiUpdateMyPost
|
||||
*/
|
||||
readonly name: string;
|
||||
|
||||
/**
|
||||
*
|
||||
* @type {Post}
|
||||
* @memberof UcContentHaloRunV1alpha1PostApiUpdateMyPost
|
||||
*/
|
||||
readonly post?: Post;
|
||||
}
|
||||
|
||||
/**
|
||||
* UcContentHaloRunV1alpha1PostApi - object-oriented interface
|
||||
* @export
|
||||
* @class UcContentHaloRunV1alpha1PostApi
|
||||
* @extends {BaseAPI}
|
||||
*/
|
||||
export class UcContentHaloRunV1alpha1PostApi extends BaseAPI {
|
||||
/**
|
||||
* Create my post. If you want to create a post with content, please set annotation: \"content.halo.run/content-json\" into annotations and refer to PostRequestContent for corresponding data type.
|
||||
* @param {UcContentHaloRunV1alpha1PostApiCreateMyPostRequest} requestParameters Request parameters.
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
* @memberof UcContentHaloRunV1alpha1PostApi
|
||||
*/
|
||||
public createMyPost(
|
||||
requestParameters: UcContentHaloRunV1alpha1PostApiCreateMyPostRequest = {},
|
||||
options?: AxiosRequestConfig
|
||||
) {
|
||||
return UcContentHaloRunV1alpha1PostApiFp(this.configuration)
|
||||
.createMyPost(requestParameters.post, options)
|
||||
.then((request) => request(this.axios, this.basePath));
|
||||
}
|
||||
|
||||
/**
|
||||
* List posts owned by the current user.
|
||||
* @param {UcContentHaloRunV1alpha1PostApiListMyPostsRequest} requestParameters Request parameters.
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
* @memberof UcContentHaloRunV1alpha1PostApi
|
||||
*/
|
||||
public listMyPosts(
|
||||
requestParameters: UcContentHaloRunV1alpha1PostApiListMyPostsRequest = {},
|
||||
options?: AxiosRequestConfig
|
||||
) {
|
||||
return UcContentHaloRunV1alpha1PostApiFp(this.configuration)
|
||||
.listMyPosts(
|
||||
requestParameters.category,
|
||||
requestParameters.contributor,
|
||||
requestParameters.fieldSelector,
|
||||
requestParameters.keyword,
|
||||
requestParameters.labelSelector,
|
||||
requestParameters.page,
|
||||
requestParameters.publishPhase,
|
||||
requestParameters.size,
|
||||
requestParameters.sort,
|
||||
requestParameters.tag,
|
||||
requestParameters.visible,
|
||||
options
|
||||
)
|
||||
.then((request) => request(this.axios, this.basePath));
|
||||
}
|
||||
|
||||
/**
|
||||
* Update my post. If you want to update a post with content, please set annotation: \"content.halo.run/content-json\" into annotations and refer to PostRequestContent for corresponding data type.
|
||||
* @param {UcContentHaloRunV1alpha1PostApiUpdateMyPostRequest} requestParameters Request parameters.
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
* @memberof UcContentHaloRunV1alpha1PostApi
|
||||
*/
|
||||
public updateMyPost(
|
||||
requestParameters: UcContentHaloRunV1alpha1PostApiUpdateMyPostRequest,
|
||||
options?: AxiosRequestConfig
|
||||
) {
|
||||
return UcContentHaloRunV1alpha1PostApiFp(this.configuration)
|
||||
.updateMyPost(requestParameters.name, requestParameters.post, options)
|
||||
.then((request) => request(this.axios, this.basePath));
|
||||
}
|
||||
}
|
|
@ -181,7 +181,6 @@ export * from "./site-stats-vo";
|
|||
export * from "./snap-shot-spec";
|
||||
export * from "./snapshot";
|
||||
export * from "./snapshot-list";
|
||||
export * from "./spec";
|
||||
export * from "./stats";
|
||||
export * from "./stats-vo";
|
||||
export * from "./subject";
|
||||
|
|
|
@ -0,0 +1,39 @@
|
|||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
/**
|
||||
* Halo Next API
|
||||
* No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)
|
||||
*
|
||||
* The version of the OpenAPI document: 2.0.0
|
||||
*
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||
* https://openapi-generator.tech
|
||||
* Do not edit the class manually.
|
||||
*/
|
||||
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
* @interface PostRequestContent
|
||||
*/
|
||||
export interface PostRequestContent {
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof PostRequestContent
|
||||
*/
|
||||
content?: string;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof PostRequestContent
|
||||
*/
|
||||
raw?: string;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof PostRequestContent
|
||||
*/
|
||||
rawType?: string;
|
||||
}
|
|
@ -30,7 +30,7 @@ export interface PostRequest {
|
|||
* @type {Content}
|
||||
* @memberof PostRequest
|
||||
*/
|
||||
content: Content;
|
||||
content?: Content;
|
||||
/**
|
||||
*
|
||||
* @type {Post}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<script lang="ts" setup>
|
||||
import { useEditorExtensionPoints } from "@console/composables/use-editor-extension-points";
|
||||
import { useEditorExtensionPoints } from "@/composables/use-editor-extension-points";
|
||||
import type { EditorProvider } from "@halo-dev/console-shared";
|
||||
import {
|
||||
VAvatar,
|
||||
|
|
|
@ -51,7 +51,6 @@ import {
|
|||
IconFolder,
|
||||
IconLink,
|
||||
IconUserFollow,
|
||||
Toast,
|
||||
VTabItem,
|
||||
VTabs,
|
||||
} from "@halo-dev/components";
|
||||
|
@ -75,11 +74,9 @@ import {
|
|||
} from "vue";
|
||||
import { formatDatetime } from "@/utils/date";
|
||||
import { useAttachmentSelect } from "@console/modules/contents/attachments/composables/use-attachment";
|
||||
import { apiClient } from "@/utils/api-client";
|
||||
import * as fastq from "fastq";
|
||||
import type { queueAsPromised } from "fastq";
|
||||
import type { Attachment } from "@halo-dev/api-client";
|
||||
import { useFetchAttachmentPolicy } from "@console/modules/contents/attachments/composables/use-attachment-policy";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { i18n } from "@/locales";
|
||||
import { OverlayScrollbarsComponent } from "overlayscrollbars-vue";
|
||||
|
@ -88,17 +85,21 @@ import type { PluginModule } from "@halo-dev/console-shared";
|
|||
import { useDebounceFn } from "@vueuse/core";
|
||||
import { onBeforeUnmount } from "vue";
|
||||
import { generateAnchor } from "@/utils/anchor";
|
||||
import { usePermission } from "@/utils/permission";
|
||||
|
||||
const { t } = useI18n();
|
||||
const { currentUserHasPermission } = usePermission();
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
raw?: string;
|
||||
content: string;
|
||||
uploadImage?: (file: File) => Promise<Attachment>;
|
||||
}>(),
|
||||
{
|
||||
raw: "",
|
||||
content: "",
|
||||
uploadImage: undefined,
|
||||
}
|
||||
);
|
||||
|
||||
|
@ -236,6 +237,11 @@ onMounted(() => {
|
|||
}),
|
||||
Extension.create({
|
||||
addOptions() {
|
||||
// If user has no permission to view attachments, return
|
||||
if (!currentUserHasPermission(["system:attachments:view"])) {
|
||||
return this;
|
||||
}
|
||||
|
||||
return {
|
||||
getToolboxItems({ editor }: { editor: Editor }) {
|
||||
return [
|
||||
|
@ -362,8 +368,6 @@ onBeforeUnmount(() => {
|
|||
});
|
||||
|
||||
// image drag and paste upload
|
||||
const { policies } = useFetchAttachmentPolicy();
|
||||
|
||||
type Task = {
|
||||
file: File;
|
||||
process: (permalink: string) => void;
|
||||
|
@ -372,61 +376,17 @@ type Task = {
|
|||
const uploadQueue: queueAsPromised<Task> = fastq.promise(asyncWorker, 1);
|
||||
|
||||
async function asyncWorker(arg: Task): Promise<void> {
|
||||
if (!policies.value?.length) {
|
||||
Toast.warning(
|
||||
t(
|
||||
"core.components.default_editor.upload_attachment.toast.no_available_policy"
|
||||
)
|
||||
);
|
||||
if (!props.uploadImage) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { data: attachmentData } = await apiClient.attachment.uploadAttachment({
|
||||
file: arg.file,
|
||||
policyName: policies.value[0].metadata.name,
|
||||
});
|
||||
const attachmentData = await props.uploadImage(arg.file);
|
||||
|
||||
const permalink = await handleFetchPermalink(attachmentData, 3);
|
||||
|
||||
if (permalink) {
|
||||
arg.process(permalink);
|
||||
if (attachmentData.status?.permalink) {
|
||||
arg.process(attachmentData.status.permalink);
|
||||
}
|
||||
}
|
||||
|
||||
const handleFetchPermalink = async (
|
||||
attachment: Attachment,
|
||||
maxRetry: number
|
||||
): Promise<string | undefined> => {
|
||||
if (maxRetry === 0) {
|
||||
Toast.error(
|
||||
t(
|
||||
"core.components.default_editor.upload_attachment.toast.failed_fetch_permalink",
|
||||
{ display_name: attachment.spec.displayName }
|
||||
)
|
||||
);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const { data } =
|
||||
await apiClient.extension.storage.attachment.getstorageHaloRunV1alpha1Attachment(
|
||||
{
|
||||
name: attachment.metadata.name,
|
||||
}
|
||||
);
|
||||
|
||||
if (data.status?.permalink) {
|
||||
return data.status.permalink;
|
||||
}
|
||||
|
||||
return await new Promise((resolve) => {
|
||||
const timer = setTimeout(() => {
|
||||
const permalink = handleFetchPermalink(attachment, maxRetry - 1);
|
||||
clearTimeout(timer);
|
||||
resolve(permalink);
|
||||
}, 300);
|
||||
});
|
||||
};
|
||||
|
||||
const handleGenerateTableOfContent = () => {
|
||||
if (!editor.value) {
|
||||
return;
|
||||
|
|
|
@ -18,6 +18,9 @@ export enum rbacAnnotations {
|
|||
|
||||
export enum contentAnnotations {
|
||||
PREFERRED_EDITOR = "content.halo.run/preferred-editor",
|
||||
PATCHED_CONTENT = "content.halo.run/patched-content",
|
||||
PATCHED_RAW = "content.halo.run/patched-raw",
|
||||
CONTENT_JSON = "content.halo.run/content-json",
|
||||
}
|
||||
|
||||
// pat
|
||||
|
|
|
@ -14,6 +14,7 @@ import SearchResultListItem from "./components/SearchResultListItem.vue";
|
|||
import { apiClient } from "@/utils/api-client";
|
||||
import { usePermission } from "@/utils/permission";
|
||||
import { slugify } from "transliteration";
|
||||
import HasPermission from "@/components/permission/HasPermission.vue";
|
||||
|
||||
const { currentUserHasPermission } = usePermission();
|
||||
|
||||
|
@ -289,18 +290,24 @@ const handleDelete = () => {
|
|||
|
||||
<div v-if="dropdownVisible" :class="context.classes['dropdown-wrapper']">
|
||||
<ul class="p-1">
|
||||
<li
|
||||
<HasPermission
|
||||
v-if="text.trim() && !searchResults?.length"
|
||||
v-permission="['system:posts:manage']"
|
||||
:permissions="['system:posts:manage']"
|
||||
>
|
||||
<li
|
||||
class="group flex cursor-pointer items-center justify-between rounded bg-gray-100 p-2"
|
||||
@click="handleCreateCategory"
|
||||
>
|
||||
<span class="text-xs text-gray-700 group-hover:text-gray-900">
|
||||
{{
|
||||
$t("core.formkit.category_select.creation_label", { text: text })
|
||||
$t("core.formkit.category_select.creation_label", {
|
||||
text: text,
|
||||
})
|
||||
}}
|
||||
</span>
|
||||
</li>
|
||||
</HasPermission>
|
||||
|
||||
<template v-if="text">
|
||||
<SearchResultListItem
|
||||
v-for="category in searchResults"
|
||||
|
|
|
@ -14,6 +14,7 @@ import Fuse from "fuse.js";
|
|||
import { usePermission } from "@/utils/permission";
|
||||
import { slugify } from "transliteration";
|
||||
import { usePostTag } from "@console/modules/contents/posts/tags/composables/use-post-tag";
|
||||
import HasPermission from "@/components/permission/HasPermission.vue";
|
||||
|
||||
const { currentUserHasPermission } = usePermission();
|
||||
|
||||
|
@ -279,9 +280,11 @@ const handleDelete = () => {
|
|||
|
||||
<div v-if="dropdownVisible" :class="context.classes['dropdown-wrapper']">
|
||||
<ul class="p-1">
|
||||
<li
|
||||
<HasPermission
|
||||
v-if="text.trim() && !searchResults?.length"
|
||||
v-permission="['system:posts:manage']"
|
||||
:permissions="['system:posts:manage']"
|
||||
>
|
||||
<li
|
||||
class="group flex cursor-pointer items-center justify-between rounded bg-gray-100 p-2"
|
||||
@click="handleCreateTag"
|
||||
>
|
||||
|
@ -289,6 +292,7 @@ const handleDelete = () => {
|
|||
{{ $t("core.formkit.tag_select.creation_label", { text: text }) }}
|
||||
</span>
|
||||
</li>
|
||||
</HasPermission>
|
||||
<li
|
||||
v-for="tag in searchResults"
|
||||
:id="tag.metadata.name"
|
||||
|
|
|
@ -1205,10 +1205,6 @@ core:
|
|||
placeholder: "Enter / to select input type."
|
||||
toolbox:
|
||||
attachment: Attachment
|
||||
upload_attachment:
|
||||
toast:
|
||||
no_available_policy: There is currently no available storage policy
|
||||
failed_fetch_permalink: "Failed to get the permalink of the attachment: {display_name}"
|
||||
global_search:
|
||||
placeholder: Enter keywords to search
|
||||
no_results: No search results
|
||||
|
|
|
@ -1133,10 +1133,6 @@ core:
|
|||
placeholder: "Ingresa / para seleccionar el tipo de entrada."
|
||||
toolbox:
|
||||
attachment: Adjunto
|
||||
upload_attachment:
|
||||
toast:
|
||||
no_available_policy: Actualmente no hay una política de almacenamiento disponible
|
||||
failed_fetch_permalink: "Error al obtener el enlace permanente del adjunto: {display_name}"
|
||||
global_search:
|
||||
placeholder: Ingresa palabras clave para buscar
|
||||
no_results: Sin resultados de búsqueda
|
||||
|
|
|
@ -1205,10 +1205,6 @@ core:
|
|||
placeholder: "输入 / 以选择输入类型"
|
||||
toolbox:
|
||||
attachment: 选择附件
|
||||
upload_attachment:
|
||||
toast:
|
||||
no_available_policy: 目前没有可用的存储策略
|
||||
failed_fetch_permalink: 获取附件永久链接失败:{display_name}
|
||||
global_search:
|
||||
placeholder: 输入关键词以搜索
|
||||
no_results: 没有搜索结果
|
||||
|
@ -1294,7 +1290,7 @@ core:
|
|||
delete_success: 删除成功
|
||||
save_success: 保存成功
|
||||
publish_success: 发布成功
|
||||
cancel_publish_success: 发布成功
|
||||
cancel_publish_success: 取消发布成功
|
||||
recovery_success: 恢复成功
|
||||
uninstall_success: 卸载成功
|
||||
active_success: 启用成功
|
||||
|
|
|
@ -1205,10 +1205,6 @@ core:
|
|||
placeholder: "輸入 / 以選擇輸入類型"
|
||||
toolbox:
|
||||
attachment: 選擇附件
|
||||
upload_attachment:
|
||||
toast:
|
||||
no_available_policy: 目前沒有可用的存儲策略
|
||||
failed_fetch_permalink: 獲取附件永久連結失敗:{display_name}
|
||||
global_search:
|
||||
placeholder: 輸入關鍵字以搜尋
|
||||
no_results: 沒有搜尋結果
|
||||
|
@ -1294,7 +1290,7 @@ core:
|
|||
delete_success: 刪除成功
|
||||
save_success: 保存成功
|
||||
publish_success: 發布成功
|
||||
cancel_publish_success: 發布成功
|
||||
cancel_publish_success: 取消發布成功
|
||||
recovery_success: 恢復成功
|
||||
uninstall_success: 卸載成功
|
||||
active_success: 啟用成功
|
||||
|
|
|
@ -45,6 +45,9 @@ import {
|
|||
NotificationHaloRunV1alpha1NotifierDescriptorApi,
|
||||
ApiSecurityHaloRunV1alpha1PersonalAccessTokenApi,
|
||||
SecurityHaloRunV1alpha1PersonalAccessTokenApi,
|
||||
UcApiContentHaloRunV1alpha1AttachmentApi,
|
||||
UcApiContentHaloRunV1alpha1PostApi,
|
||||
UcApiContentHaloRunV1alpha1SnapshotApi,
|
||||
} from "@halo-dev/api-client";
|
||||
import type { AxiosError, AxiosInstance } from "axios";
|
||||
import axios from "axios";
|
||||
|
@ -244,6 +247,19 @@ function setupApiClient(axios: AxiosInstance) {
|
|||
baseURL,
|
||||
axios
|
||||
),
|
||||
uc: {
|
||||
post: new UcApiContentHaloRunV1alpha1PostApi(undefined, baseURL, axios),
|
||||
attachment: new UcApiContentHaloRunV1alpha1AttachmentApi(
|
||||
undefined,
|
||||
baseURL,
|
||||
axios
|
||||
),
|
||||
snapshot: new UcApiContentHaloRunV1alpha1SnapshotApi(
|
||||
undefined,
|
||||
baseURL,
|
||||
axios
|
||||
),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,439 @@
|
|||
<script lang="ts" setup>
|
||||
import { useEditorExtensionPoints } from "@/composables/use-editor-extension-points";
|
||||
import type { EditorProvider } from "@halo-dev/console-shared";
|
||||
import {
|
||||
Dialog,
|
||||
IconBookRead,
|
||||
IconSave,
|
||||
IconSendPlaneFill,
|
||||
IconSettings,
|
||||
Toast,
|
||||
VButton,
|
||||
VPageHeader,
|
||||
VSpace,
|
||||
} from "@halo-dev/components";
|
||||
import EditorProviderSelector from "@/components/dropdown-selector/EditorProviderSelector.vue";
|
||||
import { ref } from "vue";
|
||||
import { useLocalStorage } from "@vueuse/core";
|
||||
import type { Post, Content, Snapshot } from "@halo-dev/api-client";
|
||||
import { randomUUID } from "@/utils/id";
|
||||
import { contentAnnotations } from "@/constants/annotations";
|
||||
import { useRouteQuery } from "@vueuse/router";
|
||||
import { onMounted } from "vue";
|
||||
import { apiClient } from "@/utils/api-client";
|
||||
import { nextTick } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { useMutation } from "@tanstack/vue-query";
|
||||
import { computed } from "vue";
|
||||
import { useSaveKeybinding } from "@console/composables/use-save-keybinding";
|
||||
import PostCreationModal from "./components/PostCreationModal.vue";
|
||||
import PostSettingEditModal from "./components/PostSettingEditModal.vue";
|
||||
import HasPermission from "@/components/permission/HasPermission.vue";
|
||||
import { provide } from "vue";
|
||||
import type { ComputedRef } from "vue";
|
||||
|
||||
const router = useRouter();
|
||||
const { t } = useI18n();
|
||||
|
||||
const formState = ref<Post>({
|
||||
apiVersion: "content.halo.run/v1alpha1",
|
||||
kind: "Post",
|
||||
metadata: {
|
||||
annotations: {},
|
||||
name: randomUUID(),
|
||||
},
|
||||
spec: {
|
||||
allowComment: true,
|
||||
baseSnapshot: "",
|
||||
categories: [],
|
||||
cover: "",
|
||||
deleted: false,
|
||||
excerpt: {
|
||||
autoGenerate: true,
|
||||
raw: "",
|
||||
},
|
||||
headSnapshot: "",
|
||||
htmlMetas: [],
|
||||
owner: "",
|
||||
pinned: false,
|
||||
priority: 0,
|
||||
publish: false,
|
||||
publishTime: "",
|
||||
releaseSnapshot: "",
|
||||
slug: "",
|
||||
tags: [],
|
||||
template: "",
|
||||
title: "",
|
||||
visible: "PUBLIC",
|
||||
},
|
||||
});
|
||||
|
||||
const content = ref<Content>({
|
||||
content: "",
|
||||
raw: "",
|
||||
rawType: "",
|
||||
});
|
||||
const snapshot = ref<Snapshot>();
|
||||
|
||||
// provide some data to editor
|
||||
provide<ComputedRef<string | undefined>>(
|
||||
"owner",
|
||||
computed(() => formState.value.spec.owner)
|
||||
);
|
||||
provide<ComputedRef<string | undefined>>(
|
||||
"publishTime",
|
||||
computed(() => formState.value.spec.publishTime)
|
||||
);
|
||||
provide<ComputedRef<string | undefined>>(
|
||||
"permalink",
|
||||
computed(() => formState.value.status?.permalink)
|
||||
);
|
||||
|
||||
// Editor providers
|
||||
const { editorProviders } = useEditorExtensionPoints();
|
||||
const currentEditorProvider = ref<EditorProvider>();
|
||||
const storedEditorProviderName = useLocalStorage("editor-provider-name", "");
|
||||
|
||||
const handleChangeEditorProvider = async (provider: EditorProvider) => {
|
||||
currentEditorProvider.value = provider;
|
||||
|
||||
const { name, rawType } = provider;
|
||||
|
||||
storedEditorProviderName.value = name;
|
||||
|
||||
content.value.rawType = rawType;
|
||||
|
||||
formState.value.metadata.annotations = {
|
||||
...formState.value.metadata.annotations,
|
||||
[contentAnnotations.PREFERRED_EDITOR]: name,
|
||||
};
|
||||
};
|
||||
|
||||
// Fetch post data when the route contains the name parameter
|
||||
const name = useRouteQuery<string | undefined>("name");
|
||||
|
||||
onMounted(async () => {
|
||||
if (name.value) {
|
||||
const { data: post } = await apiClient.uc.post.getMyPost({
|
||||
name: name.value,
|
||||
});
|
||||
|
||||
formState.value = post;
|
||||
|
||||
await handleFetchContent();
|
||||
return;
|
||||
}
|
||||
|
||||
// New post, set default editor
|
||||
const provider =
|
||||
editorProviders.value.find(
|
||||
(provider) => provider.name === storedEditorProviderName.value
|
||||
) || editorProviders.value[0];
|
||||
|
||||
if (provider) {
|
||||
currentEditorProvider.value = provider;
|
||||
content.value.rawType = provider.rawType;
|
||||
formState.value.metadata.annotations = {
|
||||
[contentAnnotations.PREFERRED_EDITOR]: provider.name,
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Fetch content from the head snapshot.
|
||||
*/
|
||||
async function handleFetchContent() {
|
||||
const { headSnapshot } = formState.value.spec || {};
|
||||
|
||||
if (!headSnapshot || !name.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { data } = await apiClient.uc.post.getMyPostDraft({
|
||||
name: name.value,
|
||||
patched: true,
|
||||
});
|
||||
|
||||
const {
|
||||
[contentAnnotations.PATCHED_CONTENT]: patchedContent,
|
||||
[contentAnnotations.PATCHED_RAW]: patchedRaw,
|
||||
} = data.metadata.annotations || {};
|
||||
|
||||
const { rawType } = data.spec || {};
|
||||
|
||||
content.value = {
|
||||
content: patchedContent,
|
||||
raw: patchedRaw,
|
||||
rawType,
|
||||
};
|
||||
|
||||
snapshot.value = data;
|
||||
|
||||
if (currentEditorProvider.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
await handleSetEditorProviderFromRemote();
|
||||
}
|
||||
|
||||
async function handleSetEditorProviderFromRemote() {
|
||||
const { [contentAnnotations.PREFERRED_EDITOR]: preferredEditorName } =
|
||||
formState.value.metadata.annotations || {};
|
||||
|
||||
const preferredEditor = editorProviders.value.find(
|
||||
(provider) => provider.name === preferredEditorName
|
||||
);
|
||||
|
||||
const provider =
|
||||
preferredEditor ||
|
||||
editorProviders.value.find(
|
||||
(provider) => provider.rawType === content.value.rawType
|
||||
);
|
||||
|
||||
if (provider) {
|
||||
currentEditorProvider.value = provider;
|
||||
|
||||
formState.value.metadata.annotations = {
|
||||
...formState.value.metadata.annotations,
|
||||
[contentAnnotations.PREFERRED_EDITOR]: provider.name,
|
||||
};
|
||||
} else {
|
||||
Dialog.warning({
|
||||
title: t("core.common.dialog.titles.warning"),
|
||||
description: t("core.common.dialog.descriptions.editor_not_found", {
|
||||
raw_type: content.value.rawType,
|
||||
}),
|
||||
confirmText: t("core.common.buttons.confirm"),
|
||||
showCancel: false,
|
||||
onConfirm: () => {
|
||||
router.back();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
await nextTick();
|
||||
}
|
||||
|
||||
// Create post
|
||||
const postCreationModal = ref(false);
|
||||
|
||||
function handleSaveClick() {
|
||||
if (isUpdateMode.value) {
|
||||
handleSave({ mute: false });
|
||||
} else {
|
||||
postCreationModal.value = true;
|
||||
}
|
||||
}
|
||||
|
||||
function onCreatePostSuccess(data: Post) {
|
||||
formState.value = data;
|
||||
// Update route query params
|
||||
name.value = data.metadata.name;
|
||||
handleFetchContent();
|
||||
}
|
||||
|
||||
// Save post
|
||||
const isUpdateMode = computed(
|
||||
() => !!formState.value.metadata.creationTimestamp
|
||||
);
|
||||
|
||||
const { mutateAsync: handleSave, isLoading: isSaving } = useMutation({
|
||||
mutationKey: ["save-post"],
|
||||
variables: {
|
||||
mute: false,
|
||||
},
|
||||
mutationFn: async () => {
|
||||
// Snapshot always exists in update mode
|
||||
if (!snapshot.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { annotations } = snapshot.value.metadata || {};
|
||||
|
||||
snapshot.value.metadata.annotations = {
|
||||
...annotations,
|
||||
[contentAnnotations.CONTENT_JSON]: JSON.stringify(content.value),
|
||||
};
|
||||
|
||||
if (!isUpdateMode.value || !name.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { data } = await apiClient.uc.post.updateMyPostDraft({
|
||||
name: name.value,
|
||||
snapshot: snapshot.value,
|
||||
});
|
||||
|
||||
snapshot.value = data;
|
||||
|
||||
return data;
|
||||
},
|
||||
onSuccess(_, variables) {
|
||||
if (!variables.mute) Toast.success(t("core.common.toast.save_success"));
|
||||
handleFetchContent();
|
||||
},
|
||||
onError() {
|
||||
Toast.error(t("core.common.toast.save_failed_and_retry"));
|
||||
},
|
||||
});
|
||||
|
||||
useSaveKeybinding(handleSaveClick);
|
||||
|
||||
// Publish post
|
||||
|
||||
const postPublishModal = ref(false);
|
||||
|
||||
function handlePublishClick() {
|
||||
if (isUpdateMode.value) {
|
||||
handlePublish();
|
||||
} else {
|
||||
postPublishModal.value = true;
|
||||
}
|
||||
}
|
||||
|
||||
function onPublishPostSuccess() {
|
||||
router.push({ name: "Posts" });
|
||||
}
|
||||
|
||||
const { mutateAsync: handlePublish, isLoading: isPublishing } = useMutation({
|
||||
mutationKey: ["publish-post"],
|
||||
mutationFn: async () => {
|
||||
await handleSave({ mute: true });
|
||||
|
||||
return await apiClient.uc.post.publishMyPost({
|
||||
name: formState.value.metadata.name,
|
||||
});
|
||||
},
|
||||
onSuccess() {
|
||||
Toast.success(t("core.common.toast.publish_success"), {
|
||||
duration: 2000,
|
||||
});
|
||||
|
||||
router.push({ name: "Posts" });
|
||||
},
|
||||
onError() {
|
||||
Toast.error(t("core.common.toast.publish_failed_and_retry"));
|
||||
},
|
||||
});
|
||||
|
||||
// Post setting
|
||||
const postSettingEditModal = ref(false);
|
||||
|
||||
function handleOpenPostSettingEditModal() {
|
||||
handleSave({ mute: true });
|
||||
postSettingEditModal.value = true;
|
||||
}
|
||||
|
||||
function onUpdatePostSuccess(data: Post) {
|
||||
formState.value = data;
|
||||
handleFetchContent();
|
||||
}
|
||||
|
||||
// Upload image
|
||||
async function handleUploadImage(file: File) {
|
||||
if (!isUpdateMode.value) {
|
||||
formState.value.metadata.annotations = {
|
||||
...formState.value.metadata.annotations,
|
||||
[contentAnnotations.CONTENT_JSON]: JSON.stringify(content.value),
|
||||
};
|
||||
|
||||
await apiClient.uc.post.createMyPost({
|
||||
post: formState.value,
|
||||
});
|
||||
}
|
||||
|
||||
const { data } = await apiClient.uc.attachment.createAttachmentForPost({
|
||||
file,
|
||||
postName: formState.value.metadata.name,
|
||||
waitForPermalink: true,
|
||||
});
|
||||
return data;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VPageHeader :title="$t('core.post.title')">
|
||||
<template #icon>
|
||||
<IconBookRead class="mr-2 self-center" />
|
||||
</template>
|
||||
<template #actions>
|
||||
<VSpace>
|
||||
<EditorProviderSelector
|
||||
v-if="editorProviders.length > 1"
|
||||
:provider="currentEditorProvider"
|
||||
:allow-forced-select="!isUpdateMode"
|
||||
@select="handleChangeEditorProvider"
|
||||
/>
|
||||
<VButton
|
||||
size="sm"
|
||||
type="default"
|
||||
:loading="isSaving && !isPublishing"
|
||||
@click="handleSaveClick"
|
||||
>
|
||||
<template #icon>
|
||||
<IconSave class="h-full w-full" />
|
||||
</template>
|
||||
{{ $t("core.common.buttons.save") }}
|
||||
</VButton>
|
||||
<VButton
|
||||
v-if="isUpdateMode"
|
||||
size="sm"
|
||||
type="default"
|
||||
@click="handleOpenPostSettingEditModal"
|
||||
>
|
||||
<template #icon>
|
||||
<IconSettings class="h-full w-full" />
|
||||
</template>
|
||||
{{ $t("core.common.buttons.setting") }}
|
||||
</VButton>
|
||||
<HasPermission :permissions="['uc:posts:publish']">
|
||||
<VButton
|
||||
:loading="isPublishing"
|
||||
type="secondary"
|
||||
@click="handlePublishClick"
|
||||
>
|
||||
<template #icon>
|
||||
<IconSendPlaneFill class="h-full w-full" />
|
||||
</template>
|
||||
{{ $t("core.common.buttons.publish") }}
|
||||
</VButton>
|
||||
</HasPermission>
|
||||
</VSpace>
|
||||
</template>
|
||||
</VPageHeader>
|
||||
<div class="editor border-t" style="height: calc(100vh - 3.5rem)">
|
||||
<component
|
||||
:is="currentEditorProvider.component"
|
||||
v-if="currentEditorProvider"
|
||||
v-model:raw="content.raw"
|
||||
v-model:content="content.content"
|
||||
:upload-image="handleUploadImage"
|
||||
class="h-full"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<PostCreationModal
|
||||
v-if="postCreationModal"
|
||||
title="创建文章"
|
||||
:content="content"
|
||||
@close="postCreationModal = false"
|
||||
@success="onCreatePostSuccess"
|
||||
/>
|
||||
|
||||
<PostCreationModal
|
||||
v-if="postPublishModal"
|
||||
title="发布文章"
|
||||
:content="content"
|
||||
publish
|
||||
@close="postPublishModal = false"
|
||||
@success="onPublishPostSuccess"
|
||||
/>
|
||||
|
||||
<PostSettingEditModal
|
||||
v-if="postSettingEditModal"
|
||||
:post="formState"
|
||||
@close="postSettingEditModal = false"
|
||||
@success="onUpdatePostSuccess"
|
||||
/>
|
||||
</template>
|
|
@ -0,0 +1,208 @@
|
|||
<script lang="ts" setup>
|
||||
import { apiClient } from "@/utils/api-client";
|
||||
import { useQuery } from "@tanstack/vue-query";
|
||||
import {
|
||||
IconAddCircle,
|
||||
IconBookRead,
|
||||
IconRefreshLine,
|
||||
VButton,
|
||||
VCard,
|
||||
VEmpty,
|
||||
VLoading,
|
||||
VPageHeader,
|
||||
VPagination,
|
||||
VSpace,
|
||||
} from "@halo-dev/components";
|
||||
import PostListItem from "./components/PostListItem.vue";
|
||||
import { useRouteQuery } from "@vueuse/router";
|
||||
import { computed } from "vue";
|
||||
import { watch } from "vue";
|
||||
import { postLabels } from "@/constants/labels";
|
||||
|
||||
const page = useRouteQuery<number>("page", 1, {
|
||||
transform: Number,
|
||||
});
|
||||
const size = useRouteQuery<number>("size", 20, {
|
||||
transform: Number,
|
||||
});
|
||||
const keyword = useRouteQuery<string>("keyword", "");
|
||||
|
||||
const selectedPublishPhase = useRouteQuery<
|
||||
"DRAFT" | "PENDING_APPROVAL" | "PUBLISHED" | "FAILED" | undefined
|
||||
>("status");
|
||||
|
||||
function handleClearFilters() {
|
||||
selectedPublishPhase.value = undefined;
|
||||
}
|
||||
|
||||
const hasFilters = computed(() => {
|
||||
return selectedPublishPhase.value !== undefined;
|
||||
});
|
||||
|
||||
watch(
|
||||
() => [selectedPublishPhase.value, keyword.value],
|
||||
() => {
|
||||
page.value = 1;
|
||||
}
|
||||
);
|
||||
|
||||
const {
|
||||
data: posts,
|
||||
isLoading,
|
||||
isFetching,
|
||||
refetch,
|
||||
} = useQuery({
|
||||
queryKey: ["my-posts", page, size, keyword, selectedPublishPhase],
|
||||
queryFn: async () => {
|
||||
const labelSelector: string[] = ["content.halo.run/deleted=false"];
|
||||
const { data } = await apiClient.uc.post.listMyPosts({
|
||||
labelSelector,
|
||||
page: page.value,
|
||||
size: size.value,
|
||||
keyword: keyword.value,
|
||||
publishPhase: selectedPublishPhase.value,
|
||||
});
|
||||
return data;
|
||||
},
|
||||
onSuccess(data) {
|
||||
page.value = data.page;
|
||||
size.value = data.size;
|
||||
},
|
||||
refetchInterval: (data) => {
|
||||
const abnormalPosts = data?.items.filter((post) => {
|
||||
const { spec, metadata, status } = post.post;
|
||||
return (
|
||||
spec.deleted ||
|
||||
metadata.labels?.[postLabels.PUBLISHED] !== spec.publish + "" ||
|
||||
(spec.releaseSnapshot === spec.headSnapshot && status?.inProgress)
|
||||
);
|
||||
});
|
||||
|
||||
return abnormalPosts?.length ? 1000 : false;
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VPageHeader title="我的文章">
|
||||
<template #icon>
|
||||
<IconBookRead class="mr-2 self-center" />
|
||||
</template>
|
||||
<template #actions>
|
||||
<VButton :route="{ name: 'PostEditor' }" type="secondary">
|
||||
<template #icon>
|
||||
<IconAddCircle class="h-full w-full" />
|
||||
</template>
|
||||
{{ $t("core.common.buttons.new") }}
|
||||
</VButton>
|
||||
</template>
|
||||
</VPageHeader>
|
||||
|
||||
<div class="m-0 md:m-4">
|
||||
<VCard :body-class="['!p-0']">
|
||||
<template #header>
|
||||
<div class="block w-full bg-gray-50 px-4 py-3">
|
||||
<div
|
||||
class="relative flex flex-col flex-wrap items-start justify-between gap-4 sm:flex-row sm:items-center"
|
||||
>
|
||||
<SearchInput v-model="keyword" />
|
||||
|
||||
<VSpace spacing="lg" class="flex-wrap">
|
||||
<FilterCleanButton
|
||||
v-if="hasFilters"
|
||||
@click="handleClearFilters"
|
||||
/>
|
||||
<FilterDropdown
|
||||
v-model="selectedPublishPhase"
|
||||
:label="$t('core.common.filters.labels.status')"
|
||||
:items="[
|
||||
{
|
||||
label: $t('core.common.filters.item_labels.all'),
|
||||
value: undefined,
|
||||
},
|
||||
{
|
||||
label: '已发布',
|
||||
value: 'PUBLISHED',
|
||||
},
|
||||
{
|
||||
label: '待审核',
|
||||
value: 'PENDING_APPROVAL',
|
||||
},
|
||||
{
|
||||
label: '未发布',
|
||||
value: 'DRAFT',
|
||||
},
|
||||
{
|
||||
label: '发布失败',
|
||||
value: 'FAILED',
|
||||
},
|
||||
]"
|
||||
/>
|
||||
<div
|
||||
class="group cursor-pointer rounded p-1 hover:bg-gray-200"
|
||||
@click="refetch()"
|
||||
>
|
||||
<IconRefreshLine
|
||||
v-tooltip="$t('core.common.buttons.refresh')"
|
||||
:class="{ 'animate-spin text-gray-900': isFetching }"
|
||||
class="h-4 w-4 text-gray-600 group-hover:text-gray-900"
|
||||
/>
|
||||
</div>
|
||||
</VSpace>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<VLoading v-if="isLoading" />
|
||||
<Transition v-else-if="!posts?.items.length" appear name="fade">
|
||||
<VEmpty
|
||||
:message="$t('core.post.empty.message')"
|
||||
:title="$t('core.post.empty.title')"
|
||||
>
|
||||
<template #actions>
|
||||
<VSpace>
|
||||
<VButton @click="refetch">
|
||||
{{ $t("core.common.buttons.refresh") }}
|
||||
</VButton>
|
||||
<VButton
|
||||
v-permission="['system:posts:manage']"
|
||||
:route="{ name: 'PostEditor' }"
|
||||
type="primary"
|
||||
>
|
||||
<template #icon>
|
||||
<IconAddCircle class="h-full w-full" />
|
||||
</template>
|
||||
{{ $t("core.common.buttons.new") }}
|
||||
</VButton>
|
||||
</VSpace>
|
||||
</template>
|
||||
</VEmpty>
|
||||
</Transition>
|
||||
<Transition v-else appear name="fade">
|
||||
<ul
|
||||
class="box-border h-full w-full divide-y divide-gray-100"
|
||||
role="list"
|
||||
>
|
||||
<li v-for="post in posts.items" :key="post.post.metadata.name">
|
||||
<PostListItem :post="post" />
|
||||
</li>
|
||||
</ul>
|
||||
</Transition>
|
||||
|
||||
<template #footer>
|
||||
<VPagination
|
||||
v-model:page="page"
|
||||
v-model:size="size"
|
||||
:page-label="$t('core.components.pagination.page_label')"
|
||||
:size-label="$t('core.components.pagination.size_label')"
|
||||
:total-label="
|
||||
$t('core.components.pagination.total_label', {
|
||||
total: posts?.total || 0,
|
||||
})
|
||||
"
|
||||
:total="posts?.total || 0"
|
||||
:size-options="[20, 30, 50, 100]"
|
||||
/>
|
||||
</template>
|
||||
</VCard>
|
||||
</div>
|
||||
</template>
|
|
@ -0,0 +1,149 @@
|
|||
<script lang="ts" setup>
|
||||
import { Toast, VButton, VModal, VSpace } from "@halo-dev/components";
|
||||
import { nextTick } from "vue";
|
||||
import { onMounted } from "vue";
|
||||
import { ref } from "vue";
|
||||
import PostSettingForm from "./PostSettingForm.vue";
|
||||
import type { Content, Post } from "@halo-dev/api-client";
|
||||
import type { PostFormState } from "../types";
|
||||
import { useMutation } from "@tanstack/vue-query";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { randomUUID } from "@/utils/id";
|
||||
import { contentAnnotations } from "@/constants/annotations";
|
||||
import { apiClient } from "@/utils/api-client";
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
title: string;
|
||||
content: Content;
|
||||
publish?: boolean;
|
||||
}>(),
|
||||
{
|
||||
publish: false,
|
||||
}
|
||||
);
|
||||
|
||||
const emit = defineEmits<{
|
||||
(event: "close"): void;
|
||||
(event: "success", post: Post): void;
|
||||
}>();
|
||||
|
||||
// fixme: refactor VModal component
|
||||
const shouldRender = ref(false);
|
||||
const visible = ref(false);
|
||||
onMounted(() => {
|
||||
shouldRender.value = true;
|
||||
nextTick(() => {
|
||||
visible.value = true;
|
||||
});
|
||||
});
|
||||
function onClose() {
|
||||
visible.value = false;
|
||||
setTimeout(() => {
|
||||
shouldRender.value = false;
|
||||
emit("close");
|
||||
}, 200);
|
||||
}
|
||||
|
||||
const { mutate, isLoading } = useMutation({
|
||||
mutationKey: ["create-post"],
|
||||
mutationFn: async ({ data }: { data: PostFormState }) => {
|
||||
const post: Post = {
|
||||
apiVersion: "content.halo.run/v1alpha1",
|
||||
kind: "Post",
|
||||
metadata: {
|
||||
annotations: {
|
||||
[contentAnnotations.CONTENT_JSON]: JSON.stringify(props.content),
|
||||
},
|
||||
name: randomUUID(),
|
||||
},
|
||||
spec: {
|
||||
allowComment: data.allowComment,
|
||||
categories: data.categories,
|
||||
cover: data.cover,
|
||||
deleted: false,
|
||||
excerpt: {
|
||||
autoGenerate: data.excerptAutoGenerate,
|
||||
raw: data.excerptRaw,
|
||||
},
|
||||
htmlMetas: [],
|
||||
pinned: data.pinned,
|
||||
priority: 0,
|
||||
publish: false,
|
||||
publishTime: data.publishTime,
|
||||
slug: data.slug,
|
||||
tags: data.tags,
|
||||
title: data.title,
|
||||
visible: data.visible,
|
||||
},
|
||||
};
|
||||
|
||||
const { data: createdPost } = await apiClient.uc.post.createMyPost({
|
||||
post,
|
||||
});
|
||||
|
||||
if (props.publish) {
|
||||
await apiClient.uc.post.publishMyPost({
|
||||
name: post.metadata.name,
|
||||
});
|
||||
}
|
||||
|
||||
return createdPost;
|
||||
},
|
||||
onSuccess(data) {
|
||||
if (props.publish) {
|
||||
Toast.success(t("core.common.toast.publish_success"));
|
||||
} else {
|
||||
Toast.success(t("core.common.toast.save_success"));
|
||||
}
|
||||
|
||||
emit("success", data);
|
||||
emit("close");
|
||||
},
|
||||
onError() {
|
||||
if (props.publish) {
|
||||
Toast.error(t("core.common.toast.publish_failed_and_retry"));
|
||||
} else {
|
||||
Toast.error(t("core.common.toast.save_failed_and_retry"));
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
function onSubmit(data: PostFormState) {
|
||||
mutate({ data });
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VModal
|
||||
v-if="shouldRender"
|
||||
v-model:visible="visible"
|
||||
:title="title"
|
||||
:width="700"
|
||||
centered
|
||||
@close="onClose"
|
||||
>
|
||||
<PostSettingForm @submit="onSubmit" />
|
||||
|
||||
<template #footer>
|
||||
<VSpace>
|
||||
<VButton
|
||||
:loading="isLoading"
|
||||
type="secondary"
|
||||
@click="$formkit.submit('post-setting-form')"
|
||||
>
|
||||
{{
|
||||
props.publish
|
||||
? $t("core.common.buttons.publish")
|
||||
: $t("core.common.buttons.save")
|
||||
}}
|
||||
</VButton>
|
||||
<VButton type="default" @click="onClose()">
|
||||
{{ $t("core.common.buttons.close") }}
|
||||
</VButton>
|
||||
</VSpace>
|
||||
</template>
|
||||
</VModal>
|
||||
</template>
|
|
@ -0,0 +1,234 @@
|
|||
<script lang="ts" setup>
|
||||
import {
|
||||
Dialog,
|
||||
IconExternalLinkLine,
|
||||
IconEye,
|
||||
IconEyeOff,
|
||||
Toast,
|
||||
VAvatar,
|
||||
VDropdownItem,
|
||||
VEntity,
|
||||
VEntityField,
|
||||
VSpace,
|
||||
VStatusDot,
|
||||
} from "@halo-dev/components";
|
||||
import type { ListedPost } from "@halo-dev/api-client";
|
||||
import { computed } from "vue";
|
||||
import { postLabels } from "@/constants/labels";
|
||||
import PostTag from "@console/modules/contents/posts/tags/components/PostTag.vue";
|
||||
import { formatDatetime } from "@/utils/date";
|
||||
import StatusDotField from "@/components/entity-fields/StatusDotField.vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import HasPermission from "@/components/permission/HasPermission.vue";
|
||||
import { apiClient } from "@/utils/api-client";
|
||||
import { useQueryClient } from "@tanstack/vue-query";
|
||||
|
||||
const { t } = useI18n();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
post: ListedPost;
|
||||
}>(),
|
||||
{}
|
||||
);
|
||||
|
||||
const externalUrl = computed(() => {
|
||||
const { status, metadata } = props.post.post;
|
||||
if (metadata.labels?.[postLabels.PUBLISHED] === "true") {
|
||||
return status?.permalink;
|
||||
}
|
||||
return `/preview/posts/${metadata.name}`;
|
||||
});
|
||||
|
||||
const publishStatus = computed(() => {
|
||||
const { labels } = props.post.post.metadata;
|
||||
return labels?.[postLabels.PUBLISHED] === "true"
|
||||
? t("core.post.filters.status.items.published")
|
||||
: t("core.post.filters.status.items.draft");
|
||||
});
|
||||
|
||||
const isPublishing = computed(() => {
|
||||
const { spec, status, metadata } = props.post.post;
|
||||
return (
|
||||
(spec.publish && metadata.labels?.[postLabels.PUBLISHED] !== "true") ||
|
||||
(spec.releaseSnapshot === spec.headSnapshot && status?.inProgress)
|
||||
);
|
||||
});
|
||||
|
||||
async function handlePublish() {
|
||||
await apiClient.uc.post.publishMyPost({
|
||||
name: props.post.post.metadata.name,
|
||||
});
|
||||
|
||||
Toast.success(t("core.common.toast.publish_success"));
|
||||
queryClient.invalidateQueries({ queryKey: ["my-posts"] });
|
||||
}
|
||||
|
||||
function handleUnpublish() {
|
||||
Dialog.warning({
|
||||
title: "取消发布",
|
||||
description: "确定要取消发布吗?",
|
||||
async onConfirm() {
|
||||
await apiClient.uc.post.unpublishMyPost({
|
||||
name: props.post.post.metadata.name,
|
||||
});
|
||||
|
||||
Toast.success(t("core.common.toast.cancel_publish_success"));
|
||||
queryClient.invalidateQueries({ queryKey: ["my-posts"] });
|
||||
},
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VEntity>
|
||||
<template #start>
|
||||
<VEntityField
|
||||
:title="post.post.spec.title"
|
||||
:route="{
|
||||
name: 'PostEditor',
|
||||
query: { name: post.post.metadata.name },
|
||||
}"
|
||||
width="27rem"
|
||||
>
|
||||
<template #extra>
|
||||
<VSpace class="mt-1 sm:mt-0">
|
||||
<RouterLink
|
||||
v-if="post.post.status?.inProgress"
|
||||
v-tooltip="$t('core.common.tooltips.unpublished_content_tip')"
|
||||
class="flex items-center"
|
||||
:to="{
|
||||
name: 'PostEditor',
|
||||
query: { name: post.post.metadata.name },
|
||||
}"
|
||||
>
|
||||
<VStatusDot state="success" animate />
|
||||
</RouterLink>
|
||||
<a
|
||||
target="_blank"
|
||||
:href="externalUrl"
|
||||
class="hidden text-gray-600 transition-all hover:text-gray-900 group-hover:inline-block"
|
||||
>
|
||||
<IconExternalLinkLine class="h-3.5 w-3.5" />
|
||||
</a>
|
||||
</VSpace>
|
||||
</template>
|
||||
<template #description>
|
||||
<div class="flex flex-col gap-1.5">
|
||||
<VSpace class="flex-wrap !gap-y-1">
|
||||
<p
|
||||
v-if="post.categories.length"
|
||||
class="inline-flex flex-wrap gap-1 text-xs text-gray-500"
|
||||
>
|
||||
{{ $t("core.post.list.fields.categories") }}
|
||||
<a
|
||||
v-for="(category, categoryIndex) in post.categories"
|
||||
:key="categoryIndex"
|
||||
:href="category.status?.permalink"
|
||||
:title="category.status?.permalink"
|
||||
target="_blank"
|
||||
class="cursor-pointer hover:text-gray-900"
|
||||
>
|
||||
{{ category.spec.displayName }}
|
||||
</a>
|
||||
</p>
|
||||
<span class="text-xs text-gray-500">
|
||||
{{
|
||||
$t("core.post.list.fields.visits", {
|
||||
visits: post.stats.visit,
|
||||
})
|
||||
}}
|
||||
</span>
|
||||
<span class="text-xs text-gray-500">
|
||||
{{
|
||||
$t("core.post.list.fields.comments", {
|
||||
comments: post.stats.totalComment || 0,
|
||||
})
|
||||
}}
|
||||
</span>
|
||||
<span v-if="post.post.spec.pinned" class="text-xs text-gray-500">
|
||||
{{ $t("core.post.list.fields.pinned") }}
|
||||
</span>
|
||||
</VSpace>
|
||||
<VSpace v-if="post.tags.length" class="flex-wrap">
|
||||
<PostTag
|
||||
v-for="(tag, tagIndex) in post.tags"
|
||||
:key="tagIndex"
|
||||
:tag="tag"
|
||||
route
|
||||
></PostTag>
|
||||
</VSpace>
|
||||
</div>
|
||||
</template>
|
||||
</VEntityField>
|
||||
</template>
|
||||
<template #end>
|
||||
<VEntityField>
|
||||
<template #description>
|
||||
<VAvatar
|
||||
v-for="{ name, avatar, displayName } in post.contributors"
|
||||
:key="name"
|
||||
v-tooltip="displayName"
|
||||
size="xs"
|
||||
:src="avatar"
|
||||
:alt="displayName"
|
||||
circle
|
||||
></VAvatar>
|
||||
</template>
|
||||
</VEntityField>
|
||||
<VEntityField :description="publishStatus">
|
||||
<template v-if="isPublishing" #description>
|
||||
<VStatusDot :text="$t('core.common.tooltips.publishing')" animate />
|
||||
</template>
|
||||
</VEntityField>
|
||||
<VEntityField>
|
||||
<template #description>
|
||||
<IconEye
|
||||
v-if="post.post.spec.visible === 'PUBLIC'"
|
||||
v-tooltip="$t('core.post.filters.visible.items.public')"
|
||||
class="cursor-pointer text-sm transition-all hover:text-blue-600"
|
||||
/>
|
||||
<IconEyeOff
|
||||
v-if="post.post.spec.visible === 'PRIVATE'"
|
||||
v-tooltip="$t('core.post.filters.visible.items.private')"
|
||||
class="cursor-pointer text-sm transition-all hover:text-blue-600"
|
||||
/>
|
||||
</template>
|
||||
</VEntityField>
|
||||
<StatusDotField
|
||||
v-if="props.post.post.spec.deleted"
|
||||
:tooltip="$t('core.common.status.deleting')"
|
||||
state="warning"
|
||||
animate
|
||||
/>
|
||||
<VEntityField
|
||||
v-if="post.post.spec.publishTime"
|
||||
:description="formatDatetime(post.post.spec.publishTime)"
|
||||
></VEntityField>
|
||||
</template>
|
||||
<template #dropdownItems>
|
||||
<VDropdownItem
|
||||
@click="
|
||||
$router.push({
|
||||
name: 'PostEditor',
|
||||
query: { name: post.post.metadata.name },
|
||||
})
|
||||
"
|
||||
>
|
||||
编辑
|
||||
</VDropdownItem>
|
||||
<HasPermission :permissions="['uc:posts:publish']">
|
||||
<VDropdownItem
|
||||
v-if="post.post.metadata.labels?.[postLabels.PUBLISHED] === 'false'"
|
||||
@click="handlePublish"
|
||||
>
|
||||
发布
|
||||
</VDropdownItem>
|
||||
<VDropdownItem v-else type="danger" @click="handleUnpublish">
|
||||
取消发布
|
||||
</VDropdownItem>
|
||||
</HasPermission>
|
||||
</template>
|
||||
</VEntity>
|
||||
</template>
|
|
@ -0,0 +1,131 @@
|
|||
<script lang="ts" setup>
|
||||
import { Toast, VButton, VModal, VSpace } from "@halo-dev/components";
|
||||
import PostSettingForm from "./PostSettingForm.vue";
|
||||
import type { PostFormState } from "../types";
|
||||
import type { Post } from "@halo-dev/api-client";
|
||||
import { onMounted } from "vue";
|
||||
import { nextTick, ref } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { useMutation } from "@tanstack/vue-query";
|
||||
import { apiClient } from "@/utils/api-client";
|
||||
import { toDatetimeLocal } from "@/utils/date";
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
post: Post;
|
||||
}>(),
|
||||
{}
|
||||
);
|
||||
|
||||
const emit = defineEmits<{
|
||||
(event: "close"): void;
|
||||
(event: "success", post: Post): void;
|
||||
}>();
|
||||
|
||||
// fixme: refactor VModal component
|
||||
const shouldRender = ref(false);
|
||||
const visible = ref(false);
|
||||
onMounted(() => {
|
||||
shouldRender.value = true;
|
||||
nextTick(() => {
|
||||
visible.value = true;
|
||||
});
|
||||
});
|
||||
function onClose() {
|
||||
visible.value = false;
|
||||
setTimeout(() => {
|
||||
shouldRender.value = false;
|
||||
emit("close");
|
||||
}, 200);
|
||||
}
|
||||
|
||||
const { mutate, isLoading } = useMutation({
|
||||
mutationKey: ["edit-post"],
|
||||
mutationFn: async ({ data }: { data: PostFormState }) => {
|
||||
const postToUpdate: Post = {
|
||||
...props.post,
|
||||
spec: {
|
||||
...props.post.spec,
|
||||
allowComment: data.allowComment,
|
||||
categories: data.categories,
|
||||
cover: data.cover,
|
||||
excerpt: {
|
||||
autoGenerate: data.excerptAutoGenerate,
|
||||
raw: data.excerptRaw,
|
||||
},
|
||||
pinned: data.pinned,
|
||||
publishTime: data.publishTime,
|
||||
slug: data.slug,
|
||||
tags: data.tags,
|
||||
title: data.title,
|
||||
visible: data.visible,
|
||||
},
|
||||
};
|
||||
const { data: updatedPost } = await apiClient.uc.post.updateMyPost({
|
||||
name: props.post.metadata.name,
|
||||
post: postToUpdate,
|
||||
});
|
||||
return updatedPost;
|
||||
},
|
||||
onSuccess(data) {
|
||||
Toast.success(t("core.common.toast.save_success"));
|
||||
emit("success", data);
|
||||
emit("close");
|
||||
},
|
||||
onError() {
|
||||
Toast.error(t("core.common.toast.save_failed_and_retry"));
|
||||
},
|
||||
});
|
||||
|
||||
function onSubmit(data: PostFormState) {
|
||||
mutate({ data });
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VModal
|
||||
v-if="shouldRender"
|
||||
v-model:visible="visible"
|
||||
title="文章设置"
|
||||
:width="700"
|
||||
centered
|
||||
@close="onClose"
|
||||
>
|
||||
<PostSettingForm
|
||||
:form-state="{
|
||||
title: props.post.spec.title,
|
||||
slug: props.post.spec.slug,
|
||||
cover: props.post.spec.cover,
|
||||
categories: props.post.spec.categories,
|
||||
tags: props.post.spec.tags,
|
||||
allowComment: props.post.spec.allowComment,
|
||||
visible: props.post.spec.visible,
|
||||
pinned: props.post.spec.pinned,
|
||||
publishTime: props.post.spec.publishTime
|
||||
? toDatetimeLocal(props.post.spec.publishTime)
|
||||
: undefined,
|
||||
excerptAutoGenerate: props.post.spec.excerpt.autoGenerate,
|
||||
excerptRaw: props.post.spec.excerpt.raw,
|
||||
}"
|
||||
update-mode
|
||||
@submit="onSubmit"
|
||||
/>
|
||||
|
||||
<template #footer>
|
||||
<VSpace>
|
||||
<VButton
|
||||
:loading="isLoading"
|
||||
type="secondary"
|
||||
@click="$formkit.submit('post-setting-form')"
|
||||
>
|
||||
{{ $t("core.common.buttons.save") }}
|
||||
</VButton>
|
||||
<VButton type="default" @click="onClose()">
|
||||
{{ $t("core.common.buttons.close") }}
|
||||
</VButton>
|
||||
</VSpace>
|
||||
</template>
|
||||
</VModal>
|
||||
</template>
|
|
@ -0,0 +1,204 @@
|
|||
<script lang="ts" setup>
|
||||
import { IconRefreshLine } from "@halo-dev/components";
|
||||
import type { PostFormState } from "../types";
|
||||
import { 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";
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
formState?: PostFormState;
|
||||
updateMode?: boolean;
|
||||
}>(),
|
||||
{
|
||||
formState: undefined,
|
||||
updateMode: false,
|
||||
}
|
||||
);
|
||||
|
||||
const internalFormState = ref<PostFormState>(
|
||||
props.formState || {
|
||||
title: "",
|
||||
slug: "",
|
||||
categories: [],
|
||||
tags: [],
|
||||
excerptAutoGenerate: true,
|
||||
excerptRaw: "",
|
||||
allowComment: true,
|
||||
pinned: false,
|
||||
visible: "PUBLIC",
|
||||
publishTime: undefined,
|
||||
cover: undefined,
|
||||
}
|
||||
);
|
||||
|
||||
const emit = defineEmits<{
|
||||
(event: "submit", data: PostFormState): void;
|
||||
}>();
|
||||
|
||||
function onSubmit(data: PostFormState) {
|
||||
emit("submit", {
|
||||
...data,
|
||||
publishTime: data.publishTime ? toISOString(data.publishTime) : undefined,
|
||||
});
|
||||
}
|
||||
|
||||
// slug
|
||||
const { handleGenerateSlug } = useSlugify(
|
||||
computed(() => internalFormState.value?.title || ""),
|
||||
computed({
|
||||
get() {
|
||||
return internalFormState.value?.slug || "";
|
||||
},
|
||||
set(value) {
|
||||
internalFormState.value.slug = value;
|
||||
},
|
||||
}),
|
||||
computed(() => !props.updateMode),
|
||||
FormType.POST
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<FormKit
|
||||
id="post-setting-form"
|
||||
v-model="internalFormState"
|
||||
type="form"
|
||||
name="post-setting-form"
|
||||
:config="{ validationVisibility: 'submit' }"
|
||||
@submit="onSubmit"
|
||||
>
|
||||
<div>
|
||||
<div class="md:grid md:grid-cols-4 md:gap-6">
|
||||
<div class="md:col-span-1">
|
||||
<div class="sticky top-0">
|
||||
<span class="text-base font-medium text-gray-900">
|
||||
{{ $t("core.post.settings.groups.general") }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-5 divide-y divide-gray-100 md:col-span-3 md:mt-0">
|
||||
<FormKit
|
||||
:label="$t('core.post.settings.fields.title.label')"
|
||||
type="text"
|
||||
name="title"
|
||||
validation="required|length:0,100"
|
||||
></FormKit>
|
||||
<FormKit
|
||||
:label="$t('core.post.settings.fields.slug.label')"
|
||||
name="slug"
|
||||
type="text"
|
||||
validation="required|length:0,100"
|
||||
:help="$t('core.post.settings.fields.slug.help')"
|
||||
>
|
||||
<template #suffix>
|
||||
<div
|
||||
v-tooltip="$t('core.post.settings.fields.slug.refresh_message')"
|
||||
class="group flex h-full cursor-pointer items-center border-l px-3 transition-all hover:bg-gray-100"
|
||||
@click="handleGenerateSlug(true, FormType.POST)"
|
||||
>
|
||||
<IconRefreshLine
|
||||
class="h-4 w-4 text-gray-500 group-hover:text-gray-700"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</FormKit>
|
||||
<FormKit
|
||||
:label="$t('core.post.settings.fields.categories.label')"
|
||||
name="categories"
|
||||
type="categorySelect"
|
||||
:multiple="true"
|
||||
/>
|
||||
<FormKit
|
||||
:label="$t('core.post.settings.fields.tags.label')"
|
||||
name="tags"
|
||||
type="tagSelect"
|
||||
:multiple="true"
|
||||
/>
|
||||
<FormKit
|
||||
:value="true"
|
||||
:options="[
|
||||
{ label: $t('core.common.radio.yes'), value: true },
|
||||
{ label: $t('core.common.radio.no'), value: false },
|
||||
]"
|
||||
name="excerptAutoGenerate"
|
||||
:label="$t('core.post.settings.fields.auto_generate_excerpt.label')"
|
||||
type="radio"
|
||||
>
|
||||
</FormKit>
|
||||
<FormKit
|
||||
v-if="!internalFormState.excerptAutoGenerate"
|
||||
:label="$t('core.post.settings.fields.raw_excerpt.label')"
|
||||
name="excerptRaw"
|
||||
type="textarea"
|
||||
:rows="5"
|
||||
validation="length:0,1024"
|
||||
></FormKit>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="py-5">
|
||||
<div class="border-t border-gray-200"></div>
|
||||
</div>
|
||||
|
||||
<div class="md:grid md:grid-cols-4 md:gap-6">
|
||||
<div class="md:col-span-1">
|
||||
<div class="sticky top-0">
|
||||
<span class="text-base font-medium text-gray-900">
|
||||
{{ $t("core.post.settings.groups.advanced") }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-5 divide-y divide-gray-100 md:col-span-3 md:mt-0">
|
||||
<FormKit
|
||||
name="allowComment"
|
||||
:options="[
|
||||
{ label: $t('core.common.radio.yes'), value: true },
|
||||
{ label: $t('core.common.radio.no'), value: false },
|
||||
]"
|
||||
:label="$t('core.post.settings.fields.allow_comment.label')"
|
||||
type="radio"
|
||||
></FormKit>
|
||||
<FormKit
|
||||
:options="[
|
||||
{ label: $t('core.common.radio.yes'), value: true },
|
||||
{ label: $t('core.common.radio.no'), value: false },
|
||||
]"
|
||||
:label="$t('core.post.settings.fields.pinned.label')"
|
||||
name="pinned"
|
||||
type="radio"
|
||||
></FormKit>
|
||||
<FormKit
|
||||
:options="[
|
||||
{ label: $t('core.common.select.public'), value: 'PUBLIC' },
|
||||
{
|
||||
label: $t('core.common.select.private'),
|
||||
value: 'PRIVATE',
|
||||
},
|
||||
]"
|
||||
:label="$t('core.post.settings.fields.visible.label')"
|
||||
name="visible"
|
||||
type="select"
|
||||
></FormKit>
|
||||
<FormKit
|
||||
name="publishTime"
|
||||
:label="$t('core.post.settings.fields.publish_time.label')"
|
||||
type="datetime-local"
|
||||
></FormKit>
|
||||
<HasPermission :permissions="['system:attachments:view']">
|
||||
<FormKit
|
||||
name="cover"
|
||||
:label="$t('core.post.settings.fields.cover.label')"
|
||||
type="attachment"
|
||||
:accepts="['image/*']"
|
||||
validation="length:0,1024"
|
||||
></FormKit>
|
||||
</HasPermission>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</FormKit>
|
||||
</template>
|
|
@ -0,0 +1,44 @@
|
|||
import { definePlugin } from "@halo-dev/console-shared";
|
||||
import BasicLayout from "@uc/layouts/BasicLayout.vue";
|
||||
import { IconBookRead } from "@halo-dev/components";
|
||||
import PostList from "./PostList.vue";
|
||||
import { markRaw } from "vue";
|
||||
import PostEditor from "./PostEditor.vue";
|
||||
|
||||
export default definePlugin({
|
||||
ucRoutes: [
|
||||
{
|
||||
path: "/posts",
|
||||
component: BasicLayout,
|
||||
children: [
|
||||
{
|
||||
path: "",
|
||||
name: "Posts",
|
||||
component: PostList,
|
||||
meta: {
|
||||
title: "core.post.title",
|
||||
searchable: true,
|
||||
permissions: ["uc:posts:manage"],
|
||||
menu: {
|
||||
name: "core.sidebar.menu.items.posts",
|
||||
group: "content",
|
||||
icon: markRaw(IconBookRead),
|
||||
priority: 0,
|
||||
mobile: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
path: "editor",
|
||||
name: "PostEditor",
|
||||
component: PostEditor,
|
||||
meta: {
|
||||
title: "文章编辑",
|
||||
searchable: true,
|
||||
permissions: ["uc:posts:manage"],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
|
@ -0,0 +1,15 @@
|
|||
import type { PostSpecVisibleEnum } from "packages/api-client/dist";
|
||||
|
||||
export interface PostFormState {
|
||||
title: string;
|
||||
slug: string;
|
||||
categories?: string[];
|
||||
tags?: string[];
|
||||
excerptAutoGenerate: boolean;
|
||||
excerptRaw?: string;
|
||||
allowComment: boolean;
|
||||
pinned: boolean;
|
||||
visible: PostSpecVisibleEnum;
|
||||
publishTime?: string;
|
||||
cover?: string;
|
||||
}
|
|
@ -19,6 +19,7 @@ export default definePlugin({
|
|||
searchable: true,
|
||||
menu: {
|
||||
name: "消息",
|
||||
group: "dashboard",
|
||||
icon: markRaw(IconNotificationBadgeLine),
|
||||
priority: 1,
|
||||
mobile: true,
|
||||
|
|
|
@ -21,6 +21,7 @@ export default definePlugin({
|
|||
searchable: true,
|
||||
menu: {
|
||||
name: "我的",
|
||||
group: "dashboard",
|
||||
icon: markRaw(IconAccountCircleLine),
|
||||
priority: 0,
|
||||
mobile: true,
|
||||
|
|
Loading…
Reference in New Issue