diff --git a/cms-api/src/main/java/com/condation/cms/api/Constants.java b/cms-api/src/main/java/com/condation/cms/api/Constants.java index 5de9d24eb..48babc074 100644 --- a/cms-api/src/main/java/com/condation/cms/api/Constants.java +++ b/cms-api/src/main/java/com/condation/cms/api/Constants.java @@ -95,19 +95,19 @@ public static class ContentTypes { public static final Pattern TAXONOMY_VALUE = Pattern.compile("taxonomy\\.([a-zA-Z0-9-]+)\\.yaml"); - public static final Pattern SLOT_ITEM_PATTERN = Pattern.compile("\\w+\\.(?[a-zA-Z0-9-]+)\\.md"); + public static final Pattern SECTION_ENTRY_PATTERN = Pattern.compile("\\w+\\.(?
[a-zA-Z0-9-]+)\\.md"); - public static final Function SLOT_ITEM_OF_PATTERN = (fileName) -> { - return Pattern.compile("%s\\.(?[a-zA-Z0-9-]+)\\.md".formatted(Pattern.quote(fileName))); + public static final Function SECTION_ENTRY_OF_PATTERN = (fileName) -> { + return Pattern.compile("%s\\.(?
[a-zA-Z0-9-]+)\\.md".formatted(Pattern.quote(fileName))); }; - public static final Pattern SLOT_ITEM_NAMED_PATTERN = Pattern.compile("[\\w-]+\\.(?[a-zA-Z0-9-]+)\\.(?[\\w-]+)\\.md"); + public static final Pattern SECTION_ENTRY_NAMED_PATTERN = Pattern.compile("[\\w-]+\\.(?
[a-zA-Z0-9-]+)\\.(?[\\w-]+)\\.md"); - public static final Function SLOT_ITEM_NAMED_OF_PATTERN = (fileName) -> { + public static final Function SECTION_ENTRY_NAMED_OF_PATTERN = (fileName) -> { return Pattern.compile("%s\\.[a-zA-Z0-9-]+\\.[a-zA-Z0-9-]+\\.md".formatted(Pattern.quote(fileName))); }; - public static final int DEFAULT_SLOT_ITEM_LAYOUT_ORDER = 0; + public static final int DEFAULT_SECTION_ENTRY_LAYOUT_ORDER = 0; public static final double DEFAULT_MENU_POSITION = 1000f; public static final boolean DEFAULT_MENU_VISIBILITY = true; public static final int DEFAULT_EXCERPT_LENGTH = 200; diff --git a/modules/ui-module/src/main/resources/manager/actions/page/create-content.js b/cms-api/src/main/java/com/condation/cms/api/annotations/Param.java similarity index 57% rename from modules/ui-module/src/main/resources/manager/actions/page/create-content.js rename to cms-api/src/main/java/com/condation/cms/api/annotations/Param.java index f465c740b..5eee83c55 100644 --- a/modules/ui-module/src/main/resources/manager/actions/page/create-content.js +++ b/cms-api/src/main/java/com/condation/cms/api/annotations/Param.java @@ -1,6 +1,8 @@ +package com.condation.cms.api.annotations; + /*- * #%L - * UI Module + * CMS Api * %% * Copyright (C) 2023 - 2026 CondationCMS * %% @@ -18,12 +20,20 @@ * along with this program. If not, see . * #L% */ -import { openFileBrowser } from '@cms/modules/filebrowser.js'; -// hook.js -export async function runAction(params) { - openFileBrowser({ - type: "content", - uri: params.folder, - template : params.template, - }); + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Maps a hook method parameter to a named argument from the hook's argument map. + * Used on action hook methods that receive individual parameters instead of {@code ActionContext}. + * + * @author t.marx + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.PARAMETER) +public @interface Param { + String value(); } diff --git a/cms-api/src/main/java/com/condation/cms/api/db/Content.java b/cms-api/src/main/java/com/condation/cms/api/db/Content.java index 658afd26b..a872a3ab6 100644 --- a/cms-api/src/main/java/com/condation/cms/api/db/Content.java +++ b/cms-api/src/main/java/com/condation/cms/api/db/Content.java @@ -36,7 +36,7 @@ public interface Content { boolean isVisible (ContentNode node); - List listSlotItems(final ReadOnlyFile contentFile); + List listSectionEntries(final ReadOnlyFile contentFile); List listContent(final ReadOnlyFile base, final String start); diff --git a/cms-api/src/main/java/com/condation/cms/api/db/ContentNode.java b/cms-api/src/main/java/com/condation/cms/api/db/ContentNode.java index 608e8bde9..4743856f3 100644 --- a/cms-api/src/main/java/com/condation/cms/api/db/ContentNode.java +++ b/cms-api/src/main/java/com/condation/cms/api/db/ContentNode.java @@ -28,7 +28,7 @@ import com.condation.cms.api.request.RequestContextScope; import com.condation.cms.api.utils.DateRange; import com.condation.cms.api.utils.MapUtil; -import com.condation.cms.api.utils.SlotUtil; +import com.condation.cms.api.utils.SectionUtil; import java.io.Serializable; import java.time.Instant; import java.time.LocalDate; @@ -124,8 +124,8 @@ public boolean isVisible() { return DateRange.isNowWithin(publish_date, unpublish_date); } - public boolean isSlotItem() { - return SlotUtil.isSlotItem(name); + public boolean isSectionEntry() { + return SectionUtil.isSectionEntry(name); } public boolean isRedirect() { diff --git a/cms-api/src/main/java/com/condation/cms/api/hooks/HookSystem.java b/cms-api/src/main/java/com/condation/cms/api/hooks/HookSystem.java index e9c65dc73..6c4d8adb1 100644 --- a/cms-api/src/main/java/com/condation/cms/api/hooks/HookSystem.java +++ b/cms-api/src/main/java/com/condation/cms/api/hooks/HookSystem.java @@ -20,8 +20,8 @@ * along with this program. If not, see . * #L% */ +import java.util.List; import java.util.Map; -import lombok.extern.slf4j.Slf4j; /** * @@ -41,18 +41,14 @@ public interface HookSystem { public void registerFilter(final String name, final FilterFunction hookFunction, int priority); - public ActionContext doAction(final String name); + public List doAction(final String name); - public ActionContext doAction(final String name, final Map arguments); + public List doAction(final String name, final Map arguments); /** - * calls all filters with the given parameters, if no filter is executed, - * the original parameters are returned - * - * @param - * @param name - * @param parameters - * @return + * Calls all filters with the given value in priority order and returns the + * final transformed value. If no filter is registered, the original value + * is returned unchanged. */ - public FilterContext doFilter(final String name, final T parameters); + public T doFilter(final String name, final T value); } diff --git a/cms-api/src/main/java/com/condation/cms/api/hooks/Hooks.java b/cms-api/src/main/java/com/condation/cms/api/hooks/Hooks.java index a1018578e..c76fa368a 100644 --- a/cms-api/src/main/java/com/condation/cms/api/hooks/Hooks.java +++ b/cms-api/src/main/java/com/condation/cms/api/hooks/Hooks.java @@ -34,8 +34,8 @@ public enum Hooks { /* content */ CONTENT_TAGS("system/content/tags"), CONTENT_FILTER("system/content/filter"), - CONTENT_SLOT_HEADER("system/content/slot/header"), - CONTENT_SLOT_FOOTER("system/content/slot/footer"), + LAYOUT_HEADER("system/layout/header"), + LAYOUT_FOOTER("system/layout/footer"), /*query*/ DB_QUERY_OPERATIONS("system/db/query/operations"), /* scheduler */ diff --git a/cms-api/src/main/java/com/condation/cms/api/ui/elements/ContentTypes.java b/cms-api/src/main/java/com/condation/cms/api/ui/elements/ContentTypes.java index 2803b204c..19a84615e 100644 --- a/cms-api/src/main/java/com/condation/cms/api/ui/elements/ContentTypes.java +++ b/cms-api/src/main/java/com/condation/cms/api/ui/elements/ContentTypes.java @@ -35,7 +35,7 @@ public class ContentTypes { public Set pageTemplates = new HashSet(); - public Set slotItemTemplates = new HashSet<>(); + public Set sectionEntryTemplates = new HashSet<>(); public Set listItemTypes = new HashSet<>(); @@ -55,21 +55,21 @@ public void registerPageTemplate(Map pageTemplate) { pageTemplates.add(new PageTemplate(pageTemplate)); } - public void registerSlotItemTemplate(Map slotItemTemplate) { - slotItemTemplates.add(new SlotItemTemplate(slotItemTemplate)); + public void registerSectionEntryTemplate(Map sectionEntryTemplate) { + sectionEntryTemplates.add(new SectionEntryTemplate(sectionEntryTemplate)); } public Set getPageTemplates () { return new HashSet<>(pageTemplates); } - public Set getSlotItemTemplates (String slot) { - return slotItemTemplates.stream() - .filter(template -> template.slot().equals(slot)) + public Set getSectionEntryTemplates (String section) { + return sectionEntryTemplates.stream() + .filter(template -> template.section().equals(section)) .collect(Collectors.toSet()); } - public Set getSlotItemTemplates () { - return new HashSet<>(slotItemTemplates); + public Set getSectionEntryTemplates () { + return new HashSet<>(sectionEntryTemplates); } public static record PageTemplate(String name, String template, Map data) { @@ -94,17 +94,17 @@ public boolean addCreateButton () { } } - public static record SlotItemTemplate(String name, String template, Map data) { + public static record SectionEntryTemplate(String name, String template, Map data) { - public SlotItemTemplate (Map data) { + public SectionEntryTemplate (Map data) { this( (String) data.getOrDefault("name", ""), (String) data.getOrDefault("template", ""), data); } - public String slot() { - return (String) data.getOrDefault("slot", ""); + public String section() { + return (String) data.getOrDefault("section", ""); } } diff --git a/cms-api/src/main/java/com/condation/cms/api/utils/ParamAnnotationUtil.java b/cms-api/src/main/java/com/condation/cms/api/utils/ParamAnnotationUtil.java new file mode 100644 index 000000000..3fbdddb66 --- /dev/null +++ b/cms-api/src/main/java/com/condation/cms/api/utils/ParamAnnotationUtil.java @@ -0,0 +1,120 @@ +package com.condation.cms.api.utils; + +/*- + * #%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 com.condation.cms.api.annotations.Param; +import com.google.common.base.Strings; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Parameter; +import java.util.Map; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +/** + * Shared reflection helpers for annotation-driven method registration + * ({@code @Param}, context-style, no-arg). + * + * @author t.marx + */ +@Slf4j +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public final class ParamAnnotationUtil { + + /** + * Extracts {@link Param} names from every parameter in {@code params}. + *
    + *
  • Returns an empty array when {@code params} is empty (no-arg style).
  • + *
  • Returns {@code null} when any parameter lacks {@code @Param} (unsupported signature).
  • + *
+ */ + public static String[] extractParamNames(Parameter[] params) { + if (params.length == 0) { + return new String[0]; + } + String[] names = new String[params.length]; + for (int i = 0; i < params.length; i++) { + Param p = params[i].getAnnotation(Param.class); + if (p == null) { + return null; + } + names[i] = p.value(); + } + return names; + } + + /** + * Resolves positional method arguments from {@code source} using {@code names} as keys. + * Missing keys resolve to {@code null}. + */ + public static Object[] resolveArgs(Map source, String[] names) { + Object[] args = new Object[names.length]; + for (int i = 0; i < names.length; i++) { + args[i] = source.get(names[i]); + } + return args; + } + + /** + * Returns {@code true} when {@code params} contains exactly one entry whose type + * is assignable from {@code contextType}. + */ + public static boolean isContextStyle(Parameter[] params, Class contextType) { + return params.length == 1 && contextType.isAssignableFrom(params[0].getType()); + } + + /** + * Builds a {@code "namespace:name"} registration key, substituting + * {@code defaultNamespace} when the namespace is blank. + */ + public static String buildNamespaceKey(String namespace, String name, String defaultNamespace) { + String ns = Strings.isNullOrEmpty(namespace) ? defaultNamespace : namespace; + return "%s:%s".formatted(ns, name); + } + + /** + * Invokes {@code method} on {@code target} with {@code args}. + * Logs the error and returns {@code null} on failure. + */ + public static Object invokeOrNull(Object target, Method method, Object... args) { + try { + return method.invoke(target, args); + } catch (IllegalAccessException | InvocationTargetException e) { + log.error("error invoking '{}' on '{}'", method.getName(), target.getClass().getSimpleName(), e); + return null; + } + } + + /** + * Invokes {@code method} on {@code target} with {@code args}. + * Logs the error and rethrows as {@link RuntimeException} on failure. + */ + public static Object invokeOrThrow(Object target, Method method, String label, Object... args) { + try { + return method.invoke(target, args); + } catch (IllegalAccessException | InvocationTargetException e) { + log.error("error invoking '{}'", label, e); + throw new RuntimeException("Error invoking: " + label, e); + } + } +} diff --git a/cms-api/src/main/java/com/condation/cms/api/utils/SlotUtil.java b/cms-api/src/main/java/com/condation/cms/api/utils/SectionUtil.java similarity index 58% rename from cms-api/src/main/java/com/condation/cms/api/utils/SlotUtil.java rename to cms-api/src/main/java/com/condation/cms/api/utils/SectionUtil.java index 85f71624b..d160bafa7 100644 --- a/cms-api/src/main/java/com/condation/cms/api/utils/SlotUtil.java +++ b/cms-api/src/main/java/com/condation/cms/api/utils/SectionUtil.java @@ -27,26 +27,26 @@ * * @author t.marx */ -public class SlotUtil { +public class SectionUtil { - public static boolean isNamedSlotItem(final String name) { - return Constants.SLOT_ITEM_NAMED_PATTERN.matcher(name).matches(); + public static boolean isNamedSectionEntry(final String name) { + return Constants.SECTION_ENTRY_NAMED_PATTERN.matcher(name).matches(); } - public static String getSlotItemName(final String name) { - if (isNamedSlotItem(name)) { - var matcher = Constants.SLOT_ITEM_NAMED_PATTERN.matcher(name); + public static String getSectionName(final String name) { + if (isNamedSectionEntry(name)) { + var matcher = Constants.SECTION_ENTRY_NAMED_PATTERN.matcher(name); matcher.matches(); - return matcher.group("slot"); + return matcher.group("section"); } else { - var matcher = Constants.SLOT_ITEM_PATTERN.matcher(name); + var matcher = Constants.SECTION_ENTRY_PATTERN.matcher(name); matcher.matches(); - return matcher.group("slot"); + return matcher.group("section"); } } - public static boolean isSlotItem(final String name) { - return Constants.SLOT_ITEM_PATTERN.matcher(name).matches() - || Constants.SLOT_ITEM_NAMED_PATTERN.matcher(name).matches(); + public static boolean isSectionEntry(final String name) { + return Constants.SECTION_ENTRY_PATTERN.matcher(name).matches() + || Constants.SECTION_ENTRY_NAMED_PATTERN.matcher(name).matches(); } } diff --git a/cms-api/src/test/java/com/condation/cms/api/ConstantsNGTest.java b/cms-api/src/test/java/com/condation/cms/api/ConstantsNGTest.java index ce2e4d030..9f6b3b9e6 100644 --- a/cms-api/src/test/java/com/condation/cms/api/ConstantsNGTest.java +++ b/cms-api/src/test/java/com/condation/cms/api/ConstantsNGTest.java @@ -36,53 +36,53 @@ public class ConstantsNGTest { @Test - public void test_slot_pattern() { - Assertions.assertThat(Constants.SLOT_ITEM_PATTERN.matcher("index.md").matches()).isFalse(); - Assertions.assertThat(Constants.SLOT_ITEM_PATTERN.matcher(".section.md").matches()).isFalse(); + public void test_section_pattern() { + Assertions.assertThat(Constants.SECTION_ENTRY_PATTERN.matcher("index.md").matches()).isFalse(); + Assertions.assertThat(Constants.SECTION_ENTRY_PATTERN.matcher(".section.md").matches()).isFalse(); - Matcher matcher = Constants.SLOT_ITEM_PATTERN.matcher("page.section.md"); + Matcher matcher = Constants.SECTION_ENTRY_PATTERN.matcher("page.section.md"); Assertions.assertThat(matcher.matches()).isTrue(); Assertions.assertThat(matcher.group(1)).isEqualTo("section"); - Assertions.assertThat(matcher.group("slot")).isEqualTo("section"); + Assertions.assertThat(matcher.group("section")).isEqualTo("section"); - matcher = Constants.SLOT_ITEM_PATTERN.matcher("index.card.md"); + matcher = Constants.SECTION_ENTRY_PATTERN.matcher("index.card.md"); Assertions.assertThat(matcher.matches()).isTrue(); Assertions.assertThat(matcher.group(1)).isEqualTo("card"); - Assertions.assertThat(matcher.group("slot")).isEqualTo("card"); + Assertions.assertThat(matcher.group("section")).isEqualTo("card"); } @Test public void test_named_sections_pattern() { - Assertions.assertThat(Constants.SLOT_ITEM_NAMED_PATTERN.matcher("index.md").matches()).isFalse(); - Assertions.assertThat(Constants.SLOT_ITEM_NAMED_PATTERN.matcher(".section.md").matches()).isFalse(); + Assertions.assertThat(Constants.SECTION_ENTRY_NAMED_PATTERN.matcher("index.md").matches()).isFalse(); + Assertions.assertThat(Constants.SECTION_ENTRY_NAMED_PATTERN.matcher(".section.md").matches()).isFalse(); - Matcher matcher = Constants.SLOT_ITEM_NAMED_PATTERN.matcher("page.section.md"); + Matcher matcher = Constants.SECTION_ENTRY_NAMED_PATTERN.matcher("page.section.md"); Assertions.assertThat(matcher.matches()).isFalse(); - matcher = Constants.SLOT_ITEM_NAMED_PATTERN.matcher("page.section..md"); + matcher = Constants.SECTION_ENTRY_NAMED_PATTERN.matcher("page.section..md"); Assertions.assertThat(matcher.matches()).isFalse(); - matcher = Constants.SLOT_ITEM_NAMED_PATTERN.matcher("index.card.1.md"); + matcher = Constants.SECTION_ENTRY_NAMED_PATTERN.matcher("index.card.1.md"); Assertions.assertThat(matcher.matches()).isTrue(); - Assertions.assertThat(matcher.group("slot")).isEqualTo("card"); + Assertions.assertThat(matcher.group("section")).isEqualTo("card"); Assertions.assertThat(matcher.group("id")).isEqualTo("1"); - matcher = Constants.SLOT_ITEM_NAMED_PATTERN.matcher("index.card.10.md"); + matcher = Constants.SECTION_ENTRY_NAMED_PATTERN.matcher("index.card.10.md"); Assertions.assertThat(matcher.matches()).isTrue(); - Assertions.assertThat(matcher.group("slot")).isEqualTo("card"); + Assertions.assertThat(matcher.group("section")).isEqualTo("card"); Assertions.assertThat(matcher.group("id")).isEqualTo("10"); } @Test public void test_named_section_of() { - var pattern = Constants.SLOT_ITEM_NAMED_OF_PATTERN.apply("page"); + var pattern = Constants.SECTION_ENTRY_NAMED_OF_PATTERN.apply("page"); var matcher = pattern.matcher("page.left.10.md"); Assertions.assertThat(matcher.matches()).isTrue(); - pattern = Constants.SLOT_ITEM_NAMED_OF_PATTERN.apply("other"); + pattern = Constants.SECTION_ENTRY_NAMED_OF_PATTERN.apply("other"); matcher = pattern.matcher("page.left.10.md"); Assertions.assertThat(matcher.matches()).isFalse(); diff --git a/cms-api/src/test/java/com/condation/cms/api/utils/SlotUtilTest.java b/cms-api/src/test/java/com/condation/cms/api/utils/SectionUtilTest.java similarity index 85% rename from cms-api/src/test/java/com/condation/cms/api/utils/SlotUtilTest.java rename to cms-api/src/test/java/com/condation/cms/api/utils/SectionUtilTest.java index 0df397ba3..b5e73583e 100644 --- a/cms-api/src/test/java/com/condation/cms/api/utils/SlotUtilTest.java +++ b/cms-api/src/test/java/com/condation/cms/api/utils/SectionUtilTest.java @@ -29,7 +29,7 @@ * * @author thorstenmarx */ -public class SlotUtilTest { +public class SectionUtilTest { @ParameterizedTest @CsvSource({ @@ -37,7 +37,7 @@ public class SlotUtilTest { "index.asection.1.md, asection", "index.asection.blabla.md, asection" }) - public void test_getSlotName(String filename, String slotItemName) { - Assertions.assertThat(SlotUtil.getSlotItemName(filename)).isEqualTo(slotItemName); + public void test_getSectionName(String filename, String sectionName) { + Assertions.assertThat(SectionUtil.getSectionName(filename)).isEqualTo(sectionName); } } diff --git a/cms-content/pom.xml b/cms-content/pom.xml index 787d2031d..a6cb1d8a5 100644 --- a/cms-content/pom.xml +++ b/cms-content/pom.xml @@ -44,7 +44,7 @@ com.condation.cms - cms-hooksystem + cms-hooksystem org.projectlombok diff --git a/cms-content/src/main/java/com/condation/cms/content/ContentRenderer.java b/cms-content/src/main/java/com/condation/cms/content/ContentRenderer.java index b9c7b96ba..f4b7d5782 100644 --- a/cms-content/src/main/java/com/condation/cms/content/ContentRenderer.java +++ b/cms-content/src/main/java/com/condation/cms/content/ContentRenderer.java @@ -43,13 +43,13 @@ public interface ContentRenderer { String render(final ReadOnlyFile contentFile, final RequestContext context) throws IOException; - String render(final ReadOnlyFile contentFile, final RequestContext context, final Map> slotItems) throws IOException; + String render(final ReadOnlyFile contentFile, final RequestContext context, final Map> sectionEntries) throws IOException; - String render(final ReadOnlyFile contentFile, final RequestContext context, final Map> slotItems, final Map meta, final String markdownContent, final Consumer modelExtending) throws IOException; + String render(final ReadOnlyFile contentFile, final RequestContext context, final Map> sectionEntries, final Map meta, final String markdownContent, final Consumer modelExtending) throws IOException; - Map> renderSlotItems(final List slotItemNodes, final RequestContext context) throws IOException; + Map> renderSectionEntries(final List sectionEntryNodes, final RequestContext context) throws IOException; - String renderTaxonomy(final Optional contentFileOpt, final Taxonomy taxonomy, Optional taxonomyValue, final RequestContext context, final Map meta, final Page page, Map> slotItems) throws IOException; + String renderTaxonomy(final Optional contentFileOpt, final Taxonomy taxonomy, Optional taxonomyValue, final RequestContext context, final Map meta, final Page page, Map> sectionEntries) throws IOException; String renderView(final ReadOnlyFile viewFile, final View view, final ContentNode contentNode, final RequestContext requestContext, final Page page) throws IOException; diff --git a/cms-content/src/main/java/com/condation/cms/content/ContentResolver.java b/cms-content/src/main/java/com/condation/cms/content/ContentResolver.java index 69e2d410e..403ffabad 100644 --- a/cms-content/src/main/java/com/condation/cms/content/ContentResolver.java +++ b/cms-content/src/main/java/com/condation/cms/content/ContentResolver.java @@ -115,11 +115,11 @@ private Optional getContent(final RequestContext context, boole try { - List slotItems = db.getContent().listSlotItems(contentFile); + List sectionEntries = db.getContent().listSectionEntries(contentFile); - Map> renderedSlotItems = contentRenderer.renderSlotItems(slotItems, context); + Map> renderedSectionEntries = contentRenderer.renderSectionEntries(sectionEntries, context); - var content = contentRenderer.render(contentFile, context, renderedSlotItems); + var content = contentRenderer.render(contentFile, context, renderedSectionEntries); var contentType = contentNode.contentType(); diff --git a/cms-content/src/main/java/com/condation/cms/content/DefaultContentRenderer.java b/cms-content/src/main/java/com/condation/cms/content/DefaultContentRenderer.java index c56449417..4d548059d 100644 --- a/cms-content/src/main/java/com/condation/cms/content/DefaultContentRenderer.java +++ b/cms-content/src/main/java/com/condation/cms/content/DefaultContentRenderer.java @@ -45,7 +45,7 @@ import com.condation.cms.api.request.RequestContext; import com.condation.cms.api.template.TemplateEngine; import com.condation.cms.api.utils.PathUtil; -import com.condation.cms.api.utils.SlotUtil; +import com.condation.cms.api.utils.SectionUtil; import com.condation.cms.content.pipeline.ContentPipelineFactory; import com.condation.cms.content.views.model.View; import com.condation.cms.api.content.MapAccess; @@ -94,10 +94,10 @@ public String render(final ReadOnlyFile contentFile, final RequestContext contex } @Override - public String render(final ReadOnlyFile contentFile, final RequestContext context, final Map> slotItems) throws IOException { + public String render(final ReadOnlyFile contentFile, final RequestContext context, final Map> sectionEntries) throws IOException { var content = contentParser.parse(contentFile); - return render(contentFile, context, slotItems, content.meta(), content.content(), (model) -> { + return render(contentFile, context, sectionEntries, content.meta(), content.content(), (model) -> { }); } @@ -105,13 +105,13 @@ public String render(final ReadOnlyFile contentFile, final RequestContext contex public String renderTaxonomy( final Optional contentFileOpt, final Taxonomy taxonomy, Optional taxonomyValue, final RequestContext context, - final Map meta, final Page page, final Map> slotItems) throws IOException { + final Map meta, final Page page, final Map> sectionEntries) throws IOException { var contentFile = contentFileOpt.orElseGet(() -> db.getReadOnlyFileSystem().contentBase().resolve("index.md")); var content = contentFileOpt.isPresent() ? contentParser.parse(contentFileOpt.get()).content() : ""; - return render(contentFile, context, slotItems, meta, content, (model) -> { + return render(contentFile, context, sectionEntries, meta, content, (model) -> { model.values.put("taxonomy", taxonomy); model.values.put("taxonomy_values", db.getTaxonomies().values(taxonomy)); if (taxonomyValue.isPresent()) { @@ -138,7 +138,7 @@ private String renderContent(final String rawContent, final RequestContext conte @Override public String render(final ReadOnlyFile contentFile, final RequestContext context, - final Map> slotItems, + final Map> sectionEntries, final Map meta, final String rawContent, final Consumer modelExtending ) throws IOException { var uri = PathUtil.toRelativeFile(contentFile, db.getReadOnlyFileSystem().contentBase()); @@ -156,8 +156,7 @@ public String render(final ReadOnlyFile contentFile, final RequestContext contex namespace.add(Constants.TemplateNamespaces.NODE, "meta", new MapAccess(meta)); // sections will be removed - namespace.add(Constants.TemplateNamespaces.NODE, "sections", slotItems); - namespace.add(Constants.TemplateNamespaces.NODE, "slots", slotItems); + namespace.add(Constants.TemplateNamespaces.NODE, "sections", sectionEntries); namespace.add(Constants.TemplateNamespaces.NODE, "uri", uri); namespace.add(Constants.TemplateNamespaces.NODE, "translation", new NodeTranslations(contentNode.orElse(null), siteProperties)); @@ -283,36 +282,36 @@ private void extendModel(final TemplateEngine.Model model, Namespace namespace) } @Override - public Map> renderSlotItems(final List slotItemNodes, final RequestContext context) throws IOException { + public Map> renderSectionEntries(final List sectionEntryNodes, final RequestContext context) throws IOException { - if (slotItemNodes.isEmpty()) { + if (sectionEntryNodes.isEmpty()) { return Collections.emptyMap(); } - Map> slotItems = new HashMap<>(); + Map> sectionEntries = new HashMap<>(); final ReadOnlyFile contentBase = db.getReadOnlyFileSystem().contentBase(); - slotItemNodes.forEach(node -> { + sectionEntryNodes.forEach(node -> { try { - var slotItemPath = contentBase.resolve(node.uri()); - var content = render(slotItemPath, context); - var name = SlotUtil.getSlotItemName(node.name()); - var index = node.getMetaValue(Constants.MetaFields.LAYOUT_ORDER, Constants.DEFAULT_SLOT_ITEM_LAYOUT_ORDER); + var sectionEntryPath = contentBase.resolve(node.uri()); + var content = render(sectionEntryPath, context); + var name = SectionUtil.getSectionName(node.name()); + var index = node.getMetaValue(Constants.MetaFields.LAYOUT_ORDER, Constants.DEFAULT_SECTION_ENTRY_LAYOUT_ORDER); - if (!slotItems.containsKey(name)) { - slotItems.put(name, new ArrayList<>()); + if (!sectionEntries.containsKey(name)) { + sectionEntries.put(name, new ArrayList<>()); } - slotItems.get(name).add(new SlotItem(name, index, content, node.data())); + sectionEntries.get(name).add(new SectionEntry(name, index, content, node.data())); } catch (Exception ex) { - log.error("error render slotItems", ex); + log.error("error render sectionEntries", ex); } }); - slotItems.values().forEach(list -> list.sort((s1, s2) -> Integer.compare(s1.index(), s2.index()))); + sectionEntries.values().forEach(list -> list.sort((s1, s2) -> Integer.compare(s1.index(), s2.index()))); - return slotItems; + return sectionEntries; } } diff --git a/cms-content/src/main/java/com/condation/cms/content/SlotItem.java b/cms-content/src/main/java/com/condation/cms/content/SectionEntry.java similarity index 72% rename from cms-content/src/main/java/com/condation/cms/content/SlotItem.java rename to cms-content/src/main/java/com/condation/cms/content/SectionEntry.java index 67891b40a..03c5abf4d 100644 --- a/cms-content/src/main/java/com/condation/cms/content/SlotItem.java +++ b/cms-content/src/main/java/com/condation/cms/content/SectionEntry.java @@ -30,13 +30,13 @@ * * @author t.marx */ -public record SlotItem(String name, int index, String content, Map data, String uri) { +public record SectionEntry(String name, int index, String content, Map data, String uri) { - public SlotItem(String name, int index, String content, Map data) { + public SectionEntry(String name, int index, String content, Map data) { this(name, index, content, data, null); } - public SlotItem(String name, String content, Map data) { - this(name, Constants.DEFAULT_SLOT_ITEM_LAYOUT_ORDER, content, data, null); + public SectionEntry(String name, String content, Map data) { + this(name, Constants.DEFAULT_SECTION_ENTRY_LAYOUT_ORDER, content, data, null); } } diff --git a/cms-content/src/main/java/com/condation/cms/content/TaxonomyResolver.java b/cms-content/src/main/java/com/condation/cms/content/TaxonomyResolver.java index c4a57e767..2765e5eb1 100644 --- a/cms-content/src/main/java/com/condation/cms/content/TaxonomyResolver.java +++ b/cms-content/src/main/java/com/condation/cms/content/TaxonomyResolver.java @@ -119,7 +119,7 @@ public Optional getTaxonomyResponse(final RequestContext conte Optional contentFileOpt = ContentResolvingStrategy.resolve(context.get(RequestFeature.class).uri(), db); - Map> slotItems = Collections.emptyMap(); + Map> sectionEntries = Collections.emptyMap(); if (contentFileOpt.isPresent()) { var contentFile = contentFileOpt.get(); @@ -128,12 +128,12 @@ public Optional getTaxonomyResponse(final RequestContext conte meta.putAll(content.meta()); - List slotItemList = db.getContent().listSlotItems(contentFile); + List sectionEntryList = db.getContent().listSectionEntries(contentFile); - slotItems = contentRenderer.renderSlotItems(slotItemList, context); + sectionEntries = contentRenderer.renderSectionEntries(sectionEntryList, context); } - String content = contentRenderer.renderTaxonomy(contentFileOpt, taxonomy, value, context, meta, resultPage, slotItems); + String content = contentRenderer.renderTaxonomy(contentFileOpt, taxonomy, value, context, meta, resultPage, sectionEntries); return Optional.of(new TaxonomyResponse(content, taxonomy)); } catch (Exception ex) { diff --git a/cms-content/src/main/java/com/condation/cms/content/pipeline/ContentPipeline.java b/cms-content/src/main/java/com/condation/cms/content/pipeline/ContentPipeline.java index a0bb9212e..ec1aa9432 100644 --- a/cms-content/src/main/java/com/condation/cms/content/pipeline/ContentPipeline.java +++ b/cms-content/src/main/java/com/condation/cms/content/pipeline/ContentPipeline.java @@ -23,7 +23,6 @@ import com.condation.cms.api.configuration.configs.SiteConfiguration; import com.condation.cms.api.feature.features.ConfigurationFeature; import com.condation.cms.api.feature.features.TemplateEngineFeature; -import com.condation.cms.api.hooks.FilterContext; import com.condation.cms.api.hooks.HookSystem; import com.condation.cms.api.hooks.Hooks; import com.condation.cms.api.request.RequestContext; @@ -56,33 +55,33 @@ protected void init() { AtomicInteger prio = new AtomicInteger(10); pipeline.forEach(processor -> { switch (processor) { - case "markdown" -> hookSystem.registerFilter(Hooks.CONTENT_FILTER.hook(), this::processMarkdown, prio.getAndAdd(10)); - case "tags" -> hookSystem.registerFilter(Hooks.CONTENT_FILTER.hook(), this::processTags, prio.getAndAdd(10)); - case "template" -> hookSystem.registerFilter(Hooks.CONTENT_FILTER.hook(), this::processTemplate, prio.getAndAdd(10)); + case "markdown" -> hookSystem.registerFilter(Hooks.CONTENT_FILTER.hook(), ctx -> processMarkdown(ctx.value()), prio.getAndAdd(10)); + case "tags" -> hookSystem.registerFilter(Hooks.CONTENT_FILTER.hook(), ctx -> processTags(ctx.value()), prio.getAndAdd(10)); + case "template" -> hookSystem.registerFilter(Hooks.CONTENT_FILTER.hook(), ctx -> processTemplate(ctx.value()), prio.getAndAdd(10)); } }); } public String process(String rawContent) { - return hookSystem.doFilter(Hooks.CONTENT_FILTER.hook(), rawContent).value(); + return hookSystem.doFilter(Hooks.CONTENT_FILTER.hook(), rawContent); } - private String processMarkdown(FilterContext context) { - return requestContext.get(RenderContext.class).markdownRenderer().render(context.value()); + private String processMarkdown(String content) { + return requestContext.get(RenderContext.class).markdownRenderer().render(content); } - private String processTags(FilterContext context) { - return requestContext.get(RenderContext.class).tags().replace(context.value(), model.values, requestContext); + private String processTags(String content) { + return requestContext.get(RenderContext.class).tags().replace(content, model.values, requestContext); } - private String processTemplate(FilterContext context) { + private String processTemplate(String content) { try { return requestContext.get(TemplateEngineFeature.class).templateEngine() - .renderFromString(context.value(), model); + .renderFromString(content, model); } catch (IOException ex) { log.error("", ex); - return context.value(); + return content; } } } diff --git a/cms-content/src/main/java/com/condation/cms/content/pipeline/ContentPipelineFactory.java b/cms-content/src/main/java/com/condation/cms/content/pipeline/ContentPipelineFactory.java index 0357a07d8..823a4ecb9 100644 --- a/cms-content/src/main/java/com/condation/cms/content/pipeline/ContentPipelineFactory.java +++ b/cms-content/src/main/java/com/condation/cms/content/pipeline/ContentPipelineFactory.java @@ -22,7 +22,6 @@ */ import com.condation.cms.api.feature.features.HookSystemFeature; -import com.condation.cms.api.hooks.HookSystem; import com.condation.cms.api.request.RequestContext; import com.condation.cms.api.template.TemplateEngine; import com.condation.cms.hooksystem.CMSHookSystem; diff --git a/cms-content/src/main/java/com/condation/cms/content/pipeline/HTMLPipeline.java b/cms-content/src/main/java/com/condation/cms/content/pipeline/HTMLPipeline.java index 0d81892e1..e72619e8c 100644 --- a/cms-content/src/main/java/com/condation/cms/content/pipeline/HTMLPipeline.java +++ b/cms-content/src/main/java/com/condation/cms/content/pipeline/HTMLPipeline.java @@ -37,20 +37,18 @@ public class HTMLPipeline { private final HookSystem hookSystem; public String process(String rawContent) { - rawContent = updateSlot(Hooks.CONTENT_SLOT_HEADER, "", rawContent); - return updateSlot(Hooks.CONTENT_SLOT_FOOTER, "", rawContent); + rawContent = updateLayoutPosition(Hooks.LAYOUT_HEADER, "", rawContent); + return updateLayoutPosition(Hooks.LAYOUT_FOOTER, "", rawContent); } - public String updateSlot (Hooks hook, String elementName, String rawContent) { + public String updateLayoutPosition (Hooks hook, String elementName, String rawContent) { if (!rawContent.contains(elementName)) { - log.warn("No {} found, skipping header slot injection", elementName); + log.debug("No {} found, skipping layout position injection", elementName); return rawContent; } - var result = hookSystem.doAction(hook.hook()); - - var hookValues = result.results().stream() + var hookValues = hookSystem.doAction(hook.hook()).stream() .filter(Objects::nonNull) .filter(String.class::isInstance) .map(String.class::cast) diff --git a/cms-content/src/main/java/com/condation/cms/content/tags/Tags.java b/cms-content/src/main/java/com/condation/cms/content/tags/Tags.java index 507fa44d1..a168f097a 100644 --- a/cms-content/src/main/java/com/condation/cms/content/tags/Tags.java +++ b/cms-content/src/main/java/com/condation/cms/content/tags/Tags.java @@ -20,10 +20,9 @@ * along with this program. If not, see . * #L% */ -import com.condation.cms.api.Constants; import com.condation.cms.api.model.Parameter; import com.condation.cms.api.request.RequestContext; -import com.condation.cms.api.utils.AnnotationsUtil; +import com.condation.cms.content.tags.annotation.AnnotationTagRegistrar; import java.util.Collections; import java.util.HashMap; import java.util.List; @@ -32,8 +31,6 @@ import java.util.function.Function; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import com.condation.cms.api.annotations.Tag; -import com.google.common.base.Strings; /** * @@ -93,8 +90,8 @@ public static Tags.Builder builder(TagParser tagParser) { public static class Builder { private final TagParser tagParser; - private final Map> tags = new HashMap<>(); + private final AnnotationTagRegistrar annotationRegistrar = new AnnotationTagRegistrar(); private Builder(TagParser tagParser) { this.tagParser = tagParser; @@ -106,7 +103,7 @@ public Builder register(String name, Function tagFN) { } public Builder register(Map> codes) { - this.tags.putAll(codes); + tags.putAll(codes); return this; } @@ -114,41 +111,12 @@ public Builder register(List handlers) { if (handlers == null || handlers.isEmpty()) { return this; } - handlers.forEach(this::register); - return this; } public Builder register(Object handler) { - if (handler == null) { - return this; - } - - // Wir erwarten Methoden mit @Tag(Parameter) -> String - var annotations = AnnotationsUtil.process(handler, Tag.class, List.of(Parameter.class), String.class); - - for (var entry : annotations) { - String name = entry.annotation().value(); - String namespace = entry.annotation().namespace(); - if (Strings.isNullOrEmpty(namespace)) { - namespace = Constants.TemplateNamespaces.DEFAULT_MODULE_NAMESPACE; - } - String key; - if (!Strings.isNullOrEmpty(namespace)) { - key = "%s:%s".formatted(namespace, name); - } else { - key = name; - } - tags.put(key, param -> { - try { - return entry.invoke(param); - } catch (Exception e) { - throw new RuntimeException("Error calling tag: " + key, e); - } - }); - } - + annotationRegistrar.register(handler, tags); return this; } diff --git a/cms-content/src/main/java/com/condation/cms/content/tags/annotation/AnnotationTagRegistrar.java b/cms-content/src/main/java/com/condation/cms/content/tags/annotation/AnnotationTagRegistrar.java new file mode 100644 index 000000000..e55f41c86 --- /dev/null +++ b/cms-content/src/main/java/com/condation/cms/content/tags/annotation/AnnotationTagRegistrar.java @@ -0,0 +1,92 @@ +package com.condation.cms.content.tags.annotation; + +/*- + * #%L + * CMS Content + * %% + * 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.Constants; +import com.condation.cms.api.annotations.Tag; +import com.condation.cms.api.model.Parameter; +import com.condation.cms.api.utils.ParamAnnotationUtil; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.Map; +import java.util.function.Function; +import lombok.extern.slf4j.Slf4j; + +/** + * Scans an object for {@link Tag}-annotated methods and registers them into a + * tag map. + *

+ * Each method must have the signature {@code String method(Parameter param)}. + * The registration key is built from the annotation's {@code namespace} and + * {@code value}: {@code "namespace:tagname"}. + * + * @author t.marx + */ +@Slf4j +public class AnnotationTagRegistrar { + + public void register(Object handler, Map> tagMap) { + if (handler == null) { + return; + } + + for (Method method : handler.getClass().getMethods()) { + if (!Modifier.isPublic(method.getModifiers())) { + continue; + } + if (!method.isAnnotationPresent(Tag.class)) { + continue; + } + + Tag annotation = method.getAnnotation(Tag.class); + String key = buildKey(annotation); + Function fn = buildFunction(handler, method, key); + if (fn != null) { + tagMap.put(key, fn); + } + } + } + + private Function buildFunction(Object target, Method method, String key) { + java.lang.reflect.Parameter[] params = method.getParameters(); + + if (ParamAnnotationUtil.isContextStyle(params, Parameter.class)) { + return param -> (String) ParamAnnotationUtil.invokeOrThrow(target, method, key, param); + } + + String[] names = ParamAnnotationUtil.extractParamNames(params); + if (names != null) { + return param -> (String) ParamAnnotationUtil.invokeOrThrow(target, method, key, + ParamAnnotationUtil.resolveArgs(param, names)); + } + + log.warn("@Tag method '{}' in '{}' has unsupported signature — skipped", + method.getName(), target.getClass().getSimpleName()); + return null; + } + + private String buildKey(Tag annotation) { + return ParamAnnotationUtil.buildNamespaceKey( + annotation.namespace(), annotation.value(), + Constants.TemplateNamespaces.DEFAULT_MODULE_NAMESPACE); + } +} diff --git a/cms-content/src/main/java/com/condation/cms/content/template/functions/hooks/HooksTemlateFunction.java b/cms-content/src/main/java/com/condation/cms/content/template/functions/hooks/HooksTemlateFunction.java index fd25f0b06..126872bd9 100644 --- a/cms-content/src/main/java/com/condation/cms/content/template/functions/hooks/HooksTemlateFunction.java +++ b/cms-content/src/main/java/com/condation/cms/content/template/functions/hooks/HooksTemlateFunction.java @@ -10,20 +10,18 @@ * 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.hooks.ActionContext; -import com.condation.cms.api.hooks.FilterContext; import com.condation.cms.api.hooks.HookSystem; import java.util.List; import java.util.Map; @@ -35,20 +33,22 @@ */ @RequiredArgsConstructor public class HooksTemlateFunction { - + private final HookSystem hookSystem; - - public ActionContext execute (String name) { + + public List execute(String name) { return execute(name, Map.of()); } - public ActionContext execute (String name, Map arguments) { + + public List execute(String name, Map arguments) { return hookSystem.doAction(name, arguments); } - - public FilterContext filter (String name) { + + public Object filter(String name) { return filter(name, List.of()); } - public FilterContext filter (String name, List arguments) { + + public Object filter(String name, List arguments) { return hookSystem.doFilter(name, arguments); } } diff --git a/cms-content/src/main/java/com/condation/cms/content/template/functions/navigation/NavigationFunction.java b/cms-content/src/main/java/com/condation/cms/content/template/functions/navigation/NavigationFunction.java index cc33fa3f7..3da5cba5f 100644 --- a/cms-content/src/main/java/com/condation/cms/content/template/functions/navigation/NavigationFunction.java +++ b/cms-content/src/main/java/com/condation/cms/content/template/functions/navigation/NavigationFunction.java @@ -104,8 +104,7 @@ public List path() { navNodes = navNodes.reversed(); if (name != null) { - var hookContext = hookSystem.doFilter(Hooks.NAVIGATION_PATH.hook(name), navNodes); - navNodes = hookContext.value(); + navNodes = hookSystem.doFilter(Hooks.NAVIGATION_PATH.hook(name), navNodes); } return navNodes; @@ -153,8 +152,7 @@ private List getNodes(final String start, final int depth) { } if (name != null) { - var hookContext = hookSystem.doFilter(Hooks.NAVIGATION_LIST.hook(name), navNodes); - navNodes = hookContext.value(); + navNodes = hookSystem.doFilter(Hooks.NAVIGATION_LIST.hook(name), navNodes); } return navNodes; } diff --git a/cms-content/src/test/java/com/condation/cms/content/pipeline/HTMLPipelineTest.java b/cms-content/src/test/java/com/condation/cms/content/pipeline/HTMLPipelineTest.java index d831c188d..4cbcdf3ca 100644 --- a/cms-content/src/test/java/com/condation/cms/content/pipeline/HTMLPipelineTest.java +++ b/cms-content/src/test/java/com/condation/cms/content/pipeline/HTMLPipelineTest.java @@ -40,7 +40,7 @@ void setUp() { } @Test - void shouldInjectHeaderSlot() { + void shouldInjectHeader() { // given String html = """ @@ -52,7 +52,7 @@ void shouldInjectHeaderSlot() { """; - hookSystem.registerAction(Hooks.CONTENT_SLOT_HEADER.hook(), + hookSystem.registerAction(Hooks.LAYOUT_HEADER.hook(), (context) -> ""); // when String result = pipeline.process(html); @@ -64,7 +64,7 @@ void shouldInjectHeaderSlot() { } @Test - void shouldInjectFooterSlot() { + void shouldInjectFooter() { // given String html = """ @@ -76,7 +76,7 @@ void shouldInjectFooterSlot() { """; - hookSystem.registerAction(Hooks.CONTENT_SLOT_FOOTER.hook(), + hookSystem.registerAction(Hooks.LAYOUT_FOOTER.hook(), (context) -> ""); // when @@ -89,7 +89,7 @@ void shouldInjectFooterSlot() { } @Test - void shouldInjectBothSlots() { + void shouldInjectBoth() { // given String html = """ @@ -101,9 +101,9 @@ void shouldInjectBothSlots() { """; - hookSystem.registerAction(Hooks.CONTENT_SLOT_HEADER.hook(), + hookSystem.registerAction(Hooks.LAYOUT_HEADER.hook(), (context) -> ""); - hookSystem.registerAction(Hooks.CONTENT_SLOT_FOOTER.hook(), + hookSystem.registerAction(Hooks.LAYOUT_FOOTER.hook(), (context) -> ""); // when @@ -145,9 +145,9 @@ void shouldNotFailIfNoHeadOrBodyExists() { // given String html = "
no layout tags
"; - hookSystem.registerAction(Hooks.CONTENT_SLOT_HEADER.hook(), + hookSystem.registerAction(Hooks.LAYOUT_HEADER.hook(), (context) -> ""); - hookSystem.registerAction(Hooks.CONTENT_SLOT_FOOTER.hook(), + hookSystem.registerAction(Hooks.LAYOUT_FOOTER.hook(), (context) -> ""); // when diff --git a/cms-content/src/test/java/com/condation/cms/content/tags/TagsTest.java b/cms-content/src/test/java/com/condation/cms/content/tags/TagsTest.java index 151fe3bf8..a9d06a304 100644 --- a/cms-content/src/test/java/com/condation/cms/content/tags/TagsTest.java +++ b/cms-content/src/test/java/com/condation/cms/content/tags/TagsTest.java @@ -22,6 +22,7 @@ */ +import com.condation.cms.api.annotations.Param; import com.condation.cms.api.model.Parameter; import com.condation.cms.api.request.RequestContext; import com.condation.cms.content.ContentBaseTest; @@ -240,6 +241,15 @@ void test_handler () { Assertions.assertThat(result).isEqualTo("hello CondationCMS"); } + @Test + void test_handler_var () { + RequestContext requestContext = new RequestContext(); + + var result = tags.replace("[[ext:printHello2 name='CondationCMS' /]]", Map.of(), requestContext); + + Assertions.assertThat(result).isEqualTo("hello CondationCMS"); + } + @Test void test_multiline () { var template = """ @@ -265,5 +275,10 @@ public static class TagHandler { public String printHello (Parameter parameter) { return "hello " + parameter.getOrDefault("name", ""); } + + @Tag("printHello2") + public String printHello2 (@Param("name") String name) { + return "hello " + name; + } } } diff --git a/cms-content/src/test/java/com/condation/cms/content/tags/annotation/AnnotationTagRegistrarTest.java b/cms-content/src/test/java/com/condation/cms/content/tags/annotation/AnnotationTagRegistrarTest.java new file mode 100644 index 000000000..ec0b45d8c --- /dev/null +++ b/cms-content/src/test/java/com/condation/cms/content/tags/annotation/AnnotationTagRegistrarTest.java @@ -0,0 +1,206 @@ +package com.condation.cms.content.tags.annotation; + +/*- + * #%L + * CMS Content + * %% + * 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.Constants; +import com.condation.cms.api.annotations.Param; +import com.condation.cms.api.annotations.Tag; +import com.condation.cms.api.model.Parameter; +import java.util.HashMap; +import java.util.Map; +import java.util.function.Function; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +/** + * @author t.marx + */ +class AnnotationTagRegistrarTest { + + private AnnotationTagRegistrar registrar; + private Map> tagMap; + + @BeforeEach + void setup() { + registrar = new AnnotationTagRegistrar(); + tagMap = new HashMap<>(); + } + + // --- key building --- + + @Test + void tag_with_default_namespace_uses_ext_prefix() { + registrar.register(new DefaultNamespaceHandler(), tagMap); + + Assertions.assertThat(tagMap) + .containsKey(Constants.TemplateNamespaces.DEFAULT_MODULE_NAMESPACE + ":hello"); + } + + @Test + void tag_with_explicit_namespace_uses_that_prefix() { + registrar.register(new CustomNamespaceHandler(), tagMap); + + Assertions.assertThat(tagMap).containsKey("ns1:greet"); + } + + // --- invocation --- + + @Test + void tag_method_is_called_and_returns_correct_value() { + registrar.register(new DefaultNamespaceHandler(), tagMap); + + String key = Constants.TemplateNamespaces.DEFAULT_MODULE_NAMESPACE + ":hello"; + Parameter param = new Parameter(Map.of("name", "World")); + String result = tagMap.get(key).apply(param); + + Assertions.assertThat(result).isEqualTo("Hello World"); + } + + @Test + void tag_with_explicit_namespace_invocation_returns_correct_value() { + registrar.register(new CustomNamespaceHandler(), tagMap); + + Parameter param = new Parameter(Map.of("firstName", "Max", "lastName", "Mustermann")); + String result = tagMap.get("ns1:greet").apply(param); + + Assertions.assertThat(result).isEqualTo("Max Mustermann"); + } + + // --- multiple tags on one handler --- + + @Test + void multiple_tag_methods_on_same_handler_all_registered() { + registrar.register(new MultiTagHandler(), tagMap); + + Assertions.assertThat(tagMap) + .containsKey("ext:tagA") + .containsKey("ext:tagB"); + } + + @Test + void multiple_tag_methods_each_produce_correct_output() { + registrar.register(new MultiTagHandler(), tagMap); + + Parameter param = new Parameter(); + Assertions.assertThat(tagMap.get("ext:tagA").apply(param)).isEqualTo("A"); + Assertions.assertThat(tagMap.get("ext:tagB").apply(param)).isEqualTo("B"); + } + + // --- null / empty handler --- + + @Test + void null_handler_is_ignored_without_exception() { + Assertions.assertThatNoException().isThrownBy(() -> registrar.register(null, tagMap)); + Assertions.assertThat(tagMap).isEmpty(); + } + + @Test + void handler_without_tag_annotation_registers_nothing() { + registrar.register(new NoTagHandler(), tagMap); + + Assertions.assertThat(tagMap).isEmpty(); + } + + // --- @Param named-params style --- + + @Test + void tag_with_named_params_is_registered() { + registrar.register(new NamedParamHandler(), tagMap); + + Assertions.assertThat(tagMap).containsKey("ext:greet2"); + } + + @Test + void tag_with_named_params_receives_correct_values() { + registrar.register(new NamedParamHandler(), tagMap); + + Parameter param = new Parameter(Map.of("firstName", "Max", "lastName", "Mustermann")); + String result = tagMap.get("ext:greet2").apply(param); + + Assertions.assertThat(result).isEqualTo("Max Mustermann"); + } + + @Test + void tag_with_named_params_returns_null_string_for_missing_attribute() { + registrar.register(new NamedParamHandler(), tagMap); + + Parameter param = new Parameter(); // no attributes + String result = tagMap.get("ext:greet2").apply(param); + + Assertions.assertThat(result).isEqualTo("null null"); + } + + // --- default parameter value --- + + @Test + void tag_uses_getOrDefault_when_attribute_is_missing() { + registrar.register(new DefaultNamespaceHandler(), tagMap); + + String key = Constants.TemplateNamespaces.DEFAULT_MODULE_NAMESPACE + ":hello"; + Parameter param = new Parameter(); // no "name" attribute + String result = tagMap.get(key).apply(param); + + Assertions.assertThat(result).isEqualTo("Hello "); + } + + // --- handler classes --- + + public static class DefaultNamespaceHandler { + @Tag("hello") + public String hello(Parameter param) { + return "Hello " + param.getOrDefault("name", ""); + } + } + + public static class CustomNamespaceHandler { + @Tag(value = "greet", namespace = "ns1") + public String greet(Parameter param) { + return param.getOrDefault("firstName", "") + " " + param.getOrDefault("lastName", ""); + } + } + + public static class MultiTagHandler { + @Tag("tagA") + public String tagA(Parameter param) { + return "A"; + } + + @Tag("tagB") + public String tagB(Parameter param) { + return "B"; + } + } + + public static class NoTagHandler { + public String notATag(Parameter param) { + return "ignored"; + } + } + + public static class NamedParamHandler { + @Tag("greet2") + public String greet2(@Param("firstName") String firstName, @Param("lastName") String lastName) { + return firstName + " " + lastName; + } + } +} diff --git a/cms-extensions/src/main/resources/com/condation/cms/extensions/system/hooks.mjs b/cms-extensions/src/main/resources/com/condation/cms/extensions/system/hooks.mjs index d9b367342..c31cf37c0 100644 --- a/cms-extensions/src/main/resources/com/condation/cms/extensions/system/hooks.mjs +++ b/cms-extensions/src/main/resources/com/condation/cms/extensions/system/hooks.mjs @@ -5,17 +5,27 @@ const hooks = $features.get(HookSystemFeature).hookSystem() export const $hooks = { registerAction : (name, fun, priority) => { + // wrap: pass arguments as plain JS object so callers can destructure + const wrapped = (context) => { + const args = {}; + for (const key of context.arguments().keySet()) { + args[key] = context.arguments().get(key); + } + return fun(args); + }; if (priority) { - hooks.registerAction(name, fun, priority) + hooks.registerAction(name, wrapped, priority) } else { - hooks.registerAction(name, fun) + hooks.registerAction(name, wrapped) } }, registerFilter : (name, fun, priority) => { + // wrap: pass value directly instead of FilterContext + const wrapped = (context) => fun(context.value()); if (priority) { - hooks.registerFilter(name, fun, priority) + hooks.registerFilter(name, wrapped, priority) } else { - hooks.registerFilter(name, fun) + hooks.registerFilter(name, wrapped) } } -} \ No newline at end of file +} diff --git a/cms-filesystem/src/main/java/com/condation/cms/filesystem/FileContent.java b/cms-filesystem/src/main/java/com/condation/cms/filesystem/FileContent.java index b2a721c98..5cded3585 100644 --- a/cms-filesystem/src/main/java/com/condation/cms/filesystem/FileContent.java +++ b/cms-filesystem/src/main/java/com/condation/cms/filesystem/FileContent.java @@ -54,12 +54,12 @@ public boolean isVisible(ContentNode node) { } @Override - public List listSlotItems(ReadOnlyFile contentFile) { + public List listSectionEntries(ReadOnlyFile contentFile) { String folder = PathUtil.toRelativePath(contentFile, cmsFileSystem.contentBase()); String filename = contentFile.getFileName(); filename = filename.substring(0, filename.length() - 3); - return fileSystem.listSlotItems(filename, folder); + return fileSystem.listSectionEntries(filename, folder); } @Override diff --git a/cms-filesystem/src/main/java/com/condation/cms/filesystem/FileSystem.java b/cms-filesystem/src/main/java/com/condation/cms/filesystem/FileSystem.java index 21dce3f55..ac5c7d08c 100644 --- a/cms-filesystem/src/main/java/com/condation/cms/filesystem/FileSystem.java +++ b/cms-filesystem/src/main/java/com/condation/cms/filesystem/FileSystem.java @@ -216,28 +216,28 @@ public List listContent(final String folder) { } - public List listSlotItems(final Path contentFile) { + public List listSectionEntries(final Path contentFile) { String folder = PathUtil.toRelativePath(contentFile, contentBase); String filename = contentFile.getFileName().toString(); filename = filename.substring(0, filename.length() - 3); - return FileSystem.this.listSlotItems(filename, folder); + return FileSystem.this.listSectionEntries(filename, folder); } - public List listSlotItems(final String filename, String folder) { + public List listSectionEntries(final String filename, String folder) { List nodes = new ArrayList<>(); - final Pattern isSlotItemOf = Constants.SLOT_ITEM_OF_PATTERN.apply(filename); - final Pattern isNamedSlotItemOf = Constants.SLOT_ITEM_NAMED_OF_PATTERN.apply(filename); + final Pattern isSectionEntryOf = Constants.SECTION_ENTRY_OF_PATTERN.apply(filename); + final Pattern isNamedSectionEntryOf = Constants.SECTION_ENTRY_NAMED_OF_PATTERN.apply(filename); if ("".equals(folder)) { metaData.getTree().values() .stream() .filter(node -> !node.isHidden()) .filter(node -> node.isVisible()) - .filter(node -> node.isSlotItem()) + .filter(node -> node.isSectionEntry()) .filter(node -> { - return isSlotItemOf.matcher(node.name()).matches() || isNamedSlotItemOf.matcher(node.name()).matches(); + return isSectionEntryOf.matcher(node.name()).matches() || isNamedSectionEntryOf.matcher(node.name()).matches(); }) .forEach((node) -> { nodes.add(node); @@ -249,9 +249,9 @@ public List listSlotItems(final String filename, String folder) { .stream() .filter(node -> !node.isHidden()) .filter(node -> node.isVisible()) - .filter(node -> node.isSlotItem()) + .filter(node -> node.isSectionEntry()) .filter(node - -> isSlotItemOf.matcher(node.name()).matches() || isNamedSlotItemOf.matcher(node.name()).matches() + -> isSectionEntryOf.matcher(node.name()).matches() || isNamedSectionEntryOf.matcher(node.name()).matches() ) .forEach((node) -> { nodes.add(node); diff --git a/cms-filesystem/src/main/java/com/condation/cms/filesystem/metadata/PageMetaData.java b/cms-filesystem/src/main/java/com/condation/cms/filesystem/metadata/PageMetaData.java index 3dd414714..949884a16 100644 --- a/cms-filesystem/src/main/java/com/condation/cms/filesystem/metadata/PageMetaData.java +++ b/cms-filesystem/src/main/java/com/condation/cms/filesystem/metadata/PageMetaData.java @@ -32,7 +32,7 @@ public abstract class PageMetaData { public static boolean isPage (ContentNode node) { - return !node.isSlotItem(); + return !node.isSectionEntry(); } public static boolean isVisible (ContentNode node) { diff --git a/cms-hooksystem/src/main/java/com/condation/cms/hooksystem/ActionHook.java b/cms-hooksystem/src/main/java/com/condation/cms/hooksystem/ActionHook.java index 9c41cc31c..24fb4e900 100644 --- a/cms-hooksystem/src/main/java/com/condation/cms/hooksystem/ActionHook.java +++ b/cms-hooksystem/src/main/java/com/condation/cms/hooksystem/ActionHook.java @@ -1,7 +1,5 @@ package com.condation.cms.hooksystem; -import com.condation.cms.api.hooks.ActionFunction; - /*- * #%L * CMS Api @@ -12,21 +10,21 @@ * 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.hooks.ActionFunction; /** - * * @author t.marx */ -record ActionHook(String name, int priority, ActionFunction function) implements Hook { +public record ActionHook(String name, int priority, ActionFunction function) implements Hook { } diff --git a/cms-hooksystem/src/main/java/com/condation/cms/hooksystem/CMSHookSystem.java b/cms-hooksystem/src/main/java/com/condation/cms/hooksystem/CMSHookSystem.java index 9ca004282..6f63c8c84 100644 --- a/cms-hooksystem/src/main/java/com/condation/cms/hooksystem/CMSHookSystem.java +++ b/cms-hooksystem/src/main/java/com/condation/cms/hooksystem/CMSHookSystem.java @@ -10,136 +10,93 @@ * 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.annotations.Filter; -import com.condation.cms.api.annotations.Action; -import com.condation.cms.api.hooks.ActionContext; + import com.condation.cms.api.hooks.ActionFunction; -import com.condation.cms.api.hooks.FilterContext; import com.condation.cms.api.hooks.FilterFunction; import com.condation.cms.api.hooks.HookSystem; -import com.condation.cms.api.utils.AnnotationsUtil; -import com.google.common.collect.ArrayListMultimap; -import com.google.common.collect.Multimap; -import java.util.ArrayList; -import java.util.HashMap; import java.util.List; +import com.condation.cms.hooksystem.annotation.AnnotationHookRegistrar; +import com.condation.cms.hooksystem.executor.ActionExecutor; +import com.condation.cms.hooksystem.executor.FilterExecutor; +import com.condation.cms.hooksystem.registry.ActionRegistry; +import com.condation.cms.hooksystem.registry.FilterRegistry; import java.util.Map; -import lombok.extern.slf4j.Slf4j; /** - * - * Request based hook system. + * Default {@link HookSystem} implementation. Delegates to dedicated registries + * and executors; annotation scanning is handled by {@link AnnotationHookRegistrar}. * * @author t.marx */ -@Slf4j public class CMSHookSystem implements HookSystem { - Multimap actions = ArrayListMultimap.create(); - - Multimap filters = ArrayListMultimap.create(); - - public CMSHookSystem () { - - } - public CMSHookSystem(CMSHookSystem source) { - this.actions.putAll(source.actions); - this.filters.putAll(source.filters); - } - - public void register(Object sourceObject) { - // Action-Methoden registrieren - List> actionMethods - = AnnotationsUtil.process(sourceObject, Action.class, List.of(ActionContext.class), Void.class); - - for (AnnotationsUtil.CMSAnnotation ann : actionMethods) { - Action annotation = ann.annotation(); - registerAction(annotation.value(), context -> ann.invoke(context), annotation.priority()); - } - - // Filter-Methoden registrieren - List> filterMethods - = AnnotationsUtil.process(sourceObject, Filter.class, List.of(FilterContext.class), Object.class); - - for (AnnotationsUtil.CMSAnnotation ann : filterMethods) { - Filter annotation = ann.annotation(); - registerFilter(annotation.value(), context -> ann.invoke(context), annotation.priority()); - } - } - - public void registerAction(final String name, final ActionFunction hookFunction) { - registerAction(name, hookFunction, 10); - } - - public void registerAction(final String name, final ActionFunction hookFunction, int priority) { - actions.put(name, new ActionHook<>(name, priority, hookFunction)); - } - - public void registerFilter(final String name, final FilterFunction hookFunction) { - registerFilter(name, hookFunction, 10); - } - - public void registerFilter(final String name, final FilterFunction hookFunction, int priority) { - filters.put(name, new FilterHook<>(name, priority, hookFunction)); - } - - public ActionContext doAction(final String name) { - return doAction(name, Map.of()); - } - - public ActionContext doAction(final String name, final Map arguments) { - var context = new ActionContext(new HashMap<>(arguments), new ArrayList<>()); - actions.get(name).stream() - .sorted((h1, h2) -> Integer.compare(h1.priority(), h2.priority())) - .map((action) -> { - try { - return action.function().apply(context); - } catch (Exception e) { - log.error("error executing action", e); - } - return null; - }) - .filter(value -> value != null) - .forEach(context.results()::add); - - return context; - } - - /** - * calls all filters with the given parameters, if no filter is executed, - * the original parameters are returned - * - * @param - * @param name - * @param parameters - * @return - */ - public FilterContext doFilter(final String name, final T parameters) { - final FilterContext returnContext = new FilterContext( - parameters - ); - filters.get(name).stream() - .sorted((h1, h2) -> Integer.compare(h1.priority(), h2.priority())) - .forEach((var action) -> { - try { - var context = new FilterContext(returnContext.value()); - var result = action.function().apply(context); - returnContext.value((T) result); - } catch (Exception e) { - log.error("error on filter", e); - } - }); - - return returnContext; - } + private final ActionRegistry actionRegistry; + private final FilterRegistry filterRegistry; + private final ActionExecutor actionExecutor; + private final FilterExecutor filterExecutor; + private final AnnotationHookRegistrar annotationRegistrar; + + public CMSHookSystem() { + this.actionRegistry = new ActionRegistry(); + this.filterRegistry = new FilterRegistry(); + this.actionExecutor = new ActionExecutor(actionRegistry); + this.filterExecutor = new FilterExecutor(filterRegistry); + this.annotationRegistrar = new AnnotationHookRegistrar(actionRegistry, filterRegistry); + } + + public CMSHookSystem(CMSHookSystem source) { + this(); + this.actionRegistry.putAll(source.actionRegistry); + this.filterRegistry.putAll(source.filterRegistry); + } + + @Override + public void register(Object sourceObject) { + annotationRegistrar.register(sourceObject); + } + + @Override + public void registerAction(String name, ActionFunction function) { + registerAction(name, function, 10); + } + + @Override + public void registerAction(String name, ActionFunction function, int priority) { + actionRegistry.register(name, function, priority); + } + + @Override + public void registerFilter(String name, FilterFunction function) { + registerFilter(name, function, 10); + } + + @Override + public void registerFilter(String name, FilterFunction function, int priority) { + filterRegistry.register(name, function, priority); + } + + @Override + public List doAction(String name) { + return doAction(name, Map.of()); + } + + @Override + public List doAction(String name, Map arguments) { + return actionExecutor.execute(name, arguments); + } + + @Override + public T doFilter(String name, T value) { + return filterExecutor.execute(name, value); + } } diff --git a/cms-hooksystem/src/main/java/com/condation/cms/hooksystem/FilterHook.java b/cms-hooksystem/src/main/java/com/condation/cms/hooksystem/FilterHook.java index 32a4d503d..efaa02aee 100644 --- a/cms-hooksystem/src/main/java/com/condation/cms/hooksystem/FilterHook.java +++ b/cms-hooksystem/src/main/java/com/condation/cms/hooksystem/FilterHook.java @@ -1,7 +1,5 @@ package com.condation.cms.hooksystem; -import com.condation.cms.api.hooks.FilterFunction; - /*- * #%L * CMS Api @@ -12,21 +10,21 @@ * 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.hooks.FilterFunction; /** - * * @author t.marx */ -record FilterHook(String name, int priority, FilterFunction function) implements Hook { +public record FilterHook(String name, int priority, FilterFunction function) implements Hook { } diff --git a/cms-hooksystem/src/main/java/com/condation/cms/hooksystem/annotation/AnnotationHookRegistrar.java b/cms-hooksystem/src/main/java/com/condation/cms/hooksystem/annotation/AnnotationHookRegistrar.java new file mode 100644 index 000000000..b74f8d8eb --- /dev/null +++ b/cms-hooksystem/src/main/java/com/condation/cms/hooksystem/annotation/AnnotationHookRegistrar.java @@ -0,0 +1,124 @@ +package com.condation.cms.hooksystem.annotation; + +/*- + * #%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 com.condation.cms.api.annotations.Action; +import com.condation.cms.api.annotations.Filter; +import com.condation.cms.api.hooks.ActionFunction; +import com.condation.cms.api.hooks.ActionContext; +import com.condation.cms.api.hooks.FilterContext; +import com.condation.cms.api.hooks.FilterFunction; +import com.condation.cms.api.utils.ParamAnnotationUtil; +import com.condation.cms.hooksystem.registry.ActionRegistry; +import com.condation.cms.hooksystem.registry.FilterRegistry; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.lang.reflect.Parameter; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +/** + * Scans an object for {@link Action} and {@link Filter} annotated methods and + * registers them in the corresponding registries. + *

+ * Two method styles are supported for each hook type: + *

    + *
  • Context style – single {@code ActionContext} / {@code FilterContext} parameter (original API)
  • + *
  • Named-params style (actions) – every parameter carries {@link Param}; values are resolved + * by name from the hook's argument map at invocation time
  • + *
  • Direct-value style (filters) – single non-{@code FilterContext} parameter; receives the + * current filter value directly
  • + *
+ * + * @author t.marx + */ +@Slf4j +@RequiredArgsConstructor +public class AnnotationHookRegistrar { + + private final ActionRegistry actionRegistry; + private final FilterRegistry filterRegistry; + + public void register(Object source) { + for (Method method : source.getClass().getMethods()) { + if (!Modifier.isPublic(method.getModifiers())) { + continue; + } + if (method.isAnnotationPresent(Action.class)) { + registerAction(source, method); + } + if (method.isAnnotationPresent(Filter.class)) { + registerFilter(source, method); + } + } + } + + private void registerAction(Object target, Method method) { + Action annotation = method.getAnnotation(Action.class); + ActionFunction fn = buildActionFunction(target, method); + if (fn != null) { + actionRegistry.register(annotation.value(), fn, annotation.priority()); + } + } + + private void registerFilter(Object target, Method method) { + Filter annotation = method.getAnnotation(Filter.class); + FilterFunction fn = buildFilterFunction(target, method); + if (fn != null) { + filterRegistry.register(annotation.value(), fn, annotation.priority()); + } + } + + private ActionFunction buildActionFunction(Object target, Method method) { + Parameter[] params = method.getParameters(); + + if (ParamAnnotationUtil.isContextStyle(params, ActionContext.class)) { + return context -> ParamAnnotationUtil.invokeOrNull(target, method, context); + } + + String[] names = ParamAnnotationUtil.extractParamNames(params); + if (names != null) { + return context -> ParamAnnotationUtil.invokeOrNull(target, method, + ParamAnnotationUtil.resolveArgs(context.arguments(), names)); + } + + log.warn("@Action method '{}' in '{}' has unsupported signature — skipped", + method.getName(), target.getClass().getSimpleName()); + return null; + } + + private FilterFunction buildFilterFunction(Object target, Method method) { + Parameter[] params = method.getParameters(); + + if (ParamAnnotationUtil.isContextStyle(params, FilterContext.class)) { + return context -> ParamAnnotationUtil.invokeOrNull(target, method, context); + } + + if (params.length == 1) { + return context -> ParamAnnotationUtil.invokeOrNull(target, method, context.value()); + } + + log.warn("@Filter method '{}' in '{}' has unsupported signature — skipped", + method.getName(), target.getClass().getSimpleName()); + return null; + } +} diff --git a/cms-hooksystem/src/main/java/com/condation/cms/hooksystem/executor/ActionExecutor.java b/cms-hooksystem/src/main/java/com/condation/cms/hooksystem/executor/ActionExecutor.java new file mode 100644 index 000000000..98f165d3b --- /dev/null +++ b/cms-hooksystem/src/main/java/com/condation/cms/hooksystem/executor/ActionExecutor.java @@ -0,0 +1,61 @@ +package com.condation.cms.hooksystem.executor; + +/*- + * #%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 com.condation.cms.api.hooks.ActionContext; +import com.condation.cms.hooksystem.registry.ActionRegistry; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +/** + * Executes all action hooks registered under a given name. + * + * @author t.marx + */ +@Slf4j +@RequiredArgsConstructor +public class ActionExecutor { + + private final ActionRegistry registry; + + @SuppressWarnings("unchecked") + public List execute(String name, Map arguments) { + var context = new ActionContext(new HashMap<>(arguments), new ArrayList<>()); + + registry.get(name).forEach(hook -> { + try { + T result = (T) hook.function().apply(context); + if (result != null) { + context.results().add(result); + } + } catch (Exception e) { + log.error("error executing action hook '{}'", name, e); + } + }); + + return context.results(); + } +} diff --git a/cms-hooksystem/src/main/java/com/condation/cms/hooksystem/executor/FilterExecutor.java b/cms-hooksystem/src/main/java/com/condation/cms/hooksystem/executor/FilterExecutor.java new file mode 100644 index 000000000..4c3126eb9 --- /dev/null +++ b/cms-hooksystem/src/main/java/com/condation/cms/hooksystem/executor/FilterExecutor.java @@ -0,0 +1,57 @@ +package com.condation.cms.hooksystem.executor; + +/*- + * #%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 com.condation.cms.api.hooks.FilterContext; +import com.condation.cms.hooksystem.registry.FilterRegistry; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +/** + * Executes all filter hooks registered under a given name, passing the value + * through each filter in priority order. + * + * @author t.marx + */ +@Slf4j +@RequiredArgsConstructor +public class FilterExecutor { + + private final FilterRegistry registry; + + @SuppressWarnings("unchecked") + public T execute(String name, T value) { + final FilterContext result = new FilterContext<>(value); + + registry.get(name).forEach(hook -> { + try { + var context = new FilterContext<>(result.value()); + T filtered = (T) hook.function().apply(context); + result.value(filtered); + } catch (Exception e) { + log.error("error executing filter hook '{}'", name, e); + } + }); + + return result.value(); + } +} diff --git a/cms-hooksystem/src/main/java/com/condation/cms/hooksystem/extensions/TagsWrapper.java b/cms-hooksystem/src/main/java/com/condation/cms/hooksystem/extensions/TagsWrapper.java index 9b96dd707..84b55ba42 100644 --- a/cms-hooksystem/src/main/java/com/condation/cms/hooksystem/extensions/TagsWrapper.java +++ b/cms-hooksystem/src/main/java/com/condation/cms/hooksystem/extensions/TagsWrapper.java @@ -25,10 +25,13 @@ import com.condation.cms.api.Constants; import com.condation.cms.api.model.Parameter; import com.google.common.base.Strings; +import java.util.HashMap; import java.util.Map; import java.util.function.Function; import lombok.Getter; import lombok.RequiredArgsConstructor; +import org.graalvm.polyglot.Value; +import org.graalvm.polyglot.proxy.ProxyObject; /** * @@ -41,12 +44,25 @@ public class TagsWrapper { private final Map> tags; public void put(final String namespace, final String tag, final Function function) { - var ns = !Strings.isNullOrEmpty(namespace) ? namespace : "ext"; - var key = "%s:%s".formatted(ns, tag); - tags.put(key, function); + var ns = !Strings.isNullOrEmpty(namespace) ? namespace : Constants.TemplateNamespaces.DEFAULT_MODULE_NAMESPACE; + tags.put("%s:%s".formatted(ns, tag), function); + } + + public void put(final String tag, final Function function) { + put(Constants.TemplateNamespaces.DEFAULT_MODULE_NAMESPACE, tag, function); + } + + // called from JS: tags.put("tagName", ({name}) => ...) + public void put(final String tag, final Value jsFunction) { + put(Constants.TemplateNamespaces.DEFAULT_MODULE_NAMESPACE, tag, jsFunction); + } + + // called from JS: tags.put("namespace", "tagName", ({name}) => ...) + public void put(final String namespace, final String tag, final Value jsFunction) { + put(namespace, tag, (Parameter param) -> { + Map jsArgs = new HashMap<>(param); + Value result = jsFunction.execute(ProxyObject.fromMap(jsArgs)); + return result.isNull() ? "" : result.asString(); + }); } - - public void put (final String tag, final Function function) { - put(Constants.TemplateNamespaces.DEFAULT_MODULE_NAMESPACE, tag, function); - } } diff --git a/cms-hooksystem/src/main/java/com/condation/cms/hooksystem/extensions/TemplateComponentsWrapper.java b/cms-hooksystem/src/main/java/com/condation/cms/hooksystem/extensions/TemplateComponentsWrapper.java index 439a36785..9c0b00183 100644 --- a/cms-hooksystem/src/main/java/com/condation/cms/hooksystem/extensions/TemplateComponentsWrapper.java +++ b/cms-hooksystem/src/main/java/com/condation/cms/hooksystem/extensions/TemplateComponentsWrapper.java @@ -25,10 +25,13 @@ import com.condation.cms.api.Constants; import com.condation.cms.api.model.Parameter; import com.google.common.base.Strings; +import java.util.HashMap; import java.util.Map; import java.util.function.Function; import lombok.Getter; import lombok.RequiredArgsConstructor; +import org.graalvm.polyglot.Value; +import org.graalvm.polyglot.proxy.ProxyObject; /** * @@ -44,10 +47,22 @@ public void put(final String name, final Function function) { put(Constants.TemplateNamespaces.DEFAULT_MODULE_NAMESPACE, name, function); } - public void put(final String namespace, final String name, final Function function) { - var ns = !Strings.isNullOrEmpty(namespace) ? namespace : "ext"; - var key = "%s:%s".formatted(ns, name); - components.put(key, function); + public void put(final String namespace, final String name, final Function function) { + var ns = !Strings.isNullOrEmpty(namespace) ? namespace : Constants.TemplateNamespaces.DEFAULT_MODULE_NAMESPACE; + components.put("%s:%s".formatted(ns, name), function); + } + + // called from JS: components.put("name", ({param}) => ...) + public void put(final String name, final Value jsFunction) { + put(Constants.TemplateNamespaces.DEFAULT_MODULE_NAMESPACE, name, jsFunction); + } + + // called from JS: components.put("namespace", "name", ({param}) => ...) + public void put(final String namespace, final String name, final Value jsFunction) { + put(namespace, name, (Parameter param) -> { + Map jsArgs = new HashMap<>(param); + Value result = jsFunction.execute(ProxyObject.fromMap(jsArgs)); + return result.isNull() ? "" : result.asString(); + }); } - } diff --git a/cms-hooksystem/src/main/java/com/condation/cms/hooksystem/extensions/TemplateFunctionWrapper.java b/cms-hooksystem/src/main/java/com/condation/cms/hooksystem/extensions/TemplateFunctionWrapper.java index c5b3c447e..df751443a 100644 --- a/cms-hooksystem/src/main/java/com/condation/cms/hooksystem/extensions/TemplateFunctionWrapper.java +++ b/cms-hooksystem/src/main/java/com/condation/cms/hooksystem/extensions/TemplateFunctionWrapper.java @@ -27,9 +27,13 @@ import com.condation.cms.extensions.TemplateFunctionExtension; import com.google.common.base.Strings; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.function.Function; import lombok.Getter; +import org.graalvm.polyglot.Value; +import org.graalvm.polyglot.proxy.ProxyObject; /** * @@ -40,12 +44,26 @@ public class TemplateFunctionWrapper { @Getter private final List registerTemplateFunctions = new ArrayList<>(); - public void put(final String path, final Function function) { - put(Constants.TemplateNamespaces.DEFAULT_MODULE_NAMESPACE, path, function); + public void put(final String name, final Function function) { + put(Constants.TemplateNamespaces.DEFAULT_MODULE_NAMESPACE, name, function); } - - public void put(final String namespace, final String path, final Function function) { - var ns = !Strings.isNullOrEmpty(namespace) ? namespace : "ext"; - registerTemplateFunctions.add(new TemplateFunctionExtension(ns, path, function)); + + public void put(final String namespace, final String name, final Function function) { + var ns = !Strings.isNullOrEmpty(namespace) ? namespace : Constants.TemplateNamespaces.DEFAULT_MODULE_NAMESPACE; + registerTemplateFunctions.add(new TemplateFunctionExtension(ns, name, function)); + } + + // called from JS: functions.put("name", ({param}) => ...) + public void put(final String name, final Value jsFunction) { + put(Constants.TemplateNamespaces.DEFAULT_MODULE_NAMESPACE, name, jsFunction); + } + + // called from JS: functions.put("namespace", "name", ({param}) => ...) + public void put(final String namespace, final String name, final Value jsFunction) { + put(namespace, name, (Parameter param) -> { + Map jsArgs = new HashMap<>(param); + Value result = jsFunction.execute(ProxyObject.fromMap(jsArgs)); + return result.isNull() ? null : result.as(Object.class); + }); } } diff --git a/cms-hooksystem/src/main/java/com/condation/cms/hooksystem/registry/ActionRegistry.java b/cms-hooksystem/src/main/java/com/condation/cms/hooksystem/registry/ActionRegistry.java new file mode 100644 index 000000000..78b04b8ce --- /dev/null +++ b/cms-hooksystem/src/main/java/com/condation/cms/hooksystem/registry/ActionRegistry.java @@ -0,0 +1,54 @@ +package com.condation.cms.hooksystem.registry; + +/*- + * #%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 com.condation.cms.api.hooks.ActionFunction; +import com.condation.cms.hooksystem.ActionHook; +import com.google.common.collect.ArrayListMultimap; +import com.google.common.collect.Multimap; +import java.util.Collection; +import java.util.Comparator; +import java.util.List; + +/** + * Stores and provides sorted access to registered action hooks. + * + * @author t.marx + */ +public class ActionRegistry { + + private final Multimap hooks = ArrayListMultimap.create(); + + public void register(String name, ActionFunction function, int priority) { + hooks.put(name, new ActionHook<>(name, priority, function)); + } + + public List get(String name) { + return hooks.get(name).stream() + .sorted(Comparator.comparingInt(ActionHook::priority)) + .toList(); + } + + public void putAll(ActionRegistry source) { + hooks.putAll(source.hooks); + } +} diff --git a/cms-hooksystem/src/main/java/com/condation/cms/hooksystem/registry/FilterRegistry.java b/cms-hooksystem/src/main/java/com/condation/cms/hooksystem/registry/FilterRegistry.java new file mode 100644 index 000000000..7603ae8a9 --- /dev/null +++ b/cms-hooksystem/src/main/java/com/condation/cms/hooksystem/registry/FilterRegistry.java @@ -0,0 +1,53 @@ +package com.condation.cms.hooksystem.registry; + +/*- + * #%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 com.condation.cms.api.hooks.FilterFunction; +import com.condation.cms.hooksystem.FilterHook; +import com.google.common.collect.ArrayListMultimap; +import com.google.common.collect.Multimap; +import java.util.Comparator; +import java.util.List; + +/** + * Stores and provides sorted access to registered filter hooks. + * + * @author t.marx + */ +public class FilterRegistry { + + private final Multimap hooks = ArrayListMultimap.create(); + + public void register(String name, FilterFunction function, int priority) { + hooks.put(name, new FilterHook<>(name, priority, function)); + } + + public List get(String name) { + return hooks.get(name).stream() + .sorted(Comparator.comparingInt(FilterHook::priority)) + .toList(); + } + + public void putAll(FilterRegistry source) { + hooks.putAll(source.hooks); + } +} diff --git a/cms-hooksystem/src/test/java/com/condation/cms/hooksystem/CMSHookSystemTest.java b/cms-hooksystem/src/test/java/com/condation/cms/hooksystem/CMSHookSystemTest.java index a6b608d7a..08e2535f2 100644 --- a/cms-hooksystem/src/test/java/com/condation/cms/hooksystem/CMSHookSystemTest.java +++ b/cms-hooksystem/src/test/java/com/condation/cms/hooksystem/CMSHookSystemTest.java @@ -10,12 +10,12 @@ * 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% @@ -24,10 +24,12 @@ import com.condation.cms.api.annotations.Filter; import com.condation.cms.api.annotations.Action; +import com.condation.cms.api.annotations.Param; import com.condation.cms.api.hooks.ActionContext; import com.condation.cms.api.hooks.FilterContext; import java.util.ArrayList; import java.util.List; +import java.util.Map; import java.util.concurrent.atomic.AtomicInteger; import org.assertj.core.api.Assertions; import org.junit.jupiter.api.BeforeEach; @@ -40,7 +42,7 @@ public class CMSHookSystemTest { private CMSHookSystem hookSystem; - + @BeforeEach public void setup() { hookSystem = new CMSHookSystem(); @@ -48,112 +50,114 @@ public void setup() { @Test public void test_single_result() { - hookSystem.registerAction("test/test1", (context) -> { - return true; - }); - var context = hookSystem.doAction("test/test1"); - Assertions.assertThat(context.results()).hasSize(1).contains(true); + hookSystem.registerAction("test/test1", (context) -> true); + Assertions.assertThat(hookSystem.doAction("test/test1")).hasSize(1).contains(true); } - + @Test public void test_multiple_result() { - hookSystem.registerAction("test/test1", (context) -> { - return "test1"; - }); - hookSystem.registerAction("test/test1", (context) -> { - return "test2"; - }); - var context = hookSystem.doAction("test/test1"); - Assertions.assertThat(context.results()).hasSize(2).contains("test1", "test2"); + hookSystem.registerAction("test/test1", (context) -> "test1"); + hookSystem.registerAction("test/test1", (context) -> "test2"); + Assertions.assertThat(hookSystem.doAction("test/test1")).hasSize(2).contains("test1", "test2"); } - + @Test public void test_multiple_result_with_priority() { - hookSystem.registerAction("test/test1", (context) -> { - return "test3"; - }, 300); - hookSystem.registerAction("test/test1", (context) -> { - return "test1"; - }, 100); - hookSystem.registerAction("test/test1", (context) -> { - return "test2"; - }, 200); - var context = hookSystem.doAction("test/test1"); - Assertions.assertThat(context.results()).hasSize(3).containsExactly("test1", "test2", "test3"); + hookSystem.registerAction("test/test1", (context) -> "test3", 300); + hookSystem.registerAction("test/test1", (context) -> "test1", 100); + hookSystem.registerAction("test/test1", (context) -> "test2", 200); + Assertions.assertThat(hookSystem.doAction("test/test1")).hasSize(3).containsExactly("test1", "test2", "test3"); } - + @Test public void test_multiple_result_with_priority_reversed() { - hookSystem.registerAction("test/test1", (context) -> { - return "test3"; - }, 100); - hookSystem.registerAction("test/test1", (context) -> { - return "test1"; - }, 300); - hookSystem.registerAction("test/test1", (context) -> { - return "test2"; - }, 200); - var context = hookSystem.doAction("test/test1"); - Assertions.assertThat(context.results()).hasSize(3).containsExactly("test3", "test2", "test1"); + hookSystem.registerAction("test/test1", (context) -> "test3", 100); + hookSystem.registerAction("test/test1", (context) -> "test1", 300); + hookSystem.registerAction("test/test1", (context) -> "test2", 200); + Assertions.assertThat(hookSystem.doAction("test/test1")).hasSize(3).containsExactly("test3", "test2", "test1"); } - + @Test - public void test_filter_reversed () { - hookSystem.registerFilter("test/list", (context) -> ((List)context.value()).reversed()); - var context = hookSystem.doFilter("test/list", List.of("1", "2", "3")); - Assertions.assertThat(context.value()).containsExactly("3", "2", "1"); + public void test_filter_reversed() { + hookSystem.registerFilter("test/list", (context) -> ((List) context.value()).reversed()); + Assertions.assertThat(hookSystem.doFilter("test/list", List.of("1", "2", "3"))) + .containsExactly("3", "2", "1"); } - + @Test - public void test_filter_remove () { + public void test_filter_remove() { hookSystem.registerFilter("test/list", (FilterContext> context) -> { context.value().remove("2"); return context.value(); }); - var context = hookSystem.doFilter("test/list", new ArrayList<>(List.of("1", "2", "3"))); - Assertions.assertThat(context.value()).containsExactly("1", "3"); + Assertions.assertThat(hookSystem.doFilter("test/list", new ArrayList<>(List.of("1", "2", "3")))) + .containsExactly("1", "3"); } - + @Test - void test_action_annotation () { + void test_action_annotation() { var actionObject = new MyActions(); - hookSystem.register(actionObject); - hookSystem.doAction("test/annotation/action1"); - Assertions.assertThat(actionObject.counter).hasValue(2); } - + @Test - void test_filter_annotations () { + void test_filter_annotations() { var myFilters = new MyFilters(); - hookSystem.register(myFilters); - - var context = hookSystem.doFilter("test/annotation/filter1", new ArrayList<>(List.of("1", "2", "3"))); - Assertions.assertThat(context.value()).containsExactly("1", "3"); + Assertions.assertThat(hookSystem.doFilter("test/annotation/filter1", new ArrayList<>(List.of("1", "2", "3")))) + .containsExactly("1", "3"); } - + + @Test + void test_action_named_params() { + hookSystem.register(new MyNamedParamActions()); + Assertions.assertThat(hookSystem.doAction("test/named/action1", Map.of("name", "World", "count", 3))) + .hasSize(1).containsExactly("Hello World 3"); + } + + @Test + void test_filter_direct_value() { + hookSystem.register(new MyDirectValueFilters()); + Assertions.assertThat(hookSystem.doFilter("test/direct/filter1", new ArrayList<>(List.of("1", "2", "3")))) + .containsExactly("1", "3"); + } + public class MyFilters { @Filter("test/annotation/filter1") - public List filter (FilterContext> context) { + public List filter(FilterContext> context) { context.value().remove("2"); return context.value(); } } - + public class MyActions { - private AtomicInteger counter = new AtomicInteger(0); - + @Action("test/annotation/action1") - public void action1 (ActionContext context) { + public void action1(ActionContext context) { counter.incrementAndGet(); } + @Action("test/annotation/action1") - public void action2 (ActionContext context) { + public void action2(ActionContext context) { counter.incrementAndGet(); } } + + public class MyNamedParamActions { + @Action("test/named/action1") + public String greet(@Param("name") String name, @Param("count") Integer count) { + return "Hello " + name + " " + count; + } + } + + public class MyDirectValueFilters { + @Filter("test/direct/filter1") + public List filter(List values) { + values.remove("2"); + return values; + } + } } diff --git a/cms-hooksystem/src/test/java/com/condation/cms/hooksystem/extensions/ExtensionManagerTest.java b/cms-hooksystem/src/test/java/com/condation/cms/hooksystem/extensions/ExtensionManagerTest.java index f512d8afa..fa4feb136 100644 --- a/cms-hooksystem/src/test/java/com/condation/cms/hooksystem/extensions/ExtensionManagerTest.java +++ b/cms-hooksystem/src/test/java/com/condation/cms/hooksystem/extensions/ExtensionManagerTest.java @@ -26,6 +26,7 @@ import com.condation.cms.api.feature.features.AuthFeature; import com.condation.cms.api.feature.features.HookSystemFeature; import com.condation.cms.api.hooks.HookSystem; +import com.condation.cms.api.model.Parameter; import com.condation.cms.api.request.RequestContext; import com.condation.cms.api.theme.Theme; import com.condation.cms.extensions.ExtensionManager; @@ -33,6 +34,9 @@ import com.condation.cms.hooksystem.CMSHookSystem; import java.io.IOException; import java.nio.file.Path; +import java.util.HashMap; +import java.util.Map; +import java.util.function.Function; import org.assertj.core.api.Assertions; import org.graalvm.polyglot.Engine; import org.junit.jupiter.api.AfterAll; @@ -89,30 +93,343 @@ public void setup() throws Exception { extensionManager = new ExtensionManager(db, properties, engine); } - @Test - public void test_with_auth() throws IOException { - - var requestContext = new RequestContext(); + private HookSystem setupHookSystem(RequestContext requestContext) throws IOException { final HookSystem hookSystem = new CMSHookSystem(); requestContext.add(HookSystemFeature.class, new HookSystemFeature(hookSystem)); - requestContext.add(AuthFeature.class, new AuthFeature("thorsten")); extensionManager.newContext(theme, requestContext); + return hookSystem; + } + + // --- action: no arguments, reads feature context --- - Assertions.assertThat(hookSystem.doAction("test").results()) + @Test + public void test_action_no_args_with_auth() throws IOException { + var requestContext = new RequestContext(); + requestContext.add(AuthFeature.class, new AuthFeature("thorsten")); + var hookSystem = setupHookSystem(requestContext); + + Assertions.assertThat(hookSystem.doAction("test")) .hasSize(1) .containsExactly("Hallo thorsten"); } @Test - public void test_without_auth() throws IOException { + public void test_action_no_args_without_auth() throws IOException { + var hookSystem = setupHookSystem(new RequestContext()); - var requestContext = new RequestContext(); - final HookSystem hookSystem = new CMSHookSystem(); - requestContext.add(HookSystemFeature.class, new HookSystemFeature(hookSystem)); - extensionManager.newContext(theme, requestContext); - - Assertions.assertThat(hookSystem.doAction("test").results()) + Assertions.assertThat(hookSystem.doAction("test")) .hasSize(1) .containsExactly("Guten Tag"); } + + // --- action: single named argument --- + + @Test + public void test_action_single_named_arg() throws IOException { + var hookSystem = setupHookSystem(new RequestContext()); + + Assertions.assertThat(hookSystem.doAction("print_name", Map.of("name", "CondationCMS"))) + .hasSize(1) + .containsExactly("Hallo CondationCMS"); + } + + @Test + public void test_action_single_named_args() throws IOException { + var hookSystem = setupHookSystem(new RequestContext()); + + Assertions.assertThat(hookSystem.doAction("print_name_args", Map.of("name", "CondationCMS"))) + .hasSize(1) + .containsExactly("Hallo CondationCMS"); + } + + // --- action: multiple named arguments --- + + @Test + public void test_action_multiple_named_args() throws IOException { + var hookSystem = setupHookSystem(new RequestContext()); + + Assertions.assertThat(hookSystem.doAction("greet", Map.of("firstName", "Max", "lastName", "Mustermann"))) + .hasSize(1) + .containsExactly("Max Mustermann"); + } + + // --- action: multiple handlers on same hook name --- + + @Test + public void test_action_multiple_handlers_collect_all_results() throws IOException { + var hookSystem = setupHookSystem(new RequestContext()); + + Assertions.assertThat(hookSystem.doAction("multi/action")) + .hasSize(2) + .containsExactlyInAnyOrder("result1", "result2"); + } + + // --- action: priority ordering --- + + @Test + public void test_action_priority_ordering() throws IOException { + var hookSystem = setupHookSystem(new RequestContext()); + + Assertions.assertThat(hookSystem.doAction("priority/action")) + .hasSize(2) + .containsExactly("low", "high"); + } + + // --- action: void return is not added to results --- + + @Test + public void test_action_void_return_not_in_results() throws IOException { + var hookSystem = setupHookSystem(new RequestContext()); + + Assertions.assertThat(hookSystem.doAction("action/void")) + .isEmpty(); + } + + // --- filter: string transform --- + + @Test + public void test_filter_string_to_uppercase() throws IOException { + var hookSystem = setupHookSystem(new RequestContext()); + + Assertions.assertThat(hookSystem.doFilter("filter/upper", "hello")) + .isEqualTo("HELLO"); + } + + // --- filter: chained transforms in priority order --- + + @Test + public void test_filter_chained_transforms_in_priority_order() throws IOException { + var hookSystem = setupHookSystem(new RequestContext()); + + // priority 100 runs first: "base-A", then priority 200: "base-A-B" + Assertions.assertThat(hookSystem.doFilter("filter/chain", "base")) + .isEqualTo("base-A-B"); + } + + // --- filter: trim whitespace --- + + @Test + public void test_filter_trim() throws IOException { + var hookSystem = setupHookSystem(new RequestContext()); + + Assertions.assertThat(hookSystem.doFilter("filter/trim", " hello world ")) + .isEqualTo("hello world"); + } + + // --- filter: no handler registered → original value returned unchanged --- + + @Test + public void test_filter_no_handler_returns_original_value() throws IOException { + var hookSystem = setupHookSystem(new RequestContext()); + + Assertions.assertThat(hookSystem.doFilter("filter/unregistered", "original")) + .isEqualTo("original"); + } + + // --- action: no handler registered → empty results --- + + @Test + public void test_action_no_handler_returns_empty_results() throws IOException { + var hookSystem = setupHookSystem(new RequestContext()); + + Assertions.assertThat(hookSystem.doAction("action/unregistered")) + .isEmpty(); + } + + // --- template functions registered via $hooks --- + + private TemplateFunctionWrapper setupTemplateFunctions(RequestContext requestContext) throws IOException { + setupHookSystem(requestContext); + return new TemplateHooks(requestContext).getTemplateFunctions(); + } + + @Test + public void test_template_function_default_namespace_registered() throws IOException { + var wrapper = setupTemplateFunctions(new RequestContext()); + + Assertions.assertThat(wrapper.getRegisterTemplateFunctions()) + .anyMatch(f -> f.namespace().equals("ext") && f.name().equals("hello")); + } + + @Test + public void test_template_function_no_params_returns_correct_value() throws IOException { + var wrapper = setupTemplateFunctions(new RequestContext()); + + var fn = wrapper.getRegisterTemplateFunctions().stream() + .filter(f -> f.name().equals("hello")).findFirst().orElseThrow(); + Assertions.assertThat(fn.function().apply(new Parameter())).isEqualTo("Hello World"); + } + + @Test + public void test_template_function_destructured_param() throws IOException { + var wrapper = setupTemplateFunctions(new RequestContext()); + + var fn = wrapper.getRegisterTemplateFunctions().stream() + .filter(f -> f.name().equals("greet")).findFirst().orElseThrow(); + Assertions.assertThat(fn.function().apply(new Parameter(Map.of("name", "CondationCMS")))) + .isEqualTo("Hello CondationCMS"); + } + + @Test + public void test_template_function_destructured_param_js_default() throws IOException { + var wrapper = setupTemplateFunctions(new RequestContext()); + + var fn = wrapper.getRegisterTemplateFunctions().stream() + .filter(f -> f.name().equals("greet")).findFirst().orElseThrow(); + Assertions.assertThat(fn.function().apply(new Parameter())).isEqualTo("Hello stranger"); + } + + @Test + public void test_template_function_multiple_params() throws IOException { + var wrapper = setupTemplateFunctions(new RequestContext()); + + var fn = wrapper.getRegisterTemplateFunctions().stream() + .filter(f -> f.name().equals("full_name")).findFirst().orElseThrow(); + Assertions.assertThat(fn.function().apply(new Parameter(Map.of("firstName", "Max", "lastName", "Mustermann")))) + .isEqualTo("Max Mustermann"); + } + + @Test + public void test_template_function_explicit_namespace_registered() throws IOException { + var wrapper = setupTemplateFunctions(new RequestContext()); + + Assertions.assertThat(wrapper.getRegisterTemplateFunctions()) + .anyMatch(f -> f.namespace().equals("theme") && f.name().equals("version")); + } + + @Test + public void test_template_function_explicit_namespace_returns_correct_value() throws IOException { + var wrapper = setupTemplateFunctions(new RequestContext()); + + var fn = wrapper.getRegisterTemplateFunctions().stream() + .filter(f -> f.name().equals("version")).findFirst().orElseThrow(); + Assertions.assertThat(fn.function().apply(new Parameter())).isEqualTo("1.0.0"); + } + + // --- template components registered via $hooks --- + + private TemplateComponentsWrapper setupComponents(RequestContext requestContext) throws IOException { + setupHookSystem(requestContext); + return new TemplateHooks(requestContext).getComponents(new HashMap<>()); + } + + @Test + public void test_template_component_default_namespace_registered() throws IOException { + var wrapper = setupComponents(new RequestContext()); + + Assertions.assertThat(wrapper.getComponents()).containsKey("ext:badge"); + } + + @Test + public void test_template_component_no_params_returns_correct_value() throws IOException { + var wrapper = setupComponents(new RequestContext()); + + String result = wrapper.getComponents().get("ext:badge").apply(new Parameter()); + Assertions.assertThat(result).isEqualTo("badge"); + } + + @Test + public void test_template_component_destructured_param() throws IOException { + var wrapper = setupComponents(new RequestContext()); + + String result = wrapper.getComponents().get("ext:alert") + .apply(new Parameter(Map.of("message", "Watch out!"))); + Assertions.assertThat(result).isEqualTo("
Watch out!
"); + } + + @Test + public void test_template_component_destructured_param_js_default() throws IOException { + var wrapper = setupComponents(new RequestContext()); + + String result = wrapper.getComponents().get("ext:alert").apply(new Parameter()); + Assertions.assertThat(result).isEqualTo("
default
"); + } + + @Test + public void test_template_component_explicit_namespace_registered() throws IOException { + var wrapper = setupComponents(new RequestContext()); + + Assertions.assertThat(wrapper.getComponents()).containsKey("theme:card"); + } + + @Test + public void test_template_component_explicit_namespace_returns_correct_value() throws IOException { + var wrapper = setupComponents(new RequestContext()); + + String result = wrapper.getComponents().get("theme:card") + .apply(new Parameter(Map.of("title", "My Card"))); + Assertions.assertThat(result).isEqualTo("
My Card
"); + } + + // --- tags registered via $tags.register --- + + private TagsWrapper setupTags(RequestContext requestContext) throws IOException { + var hookSystem = setupHookSystem(requestContext); + var codes = new HashMap>(); + return new ContentHooks(requestContext).getTags(codes); + } + + @Test + public void test_tag_default_namespace_registered() throws IOException { + var wrapper = setupTags(new RequestContext()); + + Assertions.assertThat(wrapper.getTags()).containsKey("ext:hello"); + } + + @Test + public void test_tag_default_namespace_no_params_returns_correct_value() throws IOException { + var wrapper = setupTags(new RequestContext()); + + String result = wrapper.getTags().get("ext:hello").apply(new Parameter()); + Assertions.assertThat(result).isEqualTo("Hello World"); + } + + @Test + public void test_tag_default_namespace_with_named_param() throws IOException { + var wrapper = setupTags(new RequestContext()); + + String result = wrapper.getTags().get("ext:greet").apply(new Parameter(Map.of("name", "CondationCMS"))); + Assertions.assertThat(result).isEqualTo("Hello CondationCMS"); + } + + @Test + public void test_tag_default_namespace_with_missing_param_uses_default() throws IOException { + var wrapper = setupTags(new RequestContext()); + + String result = wrapper.getTags().get("ext:greet").apply(new Parameter()); + Assertions.assertThat(result).isEqualTo("Hello stranger"); + } + + @Test + public void test_tag_explicit_namespace_registered() throws IOException { + var wrapper = setupTags(new RequestContext()); + + Assertions.assertThat(wrapper.getTags()).containsKey("theme:info"); + } + + @Test + public void test_tag_explicit_namespace_returns_correct_value() throws IOException { + var wrapper = setupTags(new RequestContext()); + + String result = wrapper.getTags().get("theme:info").apply(new Parameter()); + Assertions.assertThat(result).isEqualTo("theme-info"); + } + + @Test + public void test_tag_multiple_destructured_params() throws IOException { + var wrapper = setupTags(new RequestContext()); + + String result = wrapper.getTags().get("ext:full_name") + .apply(new Parameter(Map.of("firstName", "Max", "lastName", "Mustermann"))); + Assertions.assertThat(result).isEqualTo("Max Mustermann"); + } + + @Test + public void test_tag_destructured_param_with_js_default() throws IOException { + var wrapper = setupTags(new RequestContext()); + + // no "name" attribute → JS default value kicks in + String result = wrapper.getTags().get("ext:greet").apply(new Parameter()); + Assertions.assertThat(result).isEqualTo("Hello stranger"); + } } diff --git a/cms-hooksystem/src/test/java/com/condation/cms/hooksystem/extensions/GlobalExtensionsTest.java b/cms-hooksystem/src/test/java/com/condation/cms/hooksystem/extensions/GlobalExtensionsTest.java index e11378814..376c4c951 100644 --- a/cms-hooksystem/src/test/java/com/condation/cms/hooksystem/extensions/GlobalExtensionsTest.java +++ b/cms-hooksystem/src/test/java/com/condation/cms/hooksystem/extensions/GlobalExtensionsTest.java @@ -71,9 +71,7 @@ public void test_init() { public void test_hook () { globalExtensions.evaluate("$hooks.registerAction('test/hook1', (context) => {return 'Hello';})"); - var context = hookSystem.doAction("test/hook1"); - - Assertions.assertThat(context.results()) + Assertions.assertThat(hookSystem.doAction("test/hook1")) .hasSize(1) .containsExactly("Hello"); } diff --git a/cms-hooksystem/src/test/resources/site/extensions/hook-test.js b/cms-hooksystem/src/test/resources/site/extensions/hook-test.js index f9d91bbf8..e07386002 100644 --- a/cms-hooksystem/src/test/resources/site/extensions/hook-test.js +++ b/cms-hooksystem/src/test/resources/site/extensions/hook-test.js @@ -23,14 +23,52 @@ import { $hooks } from 'system/hooks.mjs'; import { AuthFeature, $features } from 'system/features.mjs'; +// --- action: no arguments, uses feature context --- $hooks.registerAction( "test", - (context) => { - + (args) => { if ($features.has(AuthFeature)) { return `Hallo ${$features.get(AuthFeature).username()}` } - return 'Guten Tag' } ) + +// --- action: single named argument --- +$hooks.registerAction( + "print_name", + ({name}) => `Hallo ${name}` +) + +// --- action: single named argument --- +$hooks.registerAction( + "print_name_args", + (args) => `Hallo ${args.name}` +) + +// --- action: multiple named arguments --- +$hooks.registerAction( + "greet", + ({firstName, lastName}) => `${firstName} ${lastName}` +) + +// --- action: multiple handlers on same hook (both results collected) --- +$hooks.registerAction("multi/action", (args) => "result1") +$hooks.registerAction("multi/action", (args) => "result2") + +// --- action: priority ordering (lower number = earlier execution) --- +$hooks.registerAction("priority/action", (args) => "high", 200) +$hooks.registerAction("priority/action", (args) => "low", 100) + +// --- action: no return value → must not appear in results --- +$hooks.registerAction("action/void", (args) => { /* intentionally no return */ }) + +// --- filter: transform string to uppercase --- +$hooks.registerFilter("filter/upper", (s) => s.toUpperCase()) + +// --- filter: two chained transforms, priority controls order --- +$hooks.registerFilter("filter/chain", (s) => s + "-A", 100) +$hooks.registerFilter("filter/chain", (s) => s + "-B", 200) + +// --- filter: trim whitespace --- +$hooks.registerFilter("filter/trim", (s) => s.trim()) diff --git a/modules/ui-module/src/main/ts/dist/actions/page/create-content.js b/cms-hooksystem/src/test/resources/site/extensions/tags-test.js similarity index 59% rename from modules/ui-module/src/main/ts/dist/actions/page/create-content.js rename to cms-hooksystem/src/test/resources/site/extensions/tags-test.js index f465c740b..0491224de 100644 --- a/modules/ui-module/src/main/ts/dist/actions/page/create-content.js +++ b/cms-hooksystem/src/test/resources/site/extensions/tags-test.js @@ -1,6 +1,6 @@ /*- * #%L - * UI Module + * CMS Extensions * %% * Copyright (C) 2023 - 2026 CondationCMS * %% @@ -18,12 +18,21 @@ * along with this program. If not, see . * #L% */ -import { openFileBrowser } from '@cms/modules/filebrowser.js'; -// hook.js -export async function runAction(params) { - openFileBrowser({ - type: "content", - uri: params.folder, - template : params.template, - }); -} + +import { $hooks } from 'system/hooks.mjs'; + +$hooks.registerAction("system/content/tags", ({tags}) => { + + // default namespace (ext) — no parameters + tags.put("hello", () => "Hello World") + + // default namespace (ext) — destructured parameter + tags.put("greet", ({name = "stranger"}) => `Hello ${name}`) + + // explicit namespace + tags.put("theme", "info", () => "theme-info") + + // multiple parameters + tags.put("full_name", ({firstName, lastName}) => `${firstName} ${lastName}`) + +}) diff --git a/cms-hooksystem/src/test/resources/site/extensions/templates-test.js b/cms-hooksystem/src/test/resources/site/extensions/templates-test.js new file mode 100644 index 000000000..076dab8c8 --- /dev/null +++ b/cms-hooksystem/src/test/resources/site/extensions/templates-test.js @@ -0,0 +1,53 @@ +/*- + * #%L + * CMS Extensions + * %% + * 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 { $hooks } from 'system/hooks.mjs'; + +// --- template functions --- +$hooks.registerAction("system/template/function", ({functions}) => { + + // no parameters + functions.put("hello", () => "Hello World") + + // destructured parameter + functions.put("greet", ({name = "stranger"}) => `Hello ${name}`) + + // multiple parameters + functions.put("full_name", ({firstName, lastName}) => `${firstName} ${lastName}`) + + // explicit namespace + functions.put("theme", "version", () => "1.0.0") + +}) + +// --- template components --- +$hooks.registerAction("system/template/component", ({components}) => { + + // no parameters + components.put("badge", () => "badge") + + // destructured parameter + components.put("alert", ({message = "default"}) => `
${message}
`) + + // explicit namespace + components.put("theme", "card", ({title}) => `
${title}
`) + +}) diff --git a/cms-templates/src/main/java/com/condation/cms/templates/components/TemplateComponents.java b/cms-templates/src/main/java/com/condation/cms/templates/components/TemplateComponents.java index dc92f120a..a78b825a0 100644 --- a/cms-templates/src/main/java/com/condation/cms/templates/components/TemplateComponents.java +++ b/cms-templates/src/main/java/com/condation/cms/templates/components/TemplateComponents.java @@ -24,8 +24,9 @@ import com.condation.cms.api.annotations.TemplateComponent; import com.condation.cms.api.model.Parameter; import com.condation.cms.api.request.RequestContext; -import com.condation.cms.api.utils.AnnotationsUtil; -import com.google.common.base.Strings; +import com.condation.cms.api.utils.ParamAnnotationUtil; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; import java.util.List; import java.util.Map; import java.util.Set; @@ -64,30 +65,44 @@ public void register(Object handler) { if (handler == null) { return; } + for (Method method : handler.getClass().getMethods()) { + if (!Modifier.isPublic(method.getModifiers())) { + continue; + } + if (!method.isAnnotationPresent(TemplateComponent.class)) { + continue; + } + TemplateComponent annotation = method.getAnnotation(TemplateComponent.class); + String key = buildKey(annotation); + Function fn = buildFunction(handler, method, key); + if (fn != null) { + componentMap.put(key, fn); + } + } + } - var annotations = AnnotationsUtil.process(handler, TemplateComponent.class, List.of(Parameter.class), String.class); + private Function buildFunction(Object target, Method method, String key) { + java.lang.reflect.Parameter[] params = method.getParameters(); - for (var entry : annotations) { - String name = entry.annotation().value(); - String namespace = entry.annotation().namespace(); - if (Strings.isNullOrEmpty(namespace)) { - namespace = Constants.TemplateNamespaces.DEFAULT_MODULE_NAMESPACE; - } - String key; - if (!Strings.isNullOrEmpty(namespace)) { - key = "%s:%s".formatted(namespace, name); - } else { - key = name; - } - - componentMap.put(key, param -> { - try { - return entry.invoke(param); - } catch (Exception e) { - throw new RuntimeException("Error calling component: " + key, e); - } - }); + if (ParamAnnotationUtil.isContextStyle(params, Parameter.class)) { + return param -> (String) ParamAnnotationUtil.invokeOrThrow(target, method, key, param); } + + String[] names = ParamAnnotationUtil.extractParamNames(params); + if (names != null) { + return param -> (String) ParamAnnotationUtil.invokeOrThrow(target, method, key, + ParamAnnotationUtil.resolveArgs(param, names)); + } + + log.warn("@TemplateComponent method '{}' in '{}' has unsupported signature — skipped", + method.getName(), target.getClass().getSimpleName()); + return null; + } + + private String buildKey(TemplateComponent annotation) { + return ParamAnnotationUtil.buildNamespaceKey( + annotation.namespace(), annotation.value(), + Constants.TemplateNamespaces.DEFAULT_MODULE_NAMESPACE); } public Set getComponentNames() { diff --git a/cms-templates/src/main/java/com/condation/cms/templates/components/TemplateFunctions.java b/cms-templates/src/main/java/com/condation/cms/templates/components/TemplateFunctions.java index 3362485b8..2fc5d4199 100644 --- a/cms-templates/src/main/java/com/condation/cms/templates/components/TemplateFunctions.java +++ b/cms-templates/src/main/java/com/condation/cms/templates/components/TemplateFunctions.java @@ -23,9 +23,10 @@ import com.condation.cms.api.Constants; import com.condation.cms.api.annotations.TemplateFunction; import com.condation.cms.api.model.Parameter; -import com.condation.cms.api.request.RequestContext; -import com.condation.cms.api.utils.AnnotationsUtil; +import com.condation.cms.api.utils.ParamAnnotationUtil; import com.google.common.base.Strings; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; import java.util.List; import java.util.Map; import java.util.Set; @@ -73,38 +74,48 @@ public void register(Object handler) { if (handler == null) { return; } + for (Method method : handler.getClass().getMethods()) { + if (!Modifier.isPublic(method.getModifiers())) { + continue; + } + if (!method.isAnnotationPresent(TemplateFunction.class)) { + continue; + } + TemplateFunction annotation = method.getAnnotation(TemplateFunction.class); + String namespace = Strings.isNullOrEmpty(annotation.namespace()) + ? Constants.TemplateNamespaces.DEFAULT_MODULE_NAMESPACE + : annotation.namespace(); + String name = annotation.value(); + Function fn = buildFunction(handler, method, name); + if (fn != null) { + functionMap.put(namespace, name, fn); + } + } + } - var annotations = AnnotationsUtil.process(handler, TemplateFunction.class, List.of(Parameter.class), Object.class); + private Function buildFunction(Object target, Method method, String name) { + java.lang.reflect.Parameter[] params = method.getParameters(); - for (var entry : annotations) { - String name = entry.annotation().value(); - String namespace = entry.annotation().namespace(); - if (Strings.isNullOrEmpty(namespace)) { - namespace = Constants.TemplateNamespaces.DEFAULT_MODULE_NAMESPACE; - } - functionMap.put(namespace, name, param -> { - try { - return entry.invoke(param); - } catch (Exception e) { - throw new RuntimeException("Error calling component: " + name, e); - } - }); + // no-arg style + if (params.length == 0) { + return param -> ParamAnnotationUtil.invokeOrThrow(target, method, name); } - - annotations = AnnotationsUtil.process(handler, TemplateFunction.class, List.of(), Object.class); - for (var entry : annotations) { - String name = entry.annotation().value(); - String namespace = entry.annotation().namespace(); - - functionMap.put(namespace, name, param -> { - try { - return entry.invoke((Object[]) null); - } catch (Exception e) { - throw new RuntimeException("Error calling component: " + name, e); - } - }); + // context style: single Parameter argument + if (ParamAnnotationUtil.isContextStyle(params, Parameter.class)) { + return param -> ParamAnnotationUtil.invokeOrThrow(target, method, name, param); } + + // named-params style: all parameters carry @Param + String[] names = ParamAnnotationUtil.extractParamNames(params); + if (names != null) { + return param -> ParamAnnotationUtil.invokeOrThrow(target, method, name, + ParamAnnotationUtil.resolveArgs(param, names)); + } + + log.warn("@TemplateFunction method '{}' in '{}' has unsupported signature — skipped", + method.getName(), target.getClass().getSimpleName()); + return null; } public Set getFunctions() { diff --git a/cms-templates/src/main/java/com/condation/cms/templates/functions/impl/NodeFunction.java b/cms-templates/src/main/java/com/condation/cms/templates/functions/impl/NodeFunction.java index 042f78b2f..f88f0a93e 100644 --- a/cms-templates/src/main/java/com/condation/cms/templates/functions/impl/NodeFunction.java +++ b/cms-templates/src/main/java/com/condation/cms/templates/functions/impl/NodeFunction.java @@ -27,7 +27,7 @@ import com.condation.cms.api.feature.features.InjectorFeature; import com.condation.cms.api.request.RequestContext; import com.condation.cms.content.ContentRenderer; -import com.condation.cms.content.SlotItem; +import com.condation.cms.content.SectionEntry; import java.io.IOException; import java.util.List; import java.util.Map; @@ -51,10 +51,10 @@ protected void extendMap(Map node, ReadOnlyFile contentFile) { try { var db = requestContext.get(InjectorFeature.class).injector().getInstance(DB.class); var contentRenderer = requestContext.get(InjectorFeature.class).injector().getInstance(ContentRenderer.class); - List slotItemList = db.getContent().listSlotItems(contentFile); + List sectionEntryList = db.getContent().listSectionEntries(contentFile); - Map> slotItems = contentRenderer.renderSlotItems(slotItemList, requestContext); - node.put("slots", slotItems); + Map> sectionEntries = contentRenderer.renderSectionEntries(sectionEntryList, requestContext); + node.put("sections", sectionEntries); } catch (IOException iOException) { log.error("error loading sections", iOException); } diff --git a/cms-templates/src/test/java/com/condation/cms/templates/TemplateEngineComponentTest.java b/cms-templates/src/test/java/com/condation/cms/templates/TemplateEngineComponentTest.java index ccd889d1b..e402a3a9d 100644 --- a/cms-templates/src/test/java/com/condation/cms/templates/TemplateEngineComponentTest.java +++ b/cms-templates/src/test/java/com/condation/cms/templates/TemplateEngineComponentTest.java @@ -20,6 +20,7 @@ * along with this program. If not, see . * #L% */ +import com.condation.cms.api.annotations.Param; import com.condation.cms.api.annotations.TemplateComponent; import com.condation.cms.api.model.Parameter; import com.condation.cms.templates.components.TemplateComponents; @@ -60,18 +61,28 @@ public void setupComponents() { public static class MyComponents { @TemplateComponent("tag3") - public String tag3 (Parameter parameter) { + public String tag3(Parameter parameter) { return "
%s
".formatted(parameter.get("_content")); } - @TemplateComponent(value = "tag3", namespace = "ns") - public String custom_namespace (Parameter parameter) { + + @TemplateComponent(value = "tag3", namespace = "ns") + public String custom_namespace(Parameter parameter) { return "
%s
".formatted(parameter.get("_content")); } + + @TemplateComponent("tag4_param") + public String tag4_param(@Param("name") String name) { + return "%s".formatted(name); + } } @Override public TemplateLoader getLoader() { return new StringTemplateLoader() + .add("tag4_param", """ + {[ ext:tag4_param name="CondationCMS" ]} + {[ endext:tag4_param ]} + """) .add("tag1", """ {[ tag1 ]} @@ -166,4 +177,13 @@ public void test_render_alternate() throws IOException { Assertions.assertThat(template.evaluate(Map.of(), dynamicConfiguration)) .isEqualToIgnoringWhitespace("Hello CondationCMS!"); } + + @Test + public void test_tag4_param() throws IOException { + Template simpleTemplate = SUT.getTemplate("tag4_param"); + Assertions.assertThat(simpleTemplate).isNotNull(); + + Assertions.assertThat(simpleTemplate.evaluate(Map.of(), dynamicConfiguration)) + .isEqualToIgnoringWhitespace("CondationCMS"); + } } diff --git a/cms-templates/src/test/java/com/condation/cms/templates/TemplateEngineFunctionsTest.java b/cms-templates/src/test/java/com/condation/cms/templates/TemplateEngineFunctionsTest.java index 3a4295638..5107e2479 100644 --- a/cms-templates/src/test/java/com/condation/cms/templates/TemplateEngineFunctionsTest.java +++ b/cms-templates/src/test/java/com/condation/cms/templates/TemplateEngineFunctionsTest.java @@ -10,23 +10,23 @@ * 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.annotations.Param; import com.condation.cms.api.annotations.TemplateFunction; import com.condation.cms.api.extensions.RegisterTemplateFunctionExtensionPoint; import com.condation.cms.api.feature.features.HookSystemFeature; import com.condation.cms.api.feature.features.InjectorFeature; import com.condation.cms.api.feature.features.ModuleManagerFeature; -import com.condation.cms.api.hooks.HookSystem; import com.condation.cms.api.model.Parameter; import com.condation.cms.api.request.RequestContext; import com.condation.cms.hooksystem.CMSHookSystem; @@ -42,6 +42,7 @@ import java.util.Map; import java.util.function.Function; import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.mockito.Mockito; @@ -51,70 +52,152 @@ */ public class TemplateEngineFunctionsTest extends AbstractTemplateEngineTest { + static DynamicConfiguration dynamicConfiguration; + + @BeforeAll + public void setupFunctions() { + var requestContext = new RequestContext(); + requestContext.add(HookSystemFeature.class, new HookSystemFeature(new CMSHookSystem())); + requestContext.add(TemplateHooks.class, new TemplateHooks(requestContext)); + + var injectorMock = Mockito.mock(Injector.class); + requestContext.add(InjectorFeature.class, new InjectorFeature(injectorMock)); + + var moduleManagerMock = Mockito.mock(ModuleManager.class); + requestContext.add(ModuleManagerFeature.class, new ModuleManagerFeature(moduleManagerMock)); + + Mockito.when(injectorMock.getInstance(ModuleManager.class)).thenReturn(moduleManagerMock); + Mockito.when(moduleManagerMock.extensions(RegisterTemplateFunctionExtensionPoint.class)) + .thenReturn(List.of(new TestFunctions())); + + dynamicConfiguration = new DynamicConfiguration(new TemplateComponents(), requestContext); + } + @Override public TemplateLoader getLoader() { return new StringTemplateLoader() - .add("date", "{{ date() | date('YYYY') }}"); - + .add("date", "{{ date() | date('YYYY') }}") + // no-arg style + .add("fn_noarg", "{{ ext.testfn3() }}") + // context style (Parameter) + .add("fn_context", "{{ ext.testfn2({}) }}") + // @Param style — parameter passed as JEXL map (keys must be quoted in JEXL3) + .add("fn_param", "{{ ext.testfn4({'name': 'World'}) }}") + // explicit namespace + .add("fn_namespace", "{{ ns1.shout({'text': 'hello'}) }}") + // chained with filter + .add("fn_chained", "{{ ext.testfn3() | upper }}"); } - + + // --- built-in date function --- + @Test public void test_date() throws IOException { - var year = new SimpleDateFormat("YYYY").format(new Date()); - Template simpleTemplate = SUT.getTemplate("date"); Assertions.assertThat(simpleTemplate).isNotNull(); Assertions.assertThat(simpleTemplate.evaluate()).isEqualToIgnoringWhitespace(year); } - - + + // --- no-arg style renders correctly --- + @Test - void getFunctionsFromDynamicConig () { - var requestContext = new RequestContext(); - requestContext.add(HookSystemFeature.class, new HookSystemFeature(new CMSHookSystem())); - requestContext.add(TemplateHooks.class, new TemplateHooks(requestContext)); - - var injectorMock = Mockito.mock(Injector.class); - requestContext.add(InjectorFeature.class, new InjectorFeature(injectorMock)); - - var moduleManagerMock = Mockito.mock(ModuleManager.class); - requestContext.add(ModuleManagerFeature.class, new ModuleManagerFeature(moduleManagerMock)); - - Mockito.when(injectorMock.getInstance(ModuleManager.class)).thenReturn(moduleManagerMock); - Mockito.when(moduleManagerMock.extensions(RegisterTemplateFunctionExtensionPoint.class)).thenReturn( - List.of(new TestFunctions()) - ); - - DynamicConfiguration dc = new DynamicConfiguration(new TemplateComponents(), requestContext); - - var tfs = dc.templateFunctions(); - - Assertions.assertThat(tfs).isNotNull().hasSize(3); + void test_noarg_function_renders() throws IOException { + Template template = SUT.getTemplate("fn_noarg"); + Assertions.assertThat(template.evaluate(Map.of(), dynamicConfiguration)) + .isEqualToIgnoringWhitespace("hi I'm testfn3"); + } + + // --- context style (Parameter) renders correctly --- + + @Test + void test_context_style_function_renders() throws IOException { + Template template = SUT.getTemplate("fn_context"); + Assertions.assertThat(template.evaluate(Map.of(), dynamicConfiguration)) + .isEqualToIgnoringWhitespace("hi I'm testfn2"); + } + + // --- @Param style renders correctly --- + + @Test + void test_param_style_function_renders() throws IOException { + Template template = SUT.getTemplate("fn_param"); + Assertions.assertThat(template.evaluate(Map.of(), dynamicConfiguration)) + .isEqualToIgnoringWhitespace("hi World"); + } + + // --- explicit namespace renders correctly --- + + @Test + void test_explicit_namespace_function_renders() throws IOException { + Template template = SUT.getTemplate("fn_namespace"); + Assertions.assertThat(template.evaluate(Map.of(), dynamicConfiguration)) + .isEqualToIgnoringWhitespace("HELLO!"); + } + + // --- function result can be chained with filter --- + + @Test + void test_function_result_chained_with_filter() throws IOException { + Template template = SUT.getTemplate("fn_chained"); + Assertions.assertThat(template.evaluate(Map.of(), dynamicConfiguration)) + .isEqualToIgnoringWhitespace("HI I'M TESTFN3"); } - + + // --- registration checks --- + + @Test + void all_functions_are_registered() { + var tfs = dynamicConfiguration.templateFunctions(); + // testfn1 (map), testfn2 (Parameter), testfn3 (no-arg), testfn4 (@Param), shout (@Param, ns1) + Assertions.assertThat(tfs).isNotNull().hasSize(5); + } + + @Test + void param_style_function_invocation_returns_correct_value() { + var fn4 = dynamicConfiguration.templateFunctions().stream() + .filter(f -> f.name().equals("testfn4")).findFirst(); + Assertions.assertThat(fn4).isPresent(); + Assertions.assertThat(fn4.get().invoke(new Parameter(Map.of("name", "World")))) + .isEqualTo("hi World"); + } + + // --- test handler --- + public static class TestFunctions extends RegisterTemplateFunctionExtensionPoint { @Override public Map> functions() { - return Map.of("testfn1", (params) -> { - return "hi I'm testfn1"; - }); + return Map.of("testfn1", (params) -> "hi I'm testfn1"); } @Override public List functionDefinitions() { return List.of(this); } - + + // context style @TemplateFunction("testfn2") - public Object testfn2 (Parameter params) { + public Object testfn2(Parameter params) { return "hi I'm testfn2"; } - + + // no-arg style @TemplateFunction("testfn3") - public Object testfn3 () { + public Object testfn3() { return "hi I'm testfn3"; } + + // @Param style, default namespace + @TemplateFunction("testfn4") + public Object testfn4(@Param("name") String name) { + return "hi " + name; + } + + // @Param style, explicit namespace + @TemplateFunction(value = "shout", namespace = "ns1") + public Object shout(@Param("text") String text) { + return text.toUpperCase() + "!"; + } } } diff --git a/cms-templates/src/test/java/com/condation/cms/templates/components/TemplateComponentsRegistrationTest.java b/cms-templates/src/test/java/com/condation/cms/templates/components/TemplateComponentsRegistrationTest.java new file mode 100644 index 000000000..b17ec66d0 --- /dev/null +++ b/cms-templates/src/test/java/com/condation/cms/templates/components/TemplateComponentsRegistrationTest.java @@ -0,0 +1,167 @@ +package com.condation.cms.templates.components; + +/*- + * #%L + * CMS Templates + * %% + * 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.annotations.Param; +import com.condation.cms.api.annotations.TemplateComponent; +import com.condation.cms.api.model.Parameter; +import java.util.Map; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +/** + * @author t.marx + */ +class TemplateComponentsRegistrationTest { + + private TemplateComponents components; + + @BeforeEach + void setup() { + components = new TemplateComponents(); + } + + // --- context style (Parameter) --- + + @Test + void context_style_default_namespace_is_registered() { + components.register(new ContextStyleHandler()); + + Assertions.assertThat(components.getComponentNames()).contains("ext:badge"); + } + + @Test + void context_style_returns_correct_value() { + components.register(new ContextStyleHandler()); + + String result = components.execute("ext:badge", Map.of("label", "new"), null); + Assertions.assertThat(result).isEqualTo("new"); + } + + @Test + void context_style_explicit_namespace_is_registered() { + components.register(new ContextStyleHandler()); + + Assertions.assertThat(components.getComponentNames()).contains("theme:card"); + } + + @Test + void context_style_explicit_namespace_returns_correct_value() { + components.register(new ContextStyleHandler()); + + String result = components.execute("theme:card", Map.of("title", "My Title"), null); + Assertions.assertThat(result).isEqualTo("
My Title
"); + } + + // --- @Param named-params style --- + + @Test + void param_style_default_namespace_is_registered() { + components.register(new ParamStyleHandler()); + + Assertions.assertThat(components.getComponentNames()).contains("ext:alert"); + } + + @Test + void param_style_returns_correct_value() { + components.register(new ParamStyleHandler()); + + String result = components.execute("ext:alert", Map.of("message", "Watch out!"), null); + Assertions.assertThat(result).isEqualTo("
Watch out!
"); + } + + @Test + void param_style_multiple_params_returns_correct_value() { + components.register(new ParamStyleHandler()); + + String result = components.execute("ext:full", Map.of("first", "Hello", "second", "World"), null); + Assertions.assertThat(result).isEqualTo("Hello World"); + } + + @Test + void param_style_explicit_namespace_is_registered() { + components.register(new ParamStyleHandler()); + + Assertions.assertThat(components.getComponentNames()).contains("ns1:item"); + } + + @Test + void param_style_explicit_namespace_returns_correct_value() { + components.register(new ParamStyleHandler()); + + String result = components.execute("ns1:item", Map.of("value", "42"), null); + Assertions.assertThat(result).isEqualTo("
  • 42
  • "); + } + + // --- null / empty --- + + @Test + void null_handler_is_ignored() { + Assertions.assertThatNoException().isThrownBy(() -> components.register((Object) null)); + Assertions.assertThat(components.getComponentNames()).isEmpty(); + } + + @Test + void handler_without_annotation_registers_nothing() { + components.register(new NoAnnotationHandler()); + + Assertions.assertThat(components.getComponentNames()).isEmpty(); + } + + // --- handler classes --- + + public static class ContextStyleHandler { + @TemplateComponent("badge") + public String badge(Parameter param) { + return "%s".formatted(param.getOrDefault("label", "")); + } + + @TemplateComponent(value = "card", namespace = "theme") + public String card(Parameter param) { + return "
    %s
    ".formatted(param.getOrDefault("title", "")); + } + } + + public static class ParamStyleHandler { + @TemplateComponent("alert") + public String alert(@Param("message") String message) { + return "
    %s
    ".formatted(message); + } + + @TemplateComponent("full") + public String full(@Param("first") String first, @Param("second") String second) { + return first + " " + second; + } + + @TemplateComponent(value = "item", namespace = "ns1") + public String item(@Param("value") String value) { + return "
  • %s
  • ".formatted(value); + } + } + + public static class NoAnnotationHandler { + public String notAComponent(Parameter param) { + return "ignored"; + } + } +} diff --git a/cms-templates/src/test/java/com/condation/cms/templates/components/TemplateFunctionsRegistrationTest.java b/cms-templates/src/test/java/com/condation/cms/templates/components/TemplateFunctionsRegistrationTest.java new file mode 100644 index 000000000..634c59bb1 --- /dev/null +++ b/cms-templates/src/test/java/com/condation/cms/templates/components/TemplateFunctionsRegistrationTest.java @@ -0,0 +1,231 @@ +package com.condation.cms.templates.components; + +/*- + * #%L + * CMS Templates + * %% + * 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.annotations.Param; +import com.condation.cms.api.annotations.TemplateFunction; +import com.condation.cms.api.model.Parameter; +import java.util.Map; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +/** + * @author t.marx + */ +class TemplateFunctionsRegistrationTest { + + private TemplateFunctions functions; + + @BeforeEach + void setup() { + functions = new TemplateFunctions(); + } + + // --- no-arg style --- + + @Test + void no_arg_style_default_namespace_is_registered() { + functions.register(new NoArgHandler()); + + Assertions.assertThat(functions.getFunctions()) + .anyMatch(f -> f.namespace().equals("ext") && f.name().equals("hello")); + } + + @Test + void no_arg_style_returns_correct_value() { + functions.register(new NoArgHandler()); + + var fn = findFunction("ext", "hello"); + Assertions.assertThat(fn.function().apply(new Parameter())).isEqualTo("Hello World"); + } + + @Test + void no_arg_style_explicit_namespace_is_registered() { + functions.register(new NoArgHandler()); + + Assertions.assertThat(functions.getFunctions()) + .anyMatch(f -> f.namespace().equals("theme") && f.name().equals("version")); + } + + @Test + void no_arg_style_explicit_namespace_returns_correct_value() { + functions.register(new NoArgHandler()); + + var fn = findFunction("theme", "version"); + Assertions.assertThat(fn.function().apply(new Parameter())).isEqualTo("1.0.0"); + } + + // --- context style (Parameter) --- + + @Test + void context_style_default_namespace_is_registered() { + functions.register(new ContextStyleHandler()); + + Assertions.assertThat(functions.getFunctions()) + .anyMatch(f -> f.namespace().equals("ext") && f.name().equals("greet")); + } + + @Test + void context_style_returns_correct_value() { + functions.register(new ContextStyleHandler()); + + var fn = findFunction("ext", "greet"); + Assertions.assertThat(fn.function().apply(new Parameter(Map.of("name", "CondationCMS")))) + .isEqualTo("Hello CondationCMS"); + } + + @Test + void context_style_explicit_namespace_is_registered() { + functions.register(new ContextStyleHandler()); + + Assertions.assertThat(functions.getFunctions()) + .anyMatch(f -> f.namespace().equals("ns1") && f.name().equals("upper")); + } + + @Test + void context_style_explicit_namespace_returns_correct_value() { + functions.register(new ContextStyleHandler()); + + var fn = findFunction("ns1", "upper"); + Assertions.assertThat(fn.function().apply(new Parameter(Map.of("text", "hello")))) + .isEqualTo("HELLO"); + } + + // --- @Param named-params style --- + + @Test + void param_style_default_namespace_is_registered() { + functions.register(new ParamStyleHandler()); + + Assertions.assertThat(functions.getFunctions()) + .anyMatch(f -> f.namespace().equals("ext") && f.name().equals("add")); + } + + @Test + void param_style_returns_correct_value() { + functions.register(new ParamStyleHandler()); + + var fn = findFunction("ext", "add"); + Assertions.assertThat(fn.function().apply(new Parameter(Map.of("a", 3, "b", 4)))) + .isEqualTo(7); + } + + @Test + void param_style_single_param_returns_correct_value() { + functions.register(new ParamStyleHandler()); + + var fn = findFunction("ext", "shout"); + Assertions.assertThat(fn.function().apply(new Parameter(Map.of("text", "hello")))) + .isEqualTo("HELLO!"); + } + + @Test + void param_style_explicit_namespace_is_registered() { + functions.register(new ParamStyleHandler()); + + Assertions.assertThat(functions.getFunctions()) + .anyMatch(f -> f.namespace().equals("ns1") && f.name().equals("repeat")); + } + + @Test + void param_style_explicit_namespace_returns_correct_value() { + functions.register(new ParamStyleHandler()); + + var fn = findFunction("ns1", "repeat"); + Assertions.assertThat(fn.function().apply(new Parameter(Map.of("text", "hi", "times", 3)))) + .isEqualTo("hihihi"); + } + + // --- null / empty --- + + @Test + void null_handler_is_ignored() { + Assertions.assertThatNoException().isThrownBy(() -> functions.register((Object) null)); + Assertions.assertThat(functions.getFunctions()).isEmpty(); + } + + @Test + void handler_without_annotation_registers_nothing() { + functions.register(new NoAnnotationHandler()); + + Assertions.assertThat(functions.getFunctions()).isEmpty(); + } + + // --- helper --- + + private FunctionMap.ExtFunction findFunction(String namespace, String name) { + return functions.getFunctions().stream() + .filter(f -> f.namespace().equals(namespace) && f.name().equals(name)) + .findFirst() + .orElseThrow(() -> new AssertionError("Function not found: " + namespace + ":" + name)); + } + + // --- handler classes --- + + public static class NoArgHandler { + @TemplateFunction("hello") + public String hello() { + return "Hello World"; + } + + @TemplateFunction(value = "version", namespace = "theme") + public String version() { + return "1.0.0"; + } + } + + public static class ContextStyleHandler { + @TemplateFunction("greet") + public String greet(Parameter param) { + return "Hello " + param.getOrDefault("name", ""); + } + + @TemplateFunction(value = "upper", namespace = "ns1") + public String upper(Parameter param) { + return ((String) param.getOrDefault("text", "")).toUpperCase(); + } + } + + public static class ParamStyleHandler { + @TemplateFunction("add") + public Integer add(@Param("a") Integer a, @Param("b") Integer b) { + return a + b; + } + + @TemplateFunction("shout") + public String shout(@Param("text") String text) { + return text.toUpperCase() + "!"; + } + + @TemplateFunction(value = "repeat", namespace = "ns1") + public String repeat(@Param("text") String text, @Param("times") Integer times) { + return text.repeat(times); + } + } + + public static class NoAnnotationHandler { + public String notAFunction(Parameter param) { + return "ignored"; + } + } +} diff --git a/integration-tests/src/test/java/com/condation/cms/SlotItemsTest.java b/integration-tests/src/test/java/com/condation/cms/SectionEntriesTest.java similarity index 80% rename from integration-tests/src/test/java/com/condation/cms/SlotItemsTest.java rename to integration-tests/src/test/java/com/condation/cms/SectionEntriesTest.java index 118034a0c..207ae8405 100644 --- a/integration-tests/src/test/java/com/condation/cms/SlotItemsTest.java +++ b/integration-tests/src/test/java/com/condation/cms/SectionEntriesTest.java @@ -33,7 +33,7 @@ import com.condation.cms.api.template.TemplateEngine; import com.condation.cms.content.DefaultContentParser; import com.condation.cms.content.DefaultContentRenderer; -import com.condation.cms.content.SlotItem; +import com.condation.cms.content.SectionEntry; import com.condation.cms.core.eventbus.DefaultEventBus; import com.condation.cms.filesystem.FileDB; import com.condation.cms.template.TemplateEngineTest; @@ -52,7 +52,7 @@ * * @author t.marx */ -public class SlotItemsTest extends TemplateEngineTest { +public class SectionEntriesTest extends TemplateEngineTest { static DefaultContentRenderer contentRenderer; static MarkdownRenderer markdownRenderer; @@ -93,20 +93,20 @@ public static void beforeClass() throws IOException { } @Test - public void test_slot_items() throws IOException { - List listSlotItems = db.getContent().listSlotItems(db.getReadOnlyFileSystem().contentBase().resolve("page.md")); - Assertions.assertThat(listSlotItems).hasSize(4); + public void test_section_entries() throws IOException { + List listSectionEntries = db.getContent().listSectionEntries(db.getReadOnlyFileSystem().contentBase().resolve("page.md")); + Assertions.assertThat(listSectionEntries).hasSize(4); - Map> renderedSlotItems = contentRenderer.renderSlotItems(listSlotItems, TestHelper.requestContext()); + Map> renderedSectionEntries = contentRenderer.renderSectionEntries(listSectionEntries, TestHelper.requestContext()); - Assertions.assertThat(renderedSlotItems) + Assertions.assertThat(renderedSectionEntries) .hasSize(1) .containsKey("left"); - Assertions.assertThat(renderedSlotItems.get("left")) + Assertions.assertThat(renderedSectionEntries.get("left")) .hasSize(4); - var slotItemIndexes = renderedSlotItems.get("left").stream().map(item -> item.index()).collect(Collectors.toList()); - Assertions.assertThat(slotItemIndexes).containsExactly(0, 1, 2, 10); + var sectionEntryIndexes = renderedSectionEntries.get("left").stream().map(item -> item.index()).collect(Collectors.toList()); + Assertions.assertThat(sectionEntryIndexes).containsExactly(0, 1, 2, 10); } } diff --git a/modules/example-module/src/main/java/com/condation/cms/modules/example/ExampleJettyHttpHandlerExtension.java b/modules/example-module/src/main/java/com/condation/cms/modules/example/ExampleJettyHttpHandlerExtension.java index 8d8b35d97..b755f8ec0 100644 --- a/modules/example-module/src/main/java/com/condation/cms/modules/example/ExampleJettyHttpHandlerExtension.java +++ b/modules/example-module/src/main/java/com/condation/cms/modules/example/ExampleJettyHttpHandlerExtension.java @@ -30,6 +30,7 @@ import com.condation.modules.api.annotation.Extension; import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; +import java.util.List; import lombok.RequiredArgsConstructor; import org.eclipse.jetty.http.pathmap.PathSpec; import org.eclipse.jetty.server.Request; @@ -50,9 +51,9 @@ public Mapping getMapping() { mapping.add(PathSpec.from("/people"), new ExampleHandler("Hello people!")); mapping.add(PathSpec.from("/hook"), (request, response, callback) -> { - ActionContext hookContext = getRequestContext().get(HookSystemFeature.class).hookSystem().doAction("example/test"); + List results = getRequestContext().get(HookSystemFeature.class).hookSystem().doAction("example/test"); - var content = (String)hookContext.results().get(0); + var content = results.get(0); response.write(true, ByteBuffer.wrap(content.getBytes(StandardCharsets.UTF_8)), callback); diff --git a/modules/system-modules/src/main/java/com/condation/cms/modules/system/templates/HooksTemplateFunctionExtensions.java b/modules/system-modules/src/main/java/com/condation/cms/modules/system/templates/HooksTemplateFunctionExtensions.java index 01cd88500..3c58c6465 100644 --- a/modules/system-modules/src/main/java/com/condation/cms/modules/system/templates/HooksTemplateFunctionExtensions.java +++ b/modules/system-modules/src/main/java/com/condation/cms/modules/system/templates/HooksTemplateFunctionExtensions.java @@ -49,7 +49,7 @@ public Object hook(Parameter params){ return ""; } - List results = getRequestContext().get(HookSystemFeature.class).hookSystem().doAction(hook).results().stream() + List results = getRequestContext().get(HookSystemFeature.class).hookSystem().doAction(hook).stream() .map(Object::toString) // oder o -> o.getName() .toList(); diff --git a/modules/ui-module/src/main/java/com/condation/cms/modules/ui/extensionpoints/MenuHookExtension.java b/modules/ui-module/src/main/java/com/condation/cms/modules/ui/extensionpoints/MenuHookExtension.java index 8876925c4..e701cc49c 100644 --- a/modules/ui-module/src/main/java/com/condation/cms/modules/ui/extensionpoints/MenuHookExtension.java +++ b/modules/ui-module/src/main/java/com/condation/cms/modules/ui/extensionpoints/MenuHookExtension.java @@ -10,20 +10,18 @@ * 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.hooks.HookSystem; import com.condation.cms.api.extensions.HookSystemRegisterExtensionPoint; -import com.condation.cms.api.hooks.ActionContext; -import com.condation.cms.api.hooks.FilterContext; import com.condation.cms.api.ui.action.UIHookAction; import com.condation.cms.api.ui.action.UIScriptAction; import com.condation.cms.api.ui.elements.Menu; @@ -41,8 +39,7 @@ public class MenuHookExtension extends HookSystemRegisterExtensionPoint { @Override public void register(HookSystem hookSystem) { - hookSystem.registerFilter("module/ui/menu", (FilterContext context) - -> { + hookSystem.registerFilter("module/ui/menu", (context) -> { var menu = context.value(); menu.addMenuEntry(MenuEntry.builder() .children(new ArrayList<>( @@ -56,20 +53,16 @@ public void register(HookSystem hookSystem) { .action(new UIHookAction("module/ui/demo/menu/action", Map.of("name", "CondationCMS"))) .build() ))) - .name("ExampleMenu") - .id("example-menu") - .build()); - + .name("ExampleMenu") + .id("example-menu") + .build()); return menu; - } - ); + }); - hookSystem.registerAction("module/ui/demo/menu/action", (ActionContext context) -> { + hookSystem.registerAction("module/ui/demo/menu/action", context -> { System.out.println("hook action executed"); System.out.println("hello " + context.arguments().get("name")); return ""; }); - } - } diff --git a/modules/ui-module/src/main/java/com/condation/cms/modules/ui/extensionpoints/SiteHookExtension.java b/modules/ui-module/src/main/java/com/condation/cms/modules/ui/extensionpoints/SiteHookExtension.java index 7310dff31..7297289a7 100644 --- a/modules/ui-module/src/main/java/com/condation/cms/modules/ui/extensionpoints/SiteHookExtension.java +++ b/modules/ui-module/src/main/java/com/condation/cms/modules/ui/extensionpoints/SiteHookExtension.java @@ -31,6 +31,7 @@ import com.condation.cms.api.hooks.ActionContext; import com.condation.modules.api.annotation.Extension; + /** * * @author t.marx diff --git a/modules/ui-module/src/main/java/com/condation/cms/modules/ui/extensionpoints/remotemethods/RemoteContentEndpointsExtension.java b/modules/ui-module/src/main/java/com/condation/cms/modules/ui/extensionpoints/remotemethods/RemoteContentEndpointsExtension.java index 59d50bb86..2074105be 100644 --- a/modules/ui-module/src/main/java/com/condation/cms/modules/ui/extensionpoints/remotemethods/RemoteContentEndpointsExtension.java +++ b/modules/ui-module/src/main/java/com/condation/cms/modules/ui/extensionpoints/remotemethods/RemoteContentEndpointsExtension.java @@ -44,8 +44,8 @@ import java.util.Map; import lombok.extern.slf4j.Slf4j; import com.condation.cms.api.ui.annotations.RemoteMethod; -import com.condation.cms.api.utils.SlotUtil; -import com.condation.cms.content.SlotItem; +import com.condation.cms.api.utils.SectionUtil; +import com.condation.cms.content.SectionEntry; import com.condation.cms.modules.ui.utils.FormHelper; import com.condation.cms.modules.ui.utils.MetaConverter; import com.condation.cms.modules.ui.utils.UIFileNameUtil; @@ -194,8 +194,8 @@ public Object setMetaBatch(Map parameters) { return result; } - @RemoteMethod(name = "content.slotItem.delete", permissions = {Permissions.CONTENT_EDIT}) - public Object deleteSlotItem(Map parameters) { + @RemoteMethod(name = "content.sectionEntry.delete", permissions = {Permissions.CONTENT_EDIT}) + public Object deleteSectionEntry(Map parameters) { final DB db = getContext().get(DBFeature.class).db(); var uri = (String) parameters.get("uri"); final Path contentBase = db.getFileSystem().resolve(Constants.Folders.CONTENT); @@ -221,21 +221,21 @@ public Object deleteSlotItem(Map parameters) { return result; } - @RemoteMethod(name = "content.slotItem.add", permissions = {Permissions.CONTENT_EDIT}) - public Object addSlotItem(Map parameters) { + @RemoteMethod(name = "content.sectionEntry.add", permissions = {Permissions.CONTENT_EDIT}) + public Object addSectionEntry(Map parameters) { final DB db = getContext().get(DBFeature.class).db(); var contentBase = db.getReadOnlyFileSystem().resolve(Constants.Folders.CONTENT); var content = (String) parameters.getOrDefault("content", ""); var parentUri = (String) parameters.get("parentUri"); - var slot = (String) parameters.get("slot"); - var sectionItemName = (String) parameters.get("slotItemName"); + var section = (String) parameters.get("section"); + var sectionEntryName = (String) parameters.get("sectionEntryName"); var template = (String) parameters.get("template"); - var title = sectionItemName; - sectionItemName = UIPathUtil.slugify(sectionItemName); + var title = sectionEntryName; + sectionEntryName = UIPathUtil.slugify(sectionEntryName); - var uri = UIFileNameUtil.createSlotItemFileName(parentUri, slot, sectionItemName); + var uri = UIFileNameUtil.createSectionEntryFileName(parentUri, section, sectionEntryName); var contentFile = contentBase.resolve(uri); @@ -305,17 +305,17 @@ public Object getContentNode (Map parameters) { if (contentFile != null) { result.put("uri", PathUtil.toRelativeFile(contentFile, contentBase)); - var slotItems = db.getContent().listSlotItems(contentFile); - Map> sectionMap = new HashMap<>(); - slotItems.forEach(slotItem -> { - String uri = slotItem.uri(); - String name = SlotUtil.getSlotItemName(slotItem.name()); - var index = slotItem.getMetaValue(Constants.MetaFields.LAYOUT_ORDER, Constants.DEFAULT_SLOT_ITEM_LAYOUT_ORDER); + var sectionEntries = db.getContent().listSectionEntries(contentFile); + Map> sectionMap = new HashMap<>(); + sectionEntries.forEach(sectionEntry -> { + String uri = sectionEntry.uri(); + String name = SectionUtil.getSectionName(sectionEntry.name()); + var index = sectionEntry.getMetaValue(Constants.MetaFields.LAYOUT_ORDER, Constants.DEFAULT_SECTION_ENTRY_LAYOUT_ORDER); sectionMap.computeIfAbsent(name, k -> new ArrayList<>()) - .add(new SlotItem(slotItem.name(), index, "", slotItem.data(), uri)); + .add(new SectionEntry(sectionEntry.name(), index, "", sectionEntry.data(), uri)); }); - result.put("slots", sectionMap); + result.put("sections", sectionMap); } return result; diff --git a/modules/ui-module/src/main/java/com/condation/cms/modules/ui/extensionpoints/remotemethods/RemoteFileEnpoints.java b/modules/ui-module/src/main/java/com/condation/cms/modules/ui/extensionpoints/remotemethods/RemoteFileEnpoints.java index 6d30f042b..dd34e1e7d 100644 --- a/modules/ui-module/src/main/java/com/condation/cms/modules/ui/extensionpoints/remotemethods/RemoteFileEnpoints.java +++ b/modules/ui-module/src/main/java/com/condation/cms/modules/ui/extensionpoints/remotemethods/RemoteFileEnpoints.java @@ -27,7 +27,7 @@ import com.condation.cms.api.db.cms.ReadOnlyFile; import com.condation.cms.api.ui.extensions.UIRemoteMethodExtensionPoint; import com.condation.cms.api.utils.FileUtils; -import com.condation.cms.api.utils.SlotUtil; +import com.condation.cms.api.utils.SectionUtil; import com.condation.modules.api.annotation.Extension; import java.io.IOException; import java.nio.file.Files; @@ -100,7 +100,7 @@ public Object list(Map parameters) { files.add(new Directory("..", parent.uri())); } contentFile.children().stream() - .filter(child -> !SlotUtil.isSlotItem(child.getFileName())) + .filter(child -> !SectionUtil.isSectionEntry(child.getFileName())) .map(this::map) .forEach(files::add); } catch (IOException ex) { @@ -144,7 +144,7 @@ public Object delete(Map parameters) throws RPCException { } else if ("assets".equals(type)) { Files.deleteIfExists(writableBase.resolve(uri).resolve(name)); } else { - var sections = db.getContent().listSlotItems(contentFile); + var sections = db.getContent().listSectionEntries(contentFile); Files.deleteIfExists(writableBase.resolve(uri).resolve(name)); sections.forEach(node -> { try { @@ -202,7 +202,7 @@ public Object renameFile(Map parameters) throws RPCException { if (!"assets".equals(type) && !Files.isDirectory(targetPath)) { var contentFile = contentBase.resolve(uri).resolve(name); - var sections = db.getContent().listSlotItems(contentFile); + var sections = db.getContent().listSectionEntries(contentFile); for (var node : sections) { var sourceSectionPath = writableBase.resolve(node.uri()); diff --git a/modules/ui-module/src/main/java/com/condation/cms/modules/ui/extensionpoints/remotemethods/RemoteManagerEnpoints.java b/modules/ui-module/src/main/java/com/condation/cms/modules/ui/extensionpoints/remotemethods/RemoteManagerEnpoints.java index 342dcb47c..337e6ac02 100644 --- a/modules/ui-module/src/main/java/com/condation/cms/modules/ui/extensionpoints/remotemethods/RemoteManagerEnpoints.java +++ b/modules/ui-module/src/main/java/com/condation/cms/modules/ui/extensionpoints/remotemethods/RemoteManagerEnpoints.java @@ -65,14 +65,14 @@ public Object getMediaFormats (Map parameters) throws RPCExcepti return configuration.get(MediaConfiguration.class).getFormats(); } - @RemoteMethod(name = "manager.contentTypes.slotItems", permissions = {Permissions.CONTENT_EDIT}) + @RemoteMethod(name = "manager.contentTypes.sectionEntries", permissions = {Permissions.CONTENT_EDIT}) public Object getSectionTemplates(Map parameters) throws RPCException { try { - var slot = (String) parameters.getOrDefault("slot", ""); - if (!Strings.isNullOrEmpty(slot)) { - return uiHooks().contentTypes().getSlotItemTemplates(slot); + var section = (String) parameters.getOrDefault("section", ""); + if (!Strings.isNullOrEmpty(section)) { + return uiHooks().contentTypes().getSectionEntryTemplates(section); } else { - return uiHooks().contentTypes().getSlotItemTemplates(); + return uiHooks().contentTypes().getSectionEntryTemplates(); } } catch (Exception e) { log.error("", e); diff --git a/modules/ui-module/src/main/java/com/condation/cms/modules/ui/extensionpoints/remotemethods/RemotePageEnpoints.java b/modules/ui-module/src/main/java/com/condation/cms/modules/ui/extensionpoints/remotemethods/RemotePageEnpoints.java index e35fea5b9..ac5307952 100644 --- a/modules/ui-module/src/main/java/com/condation/cms/modules/ui/extensionpoints/remotemethods/RemotePageEnpoints.java +++ b/modules/ui-module/src/main/java/com/condation/cms/modules/ui/extensionpoints/remotemethods/RemotePageEnpoints.java @@ -138,7 +138,7 @@ public Object deletePage(Map parameters) throws RPCException { var contentFile = contentBase.resolve(uri).resolve(name); log.debug("deleting file {}", contentFile.uri()); - var sections = db.getContent().listSlotItems(contentFile); + var sections = db.getContent().listSectionEntries(contentFile); Files.deleteIfExists(db.getFileSystem().resolve(Constants.Folders.CONTENT).resolve(uri).resolve(name)); sections.forEach(node -> { try { diff --git a/modules/ui-module/src/main/java/com/condation/cms/modules/ui/http/HookHandler.java b/modules/ui-module/src/main/java/com/condation/cms/modules/ui/http/HookHandler.java index 4e467cba1..a9fbb6285 100644 --- a/modules/ui-module/src/main/java/com/condation/cms/modules/ui/http/HookHandler.java +++ b/modules/ui-module/src/main/java/com/condation/cms/modules/ui/http/HookHandler.java @@ -20,8 +20,8 @@ * along with this program. If not, see . * #L% */ -import com.condation.cms.api.hooks.ActionContext; import com.condation.cms.api.hooks.HookSystem; +import java.util.List; import com.condation.cms.modules.ui.model.HookCall; import com.condation.cms.modules.ui.utils.json.UIGsonProvider; import java.util.HashMap; @@ -55,12 +55,12 @@ public boolean handle(Request request, Response response, Callback callback) thr String body = getBody(request); var command = UIGsonProvider.INSTANCE.fromJson(body, HookCall.class); - ActionContext actionContext = hookSystem.doAction(command.hook(), command.parameters()); - + List results = hookSystem.doAction(command.hook(), command.parameters()); + Map commandResponse = new HashMap<>(); commandResponse.put("hook", command.hook()); - if (!actionContext.results().isEmpty()) { - commandResponse.put("result", actionContext.results()); + if (!results.isEmpty()) { + commandResponse.put("result", results); } response.getHeaders().put(HttpHeader.CONTENT_TYPE, "application/json; charset=UTF-8"); diff --git a/modules/ui-module/src/main/java/com/condation/cms/modules/ui/utils/UIFileNameUtil.java b/modules/ui-module/src/main/java/com/condation/cms/modules/ui/utils/UIFileNameUtil.java index f58b0a351..6971ee137 100644 --- a/modules/ui-module/src/main/java/com/condation/cms/modules/ui/utils/UIFileNameUtil.java +++ b/modules/ui-module/src/main/java/com/condation/cms/modules/ui/utils/UIFileNameUtil.java @@ -29,7 +29,7 @@ */ public class UIFileNameUtil { - public static String createSlotItemFileName(String parentUri, String slot, String slotItem) { + public static String createSectionEntryFileName(String parentUri, String section, String sectionEntry) { // Pfadteile per "/" splitten String[] parts = parentUri.split("/"); @@ -49,7 +49,7 @@ public static String createSlotItemFileName(String parentUri, String slot, Strin } // Neuen Dateinamen erstellen - String newFileName = baseName + "." + slot + "." + slotItem + extension; + String newFileName = baseName + "." + section + "." + sectionEntry + extension; // Pfad wieder zusammenbauen if (parts.length > 1) { diff --git a/modules/ui-module/src/main/java/com/condation/cms/modules/ui/utils/UIHooks.java b/modules/ui-module/src/main/java/com/condation/cms/modules/ui/utils/UIHooks.java index 3f8732439..7a75d370a 100644 --- a/modules/ui-module/src/main/java/com/condation/cms/modules/ui/utils/UIHooks.java +++ b/modules/ui-module/src/main/java/com/condation/cms/modules/ui/utils/UIHooks.java @@ -47,19 +47,19 @@ public UIHooks (final HookSystem hookSystem) { public ContentTypes contentTypes () { var contentTypes = new ContentTypes(); - return hookSystem.doFilter(HOOK_REGISTER_CONTENT_TYPES, contentTypes).value(); + return hookSystem.doFilter(HOOK_REGISTER_CONTENT_TYPES, contentTypes); } public MediaForms mediaForms () { var mediaForms = new MediaForms(); - return hookSystem.doFilter(HOOK_REGISTER_MEDIA_FORMS, mediaForms).value(); + return hookSystem.doFilter(HOOK_REGISTER_MEDIA_FORMS, mediaForms); } public Menu menu() { var menu = new Menu(); - menu = hookSystem.doFilter(HOOK_MENU, menu).value(); + menu = hookSystem.doFilter(HOOK_MENU, menu); return menu; } @@ -69,6 +69,6 @@ public Map> translations () { "de", new HashMap<>(), "en", new HashMap<>() )); - return hookSystem.doFilter(HOOK_TRANSLATIONS, translations).value(); + return hookSystem.doFilter(HOOK_TRANSLATIONS, translations); } } diff --git a/modules/ui-module/src/main/resources/manager/actions/page/add-section.js b/modules/ui-module/src/main/resources/manager/actions/page/add-section.js index 925958828..4eaed5385 100644 --- a/modules/ui-module/src/main/resources/manager/actions/page/add-section.js +++ b/modules/ui-module/src/main/resources/manager/actions/page/add-section.js @@ -24,7 +24,7 @@ import { addSection, getContentNode } from '@cms/modules/rpc/rpc-content.js'; import { getPreviewUrl, reloadPreview } from '@cms/modules/preview.utils.js'; import Handlebars from 'https://cdn.jsdelivr.net/npm/handlebars@4.7.8/+esm'; import { i18n } from '@cms/modules/localization.js'; -import { getSlotItemTemplates } from '@cms/modules/rpc/rpc-manager.js'; +import { getSectionEntryTemplates } from '@cms/modules/rpc/rpc-manager.js'; export async function runAction(params) { const contentNode = await getContentNode({ url: getPreviewUrl() @@ -41,23 +41,23 @@ export async function runAction(params) { {{/each}} `); - var sectionsResponse = await getSlotItemTemplates({ - slot: params.slot + var sectionsResponse = await getSectionEntryTemplates({ + section: params.section }); openModal({ - title: i18n.t("addsection.titles.modal", 'Add item'), + title: i18n.t("addsection.titles.modal", 'Add entry'), body: template({ templates: sectionsResponse.result, }), fullscreen: false, onCancel: (event) => { }, - validate: () => validate(contentNode, params.slot), + validate: () => validate(contentNode, params.section), onOk: async (event) => { - var result = await createSection(contentNode.result.uri, params.slot); + var result = await createSection(contentNode.result.uri, params.section); if (result) { showToast({ - title: i18n.t("manager.actions.addsection.titles.alert", "Create Item"), - message: i18n.t("manager.actions.addsection.alerts.success.message", "Item successfuly created."), + title: i18n.t("manager.actions.addsection.titles.alert", "Create Entry"), + message: i18n.t("manager.actions.addsection.alerts.success.message", "Entry successfuly created."), type: 'success', // optional: info | success | warning | error timeout: 3000 }); @@ -66,8 +66,8 @@ export async function runAction(params) { } else { showToast({ - title: i18n.t("manager.actions.addsection.titles.alert", 'Create Item'), - message: i18n.t("manager.actions.addsection.alerts.error.message", "Item not created."), + title: i18n.t("manager.actions.addsection.titles.alert", 'Create Entry'), + message: i18n.t("manager.actions.addsection.alerts.error.message", "Entry not created."), type: 'warning', // optional: info | success | warning | error timeout: 3000 }); @@ -75,25 +75,25 @@ export async function runAction(params) { } }); } -const getSlotItemName = () => { +const getSectionEntryName = () => { return document.getElementById("cms-section-name").value; }; -const validate = (contentNode, targetSlot) => { +const validate = (contentNode, targetSection) => { const template = document.getElementById("cms-section-template-selection").value; if (template === "000") { showToast({ - title: i18n.t("manager.actions.addsection.titles.alert", 'Create Item'), + title: i18n.t("manager.actions.addsection.titles.alert", 'Create Entry'), message: i18n.t("manager.actions.addsection.alerts.notemplate.message", "No template selected."), type: 'error', // optional: info | success | warning | error timeout: 3000 }); return false; } - const slotItemName = getSlotItemName(); - if (slotItemName === "" || slotItemName === null) { + const sectionEntryName = getSectionEntryName(); + if (sectionEntryName === "" || sectionEntryName === null) { showToast({ - title: i18n.t("manager.actions.addsection.titles.alert", 'Create Item'), - message: i18n.t("manager.actions.addsection.alerts.noname.message", "No Item name provided."), + title: i18n.t("manager.actions.addsection.titles.alert", 'Create Entry'), + message: i18n.t("manager.actions.addsection.alerts.noname.message", "No Entry name provided."), type: 'error', timeout: 3000 }); @@ -101,14 +101,14 @@ const validate = (contentNode, targetSlot) => { } return true; }; -function isUriInSection(data, slotItemKey, targetUri) { +function isUriInSection(data, sectionEntryKey, targetUri) { if (!data || !data.result || - !data.result.slots || - typeof data.result.slots !== 'object') { + !data.result.sections || + typeof data.result.sections !== 'object') { return false; } - const sectionArray = data.result.slots[slotItemKey]; + const sectionArray = data.result.sections[sectionEntryKey]; if (!Array.isArray(sectionArray)) { return false; } @@ -121,8 +121,8 @@ const createSection = async (parentUri, parentSectionName) => { } await addSection({ parentUri: parentUri, - slotItemName: getSlotItemName(), - slot: parentSectionName, + sectionEntryName: getSectionEntryName(), + section: parentSectionName, template: template }); return true; diff --git a/modules/ui-module/src/main/resources/manager/actions/page/edit-metaattribute-form.js b/modules/ui-module/src/main/resources/manager/actions/page/edit-metaattribute-form.js index c5fce0f9e..04c8704e8 100644 --- a/modules/ui-module/src/main/resources/manager/actions/page/edit-metaattribute-form.js +++ b/modules/ui-module/src/main/resources/manager/actions/page/edit-metaattribute-form.js @@ -25,7 +25,7 @@ import { buildValuesFromFields, getValueByPath } from '@cms/modules/node.js'; import { getContentNode, getContent, setMeta } from '@cms/modules/rpc/rpc-content.js'; import { i18n } from '@cms/modules/localization.js'; import { openSidebar } from '@cms/modules/sidebar.js'; -import { getPageTemplates, getSlotItemTemplates } from '@cms/modules/rpc/rpc-manager'; +import { getPageTemplates, getSectionEntryTemplates } from '@cms/modules/rpc/rpc-manager'; // hook.js export async function runAction(params) { var uri = null; @@ -42,8 +42,8 @@ export async function runAction(params) { uri: uri }); var templates = null; - if (params.type === "slotItem") { - templates = (await getSlotItemTemplates()).result; + if (params.type === "sectionEntry") { + templates = (await getSectionEntryTemplates()).result; } else { templates = (await getPageTemplates()).result; diff --git a/modules/ui-module/src/main/resources/manager/actions/page/edit-sections.js b/modules/ui-module/src/main/resources/manager/actions/page/edit-sections.js index f2eec5573..77791cd14 100644 --- a/modules/ui-module/src/main/resources/manager/actions/page/edit-sections.js +++ b/modules/ui-module/src/main/resources/manager/actions/page/edit-sections.js @@ -31,7 +31,7 @@ export async function runAction(params) { }); var template = Handlebars.compile(`
      - {{#each slotItems}} + {{#each sectionEntries}}
    • {{#if data.title}} {{data.title}} @@ -42,21 +42,21 @@ export async function runAction(params) { {{/each}}
    `); - var slotItems = []; - if (contentNode.result.slots[params.slot]) { - var slotItems = contentNode.result.slots[params.slot]; + var sectionEntries = []; + if (contentNode.result.sections[params.section]) { + var sectionEntries = contentNode.result.sections[params.section]; } - slotItems = slotItems.sort((a, b) => a.index - b.index); + sectionEntries = sectionEntries.sort((a, b) => a.index - b.index); openModal({ title: 'Edit items', - body: template({ slotItems: slotItems }), + body: template({ sectionEntries: sectionEntries }), fullscreen: false, onCancel: (event) => { }, onOk: async (event) => { await saveSections(); showToast({ - title: 'Items saved', - message: 'Items successfuly saved.', + title: 'Entry saved', + message: 'Entry successfuly saved.', type: 'success', // optional: info | success | warning | error timeout: 3000 }); diff --git a/modules/ui-module/src/main/resources/manager/js/manager-inject-init.js b/modules/ui-module/src/main/resources/manager/js/manager-inject-init.js index e0bd1b36e..39935f0d8 100644 --- a/modules/ui-module/src/main/resources/manager/js/manager-inject-init.js +++ b/modules/ui-module/src/main/resources/manager/js/manager-inject-init.js @@ -51,7 +51,7 @@ export function initIframe() { } }); frameMessenger.on('getContentNodeResponse', async (payload) => { - for (const [slotName, items] of Object.entries(payload.contentNode.slots)) { + for (const [sectionName, items] of Object.entries(payload.contentNode.sections)) { for (const item of items) { const sectionContainer = document.querySelector(`[data-cms-section-uri="${item.uri}"]`); if (!sectionContainer) { diff --git a/modules/ui-module/src/main/resources/manager/js/manager-inject.js b/modules/ui-module/src/main/resources/manager/js/manager-inject.js index 2b11d2b1d..75083adbc 100644 --- a/modules/ui-module/src/main/resources/manager/js/manager-inject.js +++ b/modules/ui-module/src/main/resources/manager/js/manager-inject.js @@ -1,4 +1,3 @@ -"use strict"; /*- * #%L * UI Module diff --git a/modules/ui-module/src/main/resources/manager/js/modules/filebrowser.d.ts b/modules/ui-module/src/main/resources/manager/js/modules/filebrowser.d.ts index 0869c773e..0c529bf7f 100644 --- a/modules/ui-module/src/main/resources/manager/js/modules/filebrowser.d.ts +++ b/modules/ui-module/src/main/resources/manager/js/modules/filebrowser.d.ts @@ -20,6 +20,6 @@ */ export function openFileBrowser(optionsParam: any): Promise; export namespace state { - let options: null; + let options: any; let currentFolder: string; } diff --git a/modules/ui-module/src/main/resources/manager/js/modules/form/utils.d.ts b/modules/ui-module/src/main/resources/manager/js/modules/form/utils.d.ts index 23eeb2258..7d3d89dcb 100644 --- a/modules/ui-module/src/main/resources/manager/js/modules/form/utils.d.ts +++ b/modules/ui-module/src/main/resources/manager/js/modules/form/utils.d.ts @@ -20,7 +20,7 @@ */ declare const createID: () => string; declare const utcToLocalDateTimeInputValue: (utcString: string) => string; -declare function getUTCDateTimeFromInput(inputElement: HTMLInputElement): string | null; +declare function getUTCDateTimeFromInput(inputElement: HTMLInputElement): string; declare function utcToLocalDateInputValue(utcString: string): string; -declare function getUTCDateFromInput(inputElement: HTMLInputElement): string | null; +declare function getUTCDateFromInput(inputElement: HTMLInputElement): string; export { createID, utcToLocalDateTimeInputValue, getUTCDateTimeFromInput, utcToLocalDateInputValue, getUTCDateFromInput }; diff --git a/modules/ui-module/src/main/resources/manager/js/modules/localization.d.ts b/modules/ui-module/src/main/resources/manager/js/modules/localization.d.ts index e91051759..b1e393873 100644 --- a/modules/ui-module/src/main/resources/manager/js/modules/localization.d.ts +++ b/modules/ui-module/src/main/resources/manager/js/modules/localization.d.ts @@ -21,7 +21,7 @@ export function localizeUi(): Promise; export namespace i18n { let _locale: any; - let _cache: null; + let _cache: any; /** * Loads and merges remote localizations with defaults. */ diff --git a/modules/ui-module/src/main/resources/manager/js/modules/manager/manager.message.handlers.js b/modules/ui-module/src/main/resources/manager/js/modules/manager/manager.message.handlers.js index e87e00768..083ac47a2 100644 --- a/modules/ui-module/src/main/resources/manager/js/modules/manager/manager.message.handlers.js +++ b/modules/ui-module/src/main/resources/manager/js/modules/manager/manager.message.handlers.js @@ -122,7 +122,7 @@ const initMessageHandlers = () => { "module": window.manager.baseUrl + "/actions/page/edit-sections", "function": "runAction", "parameters": { - "slot": payload.slot + "section": payload.section } }; if (payload.uri) { @@ -130,17 +130,17 @@ const initMessageHandlers = () => { } executeScriptAction(cmd); }); - frameMessenger.on('add-slotItem', (payload) => { + frameMessenger.on('add-sectionEntry', (payload) => { var cmd = { "module": window.manager.baseUrl + "/actions/page/add-section", "function": "runAction", "parameters": { - "slot": payload.slot + "section": payload.section } }; executeScriptAction(cmd); }); - frameMessenger.on('delete-slotItem', (payload) => { + frameMessenger.on('delete-sectionEntry', (payload) => { var cmd = { "module": window.manager.baseUrl + "/actions/page/delete-section", "function": "runAction", diff --git a/modules/ui-module/src/main/resources/manager/js/modules/manager/toolbar.inject.js b/modules/ui-module/src/main/resources/manager/js/modules/manager/toolbar.inject.js index 0f452cffa..580cdf532 100644 --- a/modules/ui-module/src/main/resources/manager/js/modules/manager/toolbar.inject.js +++ b/modules/ui-module/src/main/resources/manager/js/modules/manager/toolbar.inject.js @@ -24,9 +24,9 @@ const addSection = (event) => { var toolbar = event.target.closest('[data-cms-toolbar]'); var toolbarDefinition = JSON.parse(toolbar.dataset.cmsToolbar); var command = { - type: 'add-slotItem', + type: 'add-sectionEntry', payload: { - slot: toolbarDefinition.slot, + section: toolbarDefinition.section, } }; frameMessenger.send(window.parent, command); @@ -35,7 +35,7 @@ const deleteSection = (event) => { var toolbar = event.target.closest('[data-cms-toolbar]'); var toolbarDefinition = JSON.parse(toolbar.dataset.cmsToolbar); var command = { - type: 'delete-slotItem', + type: 'delete-sectionEntry', payload: { sectionUri: toolbarDefinition.uri } @@ -61,7 +61,7 @@ const orderSections = (event) => { var command = { type: 'edit-sections', payload: { - slot: toolbarDefinition.slot + section: toolbarDefinition.section } }; frameMessenger.send(window.parent, command); @@ -120,7 +120,7 @@ export const initToolbar = (container) => { if (!toolbarDefinition.actions) { return; } - if (toolbarDefinition.type === "slot") { + if (toolbarDefinition.type === "section") { container.classList.add("cms-ui-editable-sections"); } else { @@ -128,7 +128,7 @@ export const initToolbar = (container) => { } const toolbar = document.createElement('div'); toolbar.className = 'cms-ui-toolbar'; - if (toolbarDefinition.type === "slot") { + if (toolbarDefinition.type === "section") { toolbar.classList.add("cms-ui-toolbar-tl"); } else { @@ -160,7 +160,7 @@ export const initToolbar = (container) => { button.addEventListener('click', editAttributes); toolbar.appendChild(button); } - else if (action === "orderSlotItems") { + else if (action === "orderSectionEntries") { const button = document.createElement('button'); button.setAttribute('data-cms-action', 'editSections'); button.innerHTML = SECTION_SORT_ICON; @@ -168,7 +168,7 @@ export const initToolbar = (container) => { button.addEventListener('click', orderSections); toolbar.appendChild(button); } - else if (action === "addSlotItem") { + else if (action === "addSectionEntry") { const button = document.createElement('button'); button.setAttribute('data-cms-action', 'addSection'); button.innerHTML = SECTION_ADD_ICON; @@ -176,7 +176,7 @@ export const initToolbar = (container) => { button.addEventListener('click', addSection); toolbar.appendChild(button); } - else if (action === "deleteSlotItem") { + else if (action === "deleteSectionEntry") { const button = document.createElement('button'); button.setAttribute('data-cms-action', 'deleteSection'); button.innerHTML = SECTION_DELETE_ICON; @@ -185,7 +185,7 @@ export const initToolbar = (container) => { toolbar.appendChild(button); } }); - if (toolbarDefinition.type === "slotItem") { + if (toolbarDefinition.type === "sectionEntry") { const button = document.createElement('button'); button.setAttribute('data-cms-action', 'publish'); button.setAttribute('data-cms-section-uri', toolbarDefinition.uri); diff --git a/modules/ui-module/src/main/resources/manager/js/modules/preview.history.d.ts b/modules/ui-module/src/main/resources/manager/js/modules/preview.history.d.ts index 6cfeb16ad..520ce2957 100644 --- a/modules/ui-module/src/main/resources/manager/js/modules/preview.history.d.ts +++ b/modules/ui-module/src/main/resources/manager/js/modules/preview.history.d.ts @@ -22,6 +22,6 @@ export namespace PreviewHistory { export { init }; export { navigatePreview }; } -declare function init(defaultUrl?: null): void; +declare function init(defaultUrl?: any): void; declare function navigatePreview(url: any, usePush?: boolean): void; export {}; diff --git a/modules/ui-module/src/main/resources/manager/js/modules/preview.utils.d.ts b/modules/ui-module/src/main/resources/manager/js/modules/preview.utils.d.ts index 20c850ee6..e7c2a28ba 100644 --- a/modules/ui-module/src/main/resources/manager/js/modules/preview.utils.d.ts +++ b/modules/ui-module/src/main/resources/manager/js/modules/preview.utils.d.ts @@ -23,4 +23,4 @@ export function deActivatePreviewOverlay(): void; export function getPreviewUrl(): any; export function reloadPreview(): void; export function loadPreview(url: any): void; -export function getPreviewFrame(): HTMLElement | null; +export function getPreviewFrame(): HTMLElement; diff --git a/modules/ui-module/src/main/resources/manager/js/modules/rpc/rpc-content.js b/modules/ui-module/src/main/resources/manager/js/modules/rpc/rpc-content.js index bd018c133..285c9e397 100644 --- a/modules/ui-module/src/main/resources/manager/js/modules/rpc/rpc-content.js +++ b/modules/ui-module/src/main/resources/manager/js/modules/rpc/rpc-content.js @@ -56,14 +56,14 @@ const setMetaBatch = async (options) => { }; const addSection = async (options) => { var data = { - method: "content.slotItem.add", + method: "content.sectionEntry.add", parameters: options }; return await executeRemoteCall(data); }; const deleteSection = async (options) => { var data = { - method: "content.slotItem.delete", + method: "content.sectionEntry.delete", parameters: options }; return await executeRemoteCall(data); diff --git a/modules/ui-module/src/main/resources/manager/js/modules/rpc/rpc-manager.d.ts b/modules/ui-module/src/main/resources/manager/js/modules/rpc/rpc-manager.d.ts index ff1cbbc30..e137cdba7 100644 --- a/modules/ui-module/src/main/resources/manager/js/modules/rpc/rpc-manager.d.ts +++ b/modules/ui-module/src/main/resources/manager/js/modules/rpc/rpc-manager.d.ts @@ -18,7 +18,7 @@ * along with this program. If not, see . * #L% */ -declare const getSlotItemTemplates: (options: any) => Promise; +declare const getSectionEntryTemplates: (options: any) => Promise; declare const getPageTemplates: (options: any) => Promise; declare const getListItemTypes: (options: any) => Promise; declare const getMediaForm: (options: any) => Promise; @@ -41,4 +41,4 @@ export interface MediaFormatsResponse { } declare const getMediaFormats: (options: any) => Promise; declare const getTagNames: (options: any) => Promise; -export { getSlotItemTemplates, getPageTemplates, getMediaForm, getTagNames, getMediaFormats, getListItemTypes, createCSRFToken }; +export { getSectionEntryTemplates, getPageTemplates, getMediaForm, getTagNames, getMediaFormats, getListItemTypes, createCSRFToken }; diff --git a/modules/ui-module/src/main/resources/manager/js/modules/rpc/rpc-manager.js b/modules/ui-module/src/main/resources/manager/js/modules/rpc/rpc-manager.js index 5c95bafa3..cd96fc51a 100644 --- a/modules/ui-module/src/main/resources/manager/js/modules/rpc/rpc-manager.js +++ b/modules/ui-module/src/main/resources/manager/js/modules/rpc/rpc-manager.js @@ -19,9 +19,9 @@ * #L% */ import { executeRemoteCall } from '@cms/modules/rpc/rpc.js'; -const getSlotItemTemplates = async (options) => { +const getSectionEntryTemplates = async (options) => { var data = { - method: "manager.contentTypes.slotItems", + method: "manager.contentTypes.sectionEntries", parameters: options || {} }; return await executeRemoteCall(data); @@ -74,4 +74,4 @@ const getTagNames = async (options) => { }; return await executeRemoteCall(data); }; -export { getSlotItemTemplates, getPageTemplates, getMediaForm, getTagNames, getMediaFormats, getListItemTypes, createCSRFToken }; +export { getSectionEntryTemplates, getPageTemplates, getMediaForm, getTagNames, getMediaFormats, getListItemTypes, createCSRFToken }; diff --git a/modules/ui-module/src/main/resources/manager/js/modules/state.js b/modules/ui-module/src/main/resources/manager/js/modules/state.js index a0988236f..7274c0b66 100644 --- a/modules/ui-module/src/main/resources/manager/js/modules/state.js +++ b/modules/ui-module/src/main/resources/manager/js/modules/state.js @@ -1,4 +1,3 @@ -"use strict"; /*- * #%L * UI Module diff --git a/modules/ui-module/src/main/resources/manager/js/modules/ui-state.d.ts b/modules/ui-module/src/main/resources/manager/js/modules/ui-state.d.ts index 30e36cd4d..65018962d 100644 --- a/modules/ui-module/src/main/resources/manager/js/modules/ui-state.d.ts +++ b/modules/ui-module/src/main/resources/manager/js/modules/ui-state.d.ts @@ -20,11 +20,11 @@ */ export namespace UIStateManager { function setTabState(key: any, value: any): void; - function getTabState(key: any, defaultValue?: null): any; + function getTabState(key: any, defaultValue?: any): any; function setLocale(locale: any): void; function getLocale(): any; function removeTabState(key: any): void; function setAuthToken(token: any): void; - function getAuthToken(): string | null; + function getAuthToken(): string; function clearAuthToken(): void; } diff --git a/modules/ui-module/src/main/resources/manager/public/manager-login.js b/modules/ui-module/src/main/resources/manager/public/manager-login.js index 24aa077ae..6f21eee99 100644 --- a/modules/ui-module/src/main/resources/manager/public/manager-login.js +++ b/modules/ui-module/src/main/resources/manager/public/manager-login.js @@ -1,4 +1,3 @@ -"use strict"; /*- * #%L * UI Module diff --git a/modules/ui-module/src/main/ts/dist/actions/page/add-section.js b/modules/ui-module/src/main/ts/dist/actions/page/add-section.js index 925958828..4eaed5385 100644 --- a/modules/ui-module/src/main/ts/dist/actions/page/add-section.js +++ b/modules/ui-module/src/main/ts/dist/actions/page/add-section.js @@ -24,7 +24,7 @@ import { addSection, getContentNode } from '@cms/modules/rpc/rpc-content.js'; import { getPreviewUrl, reloadPreview } from '@cms/modules/preview.utils.js'; import Handlebars from 'https://cdn.jsdelivr.net/npm/handlebars@4.7.8/+esm'; import { i18n } from '@cms/modules/localization.js'; -import { getSlotItemTemplates } from '@cms/modules/rpc/rpc-manager.js'; +import { getSectionEntryTemplates } from '@cms/modules/rpc/rpc-manager.js'; export async function runAction(params) { const contentNode = await getContentNode({ url: getPreviewUrl() @@ -41,23 +41,23 @@ export async function runAction(params) { {{/each}} `); - var sectionsResponse = await getSlotItemTemplates({ - slot: params.slot + var sectionsResponse = await getSectionEntryTemplates({ + section: params.section }); openModal({ - title: i18n.t("addsection.titles.modal", 'Add item'), + title: i18n.t("addsection.titles.modal", 'Add entry'), body: template({ templates: sectionsResponse.result, }), fullscreen: false, onCancel: (event) => { }, - validate: () => validate(contentNode, params.slot), + validate: () => validate(contentNode, params.section), onOk: async (event) => { - var result = await createSection(contentNode.result.uri, params.slot); + var result = await createSection(contentNode.result.uri, params.section); if (result) { showToast({ - title: i18n.t("manager.actions.addsection.titles.alert", "Create Item"), - message: i18n.t("manager.actions.addsection.alerts.success.message", "Item successfuly created."), + title: i18n.t("manager.actions.addsection.titles.alert", "Create Entry"), + message: i18n.t("manager.actions.addsection.alerts.success.message", "Entry successfuly created."), type: 'success', // optional: info | success | warning | error timeout: 3000 }); @@ -66,8 +66,8 @@ export async function runAction(params) { } else { showToast({ - title: i18n.t("manager.actions.addsection.titles.alert", 'Create Item'), - message: i18n.t("manager.actions.addsection.alerts.error.message", "Item not created."), + title: i18n.t("manager.actions.addsection.titles.alert", 'Create Entry'), + message: i18n.t("manager.actions.addsection.alerts.error.message", "Entry not created."), type: 'warning', // optional: info | success | warning | error timeout: 3000 }); @@ -75,25 +75,25 @@ export async function runAction(params) { } }); } -const getSlotItemName = () => { +const getSectionEntryName = () => { return document.getElementById("cms-section-name").value; }; -const validate = (contentNode, targetSlot) => { +const validate = (contentNode, targetSection) => { const template = document.getElementById("cms-section-template-selection").value; if (template === "000") { showToast({ - title: i18n.t("manager.actions.addsection.titles.alert", 'Create Item'), + title: i18n.t("manager.actions.addsection.titles.alert", 'Create Entry'), message: i18n.t("manager.actions.addsection.alerts.notemplate.message", "No template selected."), type: 'error', // optional: info | success | warning | error timeout: 3000 }); return false; } - const slotItemName = getSlotItemName(); - if (slotItemName === "" || slotItemName === null) { + const sectionEntryName = getSectionEntryName(); + if (sectionEntryName === "" || sectionEntryName === null) { showToast({ - title: i18n.t("manager.actions.addsection.titles.alert", 'Create Item'), - message: i18n.t("manager.actions.addsection.alerts.noname.message", "No Item name provided."), + title: i18n.t("manager.actions.addsection.titles.alert", 'Create Entry'), + message: i18n.t("manager.actions.addsection.alerts.noname.message", "No Entry name provided."), type: 'error', timeout: 3000 }); @@ -101,14 +101,14 @@ const validate = (contentNode, targetSlot) => { } return true; }; -function isUriInSection(data, slotItemKey, targetUri) { +function isUriInSection(data, sectionEntryKey, targetUri) { if (!data || !data.result || - !data.result.slots || - typeof data.result.slots !== 'object') { + !data.result.sections || + typeof data.result.sections !== 'object') { return false; } - const sectionArray = data.result.slots[slotItemKey]; + const sectionArray = data.result.sections[sectionEntryKey]; if (!Array.isArray(sectionArray)) { return false; } @@ -121,8 +121,8 @@ const createSection = async (parentUri, parentSectionName) => { } await addSection({ parentUri: parentUri, - slotItemName: getSlotItemName(), - slot: parentSectionName, + sectionEntryName: getSectionEntryName(), + section: parentSectionName, template: template }); return true; diff --git a/modules/ui-module/src/main/ts/dist/actions/page/edit-metaattribute-form.js b/modules/ui-module/src/main/ts/dist/actions/page/edit-metaattribute-form.js index c5fce0f9e..04c8704e8 100644 --- a/modules/ui-module/src/main/ts/dist/actions/page/edit-metaattribute-form.js +++ b/modules/ui-module/src/main/ts/dist/actions/page/edit-metaattribute-form.js @@ -25,7 +25,7 @@ import { buildValuesFromFields, getValueByPath } from '@cms/modules/node.js'; import { getContentNode, getContent, setMeta } from '@cms/modules/rpc/rpc-content.js'; import { i18n } from '@cms/modules/localization.js'; import { openSidebar } from '@cms/modules/sidebar.js'; -import { getPageTemplates, getSlotItemTemplates } from '@cms/modules/rpc/rpc-manager'; +import { getPageTemplates, getSectionEntryTemplates } from '@cms/modules/rpc/rpc-manager'; // hook.js export async function runAction(params) { var uri = null; @@ -42,8 +42,8 @@ export async function runAction(params) { uri: uri }); var templates = null; - if (params.type === "slotItem") { - templates = (await getSlotItemTemplates()).result; + if (params.type === "sectionEntry") { + templates = (await getSectionEntryTemplates()).result; } else { templates = (await getPageTemplates()).result; diff --git a/modules/ui-module/src/main/ts/dist/actions/page/edit-sections.js b/modules/ui-module/src/main/ts/dist/actions/page/edit-sections.js index f2eec5573..77791cd14 100644 --- a/modules/ui-module/src/main/ts/dist/actions/page/edit-sections.js +++ b/modules/ui-module/src/main/ts/dist/actions/page/edit-sections.js @@ -31,7 +31,7 @@ export async function runAction(params) { }); var template = Handlebars.compile(`
      - {{#each slotItems}} + {{#each sectionEntries}}
    • {{#if data.title}} {{data.title}} @@ -42,21 +42,21 @@ export async function runAction(params) { {{/each}}
    `); - var slotItems = []; - if (contentNode.result.slots[params.slot]) { - var slotItems = contentNode.result.slots[params.slot]; + var sectionEntries = []; + if (contentNode.result.sections[params.section]) { + var sectionEntries = contentNode.result.sections[params.section]; } - slotItems = slotItems.sort((a, b) => a.index - b.index); + sectionEntries = sectionEntries.sort((a, b) => a.index - b.index); openModal({ title: 'Edit items', - body: template({ slotItems: slotItems }), + body: template({ sectionEntries: sectionEntries }), fullscreen: false, onCancel: (event) => { }, onOk: async (event) => { await saveSections(); showToast({ - title: 'Items saved', - message: 'Items successfuly saved.', + title: 'Entry saved', + message: 'Entry successfuly saved.', type: 'success', // optional: info | success | warning | error timeout: 3000 }); diff --git a/modules/ui-module/src/main/ts/dist/js/manager-inject-init.js b/modules/ui-module/src/main/ts/dist/js/manager-inject-init.js index e0bd1b36e..39935f0d8 100644 --- a/modules/ui-module/src/main/ts/dist/js/manager-inject-init.js +++ b/modules/ui-module/src/main/ts/dist/js/manager-inject-init.js @@ -51,7 +51,7 @@ export function initIframe() { } }); frameMessenger.on('getContentNodeResponse', async (payload) => { - for (const [slotName, items] of Object.entries(payload.contentNode.slots)) { + for (const [sectionName, items] of Object.entries(payload.contentNode.sections)) { for (const item of items) { const sectionContainer = document.querySelector(`[data-cms-section-uri="${item.uri}"]`); if (!sectionContainer) { diff --git a/modules/ui-module/src/main/ts/dist/js/manager-inject.js b/modules/ui-module/src/main/ts/dist/js/manager-inject.js index 2b11d2b1d..75083adbc 100644 --- a/modules/ui-module/src/main/ts/dist/js/manager-inject.js +++ b/modules/ui-module/src/main/ts/dist/js/manager-inject.js @@ -1,4 +1,3 @@ -"use strict"; /*- * #%L * UI Module diff --git a/modules/ui-module/src/main/ts/dist/js/modules/filebrowser.d.ts b/modules/ui-module/src/main/ts/dist/js/modules/filebrowser.d.ts index e10842a8d..34d9f9cb9 100644 --- a/modules/ui-module/src/main/ts/dist/js/modules/filebrowser.d.ts +++ b/modules/ui-module/src/main/ts/dist/js/modules/filebrowser.d.ts @@ -1,5 +1,5 @@ export function openFileBrowser(optionsParam: any): Promise; export namespace state { - let options: null; + let options: any; let currentFolder: string; } diff --git a/modules/ui-module/src/main/ts/dist/js/modules/form/utils.d.ts b/modules/ui-module/src/main/ts/dist/js/modules/form/utils.d.ts index 525b147b7..79c944de7 100644 --- a/modules/ui-module/src/main/ts/dist/js/modules/form/utils.d.ts +++ b/modules/ui-module/src/main/ts/dist/js/modules/form/utils.d.ts @@ -1,6 +1,6 @@ declare const createID: () => string; declare const utcToLocalDateTimeInputValue: (utcString: string) => string; -declare function getUTCDateTimeFromInput(inputElement: HTMLInputElement): string | null; +declare function getUTCDateTimeFromInput(inputElement: HTMLInputElement): string; declare function utcToLocalDateInputValue(utcString: string): string; -declare function getUTCDateFromInput(inputElement: HTMLInputElement): string | null; +declare function getUTCDateFromInput(inputElement: HTMLInputElement): string; export { createID, utcToLocalDateTimeInputValue, getUTCDateTimeFromInput, utcToLocalDateInputValue, getUTCDateFromInput }; diff --git a/modules/ui-module/src/main/ts/dist/js/modules/localization.d.ts b/modules/ui-module/src/main/ts/dist/js/modules/localization.d.ts index 9eeed5a15..0b22efabb 100644 --- a/modules/ui-module/src/main/ts/dist/js/modules/localization.d.ts +++ b/modules/ui-module/src/main/ts/dist/js/modules/localization.d.ts @@ -1,7 +1,7 @@ export function localizeUi(): Promise; export namespace i18n { let _locale: any; - let _cache: null; + let _cache: any; /** * Loads and merges remote localizations with defaults. */ diff --git a/modules/ui-module/src/main/ts/dist/js/modules/manager/manager.message.handlers.js b/modules/ui-module/src/main/ts/dist/js/modules/manager/manager.message.handlers.js index e87e00768..083ac47a2 100644 --- a/modules/ui-module/src/main/ts/dist/js/modules/manager/manager.message.handlers.js +++ b/modules/ui-module/src/main/ts/dist/js/modules/manager/manager.message.handlers.js @@ -122,7 +122,7 @@ const initMessageHandlers = () => { "module": window.manager.baseUrl + "/actions/page/edit-sections", "function": "runAction", "parameters": { - "slot": payload.slot + "section": payload.section } }; if (payload.uri) { @@ -130,17 +130,17 @@ const initMessageHandlers = () => { } executeScriptAction(cmd); }); - frameMessenger.on('add-slotItem', (payload) => { + frameMessenger.on('add-sectionEntry', (payload) => { var cmd = { "module": window.manager.baseUrl + "/actions/page/add-section", "function": "runAction", "parameters": { - "slot": payload.slot + "section": payload.section } }; executeScriptAction(cmd); }); - frameMessenger.on('delete-slotItem', (payload) => { + frameMessenger.on('delete-sectionEntry', (payload) => { var cmd = { "module": window.manager.baseUrl + "/actions/page/delete-section", "function": "runAction", diff --git a/modules/ui-module/src/main/ts/dist/js/modules/manager/toolbar.inject.js b/modules/ui-module/src/main/ts/dist/js/modules/manager/toolbar.inject.js index 0f452cffa..580cdf532 100644 --- a/modules/ui-module/src/main/ts/dist/js/modules/manager/toolbar.inject.js +++ b/modules/ui-module/src/main/ts/dist/js/modules/manager/toolbar.inject.js @@ -24,9 +24,9 @@ const addSection = (event) => { var toolbar = event.target.closest('[data-cms-toolbar]'); var toolbarDefinition = JSON.parse(toolbar.dataset.cmsToolbar); var command = { - type: 'add-slotItem', + type: 'add-sectionEntry', payload: { - slot: toolbarDefinition.slot, + section: toolbarDefinition.section, } }; frameMessenger.send(window.parent, command); @@ -35,7 +35,7 @@ const deleteSection = (event) => { var toolbar = event.target.closest('[data-cms-toolbar]'); var toolbarDefinition = JSON.parse(toolbar.dataset.cmsToolbar); var command = { - type: 'delete-slotItem', + type: 'delete-sectionEntry', payload: { sectionUri: toolbarDefinition.uri } @@ -61,7 +61,7 @@ const orderSections = (event) => { var command = { type: 'edit-sections', payload: { - slot: toolbarDefinition.slot + section: toolbarDefinition.section } }; frameMessenger.send(window.parent, command); @@ -120,7 +120,7 @@ export const initToolbar = (container) => { if (!toolbarDefinition.actions) { return; } - if (toolbarDefinition.type === "slot") { + if (toolbarDefinition.type === "section") { container.classList.add("cms-ui-editable-sections"); } else { @@ -128,7 +128,7 @@ export const initToolbar = (container) => { } const toolbar = document.createElement('div'); toolbar.className = 'cms-ui-toolbar'; - if (toolbarDefinition.type === "slot") { + if (toolbarDefinition.type === "section") { toolbar.classList.add("cms-ui-toolbar-tl"); } else { @@ -160,7 +160,7 @@ export const initToolbar = (container) => { button.addEventListener('click', editAttributes); toolbar.appendChild(button); } - else if (action === "orderSlotItems") { + else if (action === "orderSectionEntries") { const button = document.createElement('button'); button.setAttribute('data-cms-action', 'editSections'); button.innerHTML = SECTION_SORT_ICON; @@ -168,7 +168,7 @@ export const initToolbar = (container) => { button.addEventListener('click', orderSections); toolbar.appendChild(button); } - else if (action === "addSlotItem") { + else if (action === "addSectionEntry") { const button = document.createElement('button'); button.setAttribute('data-cms-action', 'addSection'); button.innerHTML = SECTION_ADD_ICON; @@ -176,7 +176,7 @@ export const initToolbar = (container) => { button.addEventListener('click', addSection); toolbar.appendChild(button); } - else if (action === "deleteSlotItem") { + else if (action === "deleteSectionEntry") { const button = document.createElement('button'); button.setAttribute('data-cms-action', 'deleteSection'); button.innerHTML = SECTION_DELETE_ICON; @@ -185,7 +185,7 @@ export const initToolbar = (container) => { toolbar.appendChild(button); } }); - if (toolbarDefinition.type === "slotItem") { + if (toolbarDefinition.type === "sectionEntry") { const button = document.createElement('button'); button.setAttribute('data-cms-action', 'publish'); button.setAttribute('data-cms-section-uri', toolbarDefinition.uri); diff --git a/modules/ui-module/src/main/ts/dist/js/modules/preview.history.d.ts b/modules/ui-module/src/main/ts/dist/js/modules/preview.history.d.ts index 19f953414..02a1aa102 100644 --- a/modules/ui-module/src/main/ts/dist/js/modules/preview.history.d.ts +++ b/modules/ui-module/src/main/ts/dist/js/modules/preview.history.d.ts @@ -2,6 +2,6 @@ export namespace PreviewHistory { export { init }; export { navigatePreview }; } -declare function init(defaultUrl?: null): void; +declare function init(defaultUrl?: any): void; declare function navigatePreview(url: any, usePush?: boolean): void; export {}; diff --git a/modules/ui-module/src/main/ts/dist/js/modules/preview.utils.d.ts b/modules/ui-module/src/main/ts/dist/js/modules/preview.utils.d.ts index 24beb8ecc..f2f167f34 100644 --- a/modules/ui-module/src/main/ts/dist/js/modules/preview.utils.d.ts +++ b/modules/ui-module/src/main/ts/dist/js/modules/preview.utils.d.ts @@ -3,4 +3,4 @@ export function deActivatePreviewOverlay(): void; export function getPreviewUrl(): any; export function reloadPreview(): void; export function loadPreview(url: any): void; -export function getPreviewFrame(): HTMLElement | null; +export function getPreviewFrame(): HTMLElement; diff --git a/modules/ui-module/src/main/ts/dist/js/modules/rpc/rpc-content.js b/modules/ui-module/src/main/ts/dist/js/modules/rpc/rpc-content.js index bd018c133..285c9e397 100644 --- a/modules/ui-module/src/main/ts/dist/js/modules/rpc/rpc-content.js +++ b/modules/ui-module/src/main/ts/dist/js/modules/rpc/rpc-content.js @@ -56,14 +56,14 @@ const setMetaBatch = async (options) => { }; const addSection = async (options) => { var data = { - method: "content.slotItem.add", + method: "content.sectionEntry.add", parameters: options }; return await executeRemoteCall(data); }; const deleteSection = async (options) => { var data = { - method: "content.slotItem.delete", + method: "content.sectionEntry.delete", parameters: options }; return await executeRemoteCall(data); diff --git a/modules/ui-module/src/main/ts/dist/js/modules/rpc/rpc-manager.d.ts b/modules/ui-module/src/main/ts/dist/js/modules/rpc/rpc-manager.d.ts index bc5f0a7ce..0975876a6 100644 --- a/modules/ui-module/src/main/ts/dist/js/modules/rpc/rpc-manager.d.ts +++ b/modules/ui-module/src/main/ts/dist/js/modules/rpc/rpc-manager.d.ts @@ -1,4 +1,4 @@ -declare const getSlotItemTemplates: (options: any) => Promise; +declare const getSectionEntryTemplates: (options: any) => Promise; declare const getPageTemplates: (options: any) => Promise; declare const getListItemTypes: (options: any) => Promise; declare const getMediaForm: (options: any) => Promise; @@ -21,4 +21,4 @@ export interface MediaFormatsResponse { } declare const getMediaFormats: (options: any) => Promise; declare const getTagNames: (options: any) => Promise; -export { getSlotItemTemplates, getPageTemplates, getMediaForm, getTagNames, getMediaFormats, getListItemTypes, createCSRFToken }; +export { getSectionEntryTemplates, getPageTemplates, getMediaForm, getTagNames, getMediaFormats, getListItemTypes, createCSRFToken }; diff --git a/modules/ui-module/src/main/ts/dist/js/modules/rpc/rpc-manager.js b/modules/ui-module/src/main/ts/dist/js/modules/rpc/rpc-manager.js index 5c95bafa3..cd96fc51a 100644 --- a/modules/ui-module/src/main/ts/dist/js/modules/rpc/rpc-manager.js +++ b/modules/ui-module/src/main/ts/dist/js/modules/rpc/rpc-manager.js @@ -19,9 +19,9 @@ * #L% */ import { executeRemoteCall } from '@cms/modules/rpc/rpc.js'; -const getSlotItemTemplates = async (options) => { +const getSectionEntryTemplates = async (options) => { var data = { - method: "manager.contentTypes.slotItems", + method: "manager.contentTypes.sectionEntries", parameters: options || {} }; return await executeRemoteCall(data); @@ -74,4 +74,4 @@ const getTagNames = async (options) => { }; return await executeRemoteCall(data); }; -export { getSlotItemTemplates, getPageTemplates, getMediaForm, getTagNames, getMediaFormats, getListItemTypes, createCSRFToken }; +export { getSectionEntryTemplates, getPageTemplates, getMediaForm, getTagNames, getMediaFormats, getListItemTypes, createCSRFToken }; diff --git a/modules/ui-module/src/main/ts/dist/js/modules/state.js b/modules/ui-module/src/main/ts/dist/js/modules/state.js index a0988236f..7274c0b66 100644 --- a/modules/ui-module/src/main/ts/dist/js/modules/state.js +++ b/modules/ui-module/src/main/ts/dist/js/modules/state.js @@ -1,4 +1,3 @@ -"use strict"; /*- * #%L * UI Module diff --git a/modules/ui-module/src/main/ts/dist/js/modules/ui-state.d.ts b/modules/ui-module/src/main/ts/dist/js/modules/ui-state.d.ts index d064b5d45..cf3faa9ae 100644 --- a/modules/ui-module/src/main/ts/dist/js/modules/ui-state.d.ts +++ b/modules/ui-module/src/main/ts/dist/js/modules/ui-state.d.ts @@ -1,10 +1,10 @@ export namespace UIStateManager { function setTabState(key: any, value: any): void; - function getTabState(key: any, defaultValue?: null): any; + function getTabState(key: any, defaultValue?: any): any; function setLocale(locale: any): void; function getLocale(): any; function removeTabState(key: any): void; function setAuthToken(token: any): void; - function getAuthToken(): string | null; + function getAuthToken(): string; function clearAuthToken(): void; } diff --git a/modules/ui-module/src/main/ts/dist/public/manager-login.js b/modules/ui-module/src/main/ts/dist/public/manager-login.js index 24aa077ae..6f21eee99 100644 --- a/modules/ui-module/src/main/ts/dist/public/manager-login.js +++ b/modules/ui-module/src/main/ts/dist/public/manager-login.js @@ -1,4 +1,3 @@ -"use strict"; /*- * #%L * UI Module diff --git a/modules/ui-module/src/main/ts/src/actions/page/add-section.js b/modules/ui-module/src/main/ts/src/actions/page/add-section.js index 52c8e0ea7..c946f4792 100644 --- a/modules/ui-module/src/main/ts/src/actions/page/add-section.js +++ b/modules/ui-module/src/main/ts/src/actions/page/add-section.js @@ -24,7 +24,7 @@ import { addSection, getContentNode } from '@cms/modules/rpc/rpc-content.js' import { getPreviewUrl, reloadPreview } from '@cms/modules/preview.utils.js' import Handlebars from 'https://cdn.jsdelivr.net/npm/handlebars@4.7.8/+esm' import { i18n } from '@cms/modules/localization.js' -import { getSlotItemTemplates } from '@cms/modules/rpc/rpc-manager.js'; +import { getSectionEntryTemplates } from '@cms/modules/rpc/rpc-manager.js'; export async function runAction(params) { @@ -45,25 +45,25 @@ export async function runAction(params) { `); - var sectionsResponse = await getSlotItemTemplates({ - slot: params.slot + var sectionsResponse = await getSectionEntryTemplates({ + section: params.section }); openModal({ - title: i18n.t("addsection.titles.modal", 'Add item'), + title: i18n.t("addsection.titles.modal", 'Add entry'), body: template({ templates: sectionsResponse.result, }), fullscreen: false, onCancel: (event) => {}, - validate: () => validate(contentNode, params.slot), + validate: () => validate(contentNode, params.section), onOk: async (event) => { - var result = await createSection(contentNode.result.uri, params.slot); + var result = await createSection(contentNode.result.uri, params.section); if (result) { showToast({ - title: i18n.t("manager.actions.addsection.titles.alert", "Create Item"), - message: i18n.t("manager.actions.addsection.alerts.success.message", "Item successfuly created."), + title: i18n.t("manager.actions.addsection.titles.alert", "Create Entry"), + message: i18n.t("manager.actions.addsection.alerts.success.message", "Entry successfuly created."), type: 'success', // optional: info | success | warning | error timeout: 3000 }); @@ -71,8 +71,8 @@ export async function runAction(params) { reloadPreview() } else { showToast({ - title: i18n.t("manager.actions.addsection.titles.alert", 'Create Item'), - message: i18n.t("manager.actions.addsection.alerts.error.message", "Item not created."), + title: i18n.t("manager.actions.addsection.titles.alert", 'Create Entry'), + message: i18n.t("manager.actions.addsection.alerts.error.message", "Entry not created."), type: 'warning', // optional: info | success | warning | error timeout: 3000 }); @@ -82,15 +82,15 @@ export async function runAction(params) { }); } -const getSlotItemName = () => { +const getSectionEntryName = () => { return document.getElementById("cms-section-name").value } -const validate = (contentNode, targetSlot) => { +const validate = (contentNode, targetSection) => { const template = document.getElementById("cms-section-template-selection").value if (template === "000") { showToast({ - title: i18n.t("manager.actions.addsection.titles.alert", 'Create Item'), + title: i18n.t("manager.actions.addsection.titles.alert", 'Create Entry'), message: i18n.t("manager.actions.addsection.alerts.notemplate.message", "No template selected."), type: 'error', // optional: info | success | warning | error timeout: 3000 @@ -98,11 +98,11 @@ const validate = (contentNode, targetSlot) => { return false } - const slotItemName = getSlotItemName() - if (slotItemName === "" || slotItemName === null) { + const sectionEntryName = getSectionEntryName() + if (sectionEntryName === "" || sectionEntryName === null) { showToast({ - title: i18n.t("manager.actions.addsection.titles.alert", 'Create Item'), - message: i18n.t("manager.actions.addsection.alerts.noname.message", "No Item name provided."), + title: i18n.t("manager.actions.addsection.titles.alert", 'Create Entry'), + message: i18n.t("manager.actions.addsection.alerts.noname.message", "No Entry name provided."), type: 'error', timeout: 3000 }); @@ -113,17 +113,17 @@ const validate = (contentNode, targetSlot) => { } -function isUriInSection(data, slotItemKey, targetUri) { +function isUriInSection(data, sectionEntryKey, targetUri) { if ( !data || !data.result || - !data.result.slots || - typeof data.result.slots !== 'object' + !data.result.sections || + typeof data.result.sections !== 'object' ) { return false; } - const sectionArray = data.result.slots[slotItemKey]; + const sectionArray = data.result.sections[sectionEntryKey]; if (!Array.isArray(sectionArray)) { return false; @@ -140,8 +140,8 @@ const createSection = async (parentUri, parentSectionName) => { } await addSection({ parentUri: parentUri, - slotItemName: getSlotItemName(), - slot: parentSectionName, + sectionEntryName: getSectionEntryName(), + section: parentSectionName, template: template }) return true diff --git a/modules/ui-module/src/main/ts/src/actions/page/edit-metaattribute-form.js b/modules/ui-module/src/main/ts/src/actions/page/edit-metaattribute-form.js index d3b167064..73ae52883 100644 --- a/modules/ui-module/src/main/ts/src/actions/page/edit-metaattribute-form.js +++ b/modules/ui-module/src/main/ts/src/actions/page/edit-metaattribute-form.js @@ -25,7 +25,7 @@ import { buildValuesFromFields, getValueByPath } from '@cms/modules/node.js' import {getContentNode, getContent, setMeta} from '@cms/modules/rpc/rpc-content.js' import { i18n } from '@cms/modules/localization.js' import { openSidebar } from '@cms/modules/sidebar.js' -import { getPageTemplates, getSlotItemTemplates } from '@cms/modules/rpc/rpc-manager' +import { getPageTemplates, getSectionEntryTemplates } from '@cms/modules/rpc/rpc-manager' // hook.js export async function runAction(params) { @@ -46,8 +46,8 @@ export async function runAction(params) { var templates = null - if (params.type === "slotItem") { - templates = (await getSlotItemTemplates()).result + if (params.type === "sectionEntry") { + templates = (await getSectionEntryTemplates()).result } else { templates = (await getPageTemplates()).result } diff --git a/modules/ui-module/src/main/ts/src/actions/page/edit-sections.js b/modules/ui-module/src/main/ts/src/actions/page/edit-sections.js index 66f5c867e..23f7c501d 100644 --- a/modules/ui-module/src/main/ts/src/actions/page/edit-sections.js +++ b/modules/ui-module/src/main/ts/src/actions/page/edit-sections.js @@ -33,7 +33,7 @@ export async function runAction(params) { var template = Handlebars.compile(`
      - {{#each slotItems}} + {{#each sectionEntries}}
    • {{#if data.title}} {{data.title}} @@ -44,23 +44,23 @@ export async function runAction(params) { {{/each}}
    `); - var slotItems = [] - if (contentNode.result.slots[params.slot]) { - var slotItems = contentNode.result.slots[params.slot] + var sectionEntries = [] + if (contentNode.result.sections[params.section]) { + var sectionEntries = contentNode.result.sections[params.section] } - slotItems = slotItems.sort((a, b) => a.index - b.index) + sectionEntries = sectionEntries.sort((a, b) => a.index - b.index) openModal({ title: 'Edit items', - body: template({ slotItems: slotItems }), + body: template({ sectionEntries: sectionEntries }), fullscreen: false, onCancel: (event) => {}, onOk: async (event) => { await saveSections(); showToast({ - title: 'Items saved', - message: 'Items successfuly saved.', + title: 'Entry saved', + message: 'Entry successfuly saved.', type: 'success', // optional: info | success | warning | error timeout: 3000 }); diff --git a/modules/ui-module/src/main/ts/src/js/manager-inject-init.js b/modules/ui-module/src/main/ts/src/js/manager-inject-init.js index 960a1f1c0..6a5a6add4 100644 --- a/modules/ui-module/src/main/ts/src/js/manager-inject-init.js +++ b/modules/ui-module/src/main/ts/src/js/manager-inject-init.js @@ -64,7 +64,7 @@ export function initIframe() { }) frameMessenger.on('getContentNodeResponse', async (payload) => { - for (const [slotName, items] of Object.entries(payload.contentNode.slots)) { + for (const [sectionName, items] of Object.entries(payload.contentNode.sections)) { for (const item of items) { const sectionContainer = document.querySelector(`[data-cms-section-uri="${item.uri}"]`); diff --git a/modules/ui-module/src/main/ts/src/js/modules/manager/manager.message.handlers.ts b/modules/ui-module/src/main/ts/src/js/modules/manager/manager.message.handlers.ts index 9437c3d7b..9744751e3 100644 --- a/modules/ui-module/src/main/ts/src/js/modules/manager/manager.message.handlers.ts +++ b/modules/ui-module/src/main/ts/src/js/modules/manager/manager.message.handlers.ts @@ -122,7 +122,7 @@ const initMessageHandlers = () => { "module": window.manager.baseUrl + "/actions/page/edit-sections", "function": "runAction", "parameters": { - "slot": payload.slot + "section": payload.section } } if (payload.uri) { @@ -131,18 +131,18 @@ const initMessageHandlers = () => { executeScriptAction(cmd) }); - frameMessenger.on('add-slotItem', (payload: any) => { + frameMessenger.on('add-sectionEntry', (payload: any) => { var cmd : any = { "module": window.manager.baseUrl + "/actions/page/add-section", "function": "runAction", "parameters": { - "slot": payload.slot + "section": payload.section } } executeScriptAction(cmd) }); - frameMessenger.on('delete-slotItem', (payload : any) => { + frameMessenger.on('delete-sectionEntry', (payload : any) => { var cmd :any = { "module": window.manager.baseUrl + "/actions/page/delete-section", "function": "runAction", diff --git a/modules/ui-module/src/main/ts/src/js/modules/manager/toolbar.inject.ts b/modules/ui-module/src/main/ts/src/js/modules/manager/toolbar.inject.ts index ed4825488..12cfbf9b0 100644 --- a/modules/ui-module/src/main/ts/src/js/modules/manager/toolbar.inject.ts +++ b/modules/ui-module/src/main/ts/src/js/modules/manager/toolbar.inject.ts @@ -26,9 +26,9 @@ const addSection = (event : Event) => { var toolbarDefinition = JSON.parse(toolbar.dataset.cmsToolbar) var command : any = { - type: 'add-slotItem', + type: 'add-sectionEntry', payload: { - slot: toolbarDefinition.slot, + section: toolbarDefinition.section, } } frameMessenger.send(window.parent, command); @@ -39,7 +39,7 @@ const deleteSection = (event: Event) => { var toolbarDefinition = JSON.parse(toolbar.dataset.cmsToolbar) var command = { - type: 'delete-slotItem', + type: 'delete-sectionEntry', payload: { sectionUri: toolbarDefinition.uri } @@ -70,7 +70,7 @@ const orderSections = (event : Event) => { var command = { type: 'edit-sections', payload: { - slot: toolbarDefinition.slot + section: toolbarDefinition.section } } frameMessenger.send(window.parent, command); @@ -139,7 +139,7 @@ export const initToolbar = (container: HTMLElement) => { if (!toolbarDefinition.actions) { return } - if (toolbarDefinition.type === "slot") { + if (toolbarDefinition.type === "section") { container.classList.add("cms-ui-editable-sections"); } else { container.classList.add("cms-ui-editable"); @@ -148,7 +148,7 @@ export const initToolbar = (container: HTMLElement) => { const toolbar = document.createElement('div'); toolbar.className = 'cms-ui-toolbar'; - if (toolbarDefinition.type === "slot") { + if (toolbarDefinition.type === "section") { toolbar.classList.add("cms-ui-toolbar-tl"); } else { toolbar.classList.add("cms-ui-toolbar-tr"); @@ -181,7 +181,7 @@ export const initToolbar = (container: HTMLElement) => { button.addEventListener('click', editAttributes); toolbar.appendChild(button); - } else if (action === "orderSlotItems") { + } else if (action === "orderSectionEntries") { const button = document.createElement('button'); button.setAttribute('data-cms-action', 'editSections'); button.innerHTML = SECTION_SORT_ICON; @@ -189,7 +189,7 @@ export const initToolbar = (container: HTMLElement) => { button.addEventListener('click', orderSections); toolbar.appendChild(button); - } else if (action === "addSlotItem") { + } else if (action === "addSectionEntry") { const button = document.createElement('button'); button.setAttribute('data-cms-action', 'addSection'); button.innerHTML = SECTION_ADD_ICON; @@ -197,7 +197,7 @@ export const initToolbar = (container: HTMLElement) => { button.addEventListener('click', addSection); toolbar.appendChild(button); - } else if (action === "deleteSlotItem") { + } else if (action === "deleteSectionEntry") { const button = document.createElement('button'); button.setAttribute('data-cms-action', 'deleteSection'); button.innerHTML = SECTION_DELETE_ICON; @@ -208,7 +208,7 @@ export const initToolbar = (container: HTMLElement) => { } }) - if (toolbarDefinition.type === "slotItem") { + if (toolbarDefinition.type === "sectionEntry") { const button = document.createElement('button'); button.setAttribute('data-cms-action', 'publish'); button.setAttribute('data-cms-section-uri', toolbarDefinition.uri); diff --git a/modules/ui-module/src/main/ts/src/js/modules/rpc/rpc-content.ts b/modules/ui-module/src/main/ts/src/js/modules/rpc/rpc-content.ts index b66f51733..73891e88b 100644 --- a/modules/ui-module/src/main/ts/src/js/modules/rpc/rpc-content.ts +++ b/modules/ui-module/src/main/ts/src/js/modules/rpc/rpc-content.ts @@ -63,7 +63,7 @@ const setMetaBatch = async (options : any) => { const addSection = async (options : any) => { var data = { - method: "content.slotItem.add", + method: "content.sectionEntry.add", parameters: options } return await executeRemoteCall(data); @@ -71,7 +71,7 @@ const addSection = async (options : any) => { const deleteSection = async (options : any) => { var data = { - method: "content.slotItem.delete", + method: "content.sectionEntry.delete", parameters: options } return await executeRemoteCall(data); diff --git a/modules/ui-module/src/main/ts/src/js/modules/rpc/rpc-manager.ts b/modules/ui-module/src/main/ts/src/js/modules/rpc/rpc-manager.ts index fac00e660..d9093f729 100644 --- a/modules/ui-module/src/main/ts/src/js/modules/rpc/rpc-manager.ts +++ b/modules/ui-module/src/main/ts/src/js/modules/rpc/rpc-manager.ts @@ -21,9 +21,9 @@ import { executeRemoteCall } from '@cms/modules/rpc/rpc.js' -const getSlotItemTemplates = async (options : any) => { +const getSectionEntryTemplates = async (options : any) => { var data = { - method: "manager.contentTypes.slotItems", + method: "manager.contentTypes.sectionEntries", parameters: options || {} } return await executeRemoteCall(data); @@ -96,7 +96,7 @@ const getTagNames = async (options : any) => { }; export { - getSlotItemTemplates, + getSectionEntryTemplates, getPageTemplates, getMediaForm, getTagNames, diff --git a/modules/ui-module/src/test/java/com/condation/cms/modules/ui/utils/UIFilenameUtilTest.java b/modules/ui-module/src/test/java/com/condation/cms/modules/ui/utils/UIFilenameUtilTest.java index 1eaf51d8a..4d2e2c537 100644 --- a/modules/ui-module/src/test/java/com/condation/cms/modules/ui/utils/UIFilenameUtilTest.java +++ b/modules/ui-module/src/test/java/com/condation/cms/modules/ui/utils/UIFilenameUtilTest.java @@ -33,23 +33,23 @@ public class UIFilenameUtilTest { @Test void testCreateSectionFileName() { // einfache Datei - assertThat(UIFileNameUtil.createSlotItemFileName("index.md", "section", "item")) + assertThat(UIFileNameUtil.createSectionEntryFileName("index.md", "section", "item")) .isEqualTo("index.section.item.md"); // andere Datei - assertThat(UIFileNameUtil.createSlotItemFileName("about.md", "sec", "bla")) + assertThat(UIFileNameUtil.createSectionEntryFileName("about.md", "sec", "bla")) .isEqualTo("about.sec.bla.md"); // Datei in Unterordner - assertThat(UIFileNameUtil.createSlotItemFileName("ordner/file.md", "new", "one")) + assertThat(UIFileNameUtil.createSectionEntryFileName("ordner/file.md", "new", "one")) .isEqualTo("ordner/file.new.one.md"); // Datei ohne Endung - assertThat(UIFileNameUtil.createSlotItemFileName("ordner/sub/file", "new", "two")) + assertThat(UIFileNameUtil.createSectionEntryFileName("ordner/sub/file", "new", "two")) .isEqualTo("ordner/sub/file.new.two"); // nur Dateiname - assertThat(UIFileNameUtil.createSlotItemFileName("file.txt", "v2", "2025")) + assertThat(UIFileNameUtil.createSectionEntryFileName("file.txt", "v2", "2025")) .isEqualTo("file.v2.2025.txt"); } diff --git a/test-server/hosts/demo/content/index.md b/test-server/hosts/demo/content/index.md index 98e4ef6cc..de12f0cc7 100644 --- a/test-server/hosts/demo/content/index.md +++ b/test-server/hosts/demo/content/index.md @@ -53,3 +53,6 @@ Theme: [[ext:theme_name]][[/ext:theme_name]] ```java System.out.println("Hello world!"); ``` + +### say hello +[[ext:say_hello name="CondationCMS" /]] \ No newline at end of file diff --git a/test-server/themes/demo/extensions/theme.extension.js b/test-server/themes/demo/extensions/theme.extension.js index f095295b0..fb9f9b2a2 100644 --- a/test-server/themes/demo/extensions/theme.extension.js +++ b/test-server/themes/demo/extensions/theme.extension.js @@ -2,44 +2,51 @@ import { $hooks } from 'system/hooks.mjs'; import { $templates } from 'system/templates.mjs'; -$hooks.registerAction("system/content/tags", (context) => { - context.arguments().get("tags").put( +$hooks.registerAction("system/content/tags", ({tags}) => { + tags.put( "theme_name", (params) => `Hello, I'm your demo theme.` ) return null; }) -$hooks.registerAction("system/template/function", (context) => { - context.arguments().get("functions").put( +$hooks.registerAction("system/content/tags", ({tags}) => { + tags.put( + "say_hello", + ({name}) => `Hello, ${name}` + ) + return null; +}) + +$hooks.registerAction("system/template/function", ({functions}) => { + functions.put( "fn_message", - (params) => `
    ${params.message}
    ` + ({color, message}) => `
    MESSAGE: ${message}
    ` ) return null; }) -$hooks.registerAction("system/template/component", (context) => { - context.arguments().get("components").put( +$hooks.registerAction("system/template/component", ({components}) => { + components.put( "component", - (params) => `
    ${params.message}
    ` + ({color, message}) => `
    COMPONENT: ${message}
    ` ) - context.arguments().get("components").put( + components.put( "tempcomp", (params) => { var model = { - "name": params.get("name"), - "message_text": params.get("message") + "name": params.name, + "message_text": params.message } - return $templates.render("components/test.html", model); + return "rendered: " + $templates.render("components/test.html", model); } ) return null; }) -$hooks.registerFilter("module/ui/translations", (context) => { - var translations = context.value() +$hooks.registerFilter("module/ui/translations", ({translations}) => { translations.en["field.title"] = "Title"; translations.de["field.title"] = "Titel"; @@ -53,9 +60,9 @@ $hooks.registerFilter("module/ui/translations", (context) => { return translations; }) -$hooks.registerAction("system/content/slot/header", (context) => { - return ""; +$hooks.registerAction("system/layout/html/header", (args) => { + return ""; }) -$hooks.registerAction("system/content/slot/footer", (context) => { - return ""; +$hooks.registerAction("system/layout/html/footer", (args) => { + return ""; }) \ No newline at end of file diff --git a/test-server/themes/demo/extensions/theme.manager.js b/test-server/themes/demo/extensions/theme.manager.js index f17b1b610..8e54305ef 100644 --- a/test-server/themes/demo/extensions/theme.manager.js +++ b/test-server/themes/demo/extensions/theme.manager.js @@ -1,7 +1,6 @@ import { $hooks } from 'system/hooks.mjs'; -$hooks.registerFilter("manager/media/forms", (context) => { - var mediaForms = context.value(); +$hooks.registerFilter("manager/media/forms", (mediaForms) => { mediaForms.registerForm("meta", { fields: [ { @@ -39,8 +38,7 @@ const UnPublishDateField = { }; -$hooks.registerFilter("manager/contentTypes/register", (context) => { - var contentTypes = context.value(); +$hooks.registerFilter("manager/contentTypes/register", (contentTypes) => { contentTypes.registerPageTemplate({ name: "StartPage", template: "start.html", @@ -170,9 +168,9 @@ $hooks.registerFilter("manager/contentTypes/register", (context) => { template: "default.html", contentFolder: "content", }); - contentTypes.registerSlotItemTemplate({ - slot: "asection", - name: "SlotItemTemplate", + contentTypes.registerSectionEntryTemplate({ + section: "asection", + name: "SectionEntryTemplate", template: "section.html", forms: { attributes: { diff --git a/test-server/themes/demo/templates/content/content.html b/test-server/themes/demo/templates/content/content.html index a8bf7c774..65b1fea30 100644 --- a/test-server/themes/demo/templates/content/content.html +++ b/test-server/themes/demo/templates/content/content.html @@ -60,10 +60,10 @@

    template function test

    - {% if node.slots.containsKey('asection') %} - {% for item in node.slots.get('asection') %} + {% if node.sections.containsKey('asection') %} + {% for item in node.sections.get('asection') %} {{ item.content() | raw }} {% endfor %} {% endif %} diff --git a/test-server/themes/demo/templates/content/sections.html b/test-server/themes/demo/templates/content/sections.html index 893c0201d..149557b94 100644 --- a/test-server/themes/demo/templates/content/sections.html +++ b/test-server/themes/demo/templates/content/sections.html @@ -61,10 +61,10 @@

    template function test

    - {% if node.slots.containsKey('asection') %} - {% for item in node.slots.get('asection') %} + {% if node.sections.containsKey('asection') %} + {% for item in node.sections.get('asection') %} {{ item.content() | raw }} {% endfor %} {% endif %} diff --git a/test-server/themes/demo/templates/default.html b/test-server/themes/demo/templates/default.html index e1552e540..c199582d9 100644 --- a/test-server/themes/demo/templates/default.html +++ b/test-server/themes/demo/templates/default.html @@ -36,12 +36,12 @@

    - {% if !empty(node.slots) %} + {% if !empty(node.sections) %}
    - {% if node.slots.containsKey('asection') %} - {% for item in node.slots.get('asection') %} + {% if node.sections.containsKey('asection') %} + {% for item in node.sections.get('asection') %} {{ item.content() | raw }} {% endfor %} {% endif %} @@ -55,8 +55,8 @@

    Test Node references

    {% assign globals = select_node("") %} {% if globals != null %}

    load secions from start page

    - {% if globals.slots.containsKey('asection') %} - {% for item in globals.slots.get('asection') %} + {% if globals.sections.containsKey('asection') %} + {% for item in globals.sections.get('asection') %} {{ item.content() | raw }} {% endfor %} {% endif %} diff --git a/test-server/themes/demo/templates/section.html b/test-server/themes/demo/templates/section.html index e084e1d5e..1e6f66824 100644 --- a/test-server/themes/demo/templates/section.html +++ b/test-server/themes/demo/templates/section.html @@ -1,4 +1,4 @@ -
    {{ node.content | default('your content here') | raw}} diff --git a/test-server/themes/demo/templates/start.html b/test-server/themes/demo/templates/start.html index aa6d8f35d..c849ffd67 100644 --- a/test-server/themes/demo/templates/start.html +++ b/test-server/themes/demo/templates/start.html @@ -65,14 +65,13 @@

    template function test

    - {% if node.slots.containsKey('asection') %} - {% for item in node.slots.get('asection') %} + {% if node.sections.containsKey('asection') %} + {% for item in node.sections.get('asection') %} {{ item.content() | raw }} {% endfor %} {% endif %} -
    diff --git a/test-server/themes/demo/templates/taxonomy.single.brand.html b/test-server/themes/demo/templates/taxonomy.single.brand.html index 18e496374..cc05165af 100644 --- a/test-server/themes/demo/templates/taxonomy.single.brand.html +++ b/test-server/themes/demo/templates/taxonomy.single.brand.html @@ -39,12 +39,12 @@

    {{ taxonomy.title }}


    - {% if !empty(node.slots) %} + {% if !empty(node.sections) %}
    - {% if node.slots.containsKey('asection') %} - {% for item in node.slots.get('asection') %} + {% if node.section.containsKey('asection') %} + {% for item in node.section.get('asection') %} {{ item.content() | raw }} {% endfor %} {% endif %}