diff --git a/src/main/java/run/halo/app/core/extension/Setting.java b/src/main/java/run/halo/app/core/extension/Setting.java index 9a60918b6..918e859b1 100644 --- a/src/main/java/run/halo/app/core/extension/Setting.java +++ b/src/main/java/run/halo/app/core/extension/Setting.java @@ -21,17 +21,25 @@ public class Setting extends AbstractExtension { public static final String KIND = "Setting"; - @Schema(required = true, minLength = 1) - private List spec; + @Schema(required = true) + private SettingSpec spec; @Data public static class SettingSpec { + @Schema(required = true, minLength = 1) + private List forms; + } + + @Data + public static class SettingForm { + @Schema(required = true) private String group; private String label; + @Schema(required = true) private List formSchema; } } diff --git a/src/main/java/run/halo/app/core/extension/reconciler/ThemeReconciler.java b/src/main/java/run/halo/app/core/extension/reconciler/ThemeReconciler.java index 8271f474e..f8c4bac5e 100644 --- a/src/main/java/run/halo/app/core/extension/reconciler/ThemeReconciler.java +++ b/src/main/java/run/halo/app/core/extension/reconciler/ThemeReconciler.java @@ -1,16 +1,26 @@ package run.halo.app.core.extension.reconciler; +import com.fasterxml.jackson.databind.JsonNode; import java.io.IOException; import java.nio.file.Path; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.stream.Collectors; import org.apache.commons.lang3.StringUtils; +import org.springframework.util.CollectionUtils; import org.springframework.util.FileSystemUtils; 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.ExtensionClient; +import run.halo.app.extension.Metadata; import run.halo.app.extension.controller.Reconciler; import run.halo.app.extension.controller.Reconciler.Request; import run.halo.app.infra.exception.ThemeUninstallException; import run.halo.app.infra.properties.HaloProperties; +import run.halo.app.infra.utils.JsonUtils; import run.halo.app.theme.ThemePathPolicy; /** @@ -36,10 +46,77 @@ public class ThemeReconciler implements Reconciler { if (isDeleted(theme)) { reconcileThemeDeletion(theme); } + themeSettingDefaultConfig(theme); }); return new Result(false, null); } + private void themeSettingDefaultConfig(Theme theme) { + if (StringUtils.isBlank(theme.getSpec().getSettingName())) { + return; + } + final String userDefinedConfigMapName = theme.getSpec().getConfigMapName(); + + final String newConfigMapName = UUID.randomUUID().toString(); + if (StringUtils.isBlank(userDefinedConfigMapName)) { + client.fetch(Theme.class, theme.getMetadata().getName()) + .ifPresent(themeToUse -> { + Theme oldTheme = JsonUtils.deepCopy(themeToUse); + themeToUse.getSpec().setConfigMapName(newConfigMapName); + if (!oldTheme.equals(themeToUse)) { + client.update(themeToUse); + } + }); + } + + final String configMapNameToUse = + StringUtils.defaultIfBlank(userDefinedConfigMapName, newConfigMapName); + + boolean existConfigMap = client.fetch(ConfigMap.class, configMapNameToUse) + .isPresent(); + if (existConfigMap) { + return; + } + + client.fetch(Setting.class, theme.getSpec().getSettingName()) + .ifPresent(setting -> { + Map data = settingDefinedDefaultValueMap(setting); + if (CollectionUtils.isEmpty(data)) { + return; + } + ConfigMap configMap = new ConfigMap(); + configMap.setMetadata(new Metadata()); + configMap.getMetadata().setName(configMapNameToUse); + configMap.setData(data); + client.create(configMap); + }); + } + + Map settingDefinedDefaultValueMap(Setting setting) { + final String defaultValueField = "value"; + final String nameField = "name"; + List forms = setting.getSpec().getForms(); + if (CollectionUtils.isEmpty(forms)) { + return null; + } + Map data = new LinkedHashMap<>(); + for (Setting.SettingForm form : forms) { + String group = form.getGroup(); + Map groupValue = form.getFormSchema().stream() + .map(o -> JsonUtils.DEFAULT_JSON_MAPPER.convertValue(o, JsonNode.class)) + .filter(jsonNode -> jsonNode.isObject() && jsonNode.has(nameField) + && jsonNode.has(defaultValueField)) + .map(jsonNode -> { + String name = jsonNode.findValue(nameField).asText(); + JsonNode value = jsonNode.findValue(defaultValueField); + return Map.entry(name, value); + }) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + data.put(group, JsonUtils.objectToJson(groupValue)); + } + return data; + } + private void reconcileThemeDeletion(Theme theme) { deleteThemeFiles(theme); // delete theme setting form diff --git a/src/main/resources/extensions/attachment-local-policy.yaml b/src/main/resources/extensions/attachment-local-policy.yaml index 68b169750..8cea39df3 100644 --- a/src/main/resources/extensions/attachment-local-policy.yaml +++ b/src/main/resources/extensions/attachment-local-policy.yaml @@ -12,10 +12,11 @@ kind: Setting metadata: name: local-policy-template-setting spec: - - group: default - label: Default - formSchema: - - $formkit: text - name: location - label: 存储位置 - help: ~/halo-next/attachments 下的子目录 + forms: + - group: default + label: Default + formSchema: + - $formkit: text + name: location + label: 存储位置 + help: ~/halo-next/attachments 下的子目录 diff --git a/src/main/resources/extensions/system-setting.yaml b/src/main/resources/extensions/system-setting.yaml index 2b72192a6..d912b2279 100644 --- a/src/main/resources/extensions/system-setting.yaml +++ b/src/main/resources/extensions/system-setting.yaml @@ -3,130 +3,131 @@ kind: Setting metadata: name: system spec: - - group: basic - label: 基本设置 - formSchema: - - $formkit: text - label: "站点标题" - name: title - validation: required - - $formkit: text - label: "站点副标题" - name: subtitle - validation: required - - $formkit: text - label: Logo - name: logo - - $formkit: text - label: Favicon - name: favicon - - group: user - label: 用户设置 - formSchema: - - $formkit: checkbox - label: "是否公开注册" - value: false - name: allowRegistration - validation: required - - $formkit: text - label: "默认角色" - name: defaultRole - validation: required - - group: routeRules - label: 主题模板路由设置 - formSchema: - - $formkit: text - label: "分类页路由前缀" - value: "categories" - name: categories - validation: required | alphanumeric - - $formkit: text - label: "标签页路由前缀" - value: "tags" - name: tags - validation: required | alphanumeric - - $formkit: text - label: "归档页路由前缀" - value: "archives" - name: archives - validation: required | alphanumeric - - $formkit: select - label: "文章详情页访问规则" - value: "/archives/{slug}" - options: - - /archives/{slug} - - /archives/{name} - - /?p={name} - - /?p={slug} - - /{year:\d{4}}/{slug} - - /{year:\d{4}}/{month:\d{2}}/{slug} - - /{year:\d{4}}/{month:\d{2}}/{day:\d{2}}/{slug} - name: post - validation: required - - group: post - label: 文章设置 - formSchema: - - $formkit: select - label: "列表排序方式" - name: sortOrder - value: "publishTime" - options: - visitCount: "浏览量" - publishTime: "发布时间" - updateTime: "更新时间" - validation: required - - $formkit: number - label: "列表显示条数" - name: pageSize - value: 10 - min: 1 - max: 100 - validation: required - - $formkit: checkbox - label: "新文章审核" - value: false - name: review - help: "用户发布文章是否需要管理员审核" - - group: seo - label: SEO 设置 - formSchema: - - $formkit: checkbox - name: blockSpiders - label: "屏蔽搜索引擎" - value: false - - $formkit: textarea - name: keywords - label: "站点关键词" - - $formkit: textarea - name: description - label: "站点描述" - - group: comment - label: 评论设置 - formSchema: - - $formkit: checkbox - name: enable - value: true - label: "启用评论" - - $formkit: checkbox - name: requireReviewForNew - value: true - label: "新评论审核" - - $formkit: checkbox - name: systemUserOnly - value: true - label: "仅允许注册用户评论" - - group: codeInjection - label: 代码注入 - formSchema: - - $formkit: textarea - label: "全局 head" - name: globalHead - help: "插入代码到所有页面的 head 标签部分" - - $formkit: textarea - label: "内容页 head" - name: contentHead - help: "插入代码到文章页面和自定义页面的 head 标签部分" - - $formkit: textarea - label: "页脚" - name: footer - help: "插入代码到所有页面的页脚部分" + forms: + - group: basic + label: 基本设置 + formSchema: + - $formkit: text + label: "站点标题" + name: title + validation: required + - $formkit: text + label: "站点副标题" + name: subtitle + validation: required + - $formkit: text + label: Logo + name: logo + - $formkit: text + label: Favicon + name: favicon + - group: user + label: 用户设置 + formSchema: + - $formkit: checkbox + label: "是否公开注册" + value: false + name: allowRegistration + validation: required + - $formkit: text + label: "默认角色" + name: defaultRole + validation: required + - group: routeRules + label: 主题模板路由设置 + formSchema: + - $formkit: text + label: "分类页路由前缀" + value: "categories" + name: categories + validation: required | alphanumeric + - $formkit: text + label: "标签页路由前缀" + value: "tags" + name: tags + validation: required | alphanumeric + - $formkit: text + label: "归档页路由前缀" + value: "archives" + name: archives + validation: required | alphanumeric + - $formkit: select + label: "文章详情页访问规则" + value: "/archives/{slug}" + options: + - /archives/{slug} + - /archives/{name} + - /?p={name} + - /?p={slug} + - /{year:\d{4}}/{slug} + - /{year:\d{4}}/{month:\d{2}}/{slug} + - /{year:\d{4}}/{month:\d{2}}/{day:\d{2}}/{slug} + name: post + validation: required + - group: post + label: 文章设置 + formSchema: + - $formkit: select + label: "列表排序方式" + name: sortOrder + value: "publishTime" + options: + visitCount: "浏览量" + publishTime: "发布时间" + updateTime: "更新时间" + validation: required + - $formkit: number + label: "列表显示条数" + name: pageSize + value: 10 + min: 1 + max: 100 + validation: required + - $formkit: checkbox + label: "新文章审核" + value: false + name: review + help: "用户发布文章是否需要管理员审核" + - group: seo + label: SEO 设置 + formSchema: + - $formkit: checkbox + name: blockSpiders + label: "屏蔽搜索引擎" + value: false + - $formkit: textarea + name: keywords + label: "站点关键词" + - $formkit: textarea + name: description + label: "站点描述" + - group: comment + label: 评论设置 + formSchema: + - $formkit: checkbox + name: enable + value: true + label: "启用评论" + - $formkit: checkbox + name: requireReviewForNew + value: true + label: "新评论审核" + - $formkit: checkbox + name: systemUserOnly + value: true + label: "仅允许注册用户评论" + - group: codeInjection + label: 代码注入 + formSchema: + - $formkit: textarea + label: "全局 head" + name: globalHead + help: "插入代码到所有页面的 head 标签部分" + - $formkit: textarea + label: "内容页 head" + name: contentHead + help: "插入代码到文章页面和自定义页面的 head 标签部分" + - $formkit: textarea + label: "页脚" + name: footer + help: "插入代码到所有页面的页脚部分" diff --git a/src/test/java/run/halo/app/core/extension/SettingTest.java b/src/test/java/run/halo/app/core/extension/SettingTest.java index f6f5cfd3b..86910098b 100644 --- a/src/test/java/run/halo/app/core/extension/SettingTest.java +++ b/src/test/java/run/halo/app/core/extension/SettingTest.java @@ -27,24 +27,25 @@ class SettingTest { metadata: name: setting-name spec: - - group: basic - label: 基本设置 - formSchema: - - $el: h1 - children: Register - - $formkit: text - help: This will be used for your account. - label: Email - name: email - validation: required|email - - group: sns - label: 社交资料 - formSchema: - - $formkit: text - help: This will be used for your theme. - label: color - name: color - validation: required + forms: + - group: basic + label: 基本设置 + formSchema: + - $el: h1 + children: Register + - $formkit: text + help: This will be used for your account. + label: Email + name: email + validation: required|email + - group: sns + label: 社交资料 + formSchema: + - $formkit: text + help: This will be used for your theme. + label: color + name: color + validation: required """; List unstructureds = new YamlUnstructuredLoader(new InMemoryResource(settingYaml)).load(); @@ -55,43 +56,45 @@ class SettingTest { assertThat(setting).isNotNull(); JSONAssert.assertEquals(""" { - "spec": [ - { - "group": "basic", - "label": "基本设置", - "formSchema": [ - { - "$el": "h1", - "children": "Register" - }, - { - "$formkit": "text", - "help": "This will be used for your account.", - "label": "Email", - "name": "email", - "validation": "required|email" - } - ] - }, - { - "group": "sns", - "label": "社交资料", - "formSchema": [ - { - "$formkit": "text", - "help": "This will be used for your theme.", - "label": "color", - "name": "color", - "validation": "required" - } - ] - } - ], - "apiVersion": "v1alpha1", - "kind": "Setting", - "metadata": { - "name": "setting-name" - } + "spec": { + "forms": [ + { + "group": "basic", + "label": "基本设置", + "formSchema": [ + { + "$el": "h1", + "children": "Register" + }, + { + "$formkit": "text", + "help": "This will be used for your account.", + "label": "Email", + "name": "email", + "validation": "required|email" + } + ] + }, + { + "group": "sns", + "label": "社交资料", + "formSchema": [ + { + "$formkit": "text", + "help": "This will be used for your theme.", + "label": "color", + "name": "color", + "validation": "required" + } + ] + } + ] + }, + "apiVersion": "v1alpha1", + "kind": "Setting", + "metadata": { + "name": "setting-name" + } } """, JsonUtils.objectToJson(setting), false); diff --git a/src/test/java/run/halo/app/core/extension/reconciler/ThemeReconcilerTest.java b/src/test/java/run/halo/app/core/extension/reconciler/ThemeReconcilerTest.java index 68e186a6d..f8d25a44c 100644 --- a/src/test/java/run/halo/app/core/extension/reconciler/ThemeReconcilerTest.java +++ b/src/test/java/run/halo/app/core/extension/reconciler/ThemeReconcilerTest.java @@ -1,6 +1,7 @@ package run.halo.app.core.extension.reconciler; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; @@ -11,21 +12,28 @@ import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.time.Instant; +import java.util.Map; import java.util.Optional; +import org.json.JSONException; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; import org.mockito.Mock; +import org.mockito.Mockito; import org.mockito.junit.jupiter.MockitoExtension; +import org.skyscreamer.jsonassert.JSONAssert; import org.springframework.util.FileSystemUtils; import org.springframework.util.ResourceUtils; 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.ExtensionClient; import run.halo.app.extension.Metadata; import run.halo.app.extension.controller.Reconciler; import run.halo.app.infra.properties.HaloProperties; +import run.halo.app.infra.utils.JsonUtils; import run.halo.app.theme.ThemePathPolicy; /** @@ -64,8 +72,9 @@ class ThemeReconcilerTest { Files.createDirectory(testWorkDir); when(haloProperties.getWorkDir()).thenReturn(testWorkDir); - ThemeReconciler themeReconciler = new ThemeReconciler(extensionClient, haloProperties); - ThemePathPolicy themePathPolicy = new ThemePathPolicy(testWorkDir); + final ThemeReconciler themeReconciler = + new ThemeReconciler(extensionClient, haloProperties); + final ThemePathPolicy themePathPolicy = new ThemePathPolicy(testWorkDir); Theme theme = new Theme(); Metadata metadata = new Metadata(); @@ -93,10 +102,127 @@ class ThemeReconcilerTest { themeReconciler.reconcile(new Reconciler.Request(metadata.getName())); - verify(extensionClient, times(1)).fetch(eq(Theme.class), eq(metadata.getName())); - verify(extensionClient, times(1)).fetch(eq(Setting.class), eq(themeSpec.getSettingName())); + verify(extensionClient, times(2)).fetch(eq(Theme.class), eq(metadata.getName())); + verify(extensionClient, times(2)).fetch(eq(Setting.class), eq(themeSpec.getSettingName())); assertThat(Files.exists(testWorkDir)).isTrue(); assertThat(Files.exists(defaultThemePath)).isFalse(); } + + @Test + void themeSettingDefaultValue() throws IOException, JSONException { + Path testWorkDir = tempDirectory.resolve("reconcile-setting-value"); + Files.createDirectory(testWorkDir); + when(haloProperties.getWorkDir()).thenReturn(testWorkDir); + + final ThemeReconciler themeReconciler = + new ThemeReconciler(extensionClient, haloProperties); + + Theme theme = new Theme(); + Metadata metadata = new Metadata(); + metadata.setName("theme-test"); + theme.setMetadata(metadata); + theme.setKind(Theme.KIND); + theme.setApiVersion("theme.halo.run/v1alpha1"); + Theme.ThemeSpec themeSpec = new Theme.ThemeSpec(); + themeSpec.setSettingName(null); + theme.setSpec(themeSpec); + + when(extensionClient.fetch(eq(Theme.class), eq(metadata.getName()))) + .thenReturn(Optional.of(theme)); + Reconciler.Result reconcile = + themeReconciler.reconcile(new Reconciler.Request(metadata.getName())); + assertThat(reconcile.reEnqueue()).isFalse(); + verify(extensionClient, times(1)).fetch(eq(Theme.class), eq(metadata.getName())); + + // setting exists + themeSpec.setSettingName("theme-test-setting"); + when(extensionClient.fetch(eq(ConfigMap.class), any())) + .thenReturn(Optional.of(Mockito.mock(ConfigMap.class))); + assertThat(theme.getSpec().getConfigMapName()).isNull(); + ArgumentCaptor captor = ArgumentCaptor.forClass(Theme.class); + themeReconciler.reconcile(new Reconciler.Request(metadata.getName())); + verify(extensionClient, times(3)) + .fetch(eq(Theme.class), eq(metadata.getName())); + verify(extensionClient, times(1)) + .update(captor.capture()); + Theme value = captor.getValue(); + assertThat(value.getSpec().getConfigMapName()).isNotNull(); + + // populate setting name and configMap name and configMap not exists + themeSpec.setSettingName("theme-test-setting"); + themeSpec.setConfigMapName("theme-test-configmap"); + when(extensionClient.fetch(eq(ConfigMap.class), any())) + .thenReturn(Optional.empty()); + when(extensionClient.fetch(eq(Setting.class), eq(themeSpec.getSettingName()))) + .thenReturn(Optional.of(getFakeSetting())); + themeReconciler.reconcile(new Reconciler.Request(metadata.getName())); + verify(extensionClient, times(1)) + .fetch(eq(Setting.class), eq(themeSpec.getSettingName())); + ArgumentCaptor configMapCaptor = ArgumentCaptor.forClass(ConfigMap.class); + verify(extensionClient, times(1)).create(any(ConfigMap.class)); + verify(extensionClient, times(1)).create(configMapCaptor.capture()); + ConfigMap defaultValueConfigMap = configMapCaptor.getValue(); + Map data = defaultValueConfigMap.getData(); + JSONAssert.assertEquals(""" + { + "sns": "{\\"email\\":\\"example@exmple.com\\"}" + } + """, + JsonUtils.objectToJson(data), + true); + } + + @Test + void settingDefinedDefaultValueMap() throws JSONException { + Setting setting = getFakeSetting(); + when(haloProperties.getWorkDir()).thenReturn(tempDirectory); + Map map = new ThemeReconciler(extensionClient, haloProperties) + .settingDefinedDefaultValueMap(setting); + JSONAssert.assertEquals(""" + { + "sns": "{\\"email\\":\\"example@exmple.com\\"}" + } + """, + JsonUtils.objectToJson(map), + true); + } + + private static Setting getFakeSetting() { + String settingJson = """ + { + "apiVersion": "v1alpha1", + "kind": "Setting", + "metadata": { + "name": "theme-default-setting" + }, + "spec": { + "forms": [{ + "formSchema": [ + { + "$el": "h1", + "children": "Register" + }, + { + "$formkit": "text", + "label": "Email", + "name": "email", + "value": "example@exmple.com" + }, + { + "$formkit": "password", + "label": "Password", + "name": "password", + "validation": "required|length:5,16", + "value": null + } + ], + "group": "sns", + "label": "社交资料" + }] + } + } + """; + return JsonUtils.jsonToObject(settingJson, Setting.class); + } } \ No newline at end of file