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