refactor: use new apis to refactor themes management (#820)

#### What type of PR is this?

/kind improvement

#### What this PR does / why we need it:

使用新的 API 来管理主题,用于区分主题管理和配置相关的权限。适配:https://github.com/halo-dev/halo/pull/3135

#### Which issue(s) this PR fixes:

Ref https://github.com/halo-dev/halo/issues/3069

#### Screenshots:

#### Special notes for your reviewer:

重点测试:

1. Halo 需要切换到 https://github.com/halo-dev/halo/pull/3135 分支。
2. Console 需要 `pnpm install`。
1. 主题设置项,需要测试保存,安装新主题之后表单是否正常。
3. 创建一个角色,测试仅有管理/查看主题的角色,登录之后检查是否符合预期。

#### Does this PR introduce a user-facing change?

```release-note
重构 Console 端主题的设置表单逻辑。
```
pull/828/head^2
Ryan Wang 2023-01-30 14:36:10 +08:00 committed by GitHub
parent a2935de6ef
commit ab888119ef
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 245 additions and 147 deletions

View File

@ -16,11 +16,14 @@ import { computed, markRaw, ref, watch, type Component } from "vue";
import Fuse from "fuse.js";
import { apiClient } from "@/utils/api-client";
import { usePermission } from "@/utils/permission";
import { useThemeStore } from "@/stores/theme";
import { storeToRefs } from "pinia";
const router = useRouter();
const route = useRoute();
const { currentUserHasPermission } = usePermission();
const { activatedTheme } = storeToRefs(useThemeStore());
const props = withDefaults(
defineProps<{
@ -248,30 +251,17 @@ const handleBuildSearchIndex = () => {
});
});
});
}
// get theme settings
apiClient.extension.configMap
.getv1alpha1ConfigMap({
name: "system",
})
.then(({ data: systemConfigMap }) => {
if (systemConfigMap.data?.theme) {
const themeConfig = JSON.parse(systemConfigMap.data.theme);
apiClient.extension.theme
.getthemeHaloRunV1alpha1Theme({
name: themeConfig.active,
})
.then(({ data: theme }) => {
if (theme && theme.spec.settingName) {
apiClient.extension.setting
.getv1alpha1Setting({
name: theme.spec.settingName,
})
if (currentUserHasPermission(["system:themes:view"])) {
apiClient.theme
.fetchThemeSetting({ name: "-" })
.then(({ data: themeSettings }) => {
themeSettings.spec.forms.forEach((form) => {
fuse.add({
title: `${theme.spec.displayName} / ${form.label}`,
title: [activatedTheme.value?.spec.displayName, form.label].join(
" / "
),
icon: {
component: markRaw(IconPalette),
},
@ -286,10 +276,6 @@ const handleBuildSearchIndex = () => {
});
});
}
});
}
});
}
};
const handleKeydown = (e: KeyboardEvent) => {

View File

@ -1,6 +1,6 @@
// core libs
// types
import type { Ref } from "vue";
import { computed, watch, type ComputedRef, type Ref } from "vue";
import { ref } from "vue";
import { apiClient } from "../utils/api-client";
@ -179,3 +179,68 @@ export function useSettingForm(
handleReset,
};
}
interface useSettingFormConvertReturn {
formSchema: ComputedRef<
(FormKitSchemaCondition | FormKitSchemaNode)[] | undefined
>;
configMapFormData: Ref<Record<string, Record<string, string>>>;
convertToSave: () => ConfigMap | undefined;
}
export function useSettingFormConvert(
setting: Ref<Setting | undefined>,
configMap: Ref<ConfigMap | undefined>,
group: Ref<string>
): useSettingFormConvertReturn {
const configMapFormData = ref<Record<string, Record<string, string>>>({});
const formSchema = computed(() => {
if (!setting.value) {
return;
}
const { forms } = setting.value.spec;
return forms.find((item) => item.group === group?.value)?.formSchema as (
| FormKitSchemaCondition
| FormKitSchemaNode
)[];
});
watch(
() => configMap.value,
() => {
const { forms } = setting.value?.spec || {};
forms?.forEach((form) => {
configMapFormData.value[form.group] = JSON.parse(
configMap.value?.data?.[form.group] || "{}"
);
});
}
);
function convertToSave() {
const configMapToUpdate = cloneDeep(configMap.value);
if (!configMapToUpdate) {
return;
}
const data: {
[key: string]: string;
} = {};
setting.value?.spec.forms.forEach((item: SettingForm) => {
data[item.group] = JSON.stringify(configMapFormData?.value?.[item.group]);
});
configMapToUpdate.data = data;
return configMapToUpdate;
}
return {
formSchema,
configMapFormData,
convertToSave,
};
}

View File

@ -203,7 +203,11 @@ async function loadUserPermissions() {
return;
}
enable ? (el.style.backgroundColor = "red") : el.remove();
if (enable) {
//TODO
return;
}
el?.remove?.();
}
);
}

View File

@ -44,6 +44,7 @@ const actions: Action[] = [
action: () => {
themePreviewVisible.value = true;
},
permissions: ["system:themes:view"],
},
{
icon: markRaw(IconBookRead),

View File

@ -1,45 +1,73 @@
<script lang="ts" setup>
// core libs
import { computed, inject, watch } from "vue";
import { inject, ref, watch } from "vue";
// components
import { VButton } from "@halo-dev/components";
// types
import type { Ref } from "vue";
import type { Theme } from "@halo-dev/api-client";
import type { ConfigMap, Setting, Theme } from "@halo-dev/api-client";
// hooks
import { useSettingForm } from "@/composables/use-setting-form";
import { useRouteParams } from "@vueuse/router";
import type { FormKitSchemaCondition, FormKitSchemaNode } from "@formkit/core";
import { apiClient } from "@/utils/api-client";
import { useSettingFormConvert } from "@/composables/use-setting-form";
const group = useRouteParams<string>("group");
const selectedTheme = inject<Ref<Theme | undefined>>("selectedTheme");
const settingName = computed(() => selectedTheme?.value?.spec.settingName);
const configMapName = computed(() => selectedTheme?.value?.spec.configMapName);
const saving = ref(false);
const setting = ref<Setting>();
const configMap = ref<ConfigMap>();
const {
const { configMapFormData, formSchema, convertToSave } = useSettingFormConvert(
setting,
configMapFormData,
saving,
handleFetchConfigMap,
handleFetchSettings,
handleSaveConfigMap,
} = useSettingForm(settingName, configMapName);
configMap,
group
);
const formSchema = computed(() => {
if (!setting.value) {
const handleFetchSettings = async () => {
if (!selectedTheme?.value) return;
const { data } = await apiClient.theme.fetchThemeSetting({
name: selectedTheme?.value?.metadata.name,
});
setting.value = data;
};
const handleFetchConfigMap = async () => {
if (!selectedTheme?.value) return;
const { data } = await apiClient.theme.fetchThemeConfig({
name: selectedTheme?.value?.metadata.name,
});
configMap.value = data;
};
const handleSaveConfigMap = async () => {
saving.value = true;
const configMapToUpdate = convertToSave();
if (!configMapToUpdate || !selectedTheme?.value) {
saving.value = false;
return;
}
const { forms } = setting.value.spec;
return forms.find((item) => item.group === group?.value)?.formSchema as (
| FormKitSchemaCondition
| FormKitSchemaNode
)[];
});
const { data: newConfigMap } = await apiClient.theme.updateThemeConfig({
name: selectedTheme?.value?.metadata.name,
configMap: configMapToUpdate,
});
await handleFetchSettings();
configMap.value = newConfigMap;
saving.value = false;
};
await handleFetchSettings();
await handleFetchConfigMap();
@ -57,7 +85,7 @@ watch(
<div class="bg-white p-4">
<div>
<FormKit
v-if="group && formSchema && configMapFormData"
v-if="group && formSchema && configMapFormData?.[group]"
:id="group"
v-model="configMapFormData[group]"
:name="group"
@ -72,7 +100,7 @@ watch(
/>
</FormKit>
</div>
<div v-permission="['system:configmaps:manage']" class="pt-5">
<div v-permission="['system:themes:manage']" class="pt-5">
<div class="flex justify-start">
<VButton
:loading="saving"

View File

@ -1,10 +1,14 @@
<script lang="ts" setup>
import ThemePreviewListItem from "./ThemePreviewListItem.vue";
import { useSettingForm } from "@/composables/use-setting-form";
import { useSettingFormConvert } from "@/composables/use-setting-form";
import { useThemeStore } from "@/stores/theme";
import { apiClient } from "@/utils/api-client";
import type { FormKitSchemaCondition, FormKitSchemaNode } from "@formkit/core";
import type { SettingForm, Theme } from "@halo-dev/api-client";
import type {
ConfigMap,
Setting,
SettingForm,
Theme,
} from "@halo-dev/api-client";
import {
VModal,
IconLink,
@ -111,21 +115,61 @@ const modalTitle = computed(() => {
});
// theme settings
const setting = ref<Setting>();
const configMap = ref<ConfigMap>();
const saving = ref(false);
const settingTabs = ref<SettingTab[]>([] as SettingTab[]);
const activeSettingTab = ref("");
const settingsVisible = ref(false);
const settingName = computed(() => selectedTheme.value?.spec.settingName);
const configMapName = computed(() => selectedTheme.value?.spec.configMapName);
const {
const { formSchema, configMapFormData, convertToSave } = useSettingFormConvert(
setting,
configMapFormData,
saving,
handleFetchConfigMap,
handleFetchSettings,
handleSaveConfigMap,
} = useSettingForm(settingName, configMapName);
configMap,
activeSettingTab
);
const handleFetchSettings = async () => {
if (!selectedTheme?.value) return;
const { data } = await apiClient.theme.fetchThemeSetting({
name: selectedTheme?.value?.metadata.name,
});
setting.value = data;
};
const handleFetchConfigMap = async () => {
if (!selectedTheme?.value) return;
const { data } = await apiClient.theme.fetchThemeConfig({
name: selectedTheme?.value?.metadata.name,
});
configMap.value = data;
};
const handleSaveConfigMap = async () => {
saving.value = true;
const configMapToUpdate = convertToSave();
if (!configMapToUpdate || !selectedTheme?.value) {
saving.value = false;
return;
}
const { data: newConfigMap } = await apiClient.theme.updateThemeConfig({
name: selectedTheme?.value?.metadata.name,
configMap: configMapToUpdate,
});
await handleFetchSettings();
configMap.value = newConfigMap;
saving.value = false;
handleRefresh();
};
watch(
() => selectedTheme.value,
@ -157,20 +201,6 @@ const handleOpenSettings = (theme?: Theme) => {
settingsVisible.value = !settingsVisible.value;
};
const formSchema = computed(() => {
if (!setting.value) {
return;
}
const { forms } = setting.value.spec;
return forms.find((item) => item.group === activeSettingTab.value)
?.formSchema as (FormKitSchemaCondition | FormKitSchemaNode)[];
});
const handleSaveThemeConfigMap = async () => {
await handleSaveConfigMap();
handleRefresh();
};
const handleRefresh = () => {
previewFrame.value?.contentWindow?.location.reload();
};
@ -288,14 +318,14 @@ const iframeClasses = computed(() => {
configMapFormData &&
formSchema
"
:id="tab.id"
:id="`preview-setting-${tab.id}`"
:key="tab.id"
v-model="configMapFormData[tab.id]"
:name="tab.id"
:actions="false"
:preserve="true"
type="form"
@submit="handleSaveThemeConfigMap"
@submit="handleSaveConfigMap"
>
<FormKitSchema
:schema="formSchema"
@ -303,12 +333,16 @@ const iframeClasses = computed(() => {
/>
</FormKit>
</div>
<div v-permission="['system:configmaps:manage']" class="pt-5">
<div v-permission="['system:themes:manage']" class="pt-5">
<div class="flex justify-start">
<VButton
:loading="saving"
type="secondary"
@click="$formkit.submit(activeSettingTab || '')"
@click="
$formkit.submit(
`preview-setting-${activeSettingTab}` || ''
)
"
>
保存
</VButton>

View File

@ -45,23 +45,11 @@ export function useThemeLifeCycle(
description: theme.value?.spec.displayName,
onConfirm: async () => {
try {
const { data: systemConfigMap } =
await apiClient.extension.configMap.getv1alpha1ConfigMap({
name: "system",
});
if (!theme.value) return;
if (systemConfigMap.data) {
const themeConfigToUpdate = JSON.parse(
systemConfigMap.data?.theme || "{}"
);
themeConfigToUpdate.active = theme.value?.metadata.name;
systemConfigMap.data["theme"] = JSON.stringify(themeConfigToUpdate);
await apiClient.extension.configMap.updatev1alpha1ConfigMap({
name: "system",
configMap: systemConfigMap,
await apiClient.theme.activateTheme({
name: theme.value?.metadata.name,
});
}
Toast.success("启用成功");
} catch (e) {

View File

@ -1,7 +1,7 @@
<script lang="ts" setup>
// core libs
import { nextTick, onMounted, type Ref } from "vue";
import { computed, provide, ref, watch } from "vue";
import { provide, ref, watch } from "vue";
import { useRoute, useRouter } from "vue-router";
// libs
@ -11,7 +11,6 @@ import cloneDeep from "lodash.clonedeep";
import { useThemeLifeCycle } from "../composables/use-theme";
// types
import BasicLayout from "@/layouts/BasicLayout.vue";
import { useSettingForm } from "@/composables/use-setting-form";
// components
import {
@ -28,10 +27,11 @@ import {
} from "@halo-dev/components";
import ThemeListModal from "../components/ThemeListModal.vue";
import ThemePreviewModal from "../components/preview/ThemePreviewModal.vue";
import type { SettingForm, Theme } from "@halo-dev/api-client";
import type { Setting, SettingForm, Theme } from "@halo-dev/api-client";
import { usePermission } from "@/utils/permission";
import { useThemeStore } from "@/stores/theme";
import { storeToRefs } from "pinia";
import { apiClient } from "@/utils/api-client";
const { currentUserHasPermission } = usePermission();
@ -63,19 +63,23 @@ const activeTab = ref("");
const { loading, isActivated, handleActiveTheme } =
useThemeLifeCycle(selectedTheme);
const settingName = computed(() => selectedTheme.value?.spec.settingName);
const configMapName = computed(() => selectedTheme.value?.spec.configMapName);
const { setting, handleFetchSettings } = useSettingForm(
settingName,
configMapName
);
provide<Ref<Theme | undefined>>("selectedTheme", selectedTheme);
const route = useRoute();
const router = useRouter();
const setting = ref<Setting>();
const handleFetchSettings = async () => {
if (!selectedTheme.value) return;
const { data } = await apiClient.theme.fetchThemeSetting({
name: selectedTheme.value?.metadata.name,
});
setting.value = data;
};
const handleTabChange = (id: string) => {
const tab = tabs.value.find((item) => item.id === id);
if (tab) {
@ -91,7 +95,7 @@ watch(
// reset tabs
tabs.value = cloneDeep(initialTabs);
if (!currentUserHasPermission(["system:settings:view"])) {
if (!currentUserHasPermission(["system:themes:view"])) {
handleTriggerTabChange();
return;
}

View File

@ -34,7 +34,7 @@ export default definePlugin({
component: ThemeSetting,
meta: {
title: "主题设置",
permissions: ["system:settings:view"],
permissions: ["system:themes:view"],
},
},
],

View File

@ -2,36 +2,24 @@ import { apiClient } from "@/utils/api-client";
import type { Theme } from "@halo-dev/api-client";
import { defineStore } from "pinia";
import { ref } from "vue";
import { usePermission } from "@/utils/permission";
export const useThemeStore = defineStore("theme", () => {
const activatedTheme = ref<Theme>();
async function fetchActivatedTheme() {
try {
const { data } = await apiClient.extension.configMap.getv1alpha1ConfigMap(
{
name: "system",
},
{ mute: true }
);
const { currentUserHasPermission } = usePermission();
if (!data.data?.theme) {
async function fetchActivatedTheme() {
if (!currentUserHasPermission(["system:themes:view"])) {
return;
}
const themeConfig = JSON.parse(data.data.theme);
const { data: themeData } =
await apiClient.extension.theme.getthemeHaloRunV1alpha1Theme(
{
name: themeConfig.active,
},
{
try {
const { data } = await apiClient.theme.fetchActivatedTheme({
mute: true,
}
);
});
activatedTheme.value = themeData;
activatedTheme.value = data;
} catch (e) {
console.error("Failed to fetch active theme", e);
}