mirror of https://github.com/halo-dev/halo
Provide sort query param for sorting attachment (#2705)
#### What type of PR is this? /kind feature /area core /milestone 2.0 #### What this PR does / why we need it: Provide sort query param for sorting attachment. By default, we use creationTimestamp(desc) to sort the attachments. Below is an example to sort with creationTimestamp(desc) and size(asc): ```bash curl -X 'GET' \ 'http://localhost:8090/apis/api.console.halo.run/v1alpha1/attachments?sort=creationTimestamp%2Cdesc&sort=size%2Casc' \ -H 'accept: */*' ``` #### Which issue(s) this PR fixes: Fixes https://github.com/halo-dev/halo/issues/2627 #### Does this PR introduce a user-facing change? ```release-note 附件列表支持排序 ```pull/2712/head
parent
0c8ccecdf3
commit
f1ac4e3740
|
@ -1,5 +1,6 @@
|
||||||
package run.halo.app.core.extension.attachment.endpoint;
|
package run.halo.app.core.extension.attachment.endpoint;
|
||||||
|
|
||||||
|
import static java.util.Comparator.comparing;
|
||||||
import static org.springdoc.core.fn.builders.apiresponse.Builder.responseBuilder;
|
import static org.springdoc.core.fn.builders.apiresponse.Builder.responseBuilder;
|
||||||
import static org.springdoc.core.fn.builders.content.Builder.contentBuilder;
|
import static org.springdoc.core.fn.builders.content.Builder.contentBuilder;
|
||||||
import static org.springdoc.core.fn.builders.schema.Builder.schemaBuilder;
|
import static org.springdoc.core.fn.builders.schema.Builder.schemaBuilder;
|
||||||
|
@ -9,12 +10,17 @@ import static run.halo.app.extension.ListResult.generateGenericClass;
|
||||||
import static run.halo.app.extension.router.QueryParamBuildUtil.buildParametersFromType;
|
import static run.halo.app.extension.router.QueryParamBuildUtil.buildParametersFromType;
|
||||||
import static run.halo.app.extension.router.selector.SelectorUtil.labelAndFieldSelectorToPredicate;
|
import static run.halo.app.extension.router.selector.SelectorUtil.labelAndFieldSelectorToPredicate;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.ArraySchema;
|
||||||
import io.swagger.v3.oas.annotations.media.Schema;
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.function.Predicate;
|
import java.util.function.Predicate;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springdoc.core.fn.builders.requestbody.Builder;
|
import org.springdoc.core.fn.builders.requestbody.Builder;
|
||||||
import org.springdoc.webflux.core.fn.SpringdocRouteBuilder;
|
import org.springdoc.webflux.core.fn.SpringdocRouteBuilder;
|
||||||
|
import org.springframework.data.domain.Sort;
|
||||||
import org.springframework.http.HttpStatus;
|
import org.springframework.http.HttpStatus;
|
||||||
import org.springframework.http.MediaType;
|
import org.springframework.http.MediaType;
|
||||||
import org.springframework.http.codec.multipart.FilePart;
|
import org.springframework.http.codec.multipart.FilePart;
|
||||||
|
@ -30,6 +36,7 @@ import org.springframework.web.reactive.function.server.RouterFunction;
|
||||||
import org.springframework.web.reactive.function.server.ServerRequest;
|
import org.springframework.web.reactive.function.server.ServerRequest;
|
||||||
import org.springframework.web.reactive.function.server.ServerResponse;
|
import org.springframework.web.reactive.function.server.ServerResponse;
|
||||||
import org.springframework.web.server.ResponseStatusException;
|
import org.springframework.web.server.ResponseStatusException;
|
||||||
|
import org.springframework.web.server.ServerWebExchange;
|
||||||
import org.springframework.web.server.ServerWebInputException;
|
import org.springframework.web.server.ServerWebInputException;
|
||||||
import reactor.core.publisher.Flux;
|
import reactor.core.publisher.Flux;
|
||||||
import reactor.core.publisher.Mono;
|
import reactor.core.publisher.Mono;
|
||||||
|
@ -37,6 +44,8 @@ import run.halo.app.core.extension.attachment.Attachment;
|
||||||
import run.halo.app.core.extension.attachment.Policy;
|
import run.halo.app.core.extension.attachment.Policy;
|
||||||
import run.halo.app.core.extension.attachment.endpoint.AttachmentHandler.UploadOption;
|
import run.halo.app.core.extension.attachment.endpoint.AttachmentHandler.UploadOption;
|
||||||
import run.halo.app.core.extension.endpoint.CustomEndpoint;
|
import run.halo.app.core.extension.endpoint.CustomEndpoint;
|
||||||
|
import run.halo.app.core.extension.endpoint.SortResolver;
|
||||||
|
import run.halo.app.extension.Comparators;
|
||||||
import run.halo.app.extension.ConfigMap;
|
import run.halo.app.extension.ConfigMap;
|
||||||
import run.halo.app.extension.ReactiveExtensionClient;
|
import run.halo.app.extension.ReactiveExtensionClient;
|
||||||
import run.halo.app.extension.Ref;
|
import run.halo.app.extension.Ref;
|
||||||
|
@ -139,8 +148,9 @@ public class AttachmentEndpoint implements CustomEndpoint {
|
||||||
}
|
}
|
||||||
|
|
||||||
Mono<ServerResponse> search(ServerRequest request) {
|
Mono<ServerResponse> search(ServerRequest request) {
|
||||||
var searchRequest = new SearchRequest(request.queryParams());
|
var searchRequest = new SearchRequest(request);
|
||||||
return client.list(Attachment.class, searchRequest.toPredicate(), null,
|
return client.list(Attachment.class,
|
||||||
|
searchRequest.toPredicate(), searchRequest.toComparator(),
|
||||||
searchRequest.getPage(), searchRequest.getSize())
|
searchRequest.getPage(), searchRequest.getSize())
|
||||||
.flatMap(listResult -> ServerResponse.ok()
|
.flatMap(listResult -> ServerResponse.ok()
|
||||||
.contentType(MediaType.APPLICATION_JSON)
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
|
@ -161,12 +171,24 @@ public class AttachmentEndpoint implements CustomEndpoint {
|
||||||
@Schema(description = "Name of user who uploaded the attachment")
|
@Schema(description = "Name of user who uploaded the attachment")
|
||||||
Optional<String> getUploadedBy();
|
Optional<String> getUploadedBy();
|
||||||
|
|
||||||
|
@ArraySchema(uniqueItems = true,
|
||||||
|
arraySchema = @Schema(name = "sort",
|
||||||
|
description = "Sort property and direction of the list result. Supported fields: "
|
||||||
|
+ "creationTimestamp, size"),
|
||||||
|
schema = @Schema(description = "like field,asc or field,desc",
|
||||||
|
implementation = String.class,
|
||||||
|
example = "creationTimestamp,desc"))
|
||||||
|
Sort getSort();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class SearchRequest extends QueryListRequest implements ISearchRequest {
|
public static class SearchRequest extends QueryListRequest implements ISearchRequest {
|
||||||
|
|
||||||
public SearchRequest(MultiValueMap<String, String> queryParams) {
|
private final ServerWebExchange exchange;
|
||||||
super(queryParams);
|
|
||||||
|
public SearchRequest(ServerRequest request) {
|
||||||
|
super(request.queryParams());
|
||||||
|
this.exchange = request.exchange();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -193,6 +215,11 @@ public class AttachmentEndpoint implements CustomEndpoint {
|
||||||
.filter(StringUtils::hasText);
|
.filter(StringUtils::hasText);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Sort getSort() {
|
||||||
|
return SortResolver.defaultInstance.resolve(exchange);
|
||||||
|
}
|
||||||
|
|
||||||
public Predicate<Attachment> toPredicate() {
|
public Predicate<Attachment> toPredicate() {
|
||||||
var predicate = (Predicate<Attachment>) (attachment) -> getDisplayName()
|
var predicate = (Predicate<Attachment>) (attachment) -> getDisplayName()
|
||||||
.map(displayNameInParam -> {
|
.map(displayNameInParam -> {
|
||||||
|
@ -222,6 +249,37 @@ public class AttachmentEndpoint implements CustomEndpoint {
|
||||||
|
|
||||||
return predicate.and(selectorPredicate);
|
return predicate.and(selectorPredicate);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Comparator<Attachment> toComparator() {
|
||||||
|
var sort = getSort();
|
||||||
|
List<Comparator<Attachment>> comparators = new ArrayList<>();
|
||||||
|
var creationOrder = sort.getOrderFor("creationTimestamp");
|
||||||
|
if (creationOrder != null) {
|
||||||
|
Comparator<Attachment> comparator = comparing(
|
||||||
|
attachment -> attachment.getMetadata().getCreationTimestamp());
|
||||||
|
if (creationOrder.isDescending()) {
|
||||||
|
comparator = comparator.reversed();
|
||||||
|
}
|
||||||
|
comparators.add(comparator);
|
||||||
|
}
|
||||||
|
|
||||||
|
var sizeOrder = sort.getOrderFor("size");
|
||||||
|
if (sizeOrder != null) {
|
||||||
|
Comparator<Attachment> comparator =
|
||||||
|
comparing(attachment -> attachment.getSpec().getSize());
|
||||||
|
if (sizeOrder.isDescending()) {
|
||||||
|
comparator = comparator.reversed();
|
||||||
|
}
|
||||||
|
comparators.add(comparator);
|
||||||
|
}
|
||||||
|
|
||||||
|
// add default comparator
|
||||||
|
comparators.add(Comparators.compareCreationTimestamp(false));
|
||||||
|
comparators.add(Comparators.compareName(true));
|
||||||
|
return comparators.stream()
|
||||||
|
.reduce(Comparator::thenComparing)
|
||||||
|
.orElse(null);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public interface IUploadRequest {
|
public interface IUploadRequest {
|
||||||
|
|
|
@ -22,7 +22,9 @@ import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.nio.file.Paths;
|
import java.nio.file.Paths;
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.Comparator;
|
import java.util.Comparator;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.concurrent.atomic.AtomicReference;
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
import java.util.function.Predicate;
|
import java.util.function.Predicate;
|
||||||
|
@ -47,6 +49,7 @@ import reactor.core.publisher.Mono;
|
||||||
import reactor.core.scheduler.Schedulers;
|
import reactor.core.scheduler.Schedulers;
|
||||||
import reactor.util.retry.Retry;
|
import reactor.util.retry.Retry;
|
||||||
import run.halo.app.core.extension.Plugin;
|
import run.halo.app.core.extension.Plugin;
|
||||||
|
import run.halo.app.extension.Comparators;
|
||||||
import run.halo.app.extension.ReactiveExtensionClient;
|
import run.halo.app.extension.ReactiveExtensionClient;
|
||||||
import run.halo.app.extension.router.IListRequest.QueryListRequest;
|
import run.halo.app.extension.router.IListRequest.QueryListRequest;
|
||||||
import run.halo.app.infra.utils.FileUtils;
|
import run.halo.app.infra.utils.FileUtils;
|
||||||
|
@ -243,14 +246,20 @@ public class PluginEndpoint implements CustomEndpoint {
|
||||||
public Comparator<Plugin> toComparator() {
|
public Comparator<Plugin> toComparator() {
|
||||||
var sort = getSort();
|
var sort = getSort();
|
||||||
var ctOrder = sort.getOrderFor("creationTimestamp");
|
var ctOrder = sort.getOrderFor("creationTimestamp");
|
||||||
Comparator<Plugin> comparator = null;
|
List<Comparator<Plugin>> comparators = new ArrayList<>();
|
||||||
if (ctOrder != null) {
|
if (ctOrder != null) {
|
||||||
comparator = comparing(plugin -> plugin.getMetadata().getCreationTimestamp());
|
Comparator<Plugin> comparator =
|
||||||
|
comparing(plugin -> plugin.getMetadata().getCreationTimestamp());
|
||||||
if (ctOrder.isDescending()) {
|
if (ctOrder.isDescending()) {
|
||||||
comparator = comparator.reversed();
|
comparator = comparator.reversed();
|
||||||
}
|
}
|
||||||
|
comparators.add(comparator);
|
||||||
}
|
}
|
||||||
return comparator;
|
comparators.add(Comparators.compareCreationTimestamp(false));
|
||||||
|
comparators.add(Comparators.compareName(true));
|
||||||
|
return comparators.stream()
|
||||||
|
.reduce(Comparator::thenComparing)
|
||||||
|
.orElse(null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,20 @@
|
||||||
|
package run.halo.app.extension;
|
||||||
|
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.Comparator;
|
||||||
|
|
||||||
|
public enum Comparators {
|
||||||
|
;
|
||||||
|
|
||||||
|
public static <E extends Extension> Comparator<E> compareCreationTimestamp(boolean asc) {
|
||||||
|
var comparator =
|
||||||
|
Comparator.<E, Instant>comparing(e -> e.getMetadata().getCreationTimestamp());
|
||||||
|
return asc ? comparator : comparator.reversed();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static <E extends Extension> Comparator<E> compareName(boolean asc) {
|
||||||
|
var comparator = Comparator.<E, String>comparing(e -> e.getMetadata().getName());
|
||||||
|
return asc ? comparator : comparator.reversed();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,97 @@
|
||||||
|
package run.halo.app.extension;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import org.junit.jupiter.api.Nested;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
class ComparatorsTest {
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
class CompareCreationTimestamp {
|
||||||
|
|
||||||
|
FakeExtension createFake(String name, Instant creationTimestamp) {
|
||||||
|
var metadata = new Metadata();
|
||||||
|
metadata.setName(name);
|
||||||
|
metadata.setCreationTimestamp(creationTimestamp);
|
||||||
|
var fake = new FakeExtension();
|
||||||
|
fake.setMetadata(metadata);
|
||||||
|
return fake;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void desc() {
|
||||||
|
var comparator = Comparators.compareCreationTimestamp(false);
|
||||||
|
var now = Instant.now();
|
||||||
|
var before = now.minusMillis(1);
|
||||||
|
var after = now.plusMillis(1);
|
||||||
|
|
||||||
|
var fakeNow = createFake("now", now);
|
||||||
|
var fakeBefore = createFake("before", before);
|
||||||
|
var fakeAfter = createFake("after", after);
|
||||||
|
|
||||||
|
var sortedFakes = new ArrayList<>(List.of(fakeNow, fakeAfter, fakeBefore));
|
||||||
|
sortedFakes.sort(comparator);
|
||||||
|
|
||||||
|
assertEquals(List.of(fakeAfter, fakeNow, fakeBefore), sortedFakes);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void asc() {
|
||||||
|
var comparator = Comparators.compareCreationTimestamp(true);
|
||||||
|
var now = Instant.now();
|
||||||
|
var before = now.minusMillis(1);
|
||||||
|
var after = now.plusMillis(1);
|
||||||
|
|
||||||
|
var fakeNow = createFake("now", now);
|
||||||
|
var fakeBefore = createFake("before", before);
|
||||||
|
var fakeAfter = createFake("after", after);
|
||||||
|
|
||||||
|
var sortedFakes = new ArrayList<>(List.of(fakeNow, fakeAfter, fakeBefore));
|
||||||
|
sortedFakes.sort(comparator);
|
||||||
|
|
||||||
|
assertEquals(List.of(fakeBefore, fakeNow, fakeAfter), sortedFakes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
class CompareName {
|
||||||
|
|
||||||
|
FakeExtension createFake(String name) {
|
||||||
|
var metadata = new Metadata();
|
||||||
|
metadata.setName(name);
|
||||||
|
var fake = new FakeExtension();
|
||||||
|
fake.setMetadata(metadata);
|
||||||
|
return fake;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void desc() {
|
||||||
|
var comparator = Comparators.compareName(false);
|
||||||
|
var fake01 = createFake("fake01");
|
||||||
|
var fake02 = createFake("fake02");
|
||||||
|
var fake03 = createFake("fake03");
|
||||||
|
|
||||||
|
var sortedFakes = new ArrayList<>(List.of(fake02, fake01, fake03));
|
||||||
|
sortedFakes.sort(comparator);
|
||||||
|
|
||||||
|
assertEquals(List.of(fake03, fake02, fake01), sortedFakes);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void asc() {
|
||||||
|
var comparator = Comparators.compareName(true);
|
||||||
|
var fake01 = createFake("fake01");
|
||||||
|
var fake02 = createFake("fake02");
|
||||||
|
var fake03 = createFake("fake03");
|
||||||
|
|
||||||
|
var sortedFakes = new ArrayList<>(List.of(fake02, fake03, fake01));
|
||||||
|
sortedFakes.sort(comparator);
|
||||||
|
|
||||||
|
assertEquals(List.of(fake01, fake02, fake03), sortedFakes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue