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
John Niang 2022-08-11 23:52:14 +08:00 committed by GitHub
parent 7026681747
commit 84eef54603
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 297 additions and 0 deletions

View File

@ -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)

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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;
}
}