diff --git a/cms-api/src/main/java/com/condation/cms/api/ui/annotations/App.java b/cms-api/src/main/java/com/condation/cms/api/ui/annotations/App.java new file mode 100644 index 000000000..34af85591 --- /dev/null +++ b/cms-api/src/main/java/com/condation/cms/api/ui/annotations/App.java @@ -0,0 +1,45 @@ +package com.condation.cms.api.ui.annotations; + +/*- + * #%L + * CMS Api + * %% + * Copyright (C) 2023 - 2026 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * #L% + */ + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Registers a method as an application in the manager app launcher. + * The action is supplied by {@link ScriptAction}, {@link HookAction}, or + * {@link com.condation.cms.api.annotations.Action} on the same method. + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +public @interface App { + + String id(); + + String title(); + + String icon(); + + String[] permissions() default {}; +} diff --git a/cms-api/src/main/java/com/condation/cms/api/ui/annotations/ShortCut.java b/cms-api/src/main/java/com/condation/cms/api/ui/annotations/ShortCut.java index 7cc9e782d..e4b3e8e5c 100644 --- a/cms-api/src/main/java/com/condation/cms/api/ui/annotations/ShortCut.java +++ b/cms-api/src/main/java/com/condation/cms/api/ui/annotations/ShortCut.java @@ -33,14 +33,14 @@ @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface ShortCut { - String title(); - String id(); + String title() default ""; + String id() default ""; String parent() default ""; String icon() default ""; String hotkey () default ""; String section () default ""; - String[] permissions(); + String[] permissions() default {}; ScriptAction scriptAction () default @ScriptAction(function = "", module = ""); HookAction hookAction () default @HookAction(""); diff --git a/modules/ui-module/src/main/java/com/condation/cms/modules/ui/extensionpoints/AccessManagerAppExtension.java b/modules/ui-module/src/main/java/com/condation/cms/modules/ui/extensionpoints/AccessManagerAppExtension.java index 7411ecb98..edbd1ab90 100644 --- a/modules/ui-module/src/main/java/com/condation/cms/modules/ui/extensionpoints/AccessManagerAppExtension.java +++ b/modules/ui-module/src/main/java/com/condation/cms/modules/ui/extensionpoints/AccessManagerAppExtension.java @@ -23,24 +23,35 @@ import com.condation.cms.api.auth.Permissions; import com.condation.cms.api.extensions.AbstractExtensionPoint; -import com.condation.cms.api.ui.action.UIScriptAction; -import com.condation.cms.api.ui.apps.App; -import com.condation.cms.api.ui.apps.AppExtensionPoint; +import com.condation.cms.api.ui.annotations.App; +import com.condation.cms.api.ui.annotations.ScriptAction; +import com.condation.cms.api.ui.annotations.ShortCut; +import com.condation.cms.api.ui.extensions.UIActionsExtensionPoint; import com.condation.modules.api.annotation.Extension; -import java.util.List; -import java.util.Map; /** Registers role and manager-user administration apps. */ -@Extension(AppExtensionPoint.class) -public class AccessManagerAppExtension extends AbstractExtensionPoint implements AppExtensionPoint { - @Override - public List getApps() { - return List.of( - new App("role-manager", "Roles", "/manager/assets/apps/role-manager.svg", - new UIScriptAction("/manager/actions/access/manage-roles", Map.of()), - List.of(Permissions.ROLE_MANAGE)), - new App("user-manager", "Users", "/manager/assets/apps/user-manager.svg", - new UIScriptAction("/manager/actions/access/manage-users", Map.of()), - List.of(Permissions.USER_MANAGE))); +@Extension(UIActionsExtensionPoint.class) +public class AccessManagerAppExtension extends AbstractExtensionPoint implements UIActionsExtensionPoint { + + @App( + id = "role-manager", + title = "Roles", + icon = "/manager/public/apps/role-manager.svg", + permissions = Permissions.ROLE_MANAGE) + @ShortCut(section = "Apps") + @ScriptAction(module = "/manager/actions/access/manage-roles") + public void manageRoles() { + // Marker method for manager UI annotations. + } + + @App( + id = "user-manager", + title = "Users", + icon = "/manager/public/apps/user-manager.svg", + permissions = Permissions.USER_MANAGE) + @ShortCut(section = "Apps") + @ScriptAction(module = "/manager/actions/access/manage-users") + public void manageUsers() { + // Marker method for manager UI annotations. } } diff --git a/modules/ui-module/src/main/java/com/condation/cms/modules/ui/extensionpoints/MenuManagerAppExtension.java b/modules/ui-module/src/main/java/com/condation/cms/modules/ui/extensionpoints/MenuManagerAppExtension.java index f4be2e7ec..013f3c003 100644 --- a/modules/ui-module/src/main/java/com/condation/cms/modules/ui/extensionpoints/MenuManagerAppExtension.java +++ b/modules/ui-module/src/main/java/com/condation/cms/modules/ui/extensionpoints/MenuManagerAppExtension.java @@ -22,27 +22,26 @@ */ import com.condation.cms.api.auth.Permissions; import com.condation.cms.api.extensions.AbstractExtensionPoint; -import com.condation.cms.api.ui.action.UIScriptAction; -import com.condation.cms.api.ui.apps.App; -import com.condation.cms.api.ui.apps.AppExtensionPoint; +import com.condation.cms.api.ui.annotations.App; +import com.condation.cms.api.ui.annotations.ScriptAction; +import com.condation.cms.api.ui.annotations.ShortCut; import com.condation.cms.api.ui.extensions.UIActionsExtensionPoint; import com.condation.modules.api.annotation.Extension; -import java.util.List; -import java.util.Map; /** * Makes the menu manager available as the first manager app. */ -@Extension(AppExtensionPoint.class) -public class MenuManagerAppExtension extends AbstractExtensionPoint implements AppExtensionPoint, UIActionsExtensionPoint { +@Extension(UIActionsExtensionPoint.class) +public class MenuManagerAppExtension extends AbstractExtensionPoint implements UIActionsExtensionPoint { - @Override - public List getApps() { - return List.of(new App( - "menu-manager", - "Menu Manager", - "/manager/assets/apps/menu-manager.svg", - new UIScriptAction("/manager/actions/menu/manage-menus", Map.of()), - List.of(Permissions.MENU_MANAGE))); + @App( + id = "menu-manager", + title = "Menu Manager", + icon = "/manager/public/apps/menu-manager.svg", + permissions = Permissions.MENU_MANAGE) + @ShortCut(section = "Apps") + @ScriptAction(module = "/manager/actions/menu/manage-menus") + public void manageMenus() { + // Marker method for manager UI annotations. } } diff --git a/modules/ui-module/src/main/java/com/condation/cms/modules/ui/extensionpoints/MenuManagerUiActionExtension.java b/modules/ui-module/src/main/java/com/condation/cms/modules/ui/extensionpoints/MenuManagerUiActionExtension.java deleted file mode 100644 index fb934b72b..000000000 --- a/modules/ui-module/src/main/java/com/condation/cms/modules/ui/extensionpoints/MenuManagerUiActionExtension.java +++ /dev/null @@ -1,53 +0,0 @@ -package com.condation.cms.modules.ui.extensionpoints; - -/*- - * #%L - * UI Module - * %% - * Copyright (C) 2023 - 2026 CondationCMS - * %% - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - * #L% - */ -import com.condation.cms.api.auth.Permissions; -import com.condation.cms.api.extensions.AbstractExtensionPoint; -import com.condation.cms.api.ui.action.UIScriptAction; -import com.condation.cms.api.ui.annotations.ScriptAction; -import com.condation.cms.api.ui.annotations.ShortCut; -import com.condation.cms.api.ui.apps.App; -import com.condation.cms.api.ui.apps.AppExtensionPoint; -import com.condation.cms.api.ui.extensions.UIActionsExtensionPoint; -import com.condation.modules.api.annotation.Extension; -import java.util.List; -import java.util.Map; - -/** - * Makes the menu manager available as the first manager app. - */ -@Extension(UIActionsExtensionPoint.class) -public class MenuManagerUiActionExtension extends AbstractExtensionPoint implements UIActionsExtensionPoint { - - - - @ShortCut( - id = "manage_menus", - title = "Manage menus", - permissions = {Permissions.MENU_MANAGE}, - section = "tools", - scriptAction = @ScriptAction(module = "/manager/actions/menu/manage-menus") - ) - public void manage_menus() { - // can be empty - } -} diff --git a/modules/ui-module/src/main/java/com/condation/cms/modules/ui/extensionpoints/UIJettyHttpHandlerExtension.java b/modules/ui-module/src/main/java/com/condation/cms/modules/ui/extensionpoints/UIJettyHttpHandlerExtension.java index 01668824a..844b72f86 100644 --- a/modules/ui-module/src/main/java/com/condation/cms/modules/ui/extensionpoints/UIJettyHttpHandlerExtension.java +++ b/modules/ui-module/src/main/java/com/condation/cms/modules/ui/extensionpoints/UIJettyHttpHandlerExtension.java @@ -208,7 +208,10 @@ public Mapping getMapping() { createFileSystem("/manager"), "/manager", List.of( - "public/manager-login.js" + "public/manager-login.js", + "public/apps/menu-manager.svg", + "public/apps/role-manager.svg", + "public/apps/user-manager.svg" ) ) ); diff --git a/modules/ui-module/src/main/java/com/condation/cms/modules/ui/utils/ActionFactory.java b/modules/ui-module/src/main/java/com/condation/cms/modules/ui/utils/ActionFactory.java index 04ac1373f..875256991 100644 --- a/modules/ui-module/src/main/java/com/condation/cms/modules/ui/utils/ActionFactory.java +++ b/modules/ui-module/src/main/java/com/condation/cms/modules/ui/utils/ActionFactory.java @@ -82,7 +82,12 @@ private AuthorizationService authorizationService() { public List createShortCuts() { List shortCuts = new ArrayList<>(); moduleManager.extensions(UIActionsExtensionPoint.class).forEach(extension -> { - shortCuts.addAll(scanShortCuts(extension)); + try { + shortCuts.addAll(scanShortCuts(extension)); + } catch (Exception exception) { + log.error("Could not register manager shortcuts from {}", + extension.getClass().getName(), exception); + } }); return shortCuts; @@ -92,30 +97,75 @@ public List createApps() { Map apps = new LinkedHashMap<>(); moduleManager.extensions(AppExtensionPoint.class).forEach(extension -> { try { - extension.getApps().stream() - .filter(app -> authorizationService().hasAllPermissions( - user, app.permissions().toArray(String[]::new))) - .map(app -> new AppHolder( - app.id(), - app.title(), - HTTPUtil.modifyUrl(app.icon(), context), - withContext(app.action()))) - .forEach(app -> { - if (apps.putIfAbsent(app.id(), app) != null) { - log.warn("Ignoring duplicate manager app id '{}'", app.id()); - } - }); + extension.getApps().forEach(app -> registerApp(apps, app)); } catch (Exception exception) { log.error("Could not register manager apps from {}", extension.getClass().getName(), exception); } }); + moduleManager.extensions(UIActionsExtensionPoint.class).forEach(extension -> { + try { + scanApps(extension).forEach(app -> registerApp(apps, app, true)); + } catch (Exception exception) { + log.error("Could not register annotated manager apps from {}", + extension.getClass().getName(), exception); + } + }); return apps.values().stream() .sorted(Comparator.comparing(AppHolder::title, String.CASE_INSENSITIVE_ORDER)) .toList(); } + private void registerApp(Map apps, com.condation.cms.api.ui.apps.App app) { + registerApp(apps, app, false); + } + + private void registerApp( + Map apps, + com.condation.cms.api.ui.apps.App app, + boolean actionHasContext) { + if (!authorizationService().hasAllPermissions( + user, app.permissions().toArray(String[]::new))) { + return; + } + + var holder = new AppHolder( + app.id(), + app.title(), + HTTPUtil.modifyUrl(app.icon(), context), + actionHasContext ? app.action() : withContext(app.action())); + if (apps.putIfAbsent(holder.id(), holder) != null) { + log.warn("Ignoring duplicate manager app id '{}'", holder.id()); + } + } + + private List scanApps(Object moduleInstance) { + List apps = new ArrayList<>(); + + for (Method method : moduleInstance.getClass().getMethods()) { + var appAnnotation = method.getAnnotation(com.condation.cms.api.ui.annotations.App.class); + if (appAnnotation == null) { + continue; + } + + UIAction action = resolveMethodAction(method); + if (action == null) { + log.warn("Ignoring manager app '{}' without an action", appAnnotation.id()); + continue; + } + + apps.add(new com.condation.cms.api.ui.apps.App( + appAnnotation.id(), + appAnnotation.title(), + appAnnotation.icon(), + action, + Arrays.asList(appAnnotation.permissions()))); + } + + return apps; + } + public Menu createContentTypeMenu() { UIHooks uiHooks = new UIHooks(hookSystem); var contentTypes = uiHooks.contentTypes(); @@ -199,49 +249,33 @@ private List scanShortCuts(Object moduleInstance) { continue; } - method.setAccessible(true); - UIAction menuAction = null; - - // 1. Methode hat @Action? - Action actionAnn = method.getAnnotation(Action.class); - if (actionAnn != null) { - menuAction = new UIHookAction(actionAnn.value(), Map.of()); - } // 2. @Hook in @MenuEntry - else if (!shortcutAnnotation.hookAction().value().isEmpty()) { - menuAction = new UIHookAction(shortcutAnnotation.hookAction().value(), Map.of()); - } // 3. @ScriptAction in @MenuEntry - else if (!shortcutAnnotation.scriptAction().module().isEmpty()) { - menuAction = scriptAction( - shortcutAnnotation.scriptAction().module(), - shortcutAnnotation.scriptAction().function(), - Map.of()); - } - - if (menuAction == null) { - var menuAnn = method.getAnnotation(com.condation.cms.api.ui.annotations.MenuEntry.class); - if (menuAnn != null) { - if (!menuAnn.hookAction().value().isEmpty()) { - menuAction = new UIHookAction(menuAnn.hookAction().value(), Map.of()); - } // 3. @ScriptAction in @MenuEntry - else if (!menuAnn.scriptAction().module().isEmpty()) { - menuAction = scriptAction( - menuAnn.scriptAction().module(), - menuAnn.scriptAction().function(), - Map.of()); - } - } - } + var appAnnotation = method.getAnnotation(com.condation.cms.api.ui.annotations.App.class); + String id = fallback(shortcutAnnotation.id(), appAnnotation == null ? "" : appAnnotation.id()); + String title = fallback(shortcutAnnotation.title(), appAnnotation == null ? "" : appAnnotation.title()); + String icon = fallback(shortcutAnnotation.icon(), appAnnotation == null ? "" : appAnnotation.icon()); + String[] permissions = shortcutAnnotation.permissions().length > 0 + ? shortcutAnnotation.permissions() + : appAnnotation == null ? new String[0] : appAnnotation.permissions(); + + if (id.isBlank() || title.isBlank()) { + log.warn("Ignoring manager shortcut on {} without id or title", method); + continue; + } + if (!authorizationService().hasAllPermissions(user, permissions)) { + continue; + } - if (menuAction != null) { + UIAction menuAction = resolveShortcutAction(method, shortcutAnnotation); + if (menuAction != null) { shortCuts.add(new ShortCutHolder( - shortcutAnnotation.id(), - shortcutAnnotation.title(), - shortcutAnnotation.icon(), + id, + title, + icon.isBlank() ? "" : HTTPUtil.modifyUrl(icon, context), shortcutAnnotation.hotkey(), shortcutAnnotation.parent(), shortcutAnnotation.section(), menuAction, - shortcutAnnotation.permissions())); + permissions)); } } @@ -259,23 +293,7 @@ private List scanMenuEntries(Object moduleInstance) { continue; } - method.setAccessible(true); - UIAction menuAction = null; - - // 1. Methode hat @Action? - Action actionAnn = method.getAnnotation(Action.class); - if (actionAnn != null) { - menuAction = new UIHookAction(actionAnn.value(), Map.of()); - } // 2. @Hook in @MenuEntry - else if (!menuAnn.hookAction().value().isEmpty()) { - menuAction = new UIHookAction(menuAnn.hookAction().value(), Map.of()); - } // 3. @ScriptAction in @MenuEntry - else if (!menuAnn.scriptAction().module().isEmpty()) { - menuAction = scriptAction( - menuAnn.scriptAction().module(), - menuAnn.scriptAction().function(), - Map.of()); - } + UIAction menuAction = resolveMenuAction(method, menuAnn); var entry = MenuEntry.builder() .id(menuAnn.id()) @@ -360,9 +378,72 @@ private Optional findEntryByIdRecursive(MenuEntry entry, String id) { return Optional.empty(); } - private record EntryHolder(String parent, MenuEntry entry) { + private record EntryHolder(String parent, MenuEntry entry) { - } + } + + private UIAction resolveMethodAction(Method method) { + Action actionAnnotation = method.getAnnotation(Action.class); + if (actionAnnotation != null) { + return new UIHookAction(actionAnnotation.value(), Map.of()); + } + + var hookAction = method.getAnnotation(com.condation.cms.api.ui.annotations.HookAction.class); + if (hookAction != null && !hookAction.value().isBlank()) { + return new UIHookAction(hookAction.value(), Map.of()); + } + + var scriptAction = method.getAnnotation(com.condation.cms.api.ui.annotations.ScriptAction.class); + if (scriptAction != null && !scriptAction.module().isBlank()) { + return scriptAction(scriptAction.module(), scriptAction.function(), Map.of()); + } + + return null; + } + + private UIAction resolveShortcutAction( + Method method, + com.condation.cms.api.ui.annotations.ShortCut shortcutAnnotation) { + UIAction action = resolveMethodAction(method); + if (action != null) { + return action; + } + if (!shortcutAnnotation.hookAction().value().isBlank()) { + return new UIHookAction(shortcutAnnotation.hookAction().value(), Map.of()); + } + if (!shortcutAnnotation.scriptAction().module().isBlank()) { + return scriptAction( + shortcutAnnotation.scriptAction().module(), + shortcutAnnotation.scriptAction().function(), + Map.of()); + } + + var menuAnnotation = method.getAnnotation(com.condation.cms.api.ui.annotations.MenuEntry.class); + return menuAnnotation == null ? null : resolveMenuAction(method, menuAnnotation); + } + + private UIAction resolveMenuAction( + Method method, + com.condation.cms.api.ui.annotations.MenuEntry menuAnnotation) { + UIAction action = resolveMethodAction(method); + if (action != null) { + return action; + } + if (!menuAnnotation.hookAction().value().isBlank()) { + return new UIHookAction(menuAnnotation.hookAction().value(), Map.of()); + } + if (!menuAnnotation.scriptAction().module().isBlank()) { + return scriptAction( + menuAnnotation.scriptAction().module(), + menuAnnotation.scriptAction().function(), + Map.of()); + } + return null; + } + + private String fallback(String value, String fallback) { + return value == null || value.isBlank() ? fallback : value; + } private UIScriptAction scriptAction(String module, String function, Map parameters) { return new UIScriptAction(HTTPUtil.modifyUrl(module, context), function, parameters); @@ -378,12 +459,32 @@ private UIAction withContext(UIAction action) { return action; } - public record ShortCutHolder(String id, String title, String icon, String hotkey, String parent, String section, UIAction action, String[] permissions) { + public record ShortCutHolder(String id, String title, String icon, String hotkey, String parent, String section, UIAction action, String[] permissions) { - public String getActionDefinition() { - return action != null ? JSONUtil.toJson(action) : ""; - } - } + public String getActionDefinition() { + return action != null ? JSONUtil.toJson(action) : ""; + } + + public String getIdDefinition() { + return JSONUtil.toJson(id); + } + + public String getTitleDefinition() { + return JSONUtil.toJson(title); + } + + public String getIconDefinition() { + return JSONUtil.toJson(icon); + } + + public String getHotkeyDefinition() { + return JSONUtil.toJson(hotkey); + } + + public String getSectionDefinition() { + return JSONUtil.toJson(section); + } + } public record AppHolder(String id, String title, String icon, UIAction action) { diff --git a/modules/ui-module/src/main/resources/manager/assets/apps/menu-manager.svg b/modules/ui-module/src/main/resources/manager/assets/apps/menu-manager.svg deleted file mode 100644 index 9fd322aa0..000000000 --- a/modules/ui-module/src/main/resources/manager/assets/apps/menu-manager.svg +++ /dev/null @@ -1,29 +0,0 @@ - - - Menu Manager - - - - - - - diff --git a/modules/ui-module/src/main/resources/manager/assets/apps/role-manager.svg b/modules/ui-module/src/main/resources/manager/assets/apps/role-manager.svg deleted file mode 100644 index c3f75389a..000000000 --- a/modules/ui-module/src/main/resources/manager/assets/apps/role-manager.svg +++ /dev/null @@ -1,25 +0,0 @@ - - - Role Manager - - - diff --git a/modules/ui-module/src/main/resources/manager/assets/apps/user-manager.svg b/modules/ui-module/src/main/resources/manager/assets/apps/user-manager.svg deleted file mode 100644 index 94e8c50fb..000000000 --- a/modules/ui-module/src/main/resources/manager/assets/apps/user-manager.svg +++ /dev/null @@ -1,25 +0,0 @@ - - - User Manager - - - diff --git a/modules/ui-module/src/main/resources/manager/css/manager.css b/modules/ui-module/src/main/resources/manager/css/manager.css index 32928ce66..2a3f991c0 100644 --- a/modules/ui-module/src/main/resources/manager/css/manager.css +++ b/modules/ui-module/src/main/resources/manager/css/manager.css @@ -979,6 +979,13 @@ i[data-cms-section-handle] { line-height: 1.25; } +.cms-command-palette-app-icon { + display: block; + width: 24px; + height: 24px; + object-fit: contain; +} + @media (max-width: 575.98px) { .cms-app-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); diff --git a/modules/ui-module/src/main/resources/manager/index.html b/modules/ui-module/src/main/resources/manager/index.html index 7ed77cbf1..d900918b0 100644 --- a/modules/ui-module/src/main/resources/manager/index.html +++ b/modules/ui-module/src/main/resources/manager/index.html @@ -375,20 +375,36 @@ import {executeScriptAction, executeHookAction} from "@cms/js/manager-globals.js"; + const createCommandIcon = (source) => { + if (!source) { + return null; + } + const image = document.createElement('img'); + image.src = source; + image.alt = ''; + image.className = 'cms-command-palette-app-icon'; + return image; + }; + + const executeUIAction = (action) => { + if (action.type === "hook") { + executeHookAction(action) + } else if (action.type === "script") { + executeScriptAction(action) + } + }; + var shortCuts = [ {% for entry in actionFactory.createShortCuts() %} { - id: '{{ entry.id }}', - label: '{{ entry.title }}', - icon: '{{ entry.icon }}', - group: '{{ entry.section }}', + id: {{ entry.getIdDefinition() | raw }}, + label: {{ entry.getTitleDefinition() | raw }}, + icon: createCommandIcon({{ entry.getIconDefinition() | raw }}), + group: {{ entry.getSectionDefinition() | raw }}, + shortcut: {{ entry.getHotkeyDefinition() | raw }}, action: () => { - var action = {{ entry.getActionDefinition() | raw }} - if (action.type === "hook") { - executeHookAction(action) - } else if (action.type === "script") { - executeScriptAction(action) - } + const action = {{ entry.getActionDefinition() | raw }} + executeUIAction(action) } }, {% endfor %} diff --git a/modules/ui-module/src/main/resources/manager/js/modules/variant-support.d.ts b/modules/ui-module/src/main/resources/manager/js/modules/variant-support.d.ts index 4837ac7da..c3da159ad 100644 --- a/modules/ui-module/src/main/resources/manager/js/modules/variant-support.d.ts +++ b/modules/ui-module/src/main/resources/manager/js/modules/variant-support.d.ts @@ -8,14 +8,14 @@ * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. - * + * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. - * + * * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . + * along with this program. If not, see . * #L% */ import { ActivePreviewContent } from '@cms/modules/preview-context.js'; diff --git a/modules/ui-module/src/test/java/com/condation/cms/modules/ui/utils/ActionFactoryAppsTest.java b/modules/ui-module/src/test/java/com/condation/cms/modules/ui/utils/ActionFactoryAppsTest.java index 9ac0b01d4..5470f14cb 100644 --- a/modules/ui-module/src/test/java/com/condation/cms/modules/ui/utils/ActionFactoryAppsTest.java +++ b/modules/ui-module/src/test/java/com/condation/cms/modules/ui/utils/ActionFactoryAppsTest.java @@ -34,6 +34,7 @@ import com.condation.cms.api.ui.apps.AppExtensionPoint; import com.condation.cms.api.ui.elements.CollectionType; import com.condation.cms.api.ui.elements.ContentTypes; +import com.condation.cms.api.ui.extensions.UIActionsExtensionPoint; import com.condation.cms.auth.services.User; import com.condation.modules.api.ModuleManager; import java.util.List; @@ -48,6 +49,32 @@ class ActionFactoryAppsTest { + private static class AnnotatedApps extends com.condation.cms.api.extensions.AbstractExtensionPoint + implements UIActionsExtensionPoint { + + @com.condation.cms.api.ui.annotations.App( + id = "editor-app", + title = "Editor's app", + icon = "/manager/public/apps/editor.svg", + permissions = Permissions.CONTENT_EDIT) + @com.condation.cms.api.ui.annotations.ShortCut(section = "Apps") + @com.condation.cms.api.ui.annotations.ScriptAction(module = "/manager/actions/editor") + public void editorApp() { + // method is only for annotations + } + + @com.condation.cms.api.ui.annotations.App( + id = "manager-app", + title = "Manager app", + icon = "/manager/public/apps/manager.svg", + permissions = Permissions.CACHE_INVALIDATE) + @com.condation.cms.api.ui.annotations.ShortCut(section = "Apps") + @com.condation.cms.api.ui.annotations.ScriptAction(module = "/manager/actions/manager") + public void managerApp() { + // method is only for annotations + } + } + @Test void createsAuthorizedAppsWithContextAwareIconAndScriptAction() { SiteProperties siteProperties = mock(SiteProperties.class); @@ -91,6 +118,45 @@ void createsAuthorizedAppsWithContextAwareIconAndScriptAction() { }); } + @Test + void createsAnnotatedAppsAndInheritsTheirShortcutMetadata() { + SiteProperties siteProperties = mock(SiteProperties.class); + when(siteProperties.contextPath()).thenReturn("/de"); + SiteModuleContext context = mock(SiteModuleContext.class); + when(context.get(SitePropertiesFeature.class)) + .thenReturn(new SitePropertiesFeature(siteProperties)); + + ModuleManager moduleManager = mock(ModuleManager.class); + when(moduleManager.extensions(AppExtensionPoint.class)).thenReturn(List.of()); + when(moduleManager.extensions(UIActionsExtensionPoint.class)) + .thenReturn(List.of(new AnnotatedApps())); + + ActionFactory factory = new ActionFactory( + context, + siteProperties, + null, + moduleManager, + new User("editor", "hash", new String[]{"editor"})); + + Assertions.assertThat(factory.createApps()).singleElement().satisfies(app -> { + Assertions.assertThat(app.id()).isEqualTo("editor-app"); + Assertions.assertThat(app.title()).isEqualTo("Editor's app"); + Assertions.assertThat(app.icon()).isEqualTo("/de/manager/public/apps/editor.svg"); + Assertions.assertThat(app.action()).isInstanceOfSatisfying( + UIScriptAction.class, + action -> Assertions.assertThat(action.getModule()) + .isEqualTo("/de/manager/actions/editor")); + }); + + Assertions.assertThat(factory.createShortCuts()).singleElement().satisfies(shortcut -> { + Assertions.assertThat(shortcut.id()).isEqualTo("editor-app"); + Assertions.assertThat(shortcut.title()).isEqualTo("Editor's app"); + Assertions.assertThat(shortcut.icon()).isEqualTo("/de/manager/public/apps/editor.svg"); + Assertions.assertThat(shortcut.section()).isEqualTo("Apps"); + Assertions.assertThat(shortcut.permissions()).containsExactly(Permissions.CONTENT_EDIT); + }); + } + @Test void addsCollectionsToCreateContentMenu() { SiteProperties siteProperties = mock(SiteProperties.class); diff --git a/modules/ui-module/src/test/java/com/condation/cms/modules/ui/utils/TemplateEngineTest.java b/modules/ui-module/src/test/java/com/condation/cms/modules/ui/utils/TemplateEngineTest.java index 9c66255fc..730864942 100644 --- a/modules/ui-module/src/test/java/com/condation/cms/modules/ui/utils/TemplateEngineTest.java +++ b/modules/ui-module/src/test/java/com/condation/cms/modules/ui/utils/TemplateEngineTest.java @@ -30,6 +30,7 @@ import com.condation.cms.api.ui.elements.Menu; import com.condation.cms.api.ui.elements.MenuEntry; import com.condation.cms.modules.ui.utils.ActionFactory.AppHolder; +import com.condation.cms.modules.ui.utils.ActionFactory.ShortCutHolder; import com.condation.cms.modules.ui.utils.template.UILinkFunction; import com.condation.cms.auth.services.User; import com.condation.cms.core.cache.LocalCacheProvider; @@ -105,7 +106,15 @@ void rendersAppLauncher() throws Exception { new UIScriptAction("/de/manager/actions/menu/manage-menus", Map.of())))); Mockito.when(actionFactory.createContentTypeMenu()).thenReturn(new Menu()); Mockito.when(actionFactory.createMenu()).thenReturn(new Menu()); - Mockito.when(actionFactory.createShortCuts()).thenReturn(List.of()); + Mockito.when(actionFactory.createShortCuts()).thenReturn(List.of(new ShortCutHolder( + "menu-manager", + "Menu Manager", + "/de/manager/public/apps/menu-manager.svg", + "", + "", + "Apps", + new UIScriptAction("/de/manager/actions/menu/manage-menus", Map.of()), + new String[]{Permissions.MENU_MANAGE}))); UILinkFunction links = Mockito.mock(UILinkFunction.class); Mockito.when(links.createUrl(Mockito.anyString())) .thenAnswer(invocation -> invocation.getArgument(0)); @@ -126,6 +135,8 @@ void rendersAppLauncher() throws Exception { .contains("bi-grid-3x3-gap-fill") .contains("cms-app-card") .contains("/de/manager/public/apps/menu-manager.svg") + .contains("icon: createCommandIcon(\"/de/manager/public/apps/menu-manager.svg\")") + .contains("group: \"Apps\"") .contains("/de/manager/actions/menu/manage-menus"); }