mirror of https://github.com/halo-dev/halo
Add Menu and MenuItem Extension (#2303)
#### What type of PR is this? /kind feature /area core /kind api-change /milestone 2.0 #### What this PR does / why we need it: Add Menu and MenuItem Extension to realise multi menu feature. #### Does this PR introduce a user-facing change? ```release-note 添加菜单功能 ```pull/2333/head
parent
7026681747
commit
84eef54603
|
@ -6,11 +6,15 @@ import org.springframework.context.annotation.Bean;
|
|||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.web.reactive.function.server.RouterFunction;
|
||||
import org.springframework.web.reactive.function.server.ServerResponse;
|
||||
import run.halo.app.core.extension.Menu;
|
||||
import run.halo.app.core.extension.MenuItem;
|
||||
import run.halo.app.core.extension.Plugin;
|
||||
import run.halo.app.core.extension.Role;
|
||||
import run.halo.app.core.extension.RoleBinding;
|
||||
import run.halo.app.core.extension.Theme;
|
||||
import run.halo.app.core.extension.User;
|
||||
import run.halo.app.core.extension.reconciler.MenuItemReconciler;
|
||||
import run.halo.app.core.extension.reconciler.MenuReconciler;
|
||||
import run.halo.app.core.extension.reconciler.PluginReconciler;
|
||||
import run.halo.app.core.extension.reconciler.RoleBindingReconciler;
|
||||
import run.halo.app.core.extension.reconciler.RoleReconciler;
|
||||
|
@ -103,6 +107,22 @@ public class ExtensionConfiguration {
|
|||
.build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
Controller menuController(ExtensionClient client) {
|
||||
return new ControllerBuilder("menu-controller", client)
|
||||
.reconciler(new MenuReconciler(client))
|
||||
.extension(new Menu())
|
||||
.build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
Controller menuItemController(ExtensionClient client) {
|
||||
return new ControllerBuilder("menu-item-controller", client)
|
||||
.reconciler(new MenuItemReconciler(client))
|
||||
.extension(new MenuItem())
|
||||
.build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
Controller themeController(ExtensionClient client, HaloProperties haloProperties) {
|
||||
return new ControllerBuilder("theme-controller", client)
|
||||
|
|
|
@ -0,0 +1,37 @@
|
|||
package run.halo.app.core.extension;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.ArraySchema;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import java.util.LinkedHashSet;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.ToString;
|
||||
import run.halo.app.extension.AbstractExtension;
|
||||
import run.halo.app.extension.GVK;
|
||||
|
||||
@Data
|
||||
@ToString(callSuper = true)
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@GVK(group = "", version = "v1alpha1", kind = "Menu", plural = "menus", singular = "menu")
|
||||
public class Menu extends AbstractExtension {
|
||||
|
||||
@Schema(description = "The spec of menu.", required = true)
|
||||
private Spec spec;
|
||||
|
||||
@Data
|
||||
@Schema(name = "MenuSpec")
|
||||
public static class Spec {
|
||||
|
||||
@Schema(description = "The display name of the menu.", required = true)
|
||||
private String displayName;
|
||||
|
||||
@Schema(description = "Names of menu children below this menu.")
|
||||
@ArraySchema(
|
||||
arraySchema = @Schema(description = "Menu items of this menu."),
|
||||
schema = @Schema(description = "Name of menu item.")
|
||||
)
|
||||
private LinkedHashSet<String> menuItems;
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,74 @@
|
|||
package run.halo.app.core.extension;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.ArraySchema;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import java.util.LinkedHashSet;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.ToString;
|
||||
import run.halo.app.extension.AbstractExtension;
|
||||
import run.halo.app.extension.GVK;
|
||||
|
||||
@Data
|
||||
@ToString(callSuper = true)
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@GVK(group = "", version = "v1alpha1", kind = "MenuItem",
|
||||
plural = "menuitems", singular = "menuitem")
|
||||
public class MenuItem extends AbstractExtension {
|
||||
|
||||
@Schema(description = "The spec of menu item.", required = true)
|
||||
private MenuItemSpec spec;
|
||||
|
||||
@Schema(description = "The status of menu item.")
|
||||
private MenuItemStatus status;
|
||||
|
||||
@Data
|
||||
public static class MenuItemSpec {
|
||||
|
||||
@Schema(description = "The display name of menu item.")
|
||||
private String displayName;
|
||||
|
||||
@Schema(description = "The href of this menu item.")
|
||||
private String href;
|
||||
|
||||
@Schema(description = "The priority is for ordering.")
|
||||
private Integer priority;
|
||||
|
||||
@ArraySchema(
|
||||
arraySchema = @Schema(description = "Children of this menu item"),
|
||||
schema = @Schema(description = "The name of menu item child"))
|
||||
private LinkedHashSet<String> children;
|
||||
|
||||
@Schema(description = "Category reference.")
|
||||
private MenuItemRef categoryRef;
|
||||
|
||||
@Schema(description = "Tag reference.")
|
||||
private MenuItemRef tagRef;
|
||||
|
||||
@Schema(description = "Post reference.")
|
||||
private MenuItemRef postRef;
|
||||
|
||||
@Schema(description = "Page reference.")
|
||||
private MenuItemRef pageRef;
|
||||
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class MenuItemRef {
|
||||
|
||||
@Schema(description = "Reference name.", required = true)
|
||||
private String name;
|
||||
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class MenuItemStatus {
|
||||
|
||||
@Schema(description = "Calculated Display name of menu item.")
|
||||
private String displayName;
|
||||
|
||||
@Schema(description = "Calculated href of manu item.")
|
||||
private String href;
|
||||
|
||||
}
|
||||
}
|
|
@ -0,0 +1,48 @@
|
|||
package run.halo.app.core.extension.reconciler;
|
||||
|
||||
import org.springframework.util.StringUtils;
|
||||
import run.halo.app.core.extension.MenuItem;
|
||||
import run.halo.app.core.extension.MenuItem.MenuItemStatus;
|
||||
import run.halo.app.extension.ExtensionClient;
|
||||
import run.halo.app.extension.controller.Reconciler;
|
||||
|
||||
public class MenuItemReconciler implements Reconciler {
|
||||
|
||||
private final ExtensionClient client;
|
||||
|
||||
public MenuItemReconciler(ExtensionClient client) {
|
||||
this.client = client;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result reconcile(Request request) {
|
||||
client.fetch(MenuItem.class, request.name()).ifPresent(menuItem -> {
|
||||
final var spec = menuItem.getSpec();
|
||||
|
||||
if (menuItem.getStatus() == null) {
|
||||
menuItem.setStatus(new MenuItemStatus());
|
||||
}
|
||||
var status = menuItem.getStatus();
|
||||
if (spec.getCategoryRef() != null) {
|
||||
// TODO resolve permalink from category.
|
||||
} else if (spec.getTagRef() != null) {
|
||||
// TODO resolve permalink from tag.
|
||||
} else if (spec.getPageRef() != null) {
|
||||
// TODO resolve permalink from page.
|
||||
} else if (spec.getPostRef() != null) {
|
||||
// TODO resolve permalink from post.
|
||||
} else {
|
||||
// at last, we resolve href and display name from spec.
|
||||
if (spec.getHref() == null || !StringUtils.hasText(spec.getDisplayName())) {
|
||||
throw new IllegalArgumentException("Both href and displayName are required");
|
||||
}
|
||||
status.setHref(spec.getHref());
|
||||
status.setDisplayName(spec.getDisplayName());
|
||||
// update status
|
||||
client.update(menuItem);
|
||||
}
|
||||
});
|
||||
return new Result(false, null);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
package run.halo.app.core.extension.reconciler;
|
||||
|
||||
import run.halo.app.extension.ExtensionClient;
|
||||
import run.halo.app.extension.controller.Reconciler;
|
||||
|
||||
public class MenuReconciler implements Reconciler {
|
||||
|
||||
private final ExtensionClient client;
|
||||
|
||||
public MenuReconciler(ExtensionClient client) {
|
||||
this.client = client;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result reconcile(Request request) {
|
||||
return new Result(false, null);
|
||||
}
|
||||
|
||||
}
|
|
@ -4,6 +4,8 @@ import org.springframework.boot.context.event.ApplicationStartedEvent;
|
|||
import org.springframework.context.ApplicationListener;
|
||||
import org.springframework.lang.NonNull;
|
||||
import org.springframework.stereotype.Component;
|
||||
import run.halo.app.core.extension.Menu;
|
||||
import run.halo.app.core.extension.MenuItem;
|
||||
import run.halo.app.core.extension.Plugin;
|
||||
import run.halo.app.core.extension.ReverseProxy;
|
||||
import run.halo.app.core.extension.Role;
|
||||
|
@ -35,5 +37,7 @@ public class SchemeInitializer implements ApplicationListener<ApplicationStarted
|
|||
schemeManager.register(Setting.class);
|
||||
schemeManager.register(ConfigMap.class);
|
||||
schemeManager.register(Theme.class);
|
||||
schemeManager.register(Menu.class);
|
||||
schemeManager.register(MenuItem.class);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,95 @@
|
|||
package run.halo.app.core.extension.reconciler;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.function.Consumer;
|
||||
import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import run.halo.app.core.extension.MenuItem;
|
||||
import run.halo.app.core.extension.MenuItem.MenuItemSpec;
|
||||
import run.halo.app.extension.ExtensionClient;
|
||||
import run.halo.app.extension.Metadata;
|
||||
import run.halo.app.extension.controller.Reconciler.Request;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class MenuItemReconcilerTest {
|
||||
|
||||
@Mock
|
||||
ExtensionClient client;
|
||||
|
||||
@InjectMocks
|
||||
MenuItemReconciler reconciler;
|
||||
|
||||
@Nested
|
||||
class WhenOtherRefNotSet {
|
||||
|
||||
|
||||
@Test
|
||||
void shouldReEnqueueIfHrefNotSet() {
|
||||
var menuItem = createMenuItem("fake-name", spec -> {
|
||||
spec.setHref(null);
|
||||
spec.setDisplayName("Fake display name");
|
||||
});
|
||||
when(client.fetch(MenuItem.class, "fake-name")).thenReturn(Optional.of(menuItem));
|
||||
assertThrows(IllegalArgumentException.class,
|
||||
() -> reconciler.reconcile(new Request("fake-name")));
|
||||
verify(client).fetch(MenuItem.class, "fake-name");
|
||||
verify(client, never()).update(menuItem);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReEnqueueIfDisplayNameNotSet() {
|
||||
var menuItem = createMenuItem("fake-name", spec -> {
|
||||
spec.setHref("/fake");
|
||||
spec.setDisplayName(null);
|
||||
});
|
||||
when(client.fetch(MenuItem.class, "fake-name")).thenReturn(
|
||||
Optional.of(menuItem));
|
||||
|
||||
assertThrows(IllegalArgumentException.class,
|
||||
() -> reconciler.reconcile(new Request("fake-name")));
|
||||
|
||||
verify(client).fetch(MenuItem.class, "fake-name");
|
||||
verify(client, never()).update(menuItem);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReconcileIfHrefAndDisplayNameSet() {
|
||||
var menuItem = createMenuItem("fake-name", spec -> {
|
||||
spec.setHref("/fake");
|
||||
spec.setDisplayName("Fake display name");
|
||||
});
|
||||
|
||||
when(client.fetch(MenuItem.class, "fake-name")).thenReturn(
|
||||
Optional.of(menuItem));
|
||||
|
||||
var result = reconciler.reconcile(new Request("fake-name"));
|
||||
assertFalse(result.reEnqueue());
|
||||
|
||||
verify(client).fetch(MenuItem.class, "fake-name");
|
||||
verify(client).update(menuItem);
|
||||
}
|
||||
}
|
||||
|
||||
MenuItem createMenuItem(String name, Consumer<MenuItemSpec> specCustomizer) {
|
||||
var metadata = new Metadata();
|
||||
metadata.setName(name);
|
||||
var menuItem = new MenuItem();
|
||||
menuItem.setMetadata(metadata);
|
||||
var spec = new MenuItemSpec();
|
||||
if (specCustomizer != null) {
|
||||
specCustomizer.accept(spec);
|
||||
}
|
||||
menuItem.setSpec(spec);
|
||||
return menuItem;
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue