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 Fuse from "fuse.js";
import { apiClient } from "@/utils/api-client"; import { apiClient } from "@/utils/api-client";
import { usePermission } from "@/utils/permission"; import { usePermission } from "@/utils/permission";
import { useThemeStore } from "@/stores/theme";
import { storeToRefs } from "pinia";
const router = useRouter(); const router = useRouter();
const route = useRoute(); const route = useRoute();
const { currentUserHasPermission } = usePermission(); const { currentUserHasPermission } = usePermission();
const { activatedTheme } = storeToRefs(useThemeStore());
const props = withDefaults( const props = withDefaults(
defineProps<{ defineProps<{
@ -248,46 +251,29 @@ const handleBuildSearchIndex = () => {
}); });
}); });
}); });
}
// get theme settings if (currentUserHasPermission(["system:themes:view"])) {
apiClient.extension.configMap apiClient.theme
.getv1alpha1ConfigMap({ .fetchThemeSetting({ name: "-" })
name: "system", .then(({ data: themeSettings }) => {
}) themeSettings.spec.forms.forEach((form) => {
.then(({ data: systemConfigMap }) => { fuse.add({
if (systemConfigMap.data?.theme) { title: [activatedTheme.value?.spec.displayName, form.label].join(
const themeConfig = JSON.parse(systemConfigMap.data.theme); " / "
),
apiClient.extension.theme icon: {
.getthemeHaloRunV1alpha1Theme({ component: markRaw(IconPalette),
name: themeConfig.active, },
}) group: "主题设置",
.then(({ data: theme }) => { route: {
if (theme && theme.spec.settingName) { name: "ThemeSetting",
apiClient.extension.setting params: {
.getv1alpha1Setting({ group: form.group,
name: theme.spec.settingName, },
}) },
.then(({ data: themeSettings }) => { });
themeSettings.spec.forms.forEach((form) => { });
fuse.add({
title: `${theme.spec.displayName} / ${form.label}`,
icon: {
component: markRaw(IconPalette),
},
group: "主题设置",
route: {
name: "ThemeSetting",
params: {
group: form.group,
},
},
});
});
});
}
});
}
}); });
} }
}; };

View File

@ -1,6 +1,6 @@
// core libs // core libs
// types // types
import type { Ref } from "vue"; import { computed, watch, type ComputedRef, type Ref } from "vue";
import { ref } from "vue"; import { ref } from "vue";
import { apiClient } from "../utils/api-client"; import { apiClient } from "../utils/api-client";
@ -179,3 +179,68 @@ export function useSettingForm(
handleReset, 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; return;
} }
enable ? (el.style.backgroundColor = "red") : el.remove(); if (enable) {
//TODO
return;
}
el?.remove?.();
} }
); );
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -34,7 +34,7 @@ export default definePlugin({
component: ThemeSetting, component: ThemeSetting,
meta: { meta: {
title: "主题设置", 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 type { Theme } from "@halo-dev/api-client";
import { defineStore } from "pinia"; import { defineStore } from "pinia";
import { ref } from "vue"; import { ref } from "vue";
import { usePermission } from "@/utils/permission";
export const useThemeStore = defineStore("theme", () => { export const useThemeStore = defineStore("theme", () => {
const activatedTheme = ref<Theme>(); const activatedTheme = ref<Theme>();
const { currentUserHasPermission } = usePermission();
async function fetchActivatedTheme() { async function fetchActivatedTheme() {
if (!currentUserHasPermission(["system:themes:view"])) {
return;
}
try { try {
const { data } = await apiClient.extension.configMap.getv1alpha1ConfigMap( const { data } = await apiClient.theme.fetchActivatedTheme({
{ mute: true,
name: "system", });
},
{ mute: true }
);
if (!data.data?.theme) { activatedTheme.value = data;
return;
}
const themeConfig = JSON.parse(data.data.theme);
const { data: themeData } =
await apiClient.extension.theme.getthemeHaloRunV1alpha1Theme(
{
name: themeConfig.active,
},
{
mute: true,
}
);
activatedTheme.value = themeData;
} catch (e) { } catch (e) {
console.error("Failed to fetch active theme", e); console.error("Failed to fetch active theme", e);
} }