mirror of https://github.com/halo-dev/halo
feat: implement a feature for previewing posts and single pages (#3983)
#### What type of PR is this? /kind feature /area core /area console /milestone 2.6.x #### What this PR does / why we need it: 新增文章和自定义页面预览功能 提供了以下两个路由用于预览,必须登录且是对应文章或自定义页面的 contributors 才能访问,如果不是 contributor 则先得到没有权限访问异常,如果有权限访问但预览文章不存在则得到 404 - 文章预览 `GET /preview/posts/{name}` - 自定义页面预览 `GET /preview/singlepages/{name}` 两个路由都可以通过查询参数 snapshotName 来指定需要预览的内容快照,它是可选的,默认为当前正在编辑的内容 #### Which issue(s) this PR fixes: Fixes #2349 #### Does this PR introduce a user-facing change? ```release-note 新增文章和自定义页面预览功能 ```pull/3993/head^2
parent
533f0cfa66
commit
da5fb1a252
|
@ -18,7 +18,7 @@ import reactor.core.publisher.Mono;
|
||||||
import run.halo.app.theme.DefaultTemplateEnum;
|
import run.halo.app.theme.DefaultTemplateEnum;
|
||||||
import run.halo.app.theme.finders.PostFinder;
|
import run.halo.app.theme.finders.PostFinder;
|
||||||
import run.halo.app.theme.finders.SinglePageFinder;
|
import run.halo.app.theme.finders.SinglePageFinder;
|
||||||
import run.halo.app.theme.router.factories.ModelConst;
|
import run.halo.app.theme.router.ModelConst;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* <p>The <code>head</code> html snippet injection processor for content template such as post
|
* <p>The <code>head</code> html snippet injection processor for content template such as post
|
||||||
|
|
|
@ -10,7 +10,7 @@ import reactor.core.publisher.Mono;
|
||||||
import run.halo.app.infra.SystemConfigurableEnvironmentFetcher;
|
import run.halo.app.infra.SystemConfigurableEnvironmentFetcher;
|
||||||
import run.halo.app.infra.SystemSetting;
|
import run.halo.app.infra.SystemSetting;
|
||||||
import run.halo.app.theme.DefaultTemplateEnum;
|
import run.halo.app.theme.DefaultTemplateEnum;
|
||||||
import run.halo.app.theme.router.factories.ModelConst;
|
import run.halo.app.theme.router.ModelConst;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* <p>Global custom head snippet injection for theme global setting.</p>
|
* <p>Global custom head snippet injection for theme global setting.</p>
|
||||||
|
|
|
@ -0,0 +1,21 @@
|
||||||
|
package run.halo.app.theme.finders;
|
||||||
|
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
|
import run.halo.app.core.extension.content.SinglePage;
|
||||||
|
import run.halo.app.theme.finders.vo.ListedSinglePageVo;
|
||||||
|
import run.halo.app.theme.finders.vo.SinglePageVo;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A service that converts {@link SinglePage} to {@link SinglePageVo}.
|
||||||
|
*
|
||||||
|
* @author guqing
|
||||||
|
* @since 2.6.0
|
||||||
|
*/
|
||||||
|
public interface SinglePageConversionService {
|
||||||
|
|
||||||
|
Mono<SinglePageVo> convertToVo(SinglePage singlePage, String snapshotName);
|
||||||
|
|
||||||
|
Mono<SinglePageVo> convertToVo(SinglePage singlePage);
|
||||||
|
|
||||||
|
Mono<ListedSinglePageVo> convertToListedVo(SinglePage singlePage);
|
||||||
|
}
|
|
@ -133,7 +133,7 @@ public class PostFinderImpl implements PostFinder {
|
||||||
int index = elements.indexOf(currentName);
|
int index = elements.indexOf(currentName);
|
||||||
|
|
||||||
String previousPostName = null;
|
String previousPostName = null;
|
||||||
if (index != 0) {
|
if (index > 0) {
|
||||||
previousPostName = elements.get(index - 1);
|
previousPostName = elements.get(index - 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,113 @@
|
||||||
|
package run.halo.app.theme.finders.impl;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import org.springframework.util.Assert;
|
||||||
|
import org.springframework.util.CollectionUtils;
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
|
import run.halo.app.content.SinglePageService;
|
||||||
|
import run.halo.app.core.extension.content.SinglePage;
|
||||||
|
import run.halo.app.metrics.CounterService;
|
||||||
|
import run.halo.app.metrics.MeterUtils;
|
||||||
|
import run.halo.app.theme.finders.ContributorFinder;
|
||||||
|
import run.halo.app.theme.finders.SinglePageConversionService;
|
||||||
|
import run.halo.app.theme.finders.vo.ContentVo;
|
||||||
|
import run.halo.app.theme.finders.vo.ListedSinglePageVo;
|
||||||
|
import run.halo.app.theme.finders.vo.SinglePageVo;
|
||||||
|
import run.halo.app.theme.finders.vo.StatsVo;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default implementation of {@link SinglePageConversionService}.
|
||||||
|
*
|
||||||
|
* @author guqing
|
||||||
|
* @since 2.6.0
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class SinglePageConversionServiceImpl implements SinglePageConversionService {
|
||||||
|
|
||||||
|
private final SinglePageService singlePageService;
|
||||||
|
|
||||||
|
private final ContributorFinder contributorFinder;
|
||||||
|
|
||||||
|
private final CounterService counterService;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Mono<SinglePageVo> convertToVo(SinglePage singlePage, String snapshotName) {
|
||||||
|
return convert(singlePage, snapshotName);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Mono<SinglePageVo> convertToVo(SinglePage singlePage) {
|
||||||
|
return convert(singlePage, singlePage.getSpec().getReleaseSnapshot());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Mono<ListedSinglePageVo> convertToListedVo(SinglePage singlePage) {
|
||||||
|
return Mono.fromSupplier(
|
||||||
|
() -> {
|
||||||
|
ListedSinglePageVo pageVo = ListedSinglePageVo.from(singlePage);
|
||||||
|
pageVo.setContributors(List.of());
|
||||||
|
return pageVo;
|
||||||
|
})
|
||||||
|
.flatMap(this::populateStats)
|
||||||
|
.flatMap(this::populateContributors);
|
||||||
|
}
|
||||||
|
|
||||||
|
Mono<SinglePageVo> convert(SinglePage singlePage, String snapshotName) {
|
||||||
|
Assert.notNull(singlePage, "Single page must not be null");
|
||||||
|
Assert.hasText(snapshotName, "Snapshot name must not be empty");
|
||||||
|
return Mono.just(singlePage)
|
||||||
|
.map(page -> {
|
||||||
|
SinglePageVo pageVo = SinglePageVo.from(page);
|
||||||
|
pageVo.setContributors(List.of());
|
||||||
|
pageVo.setContent(ContentVo.empty());
|
||||||
|
return pageVo;
|
||||||
|
})
|
||||||
|
.flatMap(this::populateStats)
|
||||||
|
.flatMap(this::populateContributors)
|
||||||
|
.flatMap(page -> populateContent(page, snapshotName))
|
||||||
|
.flatMap(page -> contributorFinder.getContributor(page.getSpec().getOwner())
|
||||||
|
.doOnNext(page::setOwner)
|
||||||
|
.thenReturn(page)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Mono<SinglePageVo> populateContent(SinglePageVo singlePageVo, String snapshotName) {
|
||||||
|
Assert.notNull(singlePageVo, "Single page vo must not be null");
|
||||||
|
Assert.hasText(snapshotName, "Snapshot name must not be empty");
|
||||||
|
return singlePageService.getContent(snapshotName, singlePageVo.getSpec().getBaseSnapshot())
|
||||||
|
.map(contentWrapper -> ContentVo.builder()
|
||||||
|
.content(contentWrapper.getContent())
|
||||||
|
.raw(contentWrapper.getRaw())
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
.doOnNext(singlePageVo::setContent)
|
||||||
|
.thenReturn(singlePageVo);
|
||||||
|
}
|
||||||
|
|
||||||
|
<T extends ListedSinglePageVo> Mono<T> populateStats(T pageVo) {
|
||||||
|
String name = pageVo.getMetadata().getName();
|
||||||
|
return counterService.getByName(MeterUtils.nameOf(SinglePage.class, name))
|
||||||
|
.map(counter -> StatsVo.builder()
|
||||||
|
.visit(counter.getVisit())
|
||||||
|
.upvote(counter.getUpvote())
|
||||||
|
.comment(counter.getApprovedComment())
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
.doOnNext(pageVo::setStats)
|
||||||
|
.thenReturn(pageVo);
|
||||||
|
}
|
||||||
|
|
||||||
|
<T extends ListedSinglePageVo> Mono<T> populateContributors(T pageVo) {
|
||||||
|
List<String> names = pageVo.getStatus().getContributors();
|
||||||
|
if (CollectionUtils.isEmpty(names)) {
|
||||||
|
return Mono.just(pageVo);
|
||||||
|
}
|
||||||
|
return contributorFinder.getContributors(names)
|
||||||
|
.collectList()
|
||||||
|
.doOnNext(pageVo::setContributors)
|
||||||
|
.thenReturn(pageVo);
|
||||||
|
}
|
||||||
|
}
|
|
@ -10,7 +10,6 @@ import java.util.function.Predicate;
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
import org.apache.commons.lang3.ObjectUtils;
|
import org.apache.commons.lang3.ObjectUtils;
|
||||||
import org.springframework.lang.Nullable;
|
import org.springframework.lang.Nullable;
|
||||||
import org.springframework.util.CollectionUtils;
|
|
||||||
import reactor.core.publisher.Flux;
|
import reactor.core.publisher.Flux;
|
||||||
import reactor.core.publisher.Mono;
|
import reactor.core.publisher.Mono;
|
||||||
import run.halo.app.content.SinglePageService;
|
import run.halo.app.content.SinglePageService;
|
||||||
|
@ -18,15 +17,12 @@ import run.halo.app.core.extension.content.Post;
|
||||||
import run.halo.app.core.extension.content.SinglePage;
|
import run.halo.app.core.extension.content.SinglePage;
|
||||||
import run.halo.app.extension.ListResult;
|
import run.halo.app.extension.ListResult;
|
||||||
import run.halo.app.extension.ReactiveExtensionClient;
|
import run.halo.app.extension.ReactiveExtensionClient;
|
||||||
import run.halo.app.metrics.CounterService;
|
|
||||||
import run.halo.app.metrics.MeterUtils;
|
|
||||||
import run.halo.app.theme.finders.ContributorFinder;
|
|
||||||
import run.halo.app.theme.finders.Finder;
|
import run.halo.app.theme.finders.Finder;
|
||||||
|
import run.halo.app.theme.finders.SinglePageConversionService;
|
||||||
import run.halo.app.theme.finders.SinglePageFinder;
|
import run.halo.app.theme.finders.SinglePageFinder;
|
||||||
import run.halo.app.theme.finders.vo.ContentVo;
|
import run.halo.app.theme.finders.vo.ContentVo;
|
||||||
import run.halo.app.theme.finders.vo.ListedSinglePageVo;
|
import run.halo.app.theme.finders.vo.ListedSinglePageVo;
|
||||||
import run.halo.app.theme.finders.vo.SinglePageVo;
|
import run.halo.app.theme.finders.vo.SinglePageVo;
|
||||||
import run.halo.app.theme.finders.vo.StatsVo;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A default implementation of {@link SinglePage}.
|
* A default implementation of {@link SinglePage}.
|
||||||
|
@ -44,35 +40,15 @@ public class SinglePageFinderImpl implements SinglePageFinder {
|
||||||
|
|
||||||
private final ReactiveExtensionClient client;
|
private final ReactiveExtensionClient client;
|
||||||
|
|
||||||
|
private final SinglePageConversionService singlePagePublicQueryService;
|
||||||
|
|
||||||
private final SinglePageService singlePageService;
|
private final SinglePageService singlePageService;
|
||||||
|
|
||||||
private final ContributorFinder contributorFinder;
|
|
||||||
|
|
||||||
private final CounterService counterService;
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Mono<SinglePageVo> getByName(String pageName) {
|
public Mono<SinglePageVo> getByName(String pageName) {
|
||||||
return client.get(SinglePage.class, pageName)
|
return client.get(SinglePage.class, pageName)
|
||||||
.filter(FIXED_PREDICATE)
|
.filter(FIXED_PREDICATE)
|
||||||
.map(page -> {
|
.flatMap(singlePagePublicQueryService::convertToVo);
|
||||||
SinglePageVo pageVo = SinglePageVo.from(page);
|
|
||||||
pageVo.setContributors(List.of());
|
|
||||||
pageVo.setContent(ContentVo.empty());
|
|
||||||
return pageVo;
|
|
||||||
})
|
|
||||||
.flatMap(singlePageVo -> fetchStats(singlePageVo)
|
|
||||||
.doOnNext(singlePageVo::setStats)
|
|
||||||
.thenReturn(singlePageVo)
|
|
||||||
)
|
|
||||||
.flatMap(this::populateContributors)
|
|
||||||
.flatMap(page -> content(pageName)
|
|
||||||
.doOnNext(page::setContent)
|
|
||||||
.thenReturn(page)
|
|
||||||
)
|
|
||||||
.flatMap(page -> contributorFinder.getContributor(page.getSpec().getOwner())
|
|
||||||
.doOnNext(page::setOwner)
|
|
||||||
.thenReturn(page)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -98,13 +74,7 @@ public class SinglePageFinderImpl implements SinglePageFinder {
|
||||||
return client.list(SinglePage.class, predicateToUse,
|
return client.list(SinglePage.class, predicateToUse,
|
||||||
comparatorToUse, pageNullSafe(page), sizeNullSafe(size))
|
comparatorToUse, pageNullSafe(page), sizeNullSafe(size))
|
||||||
.flatMap(list -> Flux.fromStream(list.get())
|
.flatMap(list -> Flux.fromStream(list.get())
|
||||||
.map(singlePage -> {
|
.concatMap(singlePagePublicQueryService::convertToListedVo)
|
||||||
ListedSinglePageVo pageVo = ListedSinglePageVo.from(singlePage);
|
|
||||||
pageVo.setContributors(List.of());
|
|
||||||
return pageVo;
|
|
||||||
})
|
|
||||||
.flatMap(lp -> fetchStats(lp).doOnNext(lp::setStats).thenReturn(lp))
|
|
||||||
.concatMap(this::populateContributors)
|
|
||||||
.collectList()
|
.collectList()
|
||||||
.map(pageVos -> new ListResult<>(list.getPage(), list.getSize(), list.getTotal(),
|
.map(pageVos -> new ListResult<>(list.getPage(), list.getSize(), list.getTotal(),
|
||||||
pageVos)
|
pageVos)
|
||||||
|
@ -113,29 +83,6 @@ public class SinglePageFinderImpl implements SinglePageFinder {
|
||||||
.defaultIfEmpty(new ListResult<>(0, 0, 0, List.of()));
|
.defaultIfEmpty(new ListResult<>(0, 0, 0, List.of()));
|
||||||
}
|
}
|
||||||
|
|
||||||
<T extends ListedSinglePageVo> Mono<StatsVo> fetchStats(T pageVo) {
|
|
||||||
String name = pageVo.getMetadata().getName();
|
|
||||||
return counterService.getByName(MeterUtils.nameOf(SinglePage.class, name))
|
|
||||||
.map(counter -> StatsVo.builder()
|
|
||||||
.visit(counter.getVisit())
|
|
||||||
.upvote(counter.getUpvote())
|
|
||||||
.comment(counter.getApprovedComment())
|
|
||||||
.build()
|
|
||||||
)
|
|
||||||
.defaultIfEmpty(StatsVo.empty());
|
|
||||||
}
|
|
||||||
|
|
||||||
<T extends ListedSinglePageVo> Mono<T> populateContributors(T pageVo) {
|
|
||||||
List<String> names = pageVo.getStatus().getContributors();
|
|
||||||
if (CollectionUtils.isEmpty(names)) {
|
|
||||||
return Mono.just(pageVo);
|
|
||||||
}
|
|
||||||
return contributorFinder.getContributors(names)
|
|
||||||
.collectList()
|
|
||||||
.doOnNext(pageVo::setContributors)
|
|
||||||
.thenReturn(pageVo);
|
|
||||||
}
|
|
||||||
|
|
||||||
static Comparator<SinglePage> defaultComparator() {
|
static Comparator<SinglePage> defaultComparator() {
|
||||||
Function<SinglePage, Boolean> pinned =
|
Function<SinglePage, Boolean> pinned =
|
||||||
page -> Objects.requireNonNullElse(page.getSpec().getPinned(), false);
|
page -> Objects.requireNonNullElse(page.getSpec().getPinned(), false);
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
package run.halo.app.theme.router.factories;
|
package run.halo.app.theme.router;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Static variable keys for view model.
|
* Static variable keys for view model.
|
|
@ -0,0 +1,53 @@
|
||||||
|
package run.halo.app.theme.router;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import run.halo.app.core.extension.content.Post;
|
||||||
|
import run.halo.app.core.extension.content.SinglePage;
|
||||||
|
import run.halo.app.extension.Scheme;
|
||||||
|
import run.halo.app.theme.DefaultTemplateEnum;
|
||||||
|
import run.halo.app.theme.finders.vo.PostVo;
|
||||||
|
import run.halo.app.theme.finders.vo.SinglePageVo;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A util class for building model map.
|
||||||
|
*
|
||||||
|
* @author guqing
|
||||||
|
* @since 2.6.0
|
||||||
|
*/
|
||||||
|
public abstract class ModelMapUtils {
|
||||||
|
private static final Scheme POST_SCHEME = Scheme.buildFromType(Post.class);
|
||||||
|
private static final Scheme SINGLE_PAGE_SCHEME = Scheme.buildFromType(SinglePage.class);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build post view model.
|
||||||
|
*
|
||||||
|
* @param postVo post vo
|
||||||
|
* @return model map
|
||||||
|
*/
|
||||||
|
public static Map<String, Object> postModel(PostVo postVo) {
|
||||||
|
Map<String, Object> model = new HashMap<>();
|
||||||
|
model.put("name", postVo.getMetadata().getName());
|
||||||
|
model.put(ModelConst.TEMPLATE_ID, DefaultTemplateEnum.POST.getValue());
|
||||||
|
model.put("groupVersionKind", POST_SCHEME.groupVersionKind());
|
||||||
|
model.put("plural", POST_SCHEME.plural());
|
||||||
|
model.put("post", postVo);
|
||||||
|
return model;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build single page view model.
|
||||||
|
*
|
||||||
|
* @param pageVo page vo
|
||||||
|
* @return model map
|
||||||
|
*/
|
||||||
|
public static Map<String, Object> singlePageModel(SinglePageVo pageVo) {
|
||||||
|
Map<String, Object> model = new HashMap<>();
|
||||||
|
model.put("name", pageVo.getMetadata().getName());
|
||||||
|
model.put("groupVersionKind", SINGLE_PAGE_SCHEME.groupVersionKind());
|
||||||
|
model.put("plural", SINGLE_PAGE_SCHEME.plural());
|
||||||
|
model.put(ModelConst.TEMPLATE_ID, DefaultTemplateEnum.SINGLE_PAGE.getValue());
|
||||||
|
model.put("singlePage", pageVo);
|
||||||
|
return model;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,185 @@
|
||||||
|
package run.halo.app.theme.router;
|
||||||
|
|
||||||
|
import java.security.Principal;
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.security.core.context.ReactiveSecurityContextHolder;
|
||||||
|
import org.springframework.security.core.context.SecurityContext;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import org.springframework.util.Assert;
|
||||||
|
import org.springframework.web.reactive.function.server.RouterFunction;
|
||||||
|
import org.springframework.web.reactive.function.server.RouterFunctions;
|
||||||
|
import org.springframework.web.reactive.function.server.ServerRequest;
|
||||||
|
import org.springframework.web.reactive.function.server.ServerResponse;
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
|
import run.halo.app.content.PostService;
|
||||||
|
import run.halo.app.core.extension.content.Post;
|
||||||
|
import run.halo.app.core.extension.content.SinglePage;
|
||||||
|
import run.halo.app.extension.ReactiveExtensionClient;
|
||||||
|
import run.halo.app.infra.AnonymousUserConst;
|
||||||
|
import run.halo.app.infra.exception.NotFoundException;
|
||||||
|
import run.halo.app.theme.DefaultTemplateEnum;
|
||||||
|
import run.halo.app.theme.finders.PostPublicQueryService;
|
||||||
|
import run.halo.app.theme.finders.SinglePageConversionService;
|
||||||
|
import run.halo.app.theme.finders.vo.ContentVo;
|
||||||
|
import run.halo.app.theme.finders.vo.ContributorVo;
|
||||||
|
import run.halo.app.theme.finders.vo.PostVo;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <p>Preview router for previewing posts and single pages.</p>
|
||||||
|
*
|
||||||
|
* @author guqing
|
||||||
|
* @since 2.6.0
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class PreviewRouterFunction {
|
||||||
|
static final String SNAPSHOT_NAME_PARAM = "snapshotName";
|
||||||
|
|
||||||
|
private final ReactiveExtensionClient client;
|
||||||
|
|
||||||
|
private final PostPublicQueryService postPublicQueryService;
|
||||||
|
|
||||||
|
private final ViewNameResolver viewNameResolver;
|
||||||
|
|
||||||
|
private final PostService postService;
|
||||||
|
|
||||||
|
private final SinglePageConversionService singlePageConversionService;
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
RouterFunction<ServerResponse> previewRouter() {
|
||||||
|
return RouterFunctions.route()
|
||||||
|
.GET("/preview/posts/{name}", this::previewPost)
|
||||||
|
.GET("/preview/singlepages/{name}", this::previewSinglePage)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
private Mono<ServerResponse> previewPost(ServerRequest request) {
|
||||||
|
final var name = request.pathVariable("name");
|
||||||
|
return currentAuthenticatedUserName()
|
||||||
|
.flatMap(principal -> client.fetch(Post.class, name))
|
||||||
|
.flatMap(post -> {
|
||||||
|
String snapshotName = request.queryParam(SNAPSHOT_NAME_PARAM)
|
||||||
|
.orElse(post.getSpec().getHeadSnapshot());
|
||||||
|
return convertToPostVo(post, snapshotName);
|
||||||
|
})
|
||||||
|
.flatMap(post -> canPreview(post.getContributors())
|
||||||
|
.doOnNext(canPreview -> {
|
||||||
|
if (!canPreview) {
|
||||||
|
throw new NotFoundException("Page not found.");
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.thenReturn(post)
|
||||||
|
)
|
||||||
|
// Check permissions before throwing this exception
|
||||||
|
.switchIfEmpty(Mono.error(() -> new NotFoundException("Post not found.")))
|
||||||
|
.flatMap(postVo -> {
|
||||||
|
String template = postVo.getSpec().getTemplate();
|
||||||
|
Map<String, Object> model = ModelMapUtils.postModel(postVo);
|
||||||
|
return viewNameResolver.resolveViewNameOrDefault(request, template,
|
||||||
|
DefaultTemplateEnum.POST.getValue())
|
||||||
|
.flatMap(templateName -> ServerResponse.ok().render(templateName, model));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private Mono<PostVo> convertToPostVo(Post post, String snapshotName) {
|
||||||
|
return postPublicQueryService.convertToListedPostVo(post)
|
||||||
|
.map(PostVo::from)
|
||||||
|
.doOnNext(postVo -> {
|
||||||
|
// fake some attributes only for preview when they are not published
|
||||||
|
Post.PostSpec spec = postVo.getSpec();
|
||||||
|
if (spec.getPublishTime() == null) {
|
||||||
|
spec.setPublishTime(Instant.now());
|
||||||
|
}
|
||||||
|
if (spec.getPublish() == null) {
|
||||||
|
spec.setPublish(false);
|
||||||
|
}
|
||||||
|
Post.PostStatus status = postVo.getStatus();
|
||||||
|
if (status == null) {
|
||||||
|
status = new Post.PostStatus();
|
||||||
|
postVo.setStatus(status);
|
||||||
|
}
|
||||||
|
if (status.getLastModifyTime() == null) {
|
||||||
|
status.setLastModifyTime(Instant.now());
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.flatMap(postVo ->
|
||||||
|
postService.getContent(snapshotName, postVo.getSpec().getBaseSnapshot())
|
||||||
|
.map(contentWrapper -> ContentVo.builder()
|
||||||
|
.raw(contentWrapper.getRaw())
|
||||||
|
.content(contentWrapper.getContent())
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
.doOnNext(postVo::setContent)
|
||||||
|
.thenReturn(postVo)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Mono<ServerResponse> previewSinglePage(ServerRequest request) {
|
||||||
|
final var name = request.pathVariable("name");
|
||||||
|
return currentAuthenticatedUserName()
|
||||||
|
.flatMap(principal -> client.fetch(SinglePage.class, name))
|
||||||
|
.flatMap(singlePage -> {
|
||||||
|
String snapshotName = request.queryParam(SNAPSHOT_NAME_PARAM)
|
||||||
|
.orElse(singlePage.getSpec().getHeadSnapshot());
|
||||||
|
return singlePageConversionService.convertToVo(singlePage, snapshotName);
|
||||||
|
})
|
||||||
|
.doOnNext(pageVo -> {
|
||||||
|
// fake some attributes only for preview when they are not published
|
||||||
|
SinglePage.SinglePageSpec spec = pageVo.getSpec();
|
||||||
|
if (spec.getPublishTime() == null) {
|
||||||
|
spec.setPublishTime(Instant.now());
|
||||||
|
}
|
||||||
|
if (spec.getPublish() == null) {
|
||||||
|
spec.setPublish(false);
|
||||||
|
}
|
||||||
|
SinglePage.SinglePageStatus status = pageVo.getStatus();
|
||||||
|
if (status == null) {
|
||||||
|
status = new SinglePage.SinglePageStatus();
|
||||||
|
pageVo.setStatus(status);
|
||||||
|
}
|
||||||
|
if (status.getLastModifyTime() == null) {
|
||||||
|
status.setLastModifyTime(Instant.now());
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.flatMap(singlePageVo -> canPreview(singlePageVo.getContributors())
|
||||||
|
.doOnNext(canPreview -> {
|
||||||
|
if (!canPreview) {
|
||||||
|
throw new NotFoundException("Page not found.");
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.thenReturn(singlePageVo)
|
||||||
|
)
|
||||||
|
// Check permissions before throwing this exception
|
||||||
|
.switchIfEmpty(Mono.error(() -> new NotFoundException("Single page not found.")))
|
||||||
|
.flatMap(singlePageVo -> {
|
||||||
|
Map<String, Object> model = ModelMapUtils.singlePageModel(singlePageVo);
|
||||||
|
String template = singlePageVo.getSpec().getTemplate();
|
||||||
|
return viewNameResolver.resolveViewNameOrDefault(request, template,
|
||||||
|
DefaultTemplateEnum.SINGLE_PAGE.getValue())
|
||||||
|
.flatMap(viewName -> ServerResponse.ok().render(viewName, model));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private Mono<Boolean> canPreview(List<ContributorVo> contributors) {
|
||||||
|
Assert.notNull(contributors, "The contributors must not be null");
|
||||||
|
Set<String> contributorNames = contributors.stream()
|
||||||
|
.map(ContributorVo::getName)
|
||||||
|
.collect(Collectors.toSet());
|
||||||
|
return currentAuthenticatedUserName()
|
||||||
|
.map(contributorNames::contains)
|
||||||
|
.defaultIfEmpty(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
Mono<String> currentAuthenticatedUserName() {
|
||||||
|
return ReactiveSecurityContextHolder.getContext()
|
||||||
|
.map(SecurityContext::getAuthentication)
|
||||||
|
.map(Principal::getName)
|
||||||
|
.filter(name -> !AnonymousUserConst.isAnonymousUser(name));
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,7 +2,6 @@ package run.halo.app.theme.router;
|
||||||
|
|
||||||
import static org.springframework.web.reactive.function.server.RequestPredicates.GET;
|
import static org.springframework.web.reactive.function.server.RequestPredicates.GET;
|
||||||
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
@ -25,16 +24,12 @@ import reactor.core.publisher.Mono;
|
||||||
import run.halo.app.core.extension.content.SinglePage;
|
import run.halo.app.core.extension.content.SinglePage;
|
||||||
import run.halo.app.extension.ExtensionClient;
|
import run.halo.app.extension.ExtensionClient;
|
||||||
import run.halo.app.extension.ExtensionOperator;
|
import run.halo.app.extension.ExtensionOperator;
|
||||||
import run.halo.app.extension.GVK;
|
|
||||||
import run.halo.app.extension.GroupVersionKind;
|
|
||||||
import run.halo.app.extension.Scheme;
|
|
||||||
import run.halo.app.extension.controller.Controller;
|
import run.halo.app.extension.controller.Controller;
|
||||||
import run.halo.app.extension.controller.ControllerBuilder;
|
import run.halo.app.extension.controller.ControllerBuilder;
|
||||||
import run.halo.app.extension.controller.Reconciler;
|
import run.halo.app.extension.controller.Reconciler;
|
||||||
import run.halo.app.infra.exception.NotFoundException;
|
import run.halo.app.infra.exception.NotFoundException;
|
||||||
import run.halo.app.theme.DefaultTemplateEnum;
|
import run.halo.app.theme.DefaultTemplateEnum;
|
||||||
import run.halo.app.theme.finders.SinglePageFinder;
|
import run.halo.app.theme.finders.SinglePageFinder;
|
||||||
import run.halo.app.theme.router.factories.ModelConst;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The {@link SinglePageRoute} for route request to specific template <code>page.html</code>.
|
* The {@link SinglePageRoute} for route request to specific template <code>page.html</code>.
|
||||||
|
@ -46,8 +41,6 @@ import run.halo.app.theme.router.factories.ModelConst;
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class SinglePageRoute
|
public class SinglePageRoute
|
||||||
implements RouterFunction<ServerResponse>, Reconciler<Reconciler.Request>, DisposableBean {
|
implements RouterFunction<ServerResponse>, Reconciler<Reconciler.Request>, DisposableBean {
|
||||||
private final GroupVersionKind gvk = GroupVersionKind.fromExtension(SinglePage.class);
|
|
||||||
|
|
||||||
private final Map<NameSlugPair, HandlerFunction<ServerResponse>> quickRouteMap =
|
private final Map<NameSlugPair, HandlerFunction<ServerResponse>> quickRouteMap =
|
||||||
new ConcurrentHashMap<>();
|
new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
@ -123,12 +116,7 @@ public class SinglePageRoute
|
||||||
HandlerFunction<ServerResponse> handlerFunction(String name) {
|
HandlerFunction<ServerResponse> handlerFunction(String name) {
|
||||||
return request -> singlePageFinder.getByName(name)
|
return request -> singlePageFinder.getByName(name)
|
||||||
.flatMap(singlePageVo -> {
|
.flatMap(singlePageVo -> {
|
||||||
Map<String, Object> model = new HashMap<>();
|
Map<String, Object> model = ModelMapUtils.singlePageModel(singlePageVo);
|
||||||
model.put("name", singlePageVo.getMetadata().getName());
|
|
||||||
model.put("groupVersionKind", gvk);
|
|
||||||
model.put("plural", getPlural());
|
|
||||||
model.put(ModelConst.TEMPLATE_ID, DefaultTemplateEnum.SINGLE_PAGE.getValue());
|
|
||||||
model.put("singlePage", singlePageVo);
|
|
||||||
String template = singlePageVo.getSpec().getTemplate();
|
String template = singlePageVo.getSpec().getTemplate();
|
||||||
return viewNameResolver.resolveViewNameOrDefault(request, template,
|
return viewNameResolver.resolveViewNameOrDefault(request, template,
|
||||||
DefaultTemplateEnum.SINGLE_PAGE.getValue())
|
DefaultTemplateEnum.SINGLE_PAGE.getValue())
|
||||||
|
@ -138,9 +126,4 @@ public class SinglePageRoute
|
||||||
Mono.error(new NotFoundException("Single page not found"))
|
Mono.error(new NotFoundException("Single page not found"))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private String getPlural() {
|
|
||||||
GVK gvk = Scheme.getGvkFromType(SinglePage.class);
|
|
||||||
return gvk.plural();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,6 +25,7 @@ import run.halo.app.infra.utils.PathUtils;
|
||||||
import run.halo.app.theme.DefaultTemplateEnum;
|
import run.halo.app.theme.DefaultTemplateEnum;
|
||||||
import run.halo.app.theme.finders.PostFinder;
|
import run.halo.app.theme.finders.PostFinder;
|
||||||
import run.halo.app.theme.finders.vo.PostArchiveVo;
|
import run.halo.app.theme.finders.vo.PostArchiveVo;
|
||||||
|
import run.halo.app.theme.router.ModelConst;
|
||||||
import run.halo.app.theme.router.PageUrlUtils;
|
import run.halo.app.theme.router.PageUrlUtils;
|
||||||
import run.halo.app.theme.router.UrlContextListResult;
|
import run.halo.app.theme.router.UrlContextListResult;
|
||||||
|
|
||||||
|
|
|
@ -23,6 +23,7 @@ import run.halo.app.theme.DefaultTemplateEnum;
|
||||||
import run.halo.app.theme.finders.PostFinder;
|
import run.halo.app.theme.finders.PostFinder;
|
||||||
import run.halo.app.theme.finders.vo.ListedPostVo;
|
import run.halo.app.theme.finders.vo.ListedPostVo;
|
||||||
import run.halo.app.theme.finders.vo.UserVo;
|
import run.halo.app.theme.finders.vo.UserVo;
|
||||||
|
import run.halo.app.theme.router.ModelConst;
|
||||||
import run.halo.app.theme.router.PageUrlUtils;
|
import run.halo.app.theme.router.PageUrlUtils;
|
||||||
import run.halo.app.theme.router.UrlContextListResult;
|
import run.halo.app.theme.router.UrlContextListResult;
|
||||||
|
|
||||||
|
|
|
@ -12,6 +12,7 @@ import org.springframework.web.reactive.function.server.RouterFunctions;
|
||||||
import org.springframework.web.reactive.function.server.ServerResponse;
|
import org.springframework.web.reactive.function.server.ServerResponse;
|
||||||
import run.halo.app.theme.DefaultTemplateEnum;
|
import run.halo.app.theme.DefaultTemplateEnum;
|
||||||
import run.halo.app.theme.finders.CategoryFinder;
|
import run.halo.app.theme.finders.CategoryFinder;
|
||||||
|
import run.halo.app.theme.router.ModelConst;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The {@link CategoriesRouteFactory} for generate {@link RouterFunction} specific to the
|
* The {@link CategoriesRouteFactory} for generate {@link RouterFunction} specific to the
|
||||||
|
|
|
@ -25,6 +25,7 @@ import run.halo.app.theme.DefaultTemplateEnum;
|
||||||
import run.halo.app.theme.finders.PostFinder;
|
import run.halo.app.theme.finders.PostFinder;
|
||||||
import run.halo.app.theme.finders.vo.CategoryVo;
|
import run.halo.app.theme.finders.vo.CategoryVo;
|
||||||
import run.halo.app.theme.finders.vo.ListedPostVo;
|
import run.halo.app.theme.finders.vo.ListedPostVo;
|
||||||
|
import run.halo.app.theme.router.ModelConst;
|
||||||
import run.halo.app.theme.router.PageUrlUtils;
|
import run.halo.app.theme.router.PageUrlUtils;
|
||||||
import run.halo.app.theme.router.UrlContextListResult;
|
import run.halo.app.theme.router.UrlContextListResult;
|
||||||
import run.halo.app.theme.router.ViewNameResolver;
|
import run.halo.app.theme.router.ViewNameResolver;
|
||||||
|
|
|
@ -19,6 +19,7 @@ import run.halo.app.infra.SystemSetting;
|
||||||
import run.halo.app.theme.DefaultTemplateEnum;
|
import run.halo.app.theme.DefaultTemplateEnum;
|
||||||
import run.halo.app.theme.finders.PostFinder;
|
import run.halo.app.theme.finders.PostFinder;
|
||||||
import run.halo.app.theme.finders.vo.ListedPostVo;
|
import run.halo.app.theme.finders.vo.ListedPostVo;
|
||||||
|
import run.halo.app.theme.router.ModelConst;
|
||||||
import run.halo.app.theme.router.PageUrlUtils;
|
import run.halo.app.theme.router.PageUrlUtils;
|
||||||
import run.halo.app.theme.router.UrlContextListResult;
|
import run.halo.app.theme.router.UrlContextListResult;
|
||||||
|
|
||||||
|
|
|
@ -6,7 +6,6 @@ import static run.halo.app.theme.finders.PostPublicQueryService.FIXED_PREDICATE;
|
||||||
|
|
||||||
import com.google.common.cache.Cache;
|
import com.google.common.cache.Cache;
|
||||||
import com.google.common.cache.CacheBuilder;
|
import com.google.common.cache.CacheBuilder;
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.LinkedHashMap;
|
import java.util.LinkedHashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
@ -30,8 +29,6 @@ import org.springframework.web.reactive.function.server.ServerResponse;
|
||||||
import reactor.core.publisher.Flux;
|
import reactor.core.publisher.Flux;
|
||||||
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.GVK;
|
|
||||||
import run.halo.app.extension.GroupVersionKind;
|
|
||||||
import run.halo.app.extension.MetadataUtil;
|
import run.halo.app.extension.MetadataUtil;
|
||||||
import run.halo.app.extension.ReactiveExtensionClient;
|
import run.halo.app.extension.ReactiveExtensionClient;
|
||||||
import run.halo.app.infra.exception.NotFoundException;
|
import run.halo.app.infra.exception.NotFoundException;
|
||||||
|
@ -39,6 +36,7 @@ import run.halo.app.infra.utils.JsonUtils;
|
||||||
import run.halo.app.theme.DefaultTemplateEnum;
|
import run.halo.app.theme.DefaultTemplateEnum;
|
||||||
import run.halo.app.theme.finders.PostFinder;
|
import run.halo.app.theme.finders.PostFinder;
|
||||||
import run.halo.app.theme.finders.vo.PostVo;
|
import run.halo.app.theme.finders.vo.PostVo;
|
||||||
|
import run.halo.app.theme.router.ModelMapUtils;
|
||||||
import run.halo.app.theme.router.ViewNameResolver;
|
import run.halo.app.theme.router.ViewNameResolver;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -101,14 +99,7 @@ public class PostRouteFactory implements RouteFactory {
|
||||||
Mono<PostVo> postVoMono = bestMatchPost(patternVariable);
|
Mono<PostVo> postVoMono = bestMatchPost(patternVariable);
|
||||||
return postVoMono
|
return postVoMono
|
||||||
.flatMap(postVo -> {
|
.flatMap(postVo -> {
|
||||||
Map<String, Object> model = new HashMap<>();
|
Map<String, Object> model = ModelMapUtils.postModel(postVo);
|
||||||
model.put("name", postVo.getMetadata().getName());
|
|
||||||
model.put(ModelConst.TEMPLATE_ID, DefaultTemplateEnum.POST.getValue());
|
|
||||||
model.put("groupVersionKind", GroupVersionKind.fromExtension(Post.class));
|
|
||||||
GVK gvk = Post.class.getAnnotation(GVK.class);
|
|
||||||
model.put("plural", gvk.plural());
|
|
||||||
model.put("post", postVo);
|
|
||||||
|
|
||||||
String template = postVo.getSpec().getTemplate();
|
String template = postVo.getSpec().getTemplate();
|
||||||
return viewNameResolver.resolveViewNameOrDefault(request, template,
|
return viewNameResolver.resolveViewNameOrDefault(request, template,
|
||||||
DefaultTemplateEnum.POST.getValue())
|
DefaultTemplateEnum.POST.getValue())
|
||||||
|
|
|
@ -10,6 +10,7 @@ import org.springframework.web.reactive.function.server.ServerResponse;
|
||||||
import reactor.core.publisher.Mono;
|
import reactor.core.publisher.Mono;
|
||||||
import run.halo.app.infra.SystemConfigurableEnvironmentFetcher;
|
import run.halo.app.infra.SystemConfigurableEnvironmentFetcher;
|
||||||
import run.halo.app.infra.SystemSetting;
|
import run.halo.app.infra.SystemSetting;
|
||||||
|
import run.halo.app.theme.router.ModelConst;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author guqing
|
* @author guqing
|
||||||
|
|
|
@ -14,6 +14,7 @@ import org.springframework.web.reactive.function.server.RouterFunctions;
|
||||||
import org.springframework.web.reactive.function.server.ServerResponse;
|
import org.springframework.web.reactive.function.server.ServerResponse;
|
||||||
import run.halo.app.theme.DefaultTemplateEnum;
|
import run.halo.app.theme.DefaultTemplateEnum;
|
||||||
import run.halo.app.theme.finders.TagFinder;
|
import run.halo.app.theme.finders.TagFinder;
|
||||||
|
import run.halo.app.theme.router.ModelConst;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The {@link TagsRouteFactory} for generate {@link RouterFunction} specific to the template
|
* The {@link TagsRouteFactory} for generate {@link RouterFunction} specific to the template
|
||||||
|
|
|
@ -38,7 +38,7 @@ import run.halo.app.theme.finders.PostFinder;
|
||||||
import run.halo.app.theme.finders.SinglePageFinder;
|
import run.halo.app.theme.finders.SinglePageFinder;
|
||||||
import run.halo.app.theme.finders.vo.PostVo;
|
import run.halo.app.theme.finders.vo.PostVo;
|
||||||
import run.halo.app.theme.finders.vo.UserVo;
|
import run.halo.app.theme.finders.vo.UserVo;
|
||||||
import run.halo.app.theme.router.factories.ModelConst;
|
import run.halo.app.theme.router.ModelConst;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tests for {@link HaloProcessorDialect}.
|
* Tests for {@link HaloProcessorDialect}.
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
package run.halo.app.theme.finders.impl;
|
package run.halo.app.theme.finders.impl;
|
||||||
|
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
import static org.mockito.ArgumentMatchers.anyString;
|
|
||||||
import static org.mockito.ArgumentMatchers.eq;
|
import static org.mockito.ArgumentMatchers.eq;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
import static org.mockito.Mockito.verify;
|
import static org.mockito.Mockito.verify;
|
||||||
import static org.mockito.Mockito.when;
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
@ -14,13 +14,12 @@ import org.mockito.Mock;
|
||||||
import org.mockito.junit.jupiter.MockitoExtension;
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
import reactor.core.publisher.Mono;
|
import reactor.core.publisher.Mono;
|
||||||
import reactor.test.StepVerifier;
|
import reactor.test.StepVerifier;
|
||||||
import run.halo.app.content.SinglePageService;
|
|
||||||
import run.halo.app.core.extension.content.Post;
|
import run.halo.app.core.extension.content.Post;
|
||||||
import run.halo.app.core.extension.content.SinglePage;
|
import run.halo.app.core.extension.content.SinglePage;
|
||||||
import run.halo.app.extension.Metadata;
|
import run.halo.app.extension.Metadata;
|
||||||
import run.halo.app.extension.ReactiveExtensionClient;
|
import run.halo.app.extension.ReactiveExtensionClient;
|
||||||
import run.halo.app.metrics.CounterService;
|
import run.halo.app.theme.finders.SinglePageConversionService;
|
||||||
import run.halo.app.theme.finders.ContributorFinder;
|
import run.halo.app.theme.finders.vo.SinglePageVo;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tests for {@link SinglePageFinderImpl}.
|
* Tests for {@link SinglePageFinderImpl}.
|
||||||
|
@ -35,13 +34,7 @@ class SinglePageFinderImplTest {
|
||||||
private ReactiveExtensionClient client;
|
private ReactiveExtensionClient client;
|
||||||
|
|
||||||
@Mock
|
@Mock
|
||||||
private SinglePageService singlePageService;
|
private SinglePageConversionService singlePageConversionService;
|
||||||
|
|
||||||
@Mock
|
|
||||||
private ContributorFinder contributorFinder;
|
|
||||||
|
|
||||||
@Mock
|
|
||||||
private CounterService counterService;
|
|
||||||
|
|
||||||
@InjectMocks
|
@InjectMocks
|
||||||
private SinglePageFinderImpl singlePageFinder;
|
private SinglePageFinderImpl singlePageFinder;
|
||||||
|
@ -64,21 +57,14 @@ class SinglePageFinderImplTest {
|
||||||
when(client.get(eq(SinglePage.class), eq(fakePageName)))
|
when(client.get(eq(SinglePage.class), eq(fakePageName)))
|
||||||
.thenReturn(Mono.just(singlePage));
|
.thenReturn(Mono.just(singlePage));
|
||||||
|
|
||||||
when(counterService.getByName(anyString())).thenReturn(Mono.empty());
|
when(singlePageConversionService.convertToVo(eq(singlePage)))
|
||||||
when(contributorFinder.getContributor(anyString())).thenReturn(Mono.empty());
|
.thenReturn(Mono.just(mock(SinglePageVo.class)));
|
||||||
when(singlePageService.getReleaseContent(anyString())).thenReturn(Mono.empty());
|
|
||||||
|
|
||||||
singlePageFinder.getByName(fakePageName)
|
singlePageFinder.getByName(fakePageName)
|
||||||
.as(StepVerifier::create)
|
.as(StepVerifier::create)
|
||||||
.consumeNextWith(page -> {
|
.consumeNextWith(page -> assertThat(page).isNotNull())
|
||||||
assertThat(page.getStats()).isNotNull();
|
|
||||||
assertThat(page.getContent()).isNotNull();
|
|
||||||
})
|
|
||||||
.verifyComplete();
|
.verifyComplete();
|
||||||
|
|
||||||
verify(client).get(SinglePage.class, fakePageName);
|
verify(client).get(SinglePage.class, fakePageName);
|
||||||
verify(counterService).getByName(anyString());
|
|
||||||
verify(singlePageService).getReleaseContent(anyString());
|
|
||||||
verify(contributorFinder).getContributor(anyString());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -0,0 +1,180 @@
|
||||||
|
package run.halo.app.theme.router;
|
||||||
|
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.ArgumentMatchers.eq;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
import org.mockito.InjectMocks;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
|
import org.springframework.security.test.context.support.WithMockUser;
|
||||||
|
import org.springframework.test.context.junit.jupiter.SpringExtension;
|
||||||
|
import org.springframework.test.web.reactive.server.WebTestClient;
|
||||||
|
import org.springframework.web.reactive.function.server.HandlerStrategies;
|
||||||
|
import org.springframework.web.reactive.result.view.ViewResolver;
|
||||||
|
import org.springframework.web.server.ServerWebExchange;
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
|
import run.halo.app.content.ContentWrapper;
|
||||||
|
import run.halo.app.content.PostService;
|
||||||
|
import run.halo.app.core.extension.content.Post;
|
||||||
|
import run.halo.app.core.extension.content.SinglePage;
|
||||||
|
import run.halo.app.extension.Metadata;
|
||||||
|
import run.halo.app.extension.ReactiveExtensionClient;
|
||||||
|
import run.halo.app.infra.AnonymousUserConst;
|
||||||
|
import run.halo.app.theme.finders.PostPublicQueryService;
|
||||||
|
import run.halo.app.theme.finders.SinglePageConversionService;
|
||||||
|
import run.halo.app.theme.finders.vo.ContributorVo;
|
||||||
|
import run.halo.app.theme.finders.vo.PostVo;
|
||||||
|
import run.halo.app.theme.finders.vo.SinglePageVo;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests for {@link PreviewRouterFunction}.
|
||||||
|
*
|
||||||
|
* @author guqing
|
||||||
|
* @since 2.6.x
|
||||||
|
*/
|
||||||
|
@ExtendWith(SpringExtension.class)
|
||||||
|
class PreviewRouterFunctionTest {
|
||||||
|
@Mock
|
||||||
|
private ReactiveExtensionClient client;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private PostPublicQueryService postPublicQueryService;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private ViewNameResolver viewNameResolver;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private ViewResolver viewResolver;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private PostService postService;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private SinglePageConversionService singlePageConversionService;
|
||||||
|
|
||||||
|
@InjectMocks
|
||||||
|
private PreviewRouterFunction previewRouterFunction;
|
||||||
|
|
||||||
|
private WebTestClient webTestClient;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
public void setUp() {
|
||||||
|
webTestClient = WebTestClient.bindToRouterFunction(previewRouterFunction.previewRouter())
|
||||||
|
.handlerStrategies(HandlerStrategies.builder()
|
||||||
|
.viewResolver(viewResolver)
|
||||||
|
.build())
|
||||||
|
.build();
|
||||||
|
|
||||||
|
when(viewResolver.resolveViewName(any(), any()))
|
||||||
|
.thenReturn(Mono.just(new EmptyView() {
|
||||||
|
@Override
|
||||||
|
public Mono<Void> render(Map<String, ?> model, MediaType contentType,
|
||||||
|
ServerWebExchange exchange) {
|
||||||
|
return super.render(model, contentType, exchange);
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@WithMockUser(username = "testuser")
|
||||||
|
public void previewPost() {
|
||||||
|
Post post = new Post();
|
||||||
|
post.setMetadata(new Metadata());
|
||||||
|
post.getMetadata().setName("post1");
|
||||||
|
post.setSpec(new Post.PostSpec());
|
||||||
|
post.getSpec().setOwner("testuser");
|
||||||
|
post.getSpec().setHeadSnapshot("snapshot1");
|
||||||
|
post.getSpec().setBaseSnapshot("snapshot2");
|
||||||
|
post.getSpec().setTemplate("postTemplate");
|
||||||
|
when(client.fetch(eq(Post.class), eq("post1"))).thenReturn(Mono.just(post));
|
||||||
|
|
||||||
|
PostVo postVo = PostVo.from(post);
|
||||||
|
postVo.setContributors(contributorVos());
|
||||||
|
when(postPublicQueryService.convertToListedPostVo(post)).thenReturn(Mono.just(postVo));
|
||||||
|
|
||||||
|
ContentWrapper contentWrapper = ContentWrapper.builder()
|
||||||
|
.raw("raw content")
|
||||||
|
.content("formatted content")
|
||||||
|
.build();
|
||||||
|
when(postService.getContent(eq("snapshot1"), eq("snapshot2")))
|
||||||
|
.thenReturn(Mono.just(contentWrapper));
|
||||||
|
|
||||||
|
when(viewNameResolver.resolveViewNameOrDefault(any(), eq("postTemplate"),
|
||||||
|
eq("post"))).thenReturn(Mono.just("postView"));
|
||||||
|
|
||||||
|
webTestClient.get().uri("/preview/posts/post1")
|
||||||
|
.exchange()
|
||||||
|
.expectStatus().isOk();
|
||||||
|
|
||||||
|
verify(viewResolver).resolveViewName(any(), any());
|
||||||
|
verify(postService).getContent(eq("snapshot1"), eq("snapshot2"));
|
||||||
|
verify(client).fetch(eq(Post.class), eq("post1"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void previewPostWhenUnAuthenticated() {
|
||||||
|
webTestClient.get().uri("/preview/posts/post1")
|
||||||
|
.exchange()
|
||||||
|
.expectStatus().isEqualTo(404);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@WithMockUser(username = "testuser")
|
||||||
|
public void previewSinglePage() {
|
||||||
|
SinglePage singlePage = new SinglePage();
|
||||||
|
singlePage.setMetadata(new Metadata());
|
||||||
|
singlePage.getMetadata().setName("page1");
|
||||||
|
singlePage.setSpec(new SinglePage.SinglePageSpec());
|
||||||
|
singlePage.getSpec().setOwner("testuser");
|
||||||
|
singlePage.getSpec().setHeadSnapshot("snapshot1");
|
||||||
|
singlePage.getSpec().setTemplate("pageTemplate");
|
||||||
|
when(client.fetch(SinglePage.class, "page1")).thenReturn(Mono.just(singlePage));
|
||||||
|
|
||||||
|
SinglePageVo singlePageVo = SinglePageVo.from(singlePage);
|
||||||
|
singlePageVo.setContributors(contributorVos());
|
||||||
|
when(singlePageConversionService.convertToVo(singlePage, "snapshot1"))
|
||||||
|
.thenReturn(Mono.just(singlePageVo));
|
||||||
|
|
||||||
|
when(viewNameResolver.resolveViewNameOrDefault(any(), eq("pageTemplate"),
|
||||||
|
eq("page"))).thenReturn(Mono.just("pageView"));
|
||||||
|
|
||||||
|
webTestClient.get().uri("/preview/singlepages/page1")
|
||||||
|
.exchange()
|
||||||
|
.expectStatus().isOk();
|
||||||
|
|
||||||
|
verify(viewResolver).resolveViewName(any(), any());
|
||||||
|
verify(client).fetch(eq(SinglePage.class), eq("page1"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void previewSinglePageWhenUnAuthenticated() {
|
||||||
|
webTestClient.get().uri("/preview/singlepages/page1")
|
||||||
|
.exchange()
|
||||||
|
.expectStatus().isEqualTo(404);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@WithMockUser(username = AnonymousUserConst.PRINCIPAL)
|
||||||
|
public void previewWithAnonymousUser() {
|
||||||
|
webTestClient.get().uri("/preview/singlepages/page1")
|
||||||
|
.exchange()
|
||||||
|
.expectStatus().isEqualTo(404);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<ContributorVo> contributorVos() {
|
||||||
|
ContributorVo contributorA = ContributorVo.builder()
|
||||||
|
.name("fake-user")
|
||||||
|
.build();
|
||||||
|
ContributorVo contributorB = ContributorVo.builder()
|
||||||
|
.name("testuser")
|
||||||
|
.build();
|
||||||
|
return List.of(contributorA, contributorB);
|
||||||
|
}
|
||||||
|
}
|
|
@ -27,7 +27,6 @@ import run.halo.app.extension.Metadata;
|
||||||
import run.halo.app.theme.DefaultTemplateEnum;
|
import run.halo.app.theme.DefaultTemplateEnum;
|
||||||
import run.halo.app.theme.finders.SinglePageFinder;
|
import run.halo.app.theme.finders.SinglePageFinder;
|
||||||
import run.halo.app.theme.finders.vo.SinglePageVo;
|
import run.halo.app.theme.finders.vo.SinglePageVo;
|
||||||
import run.halo.app.theme.router.factories.ModelConst;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tests for {@link SinglePageRoute}.
|
* Tests for {@link SinglePageRoute}.
|
||||||
|
|
|
@ -26,6 +26,7 @@ import run.halo.app.theme.DefaultTemplateEnum;
|
||||||
import run.halo.app.theme.finders.PostFinder;
|
import run.halo.app.theme.finders.PostFinder;
|
||||||
import run.halo.app.theme.finders.vo.PostVo;
|
import run.halo.app.theme.finders.vo.PostVo;
|
||||||
import run.halo.app.theme.router.EmptyView;
|
import run.halo.app.theme.router.EmptyView;
|
||||||
|
import run.halo.app.theme.router.ModelConst;
|
||||||
import run.halo.app.theme.router.ViewNameResolver;
|
import run.halo.app.theme.router.ViewNameResolver;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -10,6 +10,7 @@ import org.springframework.web.reactive.function.server.RouterFunction;
|
||||||
import org.springframework.web.reactive.function.server.ServerResponse;
|
import org.springframework.web.reactive.function.server.ServerResponse;
|
||||||
import reactor.core.publisher.Mono;
|
import reactor.core.publisher.Mono;
|
||||||
import run.halo.app.infra.SystemSetting;
|
import run.halo.app.infra.SystemSetting;
|
||||||
|
import run.halo.app.theme.router.ModelConst;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tests for {@link RouteFactory}.
|
* Tests for {@link RouteFactory}.
|
||||||
|
|
|
@ -9,6 +9,7 @@ import {
|
||||||
IconSave,
|
IconSave,
|
||||||
Toast,
|
Toast,
|
||||||
Dialog,
|
Dialog,
|
||||||
|
IconEye,
|
||||||
} from "@halo-dev/components";
|
} from "@halo-dev/components";
|
||||||
import SinglePageSettingModal from "./components/SinglePageSettingModal.vue";
|
import SinglePageSettingModal from "./components/SinglePageSettingModal.vue";
|
||||||
import type { SinglePage, SinglePageRequest } from "@halo-dev/api-client";
|
import type { SinglePage, SinglePageRequest } from "@halo-dev/api-client";
|
||||||
|
@ -34,6 +35,7 @@ import {
|
||||||
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";
|
||||||
import { useI18n } from "vue-i18n";
|
import { useI18n } from "vue-i18n";
|
||||||
|
import UrlPreviewModal from "@/components/preview/UrlPreviewModal.vue";
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
@ -112,9 +114,11 @@ provide<ComputedRef<string | undefined>>(
|
||||||
|
|
||||||
const routeQueryName = useRouteQuery<string>("name");
|
const routeQueryName = useRouteQuery<string>("name");
|
||||||
|
|
||||||
const handleSave = async () => {
|
const handleSave = async (options?: { mute?: boolean }) => {
|
||||||
try {
|
try {
|
||||||
saving.value = true;
|
if (!options?.mute) {
|
||||||
|
saving.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
//Set default title and slug
|
//Set default title and slug
|
||||||
if (!formState.value.page.spec.title) {
|
if (!formState.value.page.spec.title) {
|
||||||
|
@ -139,7 +143,9 @@ const handleSave = async () => {
|
||||||
routeQueryName.value = data.metadata.name;
|
routeQueryName.value = data.metadata.name;
|
||||||
}
|
}
|
||||||
|
|
||||||
Toast.success(t("core.common.toast.save_success"));
|
if (!options?.mute) {
|
||||||
|
Toast.success(t("core.common.toast.save_success"));
|
||||||
|
}
|
||||||
|
|
||||||
handleClearCache(routeQueryName.value as string);
|
handleClearCache(routeQueryName.value as string);
|
||||||
await handleFetchContent();
|
await handleFetchContent();
|
||||||
|
@ -323,6 +329,17 @@ const { handleSetContentCache, handleResetCache, handleClearCache } =
|
||||||
routeQueryName,
|
routeQueryName,
|
||||||
toRef(formState.value.content, "raw")
|
toRef(formState.value.content, "raw")
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// SinglePage preview
|
||||||
|
const previewModal = ref(false);
|
||||||
|
const previewPending = ref(false);
|
||||||
|
|
||||||
|
const handlePreview = async () => {
|
||||||
|
previewPending.value = true;
|
||||||
|
await handleSave({ mute: true });
|
||||||
|
previewModal.value = true;
|
||||||
|
previewPending.value = false;
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
@ -334,6 +351,14 @@ const { handleSetContentCache, handleResetCache, handleClearCache } =
|
||||||
@saved="onSettingSaved"
|
@saved="onSettingSaved"
|
||||||
@published="onSettingPublished"
|
@published="onSettingPublished"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<UrlPreviewModal
|
||||||
|
v-if="isUpdateMode"
|
||||||
|
v-model:visible="previewModal"
|
||||||
|
:title="formState.page.spec.title"
|
||||||
|
:url="`/preview/singlepages/${formState.page.metadata.name}`"
|
||||||
|
/>
|
||||||
|
|
||||||
<VPageHeader :title="$t('core.page.title')">
|
<VPageHeader :title="$t('core.page.title')">
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<IconPages class="mr-2 self-center" />
|
<IconPages class="mr-2 self-center" />
|
||||||
|
@ -345,6 +370,17 @@ const { handleSetContentCache, handleResetCache, handleClearCache } =
|
||||||
:provider="currentEditorProvider"
|
:provider="currentEditorProvider"
|
||||||
@select="handleChangeEditorProvider"
|
@select="handleChangeEditorProvider"
|
||||||
/>
|
/>
|
||||||
|
<VButton
|
||||||
|
size="sm"
|
||||||
|
type="default"
|
||||||
|
:loading="previewPending"
|
||||||
|
@click="handlePreview"
|
||||||
|
>
|
||||||
|
<template #icon>
|
||||||
|
<IconEye class="h-full w-full" />
|
||||||
|
</template>
|
||||||
|
{{ $t("core.common.buttons.preview") }}
|
||||||
|
</VButton>
|
||||||
<VButton :loading="saving" size="sm" type="default" @click="handleSave">
|
<VButton :loading="saving" size="sm" type="default" @click="handleSave">
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<IconSave class="h-full w-full" />
|
<IconSave class="h-full w-full" />
|
||||||
|
|
|
@ -427,6 +427,13 @@ const { mutate: changeVisibleMutation } = useMutation({
|
||||||
Toast.error(t("core.common.toast.operation_failed"));
|
Toast.error(t("core.common.toast.operation_failed"));
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const getExternalUrl = (singlePage: SinglePage) => {
|
||||||
|
if (singlePage.metadata.labels?.[singlePageLabels.PUBLISHED] === "true") {
|
||||||
|
return singlePage.status?.permalink;
|
||||||
|
}
|
||||||
|
return `/preview/singlepages/${singlePage.metadata.name}`;
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
@ -737,10 +744,8 @@ const { mutate: changeVisibleMutation } = useMutation({
|
||||||
<VStatusDot state="success" animate />
|
<VStatusDot state="success" animate />
|
||||||
</RouterLink>
|
</RouterLink>
|
||||||
<a
|
<a
|
||||||
v-if="singlePage.page.status?.permalink"
|
|
||||||
target="_blank"
|
target="_blank"
|
||||||
:href="singlePage.page.status?.permalink"
|
:href="getExternalUrl(singlePage.page)"
|
||||||
:title="singlePage.page.status?.permalink"
|
|
||||||
class="hidden text-gray-600 transition-all hover:text-gray-900 group-hover:inline-block"
|
class="hidden text-gray-600 transition-all hover:text-gray-900 group-hover:inline-block"
|
||||||
>
|
>
|
||||||
<IconExternalLinkLine class="h-3.5 w-3.5" />
|
<IconExternalLinkLine class="h-3.5 w-3.5" />
|
||||||
|
|
|
@ -9,6 +9,7 @@ import {
|
||||||
VSpace,
|
VSpace,
|
||||||
Toast,
|
Toast,
|
||||||
Dialog,
|
Dialog,
|
||||||
|
IconEye,
|
||||||
} from "@halo-dev/components";
|
} from "@halo-dev/components";
|
||||||
import PostSettingModal from "./components/PostSettingModal.vue";
|
import PostSettingModal from "./components/PostSettingModal.vue";
|
||||||
import type { Post, PostRequest } from "@halo-dev/api-client";
|
import type { Post, PostRequest } from "@halo-dev/api-client";
|
||||||
|
@ -34,6 +35,7 @@ import {
|
||||||
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";
|
||||||
import { useI18n } from "vue-i18n";
|
import { useI18n } from "vue-i18n";
|
||||||
|
import UrlPreviewModal from "@/components/preview/UrlPreviewModal.vue";
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
@ -112,9 +114,11 @@ provide<ComputedRef<string | undefined>>(
|
||||||
computed(() => formState.value.post.status?.permalink)
|
computed(() => formState.value.post.status?.permalink)
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleSave = async () => {
|
const handleSave = async (options?: { mute?: boolean }) => {
|
||||||
try {
|
try {
|
||||||
saving.value = true;
|
if (!options?.mute) {
|
||||||
|
saving.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
// Set default title and slug
|
// Set default title and slug
|
||||||
if (!formState.value.post.spec.title) {
|
if (!formState.value.post.spec.title) {
|
||||||
|
@ -140,7 +144,9 @@ const handleSave = async () => {
|
||||||
name.value = data.metadata.name;
|
name.value = data.metadata.name;
|
||||||
}
|
}
|
||||||
|
|
||||||
Toast.success(t("core.common.toast.save_success"));
|
if (!options?.mute) {
|
||||||
|
Toast.success(t("core.common.toast.save_success"));
|
||||||
|
}
|
||||||
handleClearCache(name.value as string);
|
handleClearCache(name.value as string);
|
||||||
await handleFetchContent();
|
await handleFetchContent();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
@ -336,6 +342,17 @@ const { handleSetContentCache, handleResetCache, handleClearCache } =
|
||||||
name,
|
name,
|
||||||
toRef(formState.value.content, "raw")
|
toRef(formState.value.content, "raw")
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Post preview
|
||||||
|
const previewModal = ref(false);
|
||||||
|
const previewPending = ref(false);
|
||||||
|
|
||||||
|
const handlePreview = async () => {
|
||||||
|
previewPending.value = true;
|
||||||
|
await handleSave({ mute: true });
|
||||||
|
previewModal.value = true;
|
||||||
|
previewPending.value = false;
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
@ -347,6 +364,14 @@ const { handleSetContentCache, handleResetCache, handleClearCache } =
|
||||||
@saved="onSettingSaved"
|
@saved="onSettingSaved"
|
||||||
@published="onSettingPublished"
|
@published="onSettingPublished"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<UrlPreviewModal
|
||||||
|
v-if="isUpdateMode"
|
||||||
|
v-model:visible="previewModal"
|
||||||
|
:title="formState.post.spec.title"
|
||||||
|
:url="`/preview/posts/${formState.post.metadata.name}`"
|
||||||
|
/>
|
||||||
|
|
||||||
<VPageHeader :title="$t('core.post.title')">
|
<VPageHeader :title="$t('core.post.title')">
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<IconBookRead class="mr-2 self-center" />
|
<IconBookRead class="mr-2 self-center" />
|
||||||
|
@ -358,6 +383,17 @@ const { handleSetContentCache, handleResetCache, handleClearCache } =
|
||||||
:provider="currentEditorProvider"
|
:provider="currentEditorProvider"
|
||||||
@select="handleChangeEditorProvider"
|
@select="handleChangeEditorProvider"
|
||||||
/>
|
/>
|
||||||
|
<VButton
|
||||||
|
size="sm"
|
||||||
|
type="default"
|
||||||
|
:loading="previewPending"
|
||||||
|
@click="handlePreview"
|
||||||
|
>
|
||||||
|
<template #icon>
|
||||||
|
<IconEye class="h-full w-full" />
|
||||||
|
</template>
|
||||||
|
{{ $t("core.common.buttons.preview") }}
|
||||||
|
</VButton>
|
||||||
<VButton :loading="saving" size="sm" type="default" @click="handleSave">
|
<VButton :loading="saving" size="sm" type="default" @click="handleSave">
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<IconSave class="h-full w-full" />
|
<IconSave class="h-full w-full" />
|
||||||
|
|
|
@ -440,6 +440,13 @@ const { mutate: changeVisibleMutation } = useMutation({
|
||||||
Toast.error(t("core.common.toast.operation_failed"));
|
Toast.error(t("core.common.toast.operation_failed"));
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const getExternalUrl = (post: Post) => {
|
||||||
|
if (post.metadata.labels?.[postLabels.PUBLISHED] === "true") {
|
||||||
|
return post.status?.permalink;
|
||||||
|
}
|
||||||
|
return `/preview/posts/${post.metadata.name}`;
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<PostSettingModal
|
<PostSettingModal
|
||||||
|
@ -802,10 +809,8 @@ const { mutate: changeVisibleMutation } = useMutation({
|
||||||
<VStatusDot state="success" animate />
|
<VStatusDot state="success" animate />
|
||||||
</RouterLink>
|
</RouterLink>
|
||||||
<a
|
<a
|
||||||
v-if="post.post.status?.permalink"
|
|
||||||
target="_blank"
|
target="_blank"
|
||||||
:href="post.post.status?.permalink"
|
:href="getExternalUrl(post.post)"
|
||||||
:title="post.post.status?.permalink"
|
|
||||||
class="hidden text-gray-600 transition-all hover:text-gray-900 group-hover:inline-block"
|
class="hidden text-gray-600 transition-all hover:text-gray-900 group-hover:inline-block"
|
||||||
>
|
>
|
||||||
<IconExternalLinkLine class="h-3.5 w-3.5" />
|
<IconExternalLinkLine class="h-3.5 w-3.5" />
|
||||||
|
|
Loading…
Reference in New Issue