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 content is binary data of the attachment file.
|
||||||
* @param mediaType is media type of the attachment file.
|
* @param mediaType is media type of the attachment file.
|
||||||
*/
|
*/
|
||||||
record SimpleFilePart(
|
public record SimpleFilePart(
|
||||||
String filename,
|
String filename,
|
||||||
Flux<DataBuffer> content,
|
Flux<DataBuffer> content,
|
||||||
MediaType mediaType
|
MediaType mediaType
|
||||||
|
|
|
@ -9,6 +9,7 @@ import java.util.Set;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import lombok.EqualsAndHashCode;
|
import lombok.EqualsAndHashCode;
|
||||||
import lombok.ToString;
|
import lombok.ToString;
|
||||||
|
import org.springframework.lang.NonNull;
|
||||||
import org.springframework.util.Assert;
|
import org.springframework.util.Assert;
|
||||||
import run.halo.app.extension.AbstractExtension;
|
import run.halo.app.extension.AbstractExtension;
|
||||||
import run.halo.app.extension.GVK;
|
import run.halo.app.extension.GVK;
|
||||||
|
@ -27,6 +28,8 @@ import run.halo.app.extension.Ref;
|
||||||
public class Snapshot extends AbstractExtension {
|
public class Snapshot extends AbstractExtension {
|
||||||
public static final String KIND = "Snapshot";
|
public static final String KIND = "Snapshot";
|
||||||
public static final String KEEP_RAW_ANNO = "content.halo.run/keep-raw";
|
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)
|
@Schema(requiredMode = REQUIRED)
|
||||||
private SnapShotSpec spec;
|
private SnapShotSpec spec;
|
||||||
|
@ -67,4 +70,18 @@ public class Snapshot extends AbstractExtension {
|
||||||
contributors.add(name);
|
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.net.URI;
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
|
import java.util.function.Consumer;
|
||||||
import org.springframework.core.io.buffer.DataBuffer;
|
import org.springframework.core.io.buffer.DataBuffer;
|
||||||
import org.springframework.http.MediaType;
|
import org.springframework.http.MediaType;
|
||||||
|
import org.springframework.http.codec.multipart.FilePart;
|
||||||
import org.springframework.lang.NonNull;
|
import org.springframework.lang.NonNull;
|
||||||
import org.springframework.lang.Nullable;
|
import org.springframework.lang.Nullable;
|
||||||
import reactor.core.publisher.Flux;
|
import reactor.core.publisher.Flux;
|
||||||
|
@ -18,6 +20,25 @@ import run.halo.app.core.extension.attachment.Attachment;
|
||||||
*/
|
*/
|
||||||
public interface AttachmentService {
|
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
|
* 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.
|
* 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 io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
import org.springframework.lang.NonNull;
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
@Schema(description = "Extension reference object. The name is mandatory")
|
@Schema(description = "Extension reference object. The name is mandatory")
|
||||||
|
@ -59,4 +60,18 @@ public class Ref {
|
||||||
return Objects.equals(ref.getGroup(), gvk.group())
|
return Objects.equals(ref.getGroup(), gvk.group())
|
||||||
&& Objects.equals(ref.getKind(), gvk.kind());
|
&& 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;
|
Integer tagPageSize;
|
||||||
Boolean review;
|
Boolean review;
|
||||||
String slugGenerationStrategy;
|
String slugGenerationStrategy;
|
||||||
|
|
||||||
|
String attachmentPolicyName;
|
||||||
|
String attachmentGroupName;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
|
|
|
@ -75,6 +75,15 @@ public class SwaggerConfig {
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
GroupedOpenApi userCenterApi() {
|
||||||
|
return GroupedOpenApi.builder()
|
||||||
|
.group("uc.api")
|
||||||
|
.displayName("User center APIs.")
|
||||||
|
.pathsToMatch("/apis/uc.api.*/**")
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
GroupedOpenApi allApi() {
|
GroupedOpenApi allApi() {
|
||||||
return GroupedOpenApi.builder()
|
return GroupedOpenApi.builder()
|
||||||
|
|
|
@ -51,9 +51,7 @@ public abstract class AbstractContentService {
|
||||||
|
|
||||||
protected void checkBaseSnapshot(Snapshot snapshot) {
|
protected void checkBaseSnapshot(Snapshot snapshot) {
|
||||||
Assert.notNull(snapshot, "The snapshot must not be null.");
|
Assert.notNull(snapshot, "The snapshot must not be null.");
|
||||||
String keepRawAnno =
|
if (!Snapshot.isBaseSnapshot(snapshot)) {
|
||||||
MetadataUtil.nullSafeAnnotations(snapshot).get(Snapshot.KEEP_RAW_ANNO);
|
|
||||||
if (!StringUtils.equals(Boolean.TRUE.toString(), keepRawAnno)) {
|
|
||||||
throw new IllegalArgumentException(
|
throw new IllegalArgumentException(
|
||||||
String.format("The snapshot [%s] is not a base snapshot.",
|
String.format("The snapshot [%s] is not a base snapshot.",
|
||||||
snapshot.getMetadata().getName()));
|
snapshot.getMetadata().getName()));
|
||||||
|
@ -68,7 +66,7 @@ public abstract class AbstractContentService {
|
||||||
snapshot.getSpec().setParentSnapshotName(parentSnapshotName);
|
snapshot.getSpec().setParentSnapshotName(parentSnapshotName);
|
||||||
|
|
||||||
final String baseSnapshotNameToUse =
|
final String baseSnapshotNameToUse =
|
||||||
StringUtils.defaultString(baseSnapshotName, snapshot.getMetadata().getName());
|
StringUtils.defaultIfBlank(baseSnapshotName, snapshot.getMetadata().getName());
|
||||||
return client.fetch(Snapshot.class, baseSnapshotName)
|
return client.fetch(Snapshot.class, baseSnapshotName)
|
||||||
.doOnNext(this::checkBaseSnapshot)
|
.doOnNext(this::checkBaseSnapshot)
|
||||||
.defaultIfEmpty(snapshot)
|
.defaultIfEmpty(snapshot)
|
||||||
|
@ -119,7 +117,8 @@ public abstract class AbstractContentService {
|
||||||
.map(baseSnapshot -> ContentWrapper.patchSnapshot(headSnapshot, baseSnapshot));
|
.map(baseSnapshot -> ContentWrapper.patchSnapshot(headSnapshot, baseSnapshot));
|
||||||
}
|
}
|
||||||
|
|
||||||
protected Snapshot determineRawAndContentPatch(Snapshot snapshotToUse, Snapshot baseSnapshot,
|
protected Snapshot determineRawAndContentPatch(Snapshot snapshotToUse,
|
||||||
|
Snapshot baseSnapshot,
|
||||||
ContentRequest contentRequest) {
|
ContentRequest contentRequest) {
|
||||||
Assert.notNull(baseSnapshot, "The baseSnapshot must not be null.");
|
Assert.notNull(baseSnapshot, "The baseSnapshot must not be null.");
|
||||||
Assert.notNull(contentRequest, "The contentRequest 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());
|
snapshotToUse.getSpec().setLastModifyTime(Instant.now());
|
||||||
// it is the v1 snapshot, set the content directly
|
// it is the v1 snapshot, set the content directly
|
||||||
if (org.thymeleaf.util.StringUtils.equals(baseSnapshotName,
|
if (StringUtils.equals(baseSnapshotName,
|
||||||
snapshotToUse.getMetadata().getName())) {
|
snapshotToUse.getMetadata().getName())) {
|
||||||
snapshotToUse.getSpec().setRawPatch(contentRequest.raw());
|
snapshotToUse.getSpec().setRawPatch(contentRequest.raw());
|
||||||
snapshotToUse.getSpec().setContentPatch(contentRequest.content());
|
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 java.util.Comparator.comparing;
|
||||||
import static run.halo.app.extension.router.selector.SelectorUtil.labelAndFieldSelectorToPredicate;
|
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.ArraySchema;
|
||||||
import io.swagger.v3.oas.annotations.media.Schema;
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.Comparator;
|
import java.util.Comparator;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.function.Predicate;
|
import java.util.function.Predicate;
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.springframework.data.domain.Sort;
|
import org.springframework.data.domain.Sort;
|
||||||
import org.springframework.lang.Nullable;
|
import org.springframework.lang.Nullable;
|
||||||
|
import org.springframework.util.CollectionUtils;
|
||||||
import org.springframework.web.reactive.function.server.ServerRequest;
|
import org.springframework.web.reactive.function.server.ServerRequest;
|
||||||
import org.springframework.web.server.ServerWebExchange;
|
import org.springframework.web.server.ServerWebExchange;
|
||||||
import run.halo.app.core.extension.content.Post;
|
import run.halo.app.core.extension.content.Post;
|
||||||
|
@ -31,9 +34,22 @@ public class PostQuery extends IListRequest.QueryListRequest {
|
||||||
|
|
||||||
private final ServerWebExchange exchange;
|
private final ServerWebExchange exchange;
|
||||||
|
|
||||||
|
private final String username;
|
||||||
|
|
||||||
public PostQuery(ServerRequest request) {
|
public PostQuery(ServerRequest request) {
|
||||||
|
this(request, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public PostQuery(ServerRequest request, @Nullable String username) {
|
||||||
super(request.queryParams());
|
super(request.queryParams());
|
||||||
this.exchange = request.exchange();
|
this.exchange = request.exchange();
|
||||||
|
this.username = username;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Schema(hidden = true)
|
||||||
|
@JsonIgnore
|
||||||
|
public String getUsername() {
|
||||||
|
return username;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
|
@ -131,14 +147,27 @@ public class PostQuery extends IListRequest.QueryListRequest {
|
||||||
* @return a predicate
|
* @return a predicate
|
||||||
*/
|
*/
|
||||||
public Predicate<Post> toPredicate() {
|
public Predicate<Post> toPredicate() {
|
||||||
Predicate<Post> paramPredicate = post ->
|
Predicate<Post> predicate = labelAndFieldSelectorToPredicate(getLabelSelector(),
|
||||||
contains(getCategories(), post.getSpec().getCategories())
|
getFieldSelector());
|
||||||
&& contains(getTags(), post.getSpec().getTags())
|
|
||||||
&& contains(getContributors(), post.getStatusOrDefault().getContributors());
|
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();
|
String keyword = getKeyword();
|
||||||
if (keyword != null) {
|
if (keyword != null) {
|
||||||
paramPredicate = paramPredicate.and(post -> {
|
predicate = predicate.and(post -> {
|
||||||
String excerpt = post.getStatusOrDefault().getExcerpt();
|
String excerpt = post.getStatusOrDefault().getExcerpt();
|
||||||
return StringUtils.containsIgnoreCase(excerpt, keyword)
|
return StringUtils.containsIgnoreCase(excerpt, keyword)
|
||||||
|| StringUtils.containsIgnoreCase(post.getSpec().getSlug(), keyword)
|
|| StringUtils.containsIgnoreCase(post.getSpec().getSlug(), keyword)
|
||||||
|
@ -148,7 +177,7 @@ public class PostQuery extends IListRequest.QueryListRequest {
|
||||||
|
|
||||||
Post.PostPhase publishPhase = getPublishPhase();
|
Post.PostPhase publishPhase = getPublishPhase();
|
||||||
if (publishPhase != null) {
|
if (publishPhase != null) {
|
||||||
paramPredicate = paramPredicate.and(post -> {
|
predicate = predicate.and(post -> {
|
||||||
if (Post.PostPhase.PENDING_APPROVAL.equals(publishPhase)) {
|
if (Post.PostPhase.PENDING_APPROVAL.equals(publishPhase)) {
|
||||||
return !post.isPublished()
|
return !post.isPublished()
|
||||||
&& Post.PostPhase.PENDING_APPROVAL.name()
|
&& Post.PostPhase.PENDING_APPROVAL.name()
|
||||||
|
@ -165,13 +194,15 @@ public class PostQuery extends IListRequest.QueryListRequest {
|
||||||
|
|
||||||
Post.VisibleEnum visible = getVisible();
|
Post.VisibleEnum visible = getVisible();
|
||||||
if (visible != null) {
|
if (visible != null) {
|
||||||
paramPredicate =
|
predicate =
|
||||||
paramPredicate.and(post -> visible.equals(post.getSpec().getVisible()));
|
predicate.and(post -> visible.equals(post.getSpec().getVisible()));
|
||||||
}
|
}
|
||||||
|
|
||||||
Predicate<Post> predicate = labelAndFieldSelectorToPredicate(getLabelSelector(),
|
if (StringUtils.isNotBlank(username)) {
|
||||||
getFieldSelector());
|
Predicate<Post> isOwner = post -> Objects.equals(username, post.getSpec().getOwner());
|
||||||
return predicate.and(paramPredicate);
|
predicate = predicate.and(isOwner);
|
||||||
|
}
|
||||||
|
return predicate;
|
||||||
}
|
}
|
||||||
|
|
||||||
boolean contains(Collection<String> left, List<String> right) {
|
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 static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;
|
||||||
|
|
||||||
import io.swagger.v3.oas.annotations.media.Schema;
|
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.core.extension.content.Post;
|
||||||
import run.halo.app.extension.Ref;
|
import run.halo.app.extension.Ref;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* Post and content data for creating and updating post.
|
||||||
|
*
|
||||||
* @author guqing
|
* @author guqing
|
||||||
* @since 2.0.0
|
* @since 2.0.0
|
||||||
*/
|
*/
|
||||||
public record PostRequest(@Schema(requiredMode = REQUIRED) Post post,
|
public record PostRequest(@Schema(requiredMode = REQUIRED) @NonNull Post post,
|
||||||
@Schema(requiredMode = REQUIRED) Content content) {
|
Content content) {
|
||||||
|
|
||||||
public ContentRequest contentRequest() {
|
public ContentRequest contentRequest() {
|
||||||
Ref subjectRef = Ref.of(post);
|
Ref subjectRef = Ref.of(post);
|
||||||
return new ContentRequest(subjectRef, post.getSpec().getHeadSnapshot(), content.raw,
|
return new ContentRequest(subjectRef, post.getSpec().getHeadSnapshot(), content.raw(),
|
||||||
content.content, content.rawType);
|
content.content(), content.rawType());
|
||||||
}
|
}
|
||||||
|
|
||||||
public record Content(String raw, String content, String rawType) {
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
package run.halo.app.content;
|
package run.halo.app.content;
|
||||||
|
|
||||||
|
import org.springframework.lang.NonNull;
|
||||||
import reactor.core.publisher.Mono;
|
import reactor.core.publisher.Mono;
|
||||||
import run.halo.app.core.extension.content.Post;
|
import run.halo.app.core.extension.content.Post;
|
||||||
import run.halo.app.extension.ListResult;
|
import run.halo.app.extension.ListResult;
|
||||||
|
@ -18,9 +19,28 @@ public interface PostService {
|
||||||
|
|
||||||
Mono<Post> updatePost(PostRequest postRequest);
|
Mono<Post> updatePost(PostRequest postRequest);
|
||||||
|
|
||||||
|
Mono<Post> updateBy(@NonNull Post post);
|
||||||
|
|
||||||
Mono<ContentWrapper> getHeadContent(String postName);
|
Mono<ContentWrapper> getHeadContent(String postName);
|
||||||
|
|
||||||
|
Mono<ContentWrapper> getHeadContent(Post post);
|
||||||
|
|
||||||
Mono<ContentWrapper> getReleaseContent(String postName);
|
Mono<ContentWrapper> getReleaseContent(String postName);
|
||||||
|
|
||||||
|
Mono<ContentWrapper> getReleaseContent(Post post);
|
||||||
|
|
||||||
Mono<ContentWrapper> getContent(String snapshotName, String baseSnapshotName);
|
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() {
|
public ContentRequest contentRequest() {
|
||||||
Ref subjectRef = Ref.of(page);
|
Ref subjectRef = Ref.of(page);
|
||||||
return new ContentRequest(subjectRef, page.getSpec().getHeadSnapshot(), content.raw,
|
return new ContentRequest(subjectRef, page.getSpec().getHeadSnapshot(), content.raw(),
|
||||||
content.content, content.rawType);
|
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 lombok.extern.slf4j.Slf4j;
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.springframework.dao.OptimisticLockingFailureException;
|
import org.springframework.dao.OptimisticLockingFailureException;
|
||||||
|
import org.springframework.lang.NonNull;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
import org.springframework.util.Assert;
|
import org.springframework.util.Assert;
|
||||||
import reactor.core.publisher.Flux;
|
import reactor.core.publisher.Flux;
|
||||||
|
@ -185,6 +186,9 @@ public class PostServiceImpl extends AbstractContentService implements PostServi
|
||||||
)
|
)
|
||||||
.flatMap(client::create)
|
.flatMap(client::create)
|
||||||
.flatMap(post -> {
|
.flatMap(post -> {
|
||||||
|
if (postRequest.content() == null) {
|
||||||
|
return Mono.just(post);
|
||||||
|
}
|
||||||
var contentRequest =
|
var contentRequest =
|
||||||
new ContentRequest(Ref.of(post), post.getSpec().getHeadSnapshot(),
|
new ContentRequest(Ref.of(post), post.getSpec().getHeadSnapshot(),
|
||||||
postRequest.content().raw(), postRequest.content().content(),
|
postRequest.content().raw(), postRequest.content().content(),
|
||||||
|
@ -248,21 +252,59 @@ public class PostServiceImpl extends AbstractContentService implements PostServi
|
||||||
.filter(throwable -> throwable instanceof OptimisticLockingFailureException));
|
.filter(throwable -> throwable instanceof OptimisticLockingFailureException));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Mono<Post> updateBy(@NonNull Post post) {
|
||||||
|
return client.update(post);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Mono<ContentWrapper> getHeadContent(String postName) {
|
public Mono<ContentWrapper> getHeadContent(String postName) {
|
||||||
return client.get(Post.class, postName)
|
return client.get(Post.class, postName)
|
||||||
.flatMap(post -> {
|
.flatMap(this::getHeadContent);
|
||||||
String headSnapshot = post.getSpec().getHeadSnapshot();
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Mono<ContentWrapper> getHeadContent(Post post) {
|
||||||
|
var headSnapshot = post.getSpec().getHeadSnapshot();
|
||||||
return getContent(headSnapshot, post.getSpec().getBaseSnapshot());
|
return getContent(headSnapshot, post.getSpec().getBaseSnapshot());
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Mono<ContentWrapper> getReleaseContent(String postName) {
|
public Mono<ContentWrapper> getReleaseContent(String postName) {
|
||||||
return client.get(Post.class, postName)
|
return client.get(Post.class, postName)
|
||||||
.flatMap(post -> {
|
.flatMap(this::getReleaseContent);
|
||||||
String releaseSnapshot = post.getSpec().getReleaseSnapshot();
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Mono<ContentWrapper> getReleaseContent(Post post) {
|
||||||
|
var releaseSnapshot = post.getSpec().getReleaseSnapshot();
|
||||||
return getContent(releaseSnapshot, post.getSpec().getBaseSnapshot());
|
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.Exceptions;
|
||||||
import reactor.core.publisher.Mono;
|
import reactor.core.publisher.Mono;
|
||||||
import reactor.util.retry.Retry;
|
import reactor.util.retry.Retry;
|
||||||
|
import run.halo.app.content.Content;
|
||||||
import run.halo.app.content.ContentWrapper;
|
import run.halo.app.content.ContentWrapper;
|
||||||
import run.halo.app.content.ListedPost;
|
import run.halo.app.content.ListedPost;
|
||||||
import run.halo.app.content.PostQuery;
|
import run.halo.app.content.PostQuery;
|
||||||
|
@ -132,7 +133,7 @@ public class PostEndpoint implements CustomEndpoint {
|
||||||
.content(contentBuilder()
|
.content(contentBuilder()
|
||||||
.mediaType(MediaType.APPLICATION_JSON_VALUE)
|
.mediaType(MediaType.APPLICATION_JSON_VALUE)
|
||||||
.schema(Builder.schemaBuilder()
|
.schema(Builder.schemaBuilder()
|
||||||
.implementation(PostRequest.Content.class))
|
.implementation(Content.class))
|
||||||
))
|
))
|
||||||
.response(responseBuilder()
|
.response(responseBuilder()
|
||||||
.implementation(Post.class))
|
.implementation(Post.class))
|
||||||
|
@ -191,7 +192,7 @@ public class PostEndpoint implements CustomEndpoint {
|
||||||
|
|
||||||
Mono<ServerResponse> updateContent(ServerRequest request) {
|
Mono<ServerResponse> updateContent(ServerRequest request) {
|
||||||
String postName = request.pathVariable("name");
|
String postName = request.pathVariable("name");
|
||||||
return request.bodyToMono(PostRequest.Content.class)
|
return request.bodyToMono(Content.class)
|
||||||
.flatMap(content -> client.fetch(Post.class, postName)
|
.flatMap(content -> client.fetch(Post.class, postName)
|
||||||
.flatMap(post -> {
|
.flatMap(post -> {
|
||||||
PostRequest postRequest = new PostRequest(post, content);
|
PostRequest postRequest = new PostRequest(post, content);
|
||||||
|
|
|
@ -21,6 +21,7 @@ import org.springframework.web.reactive.function.server.ServerResponse;
|
||||||
import org.thymeleaf.util.StringUtils;
|
import org.thymeleaf.util.StringUtils;
|
||||||
import reactor.core.publisher.Mono;
|
import reactor.core.publisher.Mono;
|
||||||
import reactor.util.retry.Retry;
|
import reactor.util.retry.Retry;
|
||||||
|
import run.halo.app.content.Content;
|
||||||
import run.halo.app.content.ContentWrapper;
|
import run.halo.app.content.ContentWrapper;
|
||||||
import run.halo.app.content.ListedSinglePage;
|
import run.halo.app.content.ListedSinglePage;
|
||||||
import run.halo.app.content.SinglePageQuery;
|
import run.halo.app.content.SinglePageQuery;
|
||||||
|
@ -130,7 +131,7 @@ public class SinglePageEndpoint implements CustomEndpoint {
|
||||||
.content(contentBuilder()
|
.content(contentBuilder()
|
||||||
.mediaType(MediaType.APPLICATION_JSON_VALUE)
|
.mediaType(MediaType.APPLICATION_JSON_VALUE)
|
||||||
.schema(Builder.schemaBuilder()
|
.schema(Builder.schemaBuilder()
|
||||||
.implementation(SinglePageRequest.Content.class))
|
.implementation(Content.class))
|
||||||
))
|
))
|
||||||
.response(responseBuilder()
|
.response(responseBuilder()
|
||||||
.implementation(Post.class))
|
.implementation(Post.class))
|
||||||
|
@ -169,7 +170,7 @@ public class SinglePageEndpoint implements CustomEndpoint {
|
||||||
|
|
||||||
Mono<ServerResponse> updateContent(ServerRequest request) {
|
Mono<ServerResponse> updateContent(ServerRequest request) {
|
||||||
String pageName = request.pathVariable("name");
|
String pageName = request.pathVariable("name");
|
||||||
return request.bodyToMono(SinglePageRequest.Content.class)
|
return request.bodyToMono(Content.class)
|
||||||
.flatMap(content -> client.fetch(SinglePage.class, pageName)
|
.flatMap(content -> client.fetch(SinglePage.class, pageName)
|
||||||
.flatMap(page -> {
|
.flatMap(page -> {
|
||||||
SinglePageRequest pageRequest = new SinglePageRequest(page, content);
|
SinglePageRequest pageRequest = new SinglePageRequest(page, content);
|
||||||
|
|
|
@ -66,11 +66,9 @@ public class DefaultRoleService implements RoleService {
|
||||||
// search all permissions
|
// search all permissions
|
||||||
return extensionClient.list(Role.class,
|
return extensionClient.list(Role.class,
|
||||||
shouldFilterHidden(true),
|
shouldFilterHidden(true),
|
||||||
compareCreationTimestamp(true))
|
compareCreationTimestamp(true));
|
||||||
.filter(DefaultRoleService::isRoleTemplate);
|
|
||||||
}
|
}
|
||||||
return listDependencies(names, shouldFilterHidden(true))
|
return listDependencies(names, shouldFilterHidden(true));
|
||||||
.filter(DefaultRoleService::isRoleTemplate);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -2,10 +2,12 @@ package run.halo.app.core.extension.service.impl;
|
||||||
|
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
|
import java.util.function.Consumer;
|
||||||
import java.util.function.Function;
|
import java.util.function.Function;
|
||||||
import org.springframework.core.io.buffer.DataBuffer;
|
import org.springframework.core.io.buffer.DataBuffer;
|
||||||
import org.springframework.http.HttpStatus;
|
import org.springframework.http.HttpStatus;
|
||||||
import org.springframework.http.MediaType;
|
import org.springframework.http.MediaType;
|
||||||
|
import org.springframework.http.codec.multipart.FilePart;
|
||||||
import org.springframework.lang.NonNull;
|
import org.springframework.lang.NonNull;
|
||||||
import org.springframework.lang.Nullable;
|
import org.springframework.lang.Nullable;
|
||||||
import org.springframework.security.core.Authentication;
|
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.Policy;
|
||||||
import run.halo.app.core.extension.attachment.endpoint.AttachmentHandler;
|
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.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.attachment.endpoint.UploadOption;
|
||||||
import run.halo.app.core.extension.service.AttachmentService;
|
import run.halo.app.core.extension.service.AttachmentService;
|
||||||
import run.halo.app.extension.ConfigMap;
|
import run.halo.app.extension.ConfigMap;
|
||||||
|
@ -42,12 +45,13 @@ public class DefaultAttachmentService implements AttachmentService {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Mono<Attachment> upload(@NonNull String policyName,
|
public Mono<Attachment> upload(
|
||||||
|
@NonNull String username,
|
||||||
|
@NonNull String policyName,
|
||||||
@Nullable String groupName,
|
@Nullable String groupName,
|
||||||
@NonNull String filename,
|
@NonNull FilePart filePart,
|
||||||
@NonNull Flux<DataBuffer> content,
|
@Nullable Consumer<Attachment> beforeCreating) {
|
||||||
@Nullable MediaType mediaType) {
|
return client.get(Policy.class, policyName)
|
||||||
return authenticationConsumer(authentication -> client.get(Policy.class, policyName)
|
|
||||||
.flatMap(policy -> {
|
.flatMap(policy -> {
|
||||||
var configMapName = policy.getSpec().getConfigMapName();
|
var configMapName = policy.getSpec().getConfigMapName();
|
||||||
if (!StringUtils.hasText(configMapName)) {
|
if (!StringUtils.hasText(configMapName)) {
|
||||||
|
@ -55,11 +59,7 @@ public class DefaultAttachmentService implements AttachmentService {
|
||||||
"ConfigMap name not found in Policy " + policyName));
|
"ConfigMap name not found in Policy " + policyName));
|
||||||
}
|
}
|
||||||
return client.get(ConfigMap.class, configMapName)
|
return client.get(ConfigMap.class, configMapName)
|
||||||
.map(configMap -> UploadOption.from(filename,
|
.map(configMap -> new UploadOption(filePart, policy, configMap));
|
||||||
content,
|
|
||||||
mediaType,
|
|
||||||
policy,
|
|
||||||
configMap));
|
|
||||||
})
|
})
|
||||||
.flatMap(uploadContext -> {
|
.flatMap(uploadContext -> {
|
||||||
var handlers = extensionComponentsFinder.getExtensions(AttachmentHandler.class);
|
var handlers = extensionComponentsFinder.getExtensions(AttachmentHandler.class);
|
||||||
|
@ -75,13 +75,29 @@ public class DefaultAttachmentService implements AttachmentService {
|
||||||
spec = new Attachment.AttachmentSpec();
|
spec = new Attachment.AttachmentSpec();
|
||||||
attachment.setSpec(spec);
|
attachment.setSpec(spec);
|
||||||
}
|
}
|
||||||
spec.setOwnerName(authentication.getName());
|
spec.setOwnerName(username);
|
||||||
if (StringUtils.hasText(groupName)) {
|
if (StringUtils.hasText(groupName)) {
|
||||||
spec.setGroupName(groupName);
|
spec.setGroupName(groupName);
|
||||||
}
|
}
|
||||||
spec.setPolicyName(policyName);
|
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
|
@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"
|
halo.run/hidden: "true"
|
||||||
annotations:
|
annotations:
|
||||||
rbac.authorization.halo.run/dependencies: "[ \"role-template-view-categories\" ]"
|
rbac.authorization.halo.run/dependencies: "[ \"role-template-view-categories\" ]"
|
||||||
|
rbac.authorization.halo.run/ui-permissions: |
|
||||||
|
[ "system:categories:manage", "uc:categories:manage" ]
|
||||||
rules:
|
rules:
|
||||||
- apiGroups: [ "content.halo.run" ]
|
- apiGroups: [ "content.halo.run" ]
|
||||||
resources: [ "categories" ]
|
resources: [ "categories" ]
|
||||||
|
@ -19,6 +21,9 @@ metadata:
|
||||||
labels:
|
labels:
|
||||||
halo.run/role-template: "true"
|
halo.run/role-template: "true"
|
||||||
halo.run/hidden: "true"
|
halo.run/hidden: "true"
|
||||||
|
annotations:
|
||||||
|
rbac.authorization.halo.run/ui-permissions: |
|
||||||
|
[ "system:categories:view", "uc:categories:view" ]
|
||||||
rules:
|
rules:
|
||||||
- apiGroups: [ "content.halo.run" ]
|
- apiGroups: [ "content.halo.run" ]
|
||||||
resources: [ "categories" ]
|
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,
|
"archivePageSize": 10,
|
||||||
"categoryPageSize": 10,
|
"categoryPageSize": 10,
|
||||||
"tagPageSize": 10,
|
"tagPageSize": 10,
|
||||||
"slugGenerationStrategy": "generateByTitle"
|
"slugGenerationStrategy": "generateByTitle",
|
||||||
|
"attachmentPolicyName": "default-policy"
|
||||||
}
|
}
|
||||||
comment: |
|
comment: |
|
||||||
{
|
{
|
||||||
|
|
|
@ -68,6 +68,14 @@ spec:
|
||||||
value: 'shortUUID'
|
value: 'shortUUID'
|
||||||
- label: 'UUID'
|
- label: 'UUID'
|
||||||
value: 'UUID'
|
value: 'UUID'
|
||||||
|
- $formkit: attachmentPolicySelect
|
||||||
|
name: attachmentPolicyName
|
||||||
|
label: "附件存储策略"
|
||||||
|
value: "default-policy"
|
||||||
|
- $formkit: attachmentGroupSelect
|
||||||
|
name: attachmentGroupName
|
||||||
|
label: "附件存储组"
|
||||||
|
value: ""
|
||||||
- group: seo
|
- group: seo
|
||||||
label: SEO 设置
|
label: SEO 设置
|
||||||
formSchema:
|
formSchema:
|
||||||
|
|
|
@ -23,6 +23,26 @@ import run.halo.app.core.extension.content.Post;
|
||||||
@ExtendWith(MockitoExtension.class)
|
@ExtendWith(MockitoExtension.class)
|
||||||
class PostQueryTest {
|
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
|
@Test
|
||||||
void toPredicate() {
|
void toPredicate() {
|
||||||
MultiValueMap<String, String> multiValueMap = new LinkedMultiValueMap<>();
|
MultiValueMap<String, String> multiValueMap = new LinkedMultiValueMap<>();
|
||||||
|
|
|
@ -18,6 +18,7 @@ import org.springframework.context.ApplicationEventPublisher;
|
||||||
import org.springframework.dao.OptimisticLockingFailureException;
|
import org.springframework.dao.OptimisticLockingFailureException;
|
||||||
import org.springframework.test.web.reactive.server.WebTestClient;
|
import org.springframework.test.web.reactive.server.WebTestClient;
|
||||||
import reactor.core.publisher.Mono;
|
import reactor.core.publisher.Mono;
|
||||||
|
import run.halo.app.content.Content;
|
||||||
import run.halo.app.content.PostRequest;
|
import run.halo.app.content.PostRequest;
|
||||||
import run.halo.app.content.PostService;
|
import run.halo.app.content.PostService;
|
||||||
import run.halo.app.content.TestPost;
|
import run.halo.app.content.TestPost;
|
||||||
|
@ -176,6 +177,6 @@ class PostEndpointTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
PostRequest postRequest(Post post) {
|
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 { useRouter } from "vue-router";
|
||||||
import { randomUUID } from "@/utils/id";
|
import { randomUUID } from "@/utils/id";
|
||||||
import { useContentCache } from "@console/composables/use-content-cache";
|
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 type { EditorProvider } from "@halo-dev/console-shared";
|
||||||
import { useLocalStorage } from "@vueuse/core";
|
import { useLocalStorage } from "@vueuse/core";
|
||||||
import EditorProviderSelector from "@/components/dropdown-selector/EditorProviderSelector.vue";
|
import EditorProviderSelector from "@/components/dropdown-selector/EditorProviderSelector.vue";
|
||||||
|
@ -372,6 +372,20 @@ const handlePreview = async () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
useSaveKeybinding(handleSave);
|
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>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
@ -450,6 +464,7 @@ useSaveKeybinding(handleSave);
|
||||||
v-if="currentEditorProvider"
|
v-if="currentEditorProvider"
|
||||||
v-model:raw="formState.content.raw"
|
v-model:raw="formState.content.raw"
|
||||||
v-model:content="formState.content.content"
|
v-model:content="formState.content.content"
|
||||||
|
:upload-image="handleUploadImage"
|
||||||
class="h-full"
|
class="h-full"
|
||||||
@update="handleSetContentCache"
|
@update="handleSetContentCache"
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -28,7 +28,7 @@ import { useRouteQuery } from "@vueuse/router";
|
||||||
import { useRouter } from "vue-router";
|
import { useRouter } from "vue-router";
|
||||||
import { randomUUID } from "@/utils/id";
|
import { randomUUID } from "@/utils/id";
|
||||||
import { useContentCache } from "@console/composables/use-content-cache";
|
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 type { EditorProvider } from "@halo-dev/console-shared";
|
||||||
import { useLocalStorage } from "@vueuse/core";
|
import { useLocalStorage } from "@vueuse/core";
|
||||||
import EditorProviderSelector from "@/components/dropdown-selector/EditorProviderSelector.vue";
|
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
|
// Post form
|
||||||
const initialFormState: PostRequest = {
|
const initialFormState: PostRequestWithContent = {
|
||||||
post: {
|
post: {
|
||||||
spec: {
|
spec: {
|
||||||
title: "",
|
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 settingModal = ref(false);
|
||||||
const saving = ref(false);
|
const saving = ref(false);
|
||||||
const publishing = ref(false);
|
const publishing = ref(false);
|
||||||
|
@ -388,6 +397,20 @@ const handlePreview = async () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
useSaveKeybinding(handleSave);
|
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>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
@ -466,6 +489,7 @@ useSaveKeybinding(handleSave);
|
||||||
v-if="currentEditorProvider"
|
v-if="currentEditorProvider"
|
||||||
v-model:raw="formState.content.raw"
|
v-model:raw="formState.content.raw"
|
||||||
v-model:content="formState.content.content"
|
v-model:content="formState.content.content"
|
||||||
|
:upload-image="handleUploadImage"
|
||||||
class="h-full"
|
class="h-full"
|
||||||
@update="handleSetContentCache"
|
@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-single-page-api.ts
|
||||||
api/content-halo-run-v1alpha1-snapshot-api.ts
|
api/content-halo-run-v1alpha1-snapshot-api.ts
|
||||||
api/content-halo-run-v1alpha1-tag-api.ts
|
api/content-halo-run-v1alpha1-tag-api.ts
|
||||||
api/doc-halo-run-v1alpha1-doc-tree-api.ts
|
|
||||||
api/login-api.ts
|
api/login-api.ts
|
||||||
api/metrics-halo-run-v1alpha1-counter-api.ts
|
api/metrics-halo-run-v1alpha1-counter-api.ts
|
||||||
api/migration-halo-run-v1alpha1-backup-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-api.ts
|
||||||
api/storage-halo-run-v1alpha1-policy-template-api.ts
|
api/storage-halo-run-v1alpha1-policy-template-api.ts
|
||||||
api/theme-halo-run-v1alpha1-theme-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-annotation-setting-api.ts
|
||||||
api/v1alpha1-cache-api.ts
|
api/v1alpha1-cache-api.ts
|
||||||
api/v1alpha1-config-map-api.ts
|
api/v1alpha1-config-map-api.ts
|
||||||
|
@ -124,9 +126,6 @@ models/create-user-request.ts
|
||||||
models/custom-templates.ts
|
models/custom-templates.ts
|
||||||
models/dashboard-stats.ts
|
models/dashboard-stats.ts
|
||||||
models/detailed-user.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/email-verify-request.ts
|
||||||
models/excerpt.ts
|
models/excerpt.ts
|
||||||
models/extension-definition-list.ts
|
models/extension-definition-list.ts
|
||||||
|
@ -262,7 +261,6 @@ models/site-stats-vo.ts
|
||||||
models/snap-shot-spec.ts
|
models/snap-shot-spec.ts
|
||||||
models/snapshot-list.ts
|
models/snapshot-list.ts
|
||||||
models/snapshot.ts
|
models/snapshot.ts
|
||||||
models/spec.ts
|
|
||||||
models/stats-vo.ts
|
models/stats-vo.ts
|
||||||
models/stats.ts
|
models/stats.ts
|
||||||
models/subject.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-api";
|
||||||
export * from "./api/storage-halo-run-v1alpha1-policy-template-api";
|
export * from "./api/storage-halo-run-v1alpha1-policy-template-api";
|
||||||
export * from "./api/theme-halo-run-v1alpha1-theme-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-annotation-setting-api";
|
||||||
export * from "./api/v1alpha1-cache-api";
|
export * from "./api/v1alpha1-cache-api";
|
||||||
export * from "./api/v1alpha1-config-map-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 "./snap-shot-spec";
|
||||||
export * from "./snapshot";
|
export * from "./snapshot";
|
||||||
export * from "./snapshot-list";
|
export * from "./snapshot-list";
|
||||||
export * from "./spec";
|
|
||||||
export * from "./stats";
|
export * from "./stats";
|
||||||
export * from "./stats-vo";
|
export * from "./stats-vo";
|
||||||
export * from "./subject";
|
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}
|
* @type {Content}
|
||||||
* @memberof PostRequest
|
* @memberof PostRequest
|
||||||
*/
|
*/
|
||||||
content: Content;
|
content?: Content;
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @type {Post}
|
* @type {Post}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<script lang="ts" setup>
|
<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 type { EditorProvider } from "@halo-dev/console-shared";
|
||||||
import {
|
import {
|
||||||
VAvatar,
|
VAvatar,
|
||||||
|
|
|
@ -51,7 +51,6 @@ import {
|
||||||
IconFolder,
|
IconFolder,
|
||||||
IconLink,
|
IconLink,
|
||||||
IconUserFollow,
|
IconUserFollow,
|
||||||
Toast,
|
|
||||||
VTabItem,
|
VTabItem,
|
||||||
VTabs,
|
VTabs,
|
||||||
} from "@halo-dev/components";
|
} from "@halo-dev/components";
|
||||||
|
@ -75,11 +74,9 @@ import {
|
||||||
} from "vue";
|
} from "vue";
|
||||||
import { formatDatetime } from "@/utils/date";
|
import { formatDatetime } from "@/utils/date";
|
||||||
import { useAttachmentSelect } from "@console/modules/contents/attachments/composables/use-attachment";
|
import { useAttachmentSelect } from "@console/modules/contents/attachments/composables/use-attachment";
|
||||||
import { apiClient } from "@/utils/api-client";
|
|
||||||
import * as fastq from "fastq";
|
import * as fastq from "fastq";
|
||||||
import type { queueAsPromised } from "fastq";
|
import type { queueAsPromised } from "fastq";
|
||||||
import type { Attachment } from "@halo-dev/api-client";
|
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 { useI18n } from "vue-i18n";
|
||||||
import { i18n } from "@/locales";
|
import { i18n } from "@/locales";
|
||||||
import { OverlayScrollbarsComponent } from "overlayscrollbars-vue";
|
import { OverlayScrollbarsComponent } from "overlayscrollbars-vue";
|
||||||
|
@ -88,17 +85,21 @@ import type { PluginModule } from "@halo-dev/console-shared";
|
||||||
import { useDebounceFn } from "@vueuse/core";
|
import { useDebounceFn } from "@vueuse/core";
|
||||||
import { onBeforeUnmount } from "vue";
|
import { onBeforeUnmount } from "vue";
|
||||||
import { generateAnchor } from "@/utils/anchor";
|
import { generateAnchor } from "@/utils/anchor";
|
||||||
|
import { usePermission } from "@/utils/permission";
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
const { currentUserHasPermission } = usePermission();
|
||||||
|
|
||||||
const props = withDefaults(
|
const props = withDefaults(
|
||||||
defineProps<{
|
defineProps<{
|
||||||
raw?: string;
|
raw?: string;
|
||||||
content: string;
|
content: string;
|
||||||
|
uploadImage?: (file: File) => Promise<Attachment>;
|
||||||
}>(),
|
}>(),
|
||||||
{
|
{
|
||||||
raw: "",
|
raw: "",
|
||||||
content: "",
|
content: "",
|
||||||
|
uploadImage: undefined,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -236,6 +237,11 @@ onMounted(() => {
|
||||||
}),
|
}),
|
||||||
Extension.create({
|
Extension.create({
|
||||||
addOptions() {
|
addOptions() {
|
||||||
|
// If user has no permission to view attachments, return
|
||||||
|
if (!currentUserHasPermission(["system:attachments:view"])) {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
getToolboxItems({ editor }: { editor: Editor }) {
|
getToolboxItems({ editor }: { editor: Editor }) {
|
||||||
return [
|
return [
|
||||||
|
@ -362,8 +368,6 @@ onBeforeUnmount(() => {
|
||||||
});
|
});
|
||||||
|
|
||||||
// image drag and paste upload
|
// image drag and paste upload
|
||||||
const { policies } = useFetchAttachmentPolicy();
|
|
||||||
|
|
||||||
type Task = {
|
type Task = {
|
||||||
file: File;
|
file: File;
|
||||||
process: (permalink: string) => void;
|
process: (permalink: string) => void;
|
||||||
|
@ -372,61 +376,17 @@ type Task = {
|
||||||
const uploadQueue: queueAsPromised<Task> = fastq.promise(asyncWorker, 1);
|
const uploadQueue: queueAsPromised<Task> = fastq.promise(asyncWorker, 1);
|
||||||
|
|
||||||
async function asyncWorker(arg: Task): Promise<void> {
|
async function asyncWorker(arg: Task): Promise<void> {
|
||||||
if (!policies.value?.length) {
|
if (!props.uploadImage) {
|
||||||
Toast.warning(
|
|
||||||
t(
|
|
||||||
"core.components.default_editor.upload_attachment.toast.no_available_policy"
|
|
||||||
)
|
|
||||||
);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { data: attachmentData } = await apiClient.attachment.uploadAttachment({
|
const attachmentData = await props.uploadImage(arg.file);
|
||||||
file: arg.file,
|
|
||||||
policyName: policies.value[0].metadata.name,
|
|
||||||
});
|
|
||||||
|
|
||||||
const permalink = await handleFetchPermalink(attachmentData, 3);
|
if (attachmentData.status?.permalink) {
|
||||||
|
arg.process(attachmentData.status.permalink);
|
||||||
if (permalink) {
|
|
||||||
arg.process(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 = () => {
|
const handleGenerateTableOfContent = () => {
|
||||||
if (!editor.value) {
|
if (!editor.value) {
|
||||||
return;
|
return;
|
||||||
|
|
|
@ -18,6 +18,9 @@ export enum rbacAnnotations {
|
||||||
|
|
||||||
export enum contentAnnotations {
|
export enum contentAnnotations {
|
||||||
PREFERRED_EDITOR = "content.halo.run/preferred-editor",
|
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
|
// pat
|
||||||
|
|
|
@ -14,6 +14,7 @@ import SearchResultListItem from "./components/SearchResultListItem.vue";
|
||||||
import { apiClient } from "@/utils/api-client";
|
import { apiClient } from "@/utils/api-client";
|
||||||
import { usePermission } from "@/utils/permission";
|
import { usePermission } from "@/utils/permission";
|
||||||
import { slugify } from "transliteration";
|
import { slugify } from "transliteration";
|
||||||
|
import HasPermission from "@/components/permission/HasPermission.vue";
|
||||||
|
|
||||||
const { currentUserHasPermission } = usePermission();
|
const { currentUserHasPermission } = usePermission();
|
||||||
|
|
||||||
|
@ -289,18 +290,24 @@ const handleDelete = () => {
|
||||||
|
|
||||||
<div v-if="dropdownVisible" :class="context.classes['dropdown-wrapper']">
|
<div v-if="dropdownVisible" :class="context.classes['dropdown-wrapper']">
|
||||||
<ul class="p-1">
|
<ul class="p-1">
|
||||||
<li
|
<HasPermission
|
||||||
v-if="text.trim() && !searchResults?.length"
|
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"
|
class="group flex cursor-pointer items-center justify-between rounded bg-gray-100 p-2"
|
||||||
@click="handleCreateCategory"
|
@click="handleCreateCategory"
|
||||||
>
|
>
|
||||||
<span class="text-xs text-gray-700 group-hover:text-gray-900">
|
<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>
|
</span>
|
||||||
</li>
|
</li>
|
||||||
|
</HasPermission>
|
||||||
|
|
||||||
<template v-if="text">
|
<template v-if="text">
|
||||||
<SearchResultListItem
|
<SearchResultListItem
|
||||||
v-for="category in searchResults"
|
v-for="category in searchResults"
|
||||||
|
|
|
@ -14,6 +14,7 @@ import Fuse from "fuse.js";
|
||||||
import { usePermission } from "@/utils/permission";
|
import { usePermission } from "@/utils/permission";
|
||||||
import { slugify } from "transliteration";
|
import { slugify } from "transliteration";
|
||||||
import { usePostTag } from "@console/modules/contents/posts/tags/composables/use-post-tag";
|
import { usePostTag } from "@console/modules/contents/posts/tags/composables/use-post-tag";
|
||||||
|
import HasPermission from "@/components/permission/HasPermission.vue";
|
||||||
|
|
||||||
const { currentUserHasPermission } = usePermission();
|
const { currentUserHasPermission } = usePermission();
|
||||||
|
|
||||||
|
@ -279,9 +280,11 @@ const handleDelete = () => {
|
||||||
|
|
||||||
<div v-if="dropdownVisible" :class="context.classes['dropdown-wrapper']">
|
<div v-if="dropdownVisible" :class="context.classes['dropdown-wrapper']">
|
||||||
<ul class="p-1">
|
<ul class="p-1">
|
||||||
<li
|
<HasPermission
|
||||||
v-if="text.trim() && !searchResults?.length"
|
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"
|
class="group flex cursor-pointer items-center justify-between rounded bg-gray-100 p-2"
|
||||||
@click="handleCreateTag"
|
@click="handleCreateTag"
|
||||||
>
|
>
|
||||||
|
@ -289,6 +292,7 @@ const handleDelete = () => {
|
||||||
{{ $t("core.formkit.tag_select.creation_label", { text: text }) }}
|
{{ $t("core.formkit.tag_select.creation_label", { text: text }) }}
|
||||||
</span>
|
</span>
|
||||||
</li>
|
</li>
|
||||||
|
</HasPermission>
|
||||||
<li
|
<li
|
||||||
v-for="tag in searchResults"
|
v-for="tag in searchResults"
|
||||||
:id="tag.metadata.name"
|
:id="tag.metadata.name"
|
||||||
|
|
|
@ -1205,10 +1205,6 @@ core:
|
||||||
placeholder: "Enter / to select input type."
|
placeholder: "Enter / to select input type."
|
||||||
toolbox:
|
toolbox:
|
||||||
attachment: Attachment
|
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:
|
global_search:
|
||||||
placeholder: Enter keywords to search
|
placeholder: Enter keywords to search
|
||||||
no_results: No search results
|
no_results: No search results
|
||||||
|
|
|
@ -1133,10 +1133,6 @@ core:
|
||||||
placeholder: "Ingresa / para seleccionar el tipo de entrada."
|
placeholder: "Ingresa / para seleccionar el tipo de entrada."
|
||||||
toolbox:
|
toolbox:
|
||||||
attachment: Adjunto
|
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:
|
global_search:
|
||||||
placeholder: Ingresa palabras clave para buscar
|
placeholder: Ingresa palabras clave para buscar
|
||||||
no_results: Sin resultados de búsqueda
|
no_results: Sin resultados de búsqueda
|
||||||
|
|
|
@ -1205,10 +1205,6 @@ core:
|
||||||
placeholder: "输入 / 以选择输入类型"
|
placeholder: "输入 / 以选择输入类型"
|
||||||
toolbox:
|
toolbox:
|
||||||
attachment: 选择附件
|
attachment: 选择附件
|
||||||
upload_attachment:
|
|
||||||
toast:
|
|
||||||
no_available_policy: 目前没有可用的存储策略
|
|
||||||
failed_fetch_permalink: 获取附件永久链接失败:{display_name}
|
|
||||||
global_search:
|
global_search:
|
||||||
placeholder: 输入关键词以搜索
|
placeholder: 输入关键词以搜索
|
||||||
no_results: 没有搜索结果
|
no_results: 没有搜索结果
|
||||||
|
@ -1294,7 +1290,7 @@ core:
|
||||||
delete_success: 删除成功
|
delete_success: 删除成功
|
||||||
save_success: 保存成功
|
save_success: 保存成功
|
||||||
publish_success: 发布成功
|
publish_success: 发布成功
|
||||||
cancel_publish_success: 发布成功
|
cancel_publish_success: 取消发布成功
|
||||||
recovery_success: 恢复成功
|
recovery_success: 恢复成功
|
||||||
uninstall_success: 卸载成功
|
uninstall_success: 卸载成功
|
||||||
active_success: 启用成功
|
active_success: 启用成功
|
||||||
|
|
|
@ -1205,10 +1205,6 @@ core:
|
||||||
placeholder: "輸入 / 以選擇輸入類型"
|
placeholder: "輸入 / 以選擇輸入類型"
|
||||||
toolbox:
|
toolbox:
|
||||||
attachment: 選擇附件
|
attachment: 選擇附件
|
||||||
upload_attachment:
|
|
||||||
toast:
|
|
||||||
no_available_policy: 目前沒有可用的存儲策略
|
|
||||||
failed_fetch_permalink: 獲取附件永久連結失敗:{display_name}
|
|
||||||
global_search:
|
global_search:
|
||||||
placeholder: 輸入關鍵字以搜尋
|
placeholder: 輸入關鍵字以搜尋
|
||||||
no_results: 沒有搜尋結果
|
no_results: 沒有搜尋結果
|
||||||
|
@ -1294,7 +1290,7 @@ core:
|
||||||
delete_success: 刪除成功
|
delete_success: 刪除成功
|
||||||
save_success: 保存成功
|
save_success: 保存成功
|
||||||
publish_success: 發布成功
|
publish_success: 發布成功
|
||||||
cancel_publish_success: 發布成功
|
cancel_publish_success: 取消發布成功
|
||||||
recovery_success: 恢復成功
|
recovery_success: 恢復成功
|
||||||
uninstall_success: 卸載成功
|
uninstall_success: 卸載成功
|
||||||
active_success: 啟用成功
|
active_success: 啟用成功
|
||||||
|
|
|
@ -45,6 +45,9 @@ import {
|
||||||
NotificationHaloRunV1alpha1NotifierDescriptorApi,
|
NotificationHaloRunV1alpha1NotifierDescriptorApi,
|
||||||
ApiSecurityHaloRunV1alpha1PersonalAccessTokenApi,
|
ApiSecurityHaloRunV1alpha1PersonalAccessTokenApi,
|
||||||
SecurityHaloRunV1alpha1PersonalAccessTokenApi,
|
SecurityHaloRunV1alpha1PersonalAccessTokenApi,
|
||||||
|
UcApiContentHaloRunV1alpha1AttachmentApi,
|
||||||
|
UcApiContentHaloRunV1alpha1PostApi,
|
||||||
|
UcApiContentHaloRunV1alpha1SnapshotApi,
|
||||||
} from "@halo-dev/api-client";
|
} from "@halo-dev/api-client";
|
||||||
import type { AxiosError, AxiosInstance } from "axios";
|
import type { AxiosError, AxiosInstance } from "axios";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
|
@ -244,6 +247,19 @@ function setupApiClient(axios: AxiosInstance) {
|
||||||
baseURL,
|
baseURL,
|
||||||
axios
|
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,
|
searchable: true,
|
||||||
menu: {
|
menu: {
|
||||||
name: "消息",
|
name: "消息",
|
||||||
|
group: "dashboard",
|
||||||
icon: markRaw(IconNotificationBadgeLine),
|
icon: markRaw(IconNotificationBadgeLine),
|
||||||
priority: 1,
|
priority: 1,
|
||||||
mobile: true,
|
mobile: true,
|
||||||
|
|
|
@ -21,6 +21,7 @@ export default definePlugin({
|
||||||
searchable: true,
|
searchable: true,
|
||||||
menu: {
|
menu: {
|
||||||
name: "我的",
|
name: "我的",
|
||||||
|
group: "dashboard",
|
||||||
icon: markRaw(IconAccountCircleLine),
|
icon: markRaw(IconAccountCircleLine),
|
||||||
priority: 0,
|
priority: 0,
|
||||||
mobile: true,
|
mobile: true,
|
||||||
|
|
Loading…
Reference in New Issue