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.finders.PostFinder;
|
||||
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
|
||||
|
|
|
@ -10,7 +10,7 @@ import reactor.core.publisher.Mono;
|
|||
import run.halo.app.infra.SystemConfigurableEnvironmentFetcher;
|
||||
import run.halo.app.infra.SystemSetting;
|
||||
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>
|
||||
|
|
|
@ -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);
|
||||
|
||||
String previousPostName = null;
|
||||
if (index != 0) {
|
||||
if (index > 0) {
|
||||
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 org.apache.commons.lang3.ObjectUtils;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
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.extension.ListResult;
|
||||
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.SinglePageConversionService;
|
||||
import run.halo.app.theme.finders.SinglePageFinder;
|
||||
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;
|
||||
|
||||
/**
|
||||
* A default implementation of {@link SinglePage}.
|
||||
|
@ -44,35 +40,15 @@ public class SinglePageFinderImpl implements SinglePageFinder {
|
|||
|
||||
private final ReactiveExtensionClient client;
|
||||
|
||||
private final SinglePageConversionService singlePagePublicQueryService;
|
||||
|
||||
private final SinglePageService singlePageService;
|
||||
|
||||
private final ContributorFinder contributorFinder;
|
||||
|
||||
private final CounterService counterService;
|
||||
|
||||
@Override
|
||||
public Mono<SinglePageVo> getByName(String pageName) {
|
||||
return client.get(SinglePage.class, pageName)
|
||||
.filter(FIXED_PREDICATE)
|
||||
.map(page -> {
|
||||
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)
|
||||
);
|
||||
.flatMap(singlePagePublicQueryService::convertToVo);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -98,13 +74,7 @@ public class SinglePageFinderImpl implements SinglePageFinder {
|
|||
return client.list(SinglePage.class, predicateToUse,
|
||||
comparatorToUse, pageNullSafe(page), sizeNullSafe(size))
|
||||
.flatMap(list -> Flux.fromStream(list.get())
|
||||
.map(singlePage -> {
|
||||
ListedSinglePageVo pageVo = ListedSinglePageVo.from(singlePage);
|
||||
pageVo.setContributors(List.of());
|
||||
return pageVo;
|
||||
})
|
||||
.flatMap(lp -> fetchStats(lp).doOnNext(lp::setStats).thenReturn(lp))
|
||||
.concatMap(this::populateContributors)
|
||||
.concatMap(singlePagePublicQueryService::convertToListedVo)
|
||||
.collectList()
|
||||
.map(pageVos -> new ListResult<>(list.getPage(), list.getSize(), list.getTotal(),
|
||||
pageVos)
|
||||
|
@ -113,29 +83,6 @@ public class SinglePageFinderImpl implements SinglePageFinder {
|
|||
.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() {
|
||||
Function<SinglePage, Boolean> pinned =
|
||||
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.
|
|
@ -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 java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
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.extension.ExtensionClient;
|
||||
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.ControllerBuilder;
|
||||
import run.halo.app.extension.controller.Reconciler;
|
||||
import run.halo.app.infra.exception.NotFoundException;
|
||||
import run.halo.app.theme.DefaultTemplateEnum;
|
||||
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>.
|
||||
|
@ -46,8 +41,6 @@ import run.halo.app.theme.router.factories.ModelConst;
|
|||
@RequiredArgsConstructor
|
||||
public class SinglePageRoute
|
||||
implements RouterFunction<ServerResponse>, Reconciler<Reconciler.Request>, DisposableBean {
|
||||
private final GroupVersionKind gvk = GroupVersionKind.fromExtension(SinglePage.class);
|
||||
|
||||
private final Map<NameSlugPair, HandlerFunction<ServerResponse>> quickRouteMap =
|
||||
new ConcurrentHashMap<>();
|
||||
|
||||
|
@ -123,12 +116,7 @@ public class SinglePageRoute
|
|||
HandlerFunction<ServerResponse> handlerFunction(String name) {
|
||||
return request -> singlePageFinder.getByName(name)
|
||||
.flatMap(singlePageVo -> {
|
||||
Map<String, Object> model = new HashMap<>();
|
||||
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);
|
||||
Map<String, Object> model = ModelMapUtils.singlePageModel(singlePageVo);
|
||||
String template = singlePageVo.getSpec().getTemplate();
|
||||
return viewNameResolver.resolveViewNameOrDefault(request, template,
|
||||
DefaultTemplateEnum.SINGLE_PAGE.getValue())
|
||||
|
@ -138,9 +126,4 @@ public class SinglePageRoute
|
|||
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.finders.PostFinder;
|
||||
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.UrlContextListResult;
|
||||
|
||||
|
|
|
@ -23,6 +23,7 @@ import run.halo.app.theme.DefaultTemplateEnum;
|
|||
import run.halo.app.theme.finders.PostFinder;
|
||||
import run.halo.app.theme.finders.vo.ListedPostVo;
|
||||
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.UrlContextListResult;
|
||||
|
||||
|
|
|
@ -12,6 +12,7 @@ import org.springframework.web.reactive.function.server.RouterFunctions;
|
|||
import org.springframework.web.reactive.function.server.ServerResponse;
|
||||
import run.halo.app.theme.DefaultTemplateEnum;
|
||||
import run.halo.app.theme.finders.CategoryFinder;
|
||||
import run.halo.app.theme.router.ModelConst;
|
||||
|
||||
/**
|
||||
* 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.vo.CategoryVo;
|
||||
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.UrlContextListResult;
|
||||
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.finders.PostFinder;
|
||||
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.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.CacheBuilder;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
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.Mono;
|
||||
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.ReactiveExtensionClient;
|
||||
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.finders.PostFinder;
|
||||
import run.halo.app.theme.finders.vo.PostVo;
|
||||
import run.halo.app.theme.router.ModelMapUtils;
|
||||
import run.halo.app.theme.router.ViewNameResolver;
|
||||
|
||||
/**
|
||||
|
@ -101,14 +99,7 @@ public class PostRouteFactory implements RouteFactory {
|
|||
Mono<PostVo> postVoMono = bestMatchPost(patternVariable);
|
||||
return postVoMono
|
||||
.flatMap(postVo -> {
|
||||
Map<String, Object> model = new HashMap<>();
|
||||
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);
|
||||
|
||||
Map<String, Object> model = ModelMapUtils.postModel(postVo);
|
||||
String template = postVo.getSpec().getTemplate();
|
||||
return viewNameResolver.resolveViewNameOrDefault(request, template,
|
||||
DefaultTemplateEnum.POST.getValue())
|
||||
|
|
|
@ -10,6 +10,7 @@ import org.springframework.web.reactive.function.server.ServerResponse;
|
|||
import reactor.core.publisher.Mono;
|
||||
import run.halo.app.infra.SystemConfigurableEnvironmentFetcher;
|
||||
import run.halo.app.infra.SystemSetting;
|
||||
import run.halo.app.theme.router.ModelConst;
|
||||
|
||||
/**
|
||||
* @author guqing
|
||||
|
|
|
@ -14,6 +14,7 @@ import org.springframework.web.reactive.function.server.RouterFunctions;
|
|||
import org.springframework.web.reactive.function.server.ServerResponse;
|
||||
import run.halo.app.theme.DefaultTemplateEnum;
|
||||
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
|
||||
|
|
|
@ -38,7 +38,7 @@ import run.halo.app.theme.finders.PostFinder;
|
|||
import run.halo.app.theme.finders.SinglePageFinder;
|
||||
import run.halo.app.theme.finders.vo.PostVo;
|
||||
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}.
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
package run.halo.app.theme.finders.impl;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
|
@ -14,13 +14,12 @@ import org.mockito.Mock;
|
|||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import reactor.core.publisher.Mono;
|
||||
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.SinglePage;
|
||||
import run.halo.app.extension.Metadata;
|
||||
import run.halo.app.extension.ReactiveExtensionClient;
|
||||
import run.halo.app.metrics.CounterService;
|
||||
import run.halo.app.theme.finders.ContributorFinder;
|
||||
import run.halo.app.theme.finders.SinglePageConversionService;
|
||||
import run.halo.app.theme.finders.vo.SinglePageVo;
|
||||
|
||||
/**
|
||||
* Tests for {@link SinglePageFinderImpl}.
|
||||
|
@ -35,13 +34,7 @@ class SinglePageFinderImplTest {
|
|||
private ReactiveExtensionClient client;
|
||||
|
||||
@Mock
|
||||
private SinglePageService singlePageService;
|
||||
|
||||
@Mock
|
||||
private ContributorFinder contributorFinder;
|
||||
|
||||
@Mock
|
||||
private CounterService counterService;
|
||||
private SinglePageConversionService singlePageConversionService;
|
||||
|
||||
@InjectMocks
|
||||
private SinglePageFinderImpl singlePageFinder;
|
||||
|
@ -64,21 +57,14 @@ class SinglePageFinderImplTest {
|
|||
when(client.get(eq(SinglePage.class), eq(fakePageName)))
|
||||
.thenReturn(Mono.just(singlePage));
|
||||
|
||||
when(counterService.getByName(anyString())).thenReturn(Mono.empty());
|
||||
when(contributorFinder.getContributor(anyString())).thenReturn(Mono.empty());
|
||||
when(singlePageService.getReleaseContent(anyString())).thenReturn(Mono.empty());
|
||||
when(singlePageConversionService.convertToVo(eq(singlePage)))
|
||||
.thenReturn(Mono.just(mock(SinglePageVo.class)));
|
||||
|
||||
singlePageFinder.getByName(fakePageName)
|
||||
.as(StepVerifier::create)
|
||||
.consumeNextWith(page -> {
|
||||
assertThat(page.getStats()).isNotNull();
|
||||
assertThat(page.getContent()).isNotNull();
|
||||
})
|
||||
.consumeNextWith(page -> assertThat(page).isNotNull())
|
||||
.verifyComplete();
|
||||
|
||||
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.finders.SinglePageFinder;
|
||||
import run.halo.app.theme.finders.vo.SinglePageVo;
|
||||
import run.halo.app.theme.router.factories.ModelConst;
|
||||
|
||||
/**
|
||||
* 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.vo.PostVo;
|
||||
import run.halo.app.theme.router.EmptyView;
|
||||
import run.halo.app.theme.router.ModelConst;
|
||||
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 reactor.core.publisher.Mono;
|
||||
import run.halo.app.infra.SystemSetting;
|
||||
import run.halo.app.theme.router.ModelConst;
|
||||
|
||||
/**
|
||||
* Tests for {@link RouteFactory}.
|
||||
|
|
|
@ -9,6 +9,7 @@ import {
|
|||
IconSave,
|
||||
Toast,
|
||||
Dialog,
|
||||
IconEye,
|
||||
} from "@halo-dev/components";
|
||||
import SinglePageSettingModal from "./components/SinglePageSettingModal.vue";
|
||||
import type { SinglePage, SinglePageRequest } from "@halo-dev/api-client";
|
||||
|
@ -34,6 +35,7 @@ import {
|
|||
import { useLocalStorage } from "@vueuse/core";
|
||||
import EditorProviderSelector from "@/components/dropdown-selector/EditorProviderSelector.vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import UrlPreviewModal from "@/components/preview/UrlPreviewModal.vue";
|
||||
|
||||
const router = useRouter();
|
||||
const { t } = useI18n();
|
||||
|
@ -112,9 +114,11 @@ provide<ComputedRef<string | undefined>>(
|
|||
|
||||
const routeQueryName = useRouteQuery<string>("name");
|
||||
|
||||
const handleSave = async () => {
|
||||
const handleSave = async (options?: { mute?: boolean }) => {
|
||||
try {
|
||||
saving.value = true;
|
||||
if (!options?.mute) {
|
||||
saving.value = true;
|
||||
}
|
||||
|
||||
//Set default title and slug
|
||||
if (!formState.value.page.spec.title) {
|
||||
|
@ -139,7 +143,9 @@ const handleSave = async () => {
|
|||
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);
|
||||
await handleFetchContent();
|
||||
|
@ -323,6 +329,17 @@ const { handleSetContentCache, handleResetCache, handleClearCache } =
|
|||
routeQueryName,
|
||||
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>
|
||||
|
||||
<template>
|
||||
|
@ -334,6 +351,14 @@ const { handleSetContentCache, handleResetCache, handleClearCache } =
|
|||
@saved="onSettingSaved"
|
||||
@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')">
|
||||
<template #icon>
|
||||
<IconPages class="mr-2 self-center" />
|
||||
|
@ -345,6 +370,17 @@ const { handleSetContentCache, handleResetCache, handleClearCache } =
|
|||
:provider="currentEditorProvider"
|
||||
@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">
|
||||
<template #icon>
|
||||
<IconSave class="h-full w-full" />
|
||||
|
|
|
@ -427,6 +427,13 @@ const { mutate: changeVisibleMutation } = useMutation({
|
|||
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>
|
||||
|
||||
<template>
|
||||
|
@ -737,10 +744,8 @@ const { mutate: changeVisibleMutation } = useMutation({
|
|||
<VStatusDot state="success" animate />
|
||||
</RouterLink>
|
||||
<a
|
||||
v-if="singlePage.page.status?.permalink"
|
||||
target="_blank"
|
||||
:href="singlePage.page.status?.permalink"
|
||||
:title="singlePage.page.status?.permalink"
|
||||
:href="getExternalUrl(singlePage.page)"
|
||||
class="hidden text-gray-600 transition-all hover:text-gray-900 group-hover:inline-block"
|
||||
>
|
||||
<IconExternalLinkLine class="h-3.5 w-3.5" />
|
||||
|
|
|
@ -9,6 +9,7 @@ import {
|
|||
VSpace,
|
||||
Toast,
|
||||
Dialog,
|
||||
IconEye,
|
||||
} from "@halo-dev/components";
|
||||
import PostSettingModal from "./components/PostSettingModal.vue";
|
||||
import type { Post, PostRequest } from "@halo-dev/api-client";
|
||||
|
@ -34,6 +35,7 @@ import {
|
|||
import { useLocalStorage } from "@vueuse/core";
|
||||
import EditorProviderSelector from "@/components/dropdown-selector/EditorProviderSelector.vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import UrlPreviewModal from "@/components/preview/UrlPreviewModal.vue";
|
||||
|
||||
const router = useRouter();
|
||||
const { t } = useI18n();
|
||||
|
@ -112,9 +114,11 @@ provide<ComputedRef<string | undefined>>(
|
|||
computed(() => formState.value.post.status?.permalink)
|
||||
);
|
||||
|
||||
const handleSave = async () => {
|
||||
const handleSave = async (options?: { mute?: boolean }) => {
|
||||
try {
|
||||
saving.value = true;
|
||||
if (!options?.mute) {
|
||||
saving.value = true;
|
||||
}
|
||||
|
||||
// Set default title and slug
|
||||
if (!formState.value.post.spec.title) {
|
||||
|
@ -140,7 +144,9 @@ const handleSave = async () => {
|
|||
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);
|
||||
await handleFetchContent();
|
||||
} catch (e) {
|
||||
|
@ -336,6 +342,17 @@ const { handleSetContentCache, handleResetCache, handleClearCache } =
|
|||
name,
|
||||
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>
|
||||
|
||||
<template>
|
||||
|
@ -347,6 +364,14 @@ const { handleSetContentCache, handleResetCache, handleClearCache } =
|
|||
@saved="onSettingSaved"
|
||||
@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')">
|
||||
<template #icon>
|
||||
<IconBookRead class="mr-2 self-center" />
|
||||
|
@ -358,6 +383,17 @@ const { handleSetContentCache, handleResetCache, handleClearCache } =
|
|||
:provider="currentEditorProvider"
|
||||
@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">
|
||||
<template #icon>
|
||||
<IconSave class="h-full w-full" />
|
||||
|
|
|
@ -440,6 +440,13 @@ const { mutate: changeVisibleMutation } = useMutation({
|
|||
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>
|
||||
<template>
|
||||
<PostSettingModal
|
||||
|
@ -802,10 +809,8 @@ const { mutate: changeVisibleMutation } = useMutation({
|
|||
<VStatusDot state="success" animate />
|
||||
</RouterLink>
|
||||
<a
|
||||
v-if="post.post.status?.permalink"
|
||||
target="_blank"
|
||||
:href="post.post.status?.permalink"
|
||||
:title="post.post.status?.permalink"
|
||||
:href="getExternalUrl(post.post)"
|
||||
class="hidden text-gray-600 transition-all hover:text-gray-900 group-hover:inline-block"
|
||||
>
|
||||
<IconExternalLinkLine class="h-3.5 w-3.5" />
|
||||
|
|
Loading…
Reference in New Issue