halo/console/src/modules/interface/themes/components/ThemeUploadModal.vue

281 lines
7.0 KiB
Vue

<script lang="ts" setup>
import {
Dialog,
Toast,
VButton,
VModal,
VTabItem,
VTabs,
} from "@halo-dev/components";
import UppyUpload from "@/components/upload/UppyUpload.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";
import type { ErrorResponse } from "@uppy/core";
import type { UppyFile } from "@uppy/utils";
const { t } = useI18n();
const queryClient = useQueryClient();
const themeStore = useThemeStore();
const props = withDefaults(
defineProps<{
visible: boolean;
upgradeTheme?: Theme;
}>(),
{
visible: false,
upgradeTheme: undefined,
}
);
const emit = defineEmits<{
(event: "update:visible", visible: boolean): void;
(event: "close"): void;
}>();
const uploadVisible = ref(false);
const modalTitle = computed(() => {
return props.upgradeTheme
? t("core.theme.upload_modal.titles.upgrade", {
display_name: props.upgradeTheme.spec.displayName,
})
: t("core.theme.upload_modal.titles.install");
});
const handleVisibleChange = (visible: boolean) => {
emit("update:visible", visible);
if (!visible) {
emit("close");
}
};
const endpoint = computed(() => {
if (props.upgradeTheme) {
return `/apis/api.console.halo.run/v1alpha1/themes/${props.upgradeTheme.metadata.name}/upgrade`;
}
return "/apis/api.console.halo.run/v1alpha1/themes/install";
});
watch(
() => props.visible,
(newValue) => {
if (newValue) {
uploadVisible.value = true;
} else {
const uploadVisibleTimer = setTimeout(() => {
uploadVisible.value = false;
clearTimeout(uploadVisibleTimer);
}, 200);
}
}
);
const onUploaded = () => {
Toast.success(
t(
props.upgradeTheme
? "core.common.toast.upgrade_success"
: "core.common.toast.install_success"
)
);
queryClient.invalidateQueries({ queryKey: ["themes"] });
themeStore.fetchActivatedTheme();
handleVisibleChange(false);
};
interface ThemeInstallationErrorResponse {
detail: string;
instance: string;
themeName: string;
requestId: string;
status: number;
timestamp: string;
title: string;
type: string;
}
const THEME_ALREADY_EXISTS_TYPE = "https://halo.run/probs/theme-alreay-exists";
const onError = (file: UppyFile<unknown>, response: ErrorResponse) => {
const body = response.body as ThemeInstallationErrorResponse;
if (body.type === THEME_ALREADY_EXISTS_TYPE) {
handleCatchExistsException(body, file.data as File);
}
};
const handleCatchExistsException = async (
error: ThemeInstallationErrorResponse,
file?: File
) => {
Dialog.info({
title: t(
"core.theme.upload_modal.operations.existed_during_installation.title"
),
description: t(
"core.theme.upload_modal.operations.existed_during_installation.description"
),
confirmText: t("core.common.buttons.confirm"),
cancelText: t("core.common.buttons.cancel"),
onConfirm: async () => {
if (activeTabId.value === "local") {
if (!file) {
throw new Error("File is required");
}
await apiClient.theme.upgradeTheme({
name: error.themeName,
file: file,
});
} else if (activeTabId.value === "remote") {
await apiClient.theme.upgradeThemeFromUri({
name: error.themeName,
upgradeFromUriRequest: {
uri: remoteDownloadUrl.value,
},
});
} else {
throw new Error("Unknown tab id");
}
Toast.success(t("core.common.toast.upgrade_success"));
queryClient.invalidateQueries({ queryKey: ["themes"] });
themeStore.fetchActivatedTheme();
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);
// eslint-disable-next-line
} catch (error: any) {
const data = error?.response.data as ThemeInstallationErrorResponse;
if (data?.type === THEME_ALREADY_EXISTS_TYPE) {
handleCatchExistsException(data);
}
console.error("Failed to download theme", error);
} finally {
routeRemoteDownloadUrl.value = null;
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"
>
<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"
@error="onError"
/>
</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>