mirror of https://github.com/halo-dev/halo
feat: add the ability to install themes remotely via URI (#3939)
#### What type of PR is this? /kind improvement /area core /milestone 2.6.x /kind api-change #### What this PR does / why we need it: 支持通过 URI 远程安装和升级主题 how to test it? 1. 测试主题安装 ```shell curl -u admin:admin -X POST http://localhost:8090/apis/api.console.halo.run/v1alpha1/themes/-/install-from-uri --data '{ "uri": "https://halo.run/apis/api.store.halo.run/v1alpha1/applications/app-eiTyL/releases/app-release-QSyjc/download/app-release-QSyjc-JOSOB" }' ``` 2. 测试主题升级 ```shell curl -u admin:admin -X POST http://localhost:8090/apis/api.console.halo.run/v1alpha1/themes/guqing-higan/upgrade-from-uri --data '{ "uri": "https://halo.run/apis/api.store.halo.run/v1alpha1/applications/app-eiTyL/releases/app-release-QSyjc/download/app-release-QSyjc-JOSOB" }' ``` #### Which issue(s) this PR fixes: Fixes #2291 #### Does this PR introduce a user-facing change? ```release-note 支持通过 URI 远程安装和升级主题 ``` --------- Co-authored-by: Ryan Wang <i@ryanc.cc>pull/3973/head
parent
8deea08231
commit
170cf4e412
|
@ -1,5 +1,6 @@
|
|||
package run.halo.app.core.extension.theme;
|
||||
|
||||
import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;
|
||||
import static org.springdoc.core.fn.builders.apiresponse.Builder.responseBuilder;
|
||||
import static org.springdoc.core.fn.builders.content.Builder.contentBuilder;
|
||||
import static org.springdoc.core.fn.builders.parameter.Builder.parameterBuilder;
|
||||
|
@ -11,6 +12,7 @@ import static run.halo.app.infra.utils.DataBufferUtils.toInputStream;
|
|||
import io.swagger.v3.oas.annotations.enums.ParameterIn;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.time.Duration;
|
||||
|
@ -43,10 +45,14 @@ import run.halo.app.extension.ListResult;
|
|||
import run.halo.app.extension.ReactiveExtensionClient;
|
||||
import run.halo.app.extension.router.IListRequest;
|
||||
import run.halo.app.extension.router.QueryParamBuildUtil;
|
||||
import run.halo.app.infra.ReactiveUrlDataBufferFetcher;
|
||||
import run.halo.app.infra.SystemConfigurableEnvironmentFetcher;
|
||||
import run.halo.app.infra.SystemSetting;
|
||||
import run.halo.app.infra.ThemeRootGetter;
|
||||
import run.halo.app.infra.exception.NotFoundException;
|
||||
import run.halo.app.infra.exception.ThemeInstallationException;
|
||||
import run.halo.app.infra.exception.ThemeUpgradeException;
|
||||
import run.halo.app.infra.utils.DataBufferUtils;
|
||||
import run.halo.app.infra.utils.JsonUtils;
|
||||
import run.halo.app.theme.TemplateEngineManager;
|
||||
|
||||
|
@ -71,6 +77,8 @@ public class ThemeEndpoint implements CustomEndpoint {
|
|||
|
||||
private final SystemConfigurableEnvironmentFetcher systemEnvironmentFetcher;
|
||||
|
||||
private final ReactiveUrlDataBufferFetcher reactiveUrlDataBufferFetcher;
|
||||
|
||||
@Override
|
||||
public RouterFunction<ServerResponse> endpoint() {
|
||||
final var tag = "api.console.halo.run/v1alpha1/Theme";
|
||||
|
@ -89,6 +97,39 @@ public class ThemeEndpoint implements CustomEndpoint {
|
|||
.response(responseBuilder()
|
||||
.implementation(Theme.class))
|
||||
)
|
||||
.POST("themes/-/install-from-uri", this::installFromUri,
|
||||
builder -> builder.operationId("InstallThemeFromUri")
|
||||
.description("Install a theme from uri.")
|
||||
.tag(tag)
|
||||
.requestBody(requestBodyBuilder()
|
||||
.required(true)
|
||||
.content(contentBuilder()
|
||||
.mediaType(MediaType.APPLICATION_JSON_VALUE)
|
||||
.schema(schemaBuilder()
|
||||
.implementation(InstallFromUriRequest.class))
|
||||
))
|
||||
.response(responseBuilder()
|
||||
.implementation(Theme.class))
|
||||
)
|
||||
.POST("themes/{name}/upgrade-from-uri", this::upgradeFromUri,
|
||||
builder -> builder.operationId("UpgradeThemeFromUri")
|
||||
.description("Upgrade a theme from uri.")
|
||||
.tag(tag)
|
||||
.parameter(parameterBuilder()
|
||||
.in(ParameterIn.PATH)
|
||||
.name("name")
|
||||
.required(true)
|
||||
)
|
||||
.requestBody(requestBodyBuilder()
|
||||
.required(true)
|
||||
.content(contentBuilder()
|
||||
.mediaType(MediaType.APPLICATION_JSON_VALUE)
|
||||
.schema(schemaBuilder()
|
||||
.implementation(UpgradeFromUriRequest.class))
|
||||
))
|
||||
.response(responseBuilder()
|
||||
.implementation(Theme.class))
|
||||
)
|
||||
.POST("themes/{name}/upgrade", this::upgrade,
|
||||
builder -> builder.operationId("UpgradeTheme")
|
||||
.description("Upgrade theme")
|
||||
|
@ -200,6 +241,45 @@ public class ThemeEndpoint implements CustomEndpoint {
|
|||
.build();
|
||||
}
|
||||
|
||||
private Mono<ServerResponse> upgradeFromUri(ServerRequest request) {
|
||||
final var name = request.pathVariable("name");
|
||||
return request.bodyToMono(UpgradeFromUriRequest.class)
|
||||
.flatMap(upgradeRequest -> Mono.fromCallable(() -> DataBufferUtils.toInputStream(
|
||||
reactiveUrlDataBufferFetcher.fetch(upgradeRequest.uri())))
|
||||
)
|
||||
.doOnError(throwable -> {
|
||||
log.error("Failed to fetch zip file from uri.", throwable);
|
||||
throw new ThemeUpgradeException("Failed to fetch zip file from uri.", null,
|
||||
null);
|
||||
})
|
||||
.flatMap(inputStream -> themeService.upgrade(name, inputStream))
|
||||
.flatMap((updatedTheme) -> templateEngineManager.clearCache(
|
||||
updatedTheme.getMetadata().getName())
|
||||
.thenReturn(updatedTheme)
|
||||
)
|
||||
.flatMap(theme -> ServerResponse.ok()
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.bodyValue(theme)
|
||||
);
|
||||
}
|
||||
|
||||
private Mono<ServerResponse> installFromUri(ServerRequest request) {
|
||||
return request.bodyToMono(InstallFromUriRequest.class)
|
||||
.flatMap(installRequest -> Mono.fromCallable(() -> DataBufferUtils.toInputStream(
|
||||
reactiveUrlDataBufferFetcher.fetch(installRequest.uri())))
|
||||
)
|
||||
.doOnError(throwable -> {
|
||||
log.error("Failed to fetch zip file from uri.", throwable);
|
||||
throw new ThemeInstallationException("Failed to fetch zip file from uri.", null,
|
||||
null);
|
||||
})
|
||||
.flatMap(themeService::install)
|
||||
.flatMap(theme -> ServerResponse.ok()
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.bodyValue(theme)
|
||||
);
|
||||
}
|
||||
|
||||
private Mono<ServerResponse> activateTheme(ServerRequest request) {
|
||||
final var activatedThemeName = request.pathVariable("name");
|
||||
return client.fetch(Theme.class, activatedThemeName)
|
||||
|
@ -332,6 +412,9 @@ public class ThemeEndpoint implements CustomEndpoint {
|
|||
|
||||
}
|
||||
|
||||
public record UpgradeFromUriRequest(@Schema(requiredMode = REQUIRED) URI uri) {
|
||||
}
|
||||
|
||||
public static class UpgradeRequest implements IUpgradeRequest {
|
||||
|
||||
private final MultiValueMap<String, Part> multipartData;
|
||||
|
@ -418,6 +501,9 @@ public class ThemeEndpoint implements CustomEndpoint {
|
|||
@Schema(required = true, description = "Theme zip file.") FilePart file) {
|
||||
}
|
||||
|
||||
public record InstallFromUriRequest(@Schema(requiredMode = REQUIRED) URI uri) {
|
||||
}
|
||||
|
||||
Mono<ServerResponse> install(ServerRequest request) {
|
||||
return request.body(BodyExtractors.toMultipartData())
|
||||
.flatMap(this::getZipFilePart)
|
||||
|
|
|
@ -19,20 +19,25 @@ import java.util.concurrent.atomic.AtomicReference;
|
|||
import java.util.stream.BaseStream;
|
||||
import java.util.stream.Stream;
|
||||
import java.util.zip.ZipInputStream;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.core.io.FileSystemResource;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
import org.springframework.web.server.ServerWebInputException;
|
||||
import reactor.core.Exceptions;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.core.scheduler.Schedulers;
|
||||
import run.halo.app.core.extension.Theme;
|
||||
import run.halo.app.extension.Unstructured;
|
||||
import run.halo.app.infra.exception.ThemeAlreadyExistsException;
|
||||
import run.halo.app.infra.exception.ThemeInstallationException;
|
||||
import run.halo.app.infra.utils.FileUtils;
|
||||
import run.halo.app.infra.utils.YamlUnstructuredLoader;
|
||||
|
||||
@Slf4j
|
||||
class ThemeUtils {
|
||||
private static final String THEME_TMP_PREFIX = "halo-theme-";
|
||||
private static final String[] THEME_MANIFESTS = {"theme.yaml", "theme.yml"};
|
||||
|
@ -97,7 +102,11 @@ class ThemeUtils {
|
|||
}
|
||||
|
||||
static Mono<Unstructured> unzipThemeTo(InputStream inputStream, Path themeWorkDir) {
|
||||
return unzipThemeTo(inputStream, themeWorkDir, false);
|
||||
return unzipThemeTo(inputStream, themeWorkDir, false)
|
||||
.onErrorMap(e -> !(e instanceof ResponseStatusException), e -> {
|
||||
log.error("Failed to unzip theme", e);
|
||||
throw new ServerWebInputException("Failed to unzip theme");
|
||||
});
|
||||
}
|
||||
|
||||
static Mono<Unstructured> unzipThemeTo(InputStream inputStream, Path themeWorkDir,
|
||||
|
@ -130,8 +139,7 @@ class ThemeUtils {
|
|||
var themeTargetPath = themeWorkDir.resolve(themeName);
|
||||
try {
|
||||
if (!override && !FileUtils.isEmpty(themeTargetPath)) {
|
||||
throw new ThemeInstallationException("Theme already exists.",
|
||||
"problemDetail.theme.install.alreadyExists", new Object[] {themeName});
|
||||
throw new ThemeAlreadyExistsException(themeName);
|
||||
}
|
||||
// install theme to theme work dir
|
||||
copyRecursively(themeManifestPath.getParent(), themeTargetPath);
|
||||
|
@ -141,7 +149,10 @@ class ThemeUtils {
|
|||
throw Exceptions.propagate(e);
|
||||
}
|
||||
})
|
||||
.doFinally(signalType -> deleteRecursivelyAndSilently(tempDir.get()));
|
||||
.doFinally(signalType -> {
|
||||
FileUtils.closeQuietly(inputStream);
|
||||
deleteRecursivelyAndSilently(tempDir.get());
|
||||
});
|
||||
}
|
||||
|
||||
static Unstructured loadThemeManifest(Path themeManifestPath) {
|
||||
|
|
|
@ -0,0 +1,34 @@
|
|||
package run.halo.app.infra;
|
||||
|
||||
import java.net.URI;
|
||||
import org.springframework.core.io.buffer.DataBuffer;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.client.reactive.ReactorClientHttpConnector;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.reactive.function.client.WebClient;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.netty.http.client.HttpClient;
|
||||
|
||||
/**
|
||||
* <p>A default implementation of {@link ReactiveUrlDataBufferFetcher}.</p>
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.6.0
|
||||
*/
|
||||
@Component
|
||||
public class DefaultReactiveUrlDataBufferFetcher implements ReactiveUrlDataBufferFetcher {
|
||||
private final HttpClient httpClient = HttpClient.create()
|
||||
.followRedirect(true);
|
||||
private final WebClient webClient = WebClient.builder()
|
||||
.clientConnector(new ReactorClientHttpConnector(httpClient))
|
||||
.build();
|
||||
|
||||
@Override
|
||||
public Flux<DataBuffer> fetch(URI uri) {
|
||||
return webClient.get()
|
||||
.uri(uri)
|
||||
.accept(MediaType.APPLICATION_OCTET_STREAM)
|
||||
.retrieve()
|
||||
.bodyToFlux(DataBuffer.class);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
package run.halo.app.infra;
|
||||
|
||||
import java.net.URI;
|
||||
import org.springframework.core.io.buffer.DataBuffer;
|
||||
import reactor.core.publisher.Flux;
|
||||
|
||||
/**
|
||||
* <p>{@link DataBuffer} stream fetcher from uri.</p>
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.6.0
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface ReactiveUrlDataBufferFetcher {
|
||||
|
||||
/**
|
||||
* <p>Fetch data buffer flux from uri.</p>
|
||||
*
|
||||
* @param uri uri to fetch
|
||||
* @return data buffer flux
|
||||
*/
|
||||
Flux<DataBuffer> fetch(URI uri);
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
package run.halo.app.infra.exception;
|
||||
|
||||
import java.net.URI;
|
||||
import org.springframework.lang.NonNull;
|
||||
import org.springframework.web.server.ServerWebInputException;
|
||||
|
||||
/**
|
||||
* {@link ThemeAlreadyExistsException} indicates the provided theme has already installed before.
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.6.0
|
||||
*/
|
||||
public class ThemeAlreadyExistsException extends ServerWebInputException {
|
||||
|
||||
public static final String THEME_ALREADY_EXISTS_TYPE =
|
||||
"https://halo.run/probs/theme-alreay-exists";
|
||||
|
||||
/**
|
||||
* Constructs a {@code ThemeAlreadyExistsException} with the given theme name.
|
||||
*
|
||||
* @param themeName theme name must not be blank
|
||||
*/
|
||||
public ThemeAlreadyExistsException(@NonNull String themeName) {
|
||||
super("Theme already exists.", null, null, "problemDetail.theme.install.alreadyExists",
|
||||
new Object[] {themeName});
|
||||
setType(URI.create(THEME_ALREADY_EXISTS_TYPE));
|
||||
getBody().setProperty("themeName", themeName);
|
||||
}
|
||||
}
|
|
@ -15,7 +15,8 @@ rules:
|
|||
resources: [ "themes" ]
|
||||
verbs: [ "*" ]
|
||||
- apiGroups: [ "api.console.halo.run" ]
|
||||
resources: [ "themes", "themes/reload", "themes/resetconfig", "themes/config", "themes/activation" ]
|
||||
resources: [ "themes", "themes/reload", "themes/resetconfig", "themes/config", "themes/activation",
|
||||
"themes/install-from-uri", "themes/upgrade-from-uri" ]
|
||||
verbs: [ "*" ]
|
||||
- nonResourceURLs: [ "/apis/api.console.halo.run/themes/install" ]
|
||||
verbs: [ "create" ]
|
||||
|
|
|
@ -4,6 +4,7 @@ import static org.mockito.ArgumentMatchers.any;
|
|||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.ArgumentMatchers.isA;
|
||||
import static org.mockito.Mockito.lenient;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
@ -12,6 +13,7 @@ import static org.springframework.web.reactive.function.BodyInserters.fromMultip
|
|||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.URI;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
|
@ -23,18 +25,21 @@ import org.mockito.InjectMocks;
|
|||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.core.io.FileSystemResource;
|
||||
import org.springframework.core.io.buffer.DataBuffer;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.client.MultipartBodyBuilder;
|
||||
import org.springframework.test.web.reactive.server.WebTestClient;
|
||||
import org.springframework.util.FileSystemUtils;
|
||||
import org.springframework.util.ResourceUtils;
|
||||
import org.springframework.web.server.ServerWebInputException;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
import run.halo.app.core.extension.Setting;
|
||||
import run.halo.app.core.extension.Theme;
|
||||
import run.halo.app.extension.ConfigMap;
|
||||
import run.halo.app.extension.Metadata;
|
||||
import run.halo.app.extension.ReactiveExtensionClient;
|
||||
import run.halo.app.infra.ReactiveUrlDataBufferFetcher;
|
||||
import run.halo.app.infra.SystemConfigurableEnvironmentFetcher;
|
||||
import run.halo.app.infra.SystemSetting;
|
||||
import run.halo.app.infra.ThemeRootGetter;
|
||||
|
@ -64,6 +69,9 @@ class ThemeEndpointTest {
|
|||
@Mock
|
||||
private SystemConfigurableEnvironmentFetcher environmentFetcher;
|
||||
|
||||
@Mock
|
||||
private ReactiveUrlDataBufferFetcher reactiveUrlDataBufferFetcher;
|
||||
|
||||
@InjectMocks
|
||||
ThemeEndpoint themeEndpoint;
|
||||
|
||||
|
@ -138,6 +146,32 @@ class ThemeEndpointTest {
|
|||
verify(templateEngineManager, times(1)).clearCache(eq("default"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void upgradeFromUri() {
|
||||
final URI uri = URI.create("https://example.com/test-theme.zip");
|
||||
Theme fakeTheme = mock(Theme.class);
|
||||
Metadata metadata = new Metadata();
|
||||
metadata.setName("default");
|
||||
when(fakeTheme.getMetadata()).thenReturn(metadata);
|
||||
when(themeService.upgrade(eq("default"), isA(InputStream.class)))
|
||||
.thenReturn(Mono.just(fakeTheme));
|
||||
when(reactiveUrlDataBufferFetcher.fetch(eq(uri)))
|
||||
.thenReturn(Flux.just(mock(DataBuffer.class)));
|
||||
when(templateEngineManager.clearCache(eq("default")))
|
||||
.thenReturn(Mono.empty());
|
||||
var body = new ThemeEndpoint.UpgradeFromUriRequest(uri);
|
||||
webTestClient.post()
|
||||
.uri("/themes/default/upgrade-from-uri")
|
||||
.bodyValue(body)
|
||||
.exchange()
|
||||
.expectStatus().isOk();
|
||||
|
||||
verify(themeService).upgrade(eq("default"), isA(InputStream.class));
|
||||
|
||||
verify(templateEngineManager, times(1)).clearCache(eq("default"));
|
||||
|
||||
verify(reactiveUrlDataBufferFetcher).fetch(eq(uri));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -173,6 +207,25 @@ class ThemeEndpointTest {
|
|||
.expectStatus().is5xxServerError();
|
||||
}
|
||||
|
||||
@Test
|
||||
void installFromUri() {
|
||||
final URI uri = URI.create("https://example.com/test-theme.zip");
|
||||
Theme fakeTheme = mock(Theme.class);
|
||||
when(themeService.install(isA(InputStream.class)))
|
||||
.thenReturn(Mono.just(fakeTheme));
|
||||
when(reactiveUrlDataBufferFetcher.fetch(eq(uri)))
|
||||
.thenReturn(Flux.just(mock(DataBuffer.class)));
|
||||
var body = new ThemeEndpoint.UpgradeFromUriRequest(uri);
|
||||
webTestClient.post()
|
||||
.uri("/themes/-/install-from-uri")
|
||||
.bodyValue(body)
|
||||
.exchange()
|
||||
.expectStatus().isOk();
|
||||
|
||||
verify(themeService).install(isA(InputStream.class));
|
||||
verify(reactiveUrlDataBufferFetcher).fetch(eq(uri));
|
||||
}
|
||||
|
||||
@Test
|
||||
void reloadTheme() {
|
||||
when(themeService.reloadTheme(any())).thenReturn(Mono.empty());
|
||||
|
|
|
@ -120,6 +120,7 @@ models/group-spec.ts
|
|||
models/group-status.ts
|
||||
models/group.ts
|
||||
models/index.ts
|
||||
models/install-from-uri-request.ts
|
||||
models/license.ts
|
||||
models/listed-auth-provider.ts
|
||||
models/listed-comment-list.ts
|
||||
|
@ -222,6 +223,7 @@ models/theme-list.ts
|
|||
models/theme-spec.ts
|
||||
models/theme-status.ts
|
||||
models/theme.ts
|
||||
models/upgrade-from-uri-request.ts
|
||||
models/user-connection-list.ts
|
||||
models/user-connection-spec.ts
|
||||
models/user-connection.ts
|
||||
|
|
|
@ -40,11 +40,15 @@ import {
|
|||
// @ts-ignore
|
||||
import { ConfigMap } from "../models";
|
||||
// @ts-ignore
|
||||
import { InstallFromUriRequest } from "../models";
|
||||
// @ts-ignore
|
||||
import { Setting } from "../models";
|
||||
// @ts-ignore
|
||||
import { Theme } from "../models";
|
||||
// @ts-ignore
|
||||
import { ThemeList } from "../models";
|
||||
// @ts-ignore
|
||||
import { UpgradeFromUriRequest } from "../models";
|
||||
/**
|
||||
* ApiConsoleHaloRunV1alpha1ThemeApi - axios parameter creator
|
||||
* @export
|
||||
|
@ -321,6 +325,67 @@ export const ApiConsoleHaloRunV1alpha1ThemeApiAxiosParamCreator = function (
|
|||
options: localVarRequestOptions,
|
||||
};
|
||||
},
|
||||
/**
|
||||
* Install a theme from uri.
|
||||
* @param {InstallFromUriRequest} installFromUriRequest
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
installThemeFromUri: async (
|
||||
installFromUriRequest: InstallFromUriRequest,
|
||||
options: AxiosRequestConfig = {}
|
||||
): Promise<RequestArgs> => {
|
||||
// verify required parameter 'installFromUriRequest' is not null or undefined
|
||||
assertParamExists(
|
||||
"installThemeFromUri",
|
||||
"installFromUriRequest",
|
||||
installFromUriRequest
|
||||
);
|
||||
const localVarPath = `/apis/api.console.halo.run/v1alpha1/themes/-/install-from-uri`;
|
||||
// use dummy base URL string because the URL constructor only accepts absolute URLs.
|
||||
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
|
||||
let baseOptions;
|
||||
if (configuration) {
|
||||
baseOptions = configuration.baseOptions;
|
||||
}
|
||||
|
||||
const localVarRequestOptions = {
|
||||
method: "POST",
|
||||
...baseOptions,
|
||||
...options,
|
||||
};
|
||||
const localVarHeaderParameter = {} as any;
|
||||
const localVarQueryParameter = {} as any;
|
||||
|
||||
// authentication BasicAuth required
|
||||
// http basic authentication required
|
||||
setBasicAuthToObject(localVarRequestOptions, configuration);
|
||||
|
||||
// authentication BearerAuth required
|
||||
// http bearer authentication required
|
||||
await setBearerAuthToObject(localVarHeaderParameter, configuration);
|
||||
|
||||
localVarHeaderParameter["Content-Type"] = "application/json";
|
||||
|
||||
setSearchParams(localVarUrlObj, localVarQueryParameter);
|
||||
let headersFromBaseOptions =
|
||||
baseOptions && baseOptions.headers ? baseOptions.headers : {};
|
||||
localVarRequestOptions.headers = {
|
||||
...localVarHeaderParameter,
|
||||
...headersFromBaseOptions,
|
||||
...options.headers,
|
||||
};
|
||||
localVarRequestOptions.data = serializeDataIfNeeded(
|
||||
installFromUriRequest,
|
||||
localVarRequestOptions,
|
||||
configuration
|
||||
);
|
||||
|
||||
return {
|
||||
url: toPathString(localVarUrlObj),
|
||||
options: localVarRequestOptions,
|
||||
};
|
||||
},
|
||||
/**
|
||||
* List themes.
|
||||
* @param {boolean} uninstalled
|
||||
|
@ -635,6 +700,75 @@ export const ApiConsoleHaloRunV1alpha1ThemeApiAxiosParamCreator = function (
|
|||
};
|
||||
localVarRequestOptions.data = localVarFormParams;
|
||||
|
||||
return {
|
||||
url: toPathString(localVarUrlObj),
|
||||
options: localVarRequestOptions,
|
||||
};
|
||||
},
|
||||
/**
|
||||
* Upgrade a theme from uri.
|
||||
* @param {string} name
|
||||
* @param {UpgradeFromUriRequest} upgradeFromUriRequest
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
upgradeThemeFromUri: async (
|
||||
name: string,
|
||||
upgradeFromUriRequest: UpgradeFromUriRequest,
|
||||
options: AxiosRequestConfig = {}
|
||||
): Promise<RequestArgs> => {
|
||||
// verify required parameter 'name' is not null or undefined
|
||||
assertParamExists("upgradeThemeFromUri", "name", name);
|
||||
// verify required parameter 'upgradeFromUriRequest' is not null or undefined
|
||||
assertParamExists(
|
||||
"upgradeThemeFromUri",
|
||||
"upgradeFromUriRequest",
|
||||
upgradeFromUriRequest
|
||||
);
|
||||
const localVarPath =
|
||||
`/apis/api.console.halo.run/v1alpha1/themes/{name}/upgrade-from-uri`.replace(
|
||||
`{${"name"}}`,
|
||||
encodeURIComponent(String(name))
|
||||
);
|
||||
// use dummy base URL string because the URL constructor only accepts absolute URLs.
|
||||
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
|
||||
let baseOptions;
|
||||
if (configuration) {
|
||||
baseOptions = configuration.baseOptions;
|
||||
}
|
||||
|
||||
const localVarRequestOptions = {
|
||||
method: "POST",
|
||||
...baseOptions,
|
||||
...options,
|
||||
};
|
||||
const localVarHeaderParameter = {} as any;
|
||||
const localVarQueryParameter = {} as any;
|
||||
|
||||
// authentication BasicAuth required
|
||||
// http basic authentication required
|
||||
setBasicAuthToObject(localVarRequestOptions, configuration);
|
||||
|
||||
// authentication BearerAuth required
|
||||
// http bearer authentication required
|
||||
await setBearerAuthToObject(localVarHeaderParameter, configuration);
|
||||
|
||||
localVarHeaderParameter["Content-Type"] = "application/json";
|
||||
|
||||
setSearchParams(localVarUrlObj, localVarQueryParameter);
|
||||
let headersFromBaseOptions =
|
||||
baseOptions && baseOptions.headers ? baseOptions.headers : {};
|
||||
localVarRequestOptions.headers = {
|
||||
...localVarHeaderParameter,
|
||||
...headersFromBaseOptions,
|
||||
...options.headers,
|
||||
};
|
||||
localVarRequestOptions.data = serializeDataIfNeeded(
|
||||
upgradeFromUriRequest,
|
||||
localVarRequestOptions,
|
||||
configuration
|
||||
);
|
||||
|
||||
return {
|
||||
url: toPathString(localVarUrlObj),
|
||||
options: localVarRequestOptions,
|
||||
|
@ -760,6 +894,30 @@ export const ApiConsoleHaloRunV1alpha1ThemeApiFp = function (
|
|||
configuration
|
||||
);
|
||||
},
|
||||
/**
|
||||
* Install a theme from uri.
|
||||
* @param {InstallFromUriRequest} installFromUriRequest
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
async installThemeFromUri(
|
||||
installFromUriRequest: InstallFromUriRequest,
|
||||
options?: AxiosRequestConfig
|
||||
): Promise<
|
||||
(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Theme>
|
||||
> {
|
||||
const localVarAxiosArgs =
|
||||
await localVarAxiosParamCreator.installThemeFromUri(
|
||||
installFromUriRequest,
|
||||
options
|
||||
);
|
||||
return createRequestFunction(
|
||||
localVarAxiosArgs,
|
||||
globalAxios,
|
||||
BASE_PATH,
|
||||
configuration
|
||||
);
|
||||
},
|
||||
/**
|
||||
* List themes.
|
||||
* @param {boolean} uninstalled
|
||||
|
@ -892,6 +1050,33 @@ export const ApiConsoleHaloRunV1alpha1ThemeApiFp = function (
|
|||
configuration
|
||||
);
|
||||
},
|
||||
/**
|
||||
* Upgrade a theme from uri.
|
||||
* @param {string} name
|
||||
* @param {UpgradeFromUriRequest} upgradeFromUriRequest
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
async upgradeThemeFromUri(
|
||||
name: string,
|
||||
upgradeFromUriRequest: UpgradeFromUriRequest,
|
||||
options?: AxiosRequestConfig
|
||||
): Promise<
|
||||
(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Theme>
|
||||
> {
|
||||
const localVarAxiosArgs =
|
||||
await localVarAxiosParamCreator.upgradeThemeFromUri(
|
||||
name,
|
||||
upgradeFromUriRequest,
|
||||
options
|
||||
);
|
||||
return createRequestFunction(
|
||||
localVarAxiosArgs,
|
||||
globalAxios,
|
||||
BASE_PATH,
|
||||
configuration
|
||||
);
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -972,6 +1157,20 @@ export const ApiConsoleHaloRunV1alpha1ThemeApiFactory = function (
|
|||
.installTheme(requestParameters.file, options)
|
||||
.then((request) => request(axios, basePath));
|
||||
},
|
||||
/**
|
||||
* Install a theme from uri.
|
||||
* @param {ApiConsoleHaloRunV1alpha1ThemeApiInstallThemeFromUriRequest} requestParameters Request parameters.
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
installThemeFromUri(
|
||||
requestParameters: ApiConsoleHaloRunV1alpha1ThemeApiInstallThemeFromUriRequest,
|
||||
options?: AxiosRequestConfig
|
||||
): AxiosPromise<Theme> {
|
||||
return localVarFp
|
||||
.installThemeFromUri(requestParameters.installFromUriRequest, options)
|
||||
.then((request) => request(axios, basePath));
|
||||
},
|
||||
/**
|
||||
* List themes.
|
||||
* @param {ApiConsoleHaloRunV1alpha1ThemeApiListThemesRequest} requestParameters Request parameters.
|
||||
|
@ -1053,6 +1252,24 @@ export const ApiConsoleHaloRunV1alpha1ThemeApiFactory = function (
|
|||
.upgradeTheme(requestParameters.name, requestParameters.file, options)
|
||||
.then((request) => request(axios, basePath));
|
||||
},
|
||||
/**
|
||||
* Upgrade a theme from uri.
|
||||
* @param {ApiConsoleHaloRunV1alpha1ThemeApiUpgradeThemeFromUriRequest} requestParameters Request parameters.
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
upgradeThemeFromUri(
|
||||
requestParameters: ApiConsoleHaloRunV1alpha1ThemeApiUpgradeThemeFromUriRequest,
|
||||
options?: AxiosRequestConfig
|
||||
): AxiosPromise<Theme> {
|
||||
return localVarFp
|
||||
.upgradeThemeFromUri(
|
||||
requestParameters.name,
|
||||
requestParameters.upgradeFromUriRequest,
|
||||
options
|
||||
)
|
||||
.then((request) => request(axios, basePath));
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -1112,6 +1329,20 @@ export interface ApiConsoleHaloRunV1alpha1ThemeApiInstallThemeRequest {
|
|||
readonly file: File;
|
||||
}
|
||||
|
||||
/**
|
||||
* Request parameters for installThemeFromUri operation in ApiConsoleHaloRunV1alpha1ThemeApi.
|
||||
* @export
|
||||
* @interface ApiConsoleHaloRunV1alpha1ThemeApiInstallThemeFromUriRequest
|
||||
*/
|
||||
export interface ApiConsoleHaloRunV1alpha1ThemeApiInstallThemeFromUriRequest {
|
||||
/**
|
||||
*
|
||||
* @type {InstallFromUriRequest}
|
||||
* @memberof ApiConsoleHaloRunV1alpha1ThemeApiInstallThemeFromUri
|
||||
*/
|
||||
readonly installFromUriRequest: InstallFromUriRequest;
|
||||
}
|
||||
|
||||
/**
|
||||
* Request parameters for listThemes operation in ApiConsoleHaloRunV1alpha1ThemeApi.
|
||||
* @export
|
||||
|
@ -1224,6 +1455,27 @@ export interface ApiConsoleHaloRunV1alpha1ThemeApiUpgradeThemeRequest {
|
|||
readonly file: File;
|
||||
}
|
||||
|
||||
/**
|
||||
* Request parameters for upgradeThemeFromUri operation in ApiConsoleHaloRunV1alpha1ThemeApi.
|
||||
* @export
|
||||
* @interface ApiConsoleHaloRunV1alpha1ThemeApiUpgradeThemeFromUriRequest
|
||||
*/
|
||||
export interface ApiConsoleHaloRunV1alpha1ThemeApiUpgradeThemeFromUriRequest {
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof ApiConsoleHaloRunV1alpha1ThemeApiUpgradeThemeFromUri
|
||||
*/
|
||||
readonly name: string;
|
||||
|
||||
/**
|
||||
*
|
||||
* @type {UpgradeFromUriRequest}
|
||||
* @memberof ApiConsoleHaloRunV1alpha1ThemeApiUpgradeThemeFromUri
|
||||
*/
|
||||
readonly upgradeFromUriRequest: UpgradeFromUriRequest;
|
||||
}
|
||||
|
||||
/**
|
||||
* ApiConsoleHaloRunV1alpha1ThemeApi - object-oriented interface
|
||||
* @export
|
||||
|
@ -1307,6 +1559,22 @@ export class ApiConsoleHaloRunV1alpha1ThemeApi extends BaseAPI {
|
|||
.then((request) => request(this.axios, this.basePath));
|
||||
}
|
||||
|
||||
/**
|
||||
* Install a theme from uri.
|
||||
* @param {ApiConsoleHaloRunV1alpha1ThemeApiInstallThemeFromUriRequest} requestParameters Request parameters.
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
* @memberof ApiConsoleHaloRunV1alpha1ThemeApi
|
||||
*/
|
||||
public installThemeFromUri(
|
||||
requestParameters: ApiConsoleHaloRunV1alpha1ThemeApiInstallThemeFromUriRequest,
|
||||
options?: AxiosRequestConfig
|
||||
) {
|
||||
return ApiConsoleHaloRunV1alpha1ThemeApiFp(this.configuration)
|
||||
.installThemeFromUri(requestParameters.installFromUriRequest, options)
|
||||
.then((request) => request(this.axios, this.basePath));
|
||||
}
|
||||
|
||||
/**
|
||||
* List themes.
|
||||
* @param {ApiConsoleHaloRunV1alpha1ThemeApiListThemesRequest} requestParameters Request parameters.
|
||||
|
@ -1397,4 +1665,24 @@ export class ApiConsoleHaloRunV1alpha1ThemeApi extends BaseAPI {
|
|||
.upgradeTheme(requestParameters.name, requestParameters.file, options)
|
||||
.then((request) => request(this.axios, this.basePath));
|
||||
}
|
||||
|
||||
/**
|
||||
* Upgrade a theme from uri.
|
||||
* @param {ApiConsoleHaloRunV1alpha1ThemeApiUpgradeThemeFromUriRequest} requestParameters Request parameters.
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
* @memberof ApiConsoleHaloRunV1alpha1ThemeApi
|
||||
*/
|
||||
public upgradeThemeFromUri(
|
||||
requestParameters: ApiConsoleHaloRunV1alpha1ThemeApiUpgradeThemeFromUriRequest,
|
||||
options?: AxiosRequestConfig
|
||||
) {
|
||||
return ApiConsoleHaloRunV1alpha1ThemeApiFp(this.configuration)
|
||||
.upgradeThemeFromUri(
|
||||
requestParameters.name,
|
||||
requestParameters.upgradeFromUriRequest,
|
||||
options
|
||||
)
|
||||
.then((request) => request(this.axios, this.basePath));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -58,6 +58,7 @@ export * from "./group-kind";
|
|||
export * from "./group-list";
|
||||
export * from "./group-spec";
|
||||
export * from "./group-status";
|
||||
export * from "./install-from-uri-request";
|
||||
export * from "./license";
|
||||
export * from "./listed-auth-provider";
|
||||
export * from "./listed-comment";
|
||||
|
@ -160,6 +161,7 @@ export * from "./theme";
|
|||
export * from "./theme-list";
|
||||
export * from "./theme-spec";
|
||||
export * from "./theme-status";
|
||||
export * from "./upgrade-from-uri-request";
|
||||
export * from "./user";
|
||||
export * from "./user-connection";
|
||||
export * from "./user-connection-list";
|
||||
|
|
|
@ -0,0 +1,27 @@
|
|||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
/**
|
||||
* Halo Next API
|
||||
* No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)
|
||||
*
|
||||
* The version of the OpenAPI document: 2.0.0
|
||||
*
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||
* https://openapi-generator.tech
|
||||
* Do not edit the class manually.
|
||||
*/
|
||||
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
* @interface InstallFromUriRequest
|
||||
*/
|
||||
export interface InstallFromUriRequest {
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof InstallFromUriRequest
|
||||
*/
|
||||
uri: string;
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
/**
|
||||
* Halo Next API
|
||||
* No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)
|
||||
*
|
||||
* The version of the OpenAPI document: 2.0.0
|
||||
*
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||
* https://openapi-generator.tech
|
||||
* Do not edit the class manually.
|
||||
*/
|
||||
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
* @interface UpgradeFromUriRequest
|
||||
*/
|
||||
export interface UpgradeFromUriRequest {
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof UpgradeFromUriRequest
|
||||
*/
|
||||
uri: string;
|
||||
}
|
|
@ -597,10 +597,19 @@ core:
|
|||
uninstall_and_delete_config:
|
||||
button: Uninstall and delete config
|
||||
title: Are you sure you want to uninstall this theme and its corresponding settings?
|
||||
remote_download:
|
||||
title: Remote download address detected, do you want to download?
|
||||
description: "Please carefully verify whether this address can be trusted: {url}"
|
||||
upload_modal:
|
||||
titles:
|
||||
install: Install theme
|
||||
upgrade: Upgrade theme ({display_name})
|
||||
tabs:
|
||||
local: Local
|
||||
remote:
|
||||
title: Remote
|
||||
fields:
|
||||
url: Remote URL
|
||||
list_modal:
|
||||
titles:
|
||||
installed_themes: Installed Themes
|
||||
|
|
|
@ -597,10 +597,19 @@ core:
|
|||
uninstall_and_delete_config:
|
||||
button: 卸载并删除配置
|
||||
title: 确定要卸载该主题以及对应的配置吗?
|
||||
remote_download:
|
||||
title: 检测到了远程下载地址,是否需要下载?
|
||||
description: 请仔细鉴别此地址是否可信:{url}
|
||||
upload_modal:
|
||||
titles:
|
||||
install: 安装主题
|
||||
upgrade: 升级主题({display_name})
|
||||
tabs:
|
||||
local: 本地上传
|
||||
remote:
|
||||
title: 远程下载
|
||||
fields:
|
||||
url: 下载地址
|
||||
list_modal:
|
||||
titles:
|
||||
installed_themes: 已安装的主题
|
||||
|
|
|
@ -597,10 +597,19 @@ core:
|
|||
uninstall_and_delete_config:
|
||||
button: 卸載並刪除配置
|
||||
title: 確定要卸載該主題以及對應的配置嗎?
|
||||
remote_download:
|
||||
title: 偵測到遠端下載地址,是否需要下載?
|
||||
description: 請仔細鑑別此地址是否可信:{url}
|
||||
upload_modal:
|
||||
titles:
|
||||
install: 安裝主題
|
||||
upgrade: 升級主題({display_name})
|
||||
tabs:
|
||||
local: 本地上傳
|
||||
remote:
|
||||
title: 遠端下載
|
||||
fields:
|
||||
url: 下載地址
|
||||
list_modal:
|
||||
titles:
|
||||
installed_themes: 已安裝的主題
|
||||
|
|
|
@ -17,11 +17,12 @@ import LazyImage from "@/components/image/LazyImage.vue";
|
|||
import ThemePreviewModal from "./preview/ThemePreviewModal.vue";
|
||||
import ThemeUploadModal from "./ThemeUploadModal.vue";
|
||||
import ThemeListItem from "./components/ThemeListItem.vue";
|
||||
import { computed, ref } from "vue";
|
||||
import { computed, ref, nextTick, watch } from "vue";
|
||||
import type { Theme } from "@halo-dev/api-client";
|
||||
import { apiClient } from "@/utils/api-client";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { useQuery } from "@tanstack/vue-query";
|
||||
import { useRouteQuery } from "@vueuse/router";
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
|
@ -143,6 +144,19 @@ const handleOpenInstallModal = () => {
|
|||
themeToUpgrade.value = undefined;
|
||||
themeUploadVisible.value = true;
|
||||
};
|
||||
|
||||
// handle remote wordpress url from route
|
||||
const remoteDownloadUrl = useRouteQuery<string>("remote-download-url");
|
||||
watch(
|
||||
() => props.visible,
|
||||
(visible) => {
|
||||
if (visible && remoteDownloadUrl.value) {
|
||||
nextTick(() => {
|
||||
handleOpenInstallModal();
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
</script>
|
||||
<template>
|
||||
<VModal
|
||||
|
|
|
@ -1,11 +1,14 @@
|
|||
<script lang="ts" setup>
|
||||
import { Toast, VModal } from "@halo-dev/components";
|
||||
import { Toast, VButton, VModal, VTabItem, VTabs } from "@halo-dev/components";
|
||||
import UppyUpload from "@/components/upload/UppyUpload.vue";
|
||||
import { computed, ref, watch } from "vue";
|
||||
import { computed, ref, watch, nextTick } from "vue";
|
||||
import type { Theme } from "@halo-dev/api-client";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { useQueryClient } from "@tanstack/vue-query";
|
||||
import { useThemeStore } from "@/stores/theme";
|
||||
import { apiClient } from "@/utils/api-client";
|
||||
import { useRouteQuery } from "@vueuse/router";
|
||||
import { submitForm } from "@formkit/core";
|
||||
|
||||
const { t } = useI18n();
|
||||
const queryClient = useQueryClient();
|
||||
|
@ -79,23 +82,118 @@ const onUploaded = () => {
|
|||
|
||||
handleVisibleChange(false);
|
||||
};
|
||||
|
||||
// remote download
|
||||
const activeTabId = ref("local");
|
||||
const remoteDownloadUrl = ref("");
|
||||
const downloading = ref(false);
|
||||
|
||||
const handleDownloadTheme = async () => {
|
||||
try {
|
||||
downloading.value = true;
|
||||
|
||||
if (props.upgradeTheme) {
|
||||
await apiClient.theme.upgradeThemeFromUri({
|
||||
name: props.upgradeTheme.metadata.name,
|
||||
upgradeFromUriRequest: {
|
||||
uri: remoteDownloadUrl.value,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
await apiClient.theme.installThemeFromUri({
|
||||
installFromUriRequest: {
|
||||
uri: remoteDownloadUrl.value,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
Toast.success(
|
||||
t(
|
||||
props.upgradeTheme
|
||||
? "core.common.toast.upgrade_success"
|
||||
: "core.common.toast.install_success"
|
||||
)
|
||||
);
|
||||
|
||||
queryClient.invalidateQueries({ queryKey: ["themes"] });
|
||||
themeStore.fetchActivatedTheme();
|
||||
|
||||
handleVisibleChange(false);
|
||||
|
||||
routeRemoteDownloadUrl.value = null;
|
||||
} catch (error) {
|
||||
console.log("Failed to download theme", error);
|
||||
} finally {
|
||||
downloading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// handle remote download url from route
|
||||
const routeRemoteDownloadUrl = useRouteQuery<string | null>(
|
||||
"remote-download-url"
|
||||
);
|
||||
watch(
|
||||
() => props.visible,
|
||||
(visible) => {
|
||||
if (routeRemoteDownloadUrl.value && visible) {
|
||||
activeTabId.value = "remote";
|
||||
remoteDownloadUrl.value = routeRemoteDownloadUrl.value as string;
|
||||
nextTick(() => {
|
||||
submitForm("theme-remote-download-form");
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
</script>
|
||||
<template>
|
||||
<VModal
|
||||
:visible="visible"
|
||||
:width="600"
|
||||
:title="modalTitle"
|
||||
:centered="false"
|
||||
@update:visible="handleVisibleChange"
|
||||
>
|
||||
<UppyUpload
|
||||
v-if="uploadVisible"
|
||||
:restrictions="{
|
||||
maxNumberOfFiles: 1,
|
||||
allowedFileTypes: ['.zip'],
|
||||
}"
|
||||
:endpoint="endpoint"
|
||||
auto-proceed
|
||||
@uploaded="onUploaded"
|
||||
/>
|
||||
<VTabs v-model:active-id="activeTabId" type="outline" class="!rounded-none">
|
||||
<VTabItem id="local" :label="$t('core.theme.upload_modal.tabs.local')">
|
||||
<UppyUpload
|
||||
v-if="uploadVisible"
|
||||
:restrictions="{
|
||||
maxNumberOfFiles: 1,
|
||||
allowedFileTypes: ['.zip'],
|
||||
}"
|
||||
:endpoint="endpoint"
|
||||
auto-proceed
|
||||
@uploaded="onUploaded"
|
||||
/>
|
||||
</VTabItem>
|
||||
<VTabItem
|
||||
id="remote"
|
||||
:label="$t('core.theme.upload_modal.tabs.remote.title')"
|
||||
>
|
||||
<FormKit
|
||||
id="theme-remote-download-form"
|
||||
name="theme-remote-download-form"
|
||||
type="form"
|
||||
:preserve="true"
|
||||
@submit="handleDownloadTheme"
|
||||
>
|
||||
<FormKit
|
||||
v-model="remoteDownloadUrl"
|
||||
:label="$t('core.theme.upload_modal.tabs.remote.fields.url')"
|
||||
type="text"
|
||||
></FormKit>
|
||||
</FormKit>
|
||||
|
||||
<div class="pt-5">
|
||||
<VButton
|
||||
:loading="downloading"
|
||||
type="secondary"
|
||||
@click="$formkit.submit('theme-remote-download-form')"
|
||||
>
|
||||
{{ $t("core.common.buttons.download") }}
|
||||
</VButton>
|
||||
</div>
|
||||
</VTabItem>
|
||||
</VTabs>
|
||||
</VModal>
|
||||
</template>
|
||||
|
|
|
@ -24,6 +24,7 @@ import {
|
|||
VSpace,
|
||||
VTabbar,
|
||||
VLoading,
|
||||
Dialog,
|
||||
} from "@halo-dev/components";
|
||||
import ThemeListModal from "../components/ThemeListModal.vue";
|
||||
import ThemePreviewModal from "../components/preview/ThemePreviewModal.vue";
|
||||
|
@ -34,6 +35,7 @@ import { storeToRefs } from "pinia";
|
|||
import { apiClient } from "@/utils/api-client";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { useQuery } from "@tanstack/vue-query";
|
||||
import { useRouteQuery } from "@vueuse/router";
|
||||
|
||||
const { currentUserHasPermission } = usePermission();
|
||||
const { t } = useI18n();
|
||||
|
@ -157,6 +159,27 @@ onMounted(() => {
|
|||
watch([() => route.name, () => route.params], async () => {
|
||||
handleTriggerTabChange();
|
||||
});
|
||||
|
||||
// handle remote download url from route
|
||||
const remoteDownloadUrl = useRouteQuery<string | null>("remote-download-url");
|
||||
onMounted(() => {
|
||||
if (remoteDownloadUrl.value) {
|
||||
Dialog.warning({
|
||||
title: t("core.theme.operations.remote_download.title"),
|
||||
description: t("core.theme.operations.remote_download.description", {
|
||||
url: remoteDownloadUrl.value,
|
||||
}),
|
||||
confirmText: t("core.common.buttons.download"),
|
||||
cancelText: t("core.common.buttons.cancel"),
|
||||
onConfirm() {
|
||||
themesModal.value = true;
|
||||
},
|
||||
onCancel() {
|
||||
remoteDownloadUrl.value = null;
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<BasicLayout>
|
||||
|
|
Loading…
Reference in New Issue