From a3a34ce88aafa1d8b8fcc15bb2f90e99893cc3c7 Mon Sep 17 00:00:00 2001 From: Krystian Panek Date: Fri, 21 Nov 2025 06:49:59 +0100 Subject: [PATCH 01/14] Code metadata --- .../vml/es/acm/core/code/CodeMetadata.java | 149 ++++++++++++++++++ .../es/acm/core/code/CodeMetadataTest.java | 121 ++++++++++++++ .../example/ACME-200_hello-world.groovy | 5 +- .../manual/example/ACME-201_inputs.groovy | 7 +- .../manual/example/ACME-203_output-csv.groovy | 11 +- .../manual/example/ACME-203_output-xls.groovy | 11 +- 6 files changed, 289 insertions(+), 15 deletions(-) create mode 100644 core/src/main/java/dev/vml/es/acm/core/code/CodeMetadata.java create mode 100644 core/src/test/java/dev/vml/es/acm/core/code/CodeMetadataTest.java diff --git a/core/src/main/java/dev/vml/es/acm/core/code/CodeMetadata.java b/core/src/main/java/dev/vml/es/acm/core/code/CodeMetadata.java new file mode 100644 index 000000000..2523d4202 --- /dev/null +++ b/core/src/main/java/dev/vml/es/acm/core/code/CodeMetadata.java @@ -0,0 +1,149 @@ + +package dev.vml.es.acm.core.code; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import com.fasterxml.jackson.annotation.JsonAnyGetter; + +class CodeMetadata implements Serializable { + + private static final Pattern DOC_COMMENT_PATTERN = Pattern.compile( + "/\\*\\*([^*]|\\*(?!/))*\\*/", + Pattern.DOTALL + ); + + private static final Pattern TAG_PATTERN = Pattern.compile( + "(?m)^\\s*\\*?\\s*@(\\w+)\\s+(.+?)(?=(?m)^\\s*\\*?\\s*@\\w+|\\*/|$)", + Pattern.DOTALL + ); + + private Map values; + + public CodeMetadata(Map values) { + this.values = values; + } + + public static CodeMetadata of(Executable executable) { + return parse(executable.getContent()); + } + + public static CodeMetadata parse(String code) { + if (code == null || code.trim().isEmpty()) { + return new CodeMetadata(Map.of()); + } + + Map metadata = new HashMap<>(); + + // Find the first doc comment (either at the top or after imports) + String docComment = findFirstDocComment(code); + if (docComment != null) { + parseDocComment(docComment, metadata); + } + + return new CodeMetadata(metadata); + } + + private static String findFirstDocComment(String code) { + Matcher matcher = DOC_COMMENT_PATTERN.matcher(code); + + // Find first doc comment that's not attached to a method/class + // (i.e., appears before any method definition) + while (matcher.find()) { + String comment = matcher.group(); + int commentEnd = matcher.end(); + + // Check if this comment is followed by typical method/class keywords + String afterComment = code.substring(commentEnd).trim(); + + // If it's followed by a method signature (contains parentheses before opening brace) + // or class/interface keyword, skip it + if (afterComment.matches("^(public|private|protected|static|final|abstract|void|boolean|int|long|String|class|interface)\\s+.*")) { + // Check if it's actually a method (has parentheses) + int firstBrace = afterComment.indexOf('{'); + int firstParen = afterComment.indexOf('('); + + if (firstParen > 0 && (firstBrace < 0 || firstParen < firstBrace)) { + // This is a method/constructor, skip it + continue; + } + } + + // This is a top-level comment + return comment; + } + + return null; + } + + private static void parseDocComment(String docComment, Map metadata) { + // Remove /** and */ markers and leading comment decorations + String content = docComment.replaceAll("^/\\*\\*", "").replaceAll("\\*/$", ""); + + // Extract general description (text before first @tag) + // Look for @tag pattern (@ at start of word boundary, not in middle of text like email) + Pattern firstTagPattern = Pattern.compile("(?m)^\\s*\\*?\\s*@\\w+"); + Matcher firstTagMatcher = firstTagPattern.matcher(content); + + if (firstTagMatcher.find()) { + int firstTagIndex = firstTagMatcher.start(); + String description = content.substring(0, firstTagIndex) + .replaceAll("(?m)^\\s*\\*\\s?", "") + .trim(); + if (!description.isEmpty()) { + metadata.put("description", description); + } + } else { + // No tags, just description + String description = content + .replaceAll("(?m)^\\s*\\*\\s?", "") + .trim(); + if (!description.isEmpty()) { + metadata.put("description", description); + } + } + + // Parse tags + Matcher tagMatcher = TAG_PATTERN.matcher(content); + + while (tagMatcher.find()) { + String tagName = tagMatcher.group(1); + String tagValue = tagMatcher.group(2); + + if (tagValue != null) { + tagValue = tagValue + .replaceAll("(?m)^\\s*\\*\\s?", "") // Remove leading * from each line + .trim(); + + if (!tagValue.isEmpty()) { + // Store tag value, use list for potential multiple values + Object existing = metadata.get(tagName); + + if (existing == null) { + metadata.put(tagName, tagValue); + } else if (existing instanceof List) { + @SuppressWarnings("unchecked") + List list = (List) existing; + list.add(tagValue); + } else { + // Convert to list if we have multiple values + List list = new ArrayList<>(); + list.add((String) existing); + list.add(tagValue); + metadata.put(tagName, list); + } + } + } + } + } + + @JsonAnyGetter + public Map getValues() { + return values; + } +} \ No newline at end of file diff --git a/core/src/test/java/dev/vml/es/acm/core/code/CodeMetadataTest.java b/core/src/test/java/dev/vml/es/acm/core/code/CodeMetadataTest.java new file mode 100644 index 000000000..97109e86e --- /dev/null +++ b/core/src/test/java/dev/vml/es/acm/core/code/CodeMetadataTest.java @@ -0,0 +1,121 @@ +package dev.vml.es.acm.core.code; + +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +class CodeMetadataTest { + + private static final Path SCRIPTS_BASE_PATH = Paths.get("../ui.content.example/src/main/content/jcr_root/conf/acm/settings/script"); + + private String readScript(String relativePath) throws IOException { + Path scriptPath = SCRIPTS_BASE_PATH.resolve(relativePath); + return Files.readString(scriptPath); + } + + @Test + void shouldParseEmptyCode() { + CodeMetadata metadata = CodeMetadata.parse(""); + + assertTrue(metadata.getValues().isEmpty()); + } + + @Test + void shouldParseNullCode() { + CodeMetadata metadata = CodeMetadata.parse(null); + + assertTrue(metadata.getValues().isEmpty()); + } + + @Test + void shouldParseHelloWorldScript() throws IOException { + String code = readScript("manual/example/ACME-200_hello-world.groovy"); + CodeMetadata metadata = CodeMetadata.parse(code); + + assertEquals("Prints \"Hello World!\" to the console.", + metadata.getValues().get("description")); + assertEquals("Krystian Panek ", + metadata.getValues().get("author")); + } + + @Test + void shouldParseInputsScript() throws IOException { + String code = readScript("manual/example/ACME-201_inputs.groovy"); + CodeMetadata metadata = CodeMetadata.parse(code); + + assertEquals("Prints animal information to the console based on user input. This is an example of AEM Content Manager script with inputs.", + metadata.getValues().get("description")); + assertEquals("Krystian Panek ", + metadata.getValues().get("author")); + } + + @Test + void shouldParsePageThumbnailScript() throws IOException { + String code = readScript("manual/example/ACME-202_page-thumbnail.groovy"); + CodeMetadata metadata = CodeMetadata.parse(code); + String description = (String) metadata.getValues().get("description"); + + assertNotNull(description); + assertTrue(description.contains("Updates the thumbnail")); + assertTrue(description.contains("File must be a JPEG image")); + assertEquals("Krystian Panek ", + metadata.getValues().get("author")); + } + + @Test + void shouldParseScriptWithoutDocComment() throws IOException { + String code = readScript("automatic/example/ACME-20_once.groovy"); + CodeMetadata metadata = CodeMetadata.parse(code); + + assertTrue(metadata.getValues().isEmpty()); + } + + @Test + void shouldParseMultipleAuthors() { + String code = "/**\n" + + " * @author John Doe\n" + + " * @author Jane Smith\n" + + " */\n" + + "\n" + + "void run() {\n" + + " println \"Hello\"\n" + + "}"; + CodeMetadata metadata = CodeMetadata.parse(code); + Object authors = metadata.getValues().get("author"); + assertTrue(authors instanceof List); + @SuppressWarnings("unchecked") + List authorsList = (List) authors; + + assertEquals(2, authorsList.size()); + assertEquals("John Doe", authorsList.get(0)); + assertEquals("Jane Smith", authorsList.get(1)); + } + + @Test + void shouldParseCustomTags() { + String code = "/**\n" + + " * @description Custom script with metadata\n" + + " * @version 1.0.0\n" + + " * @since 2025-01-01\n" + + " * @category migration\n" + + " */\n" + + "\n" + + "void run() {\n" + + " println \"Hello\"\n" + + "}"; + + CodeMetadata metadata = CodeMetadata.parse(code); + + assertEquals("Custom script with metadata", metadata.getValues().get("description")); + assertEquals("1.0.0", metadata.getValues().get("version")); + assertEquals("2025-01-01", metadata.getValues().get("since")); + assertEquals("migration", metadata.getValues().get("category")); + } +} + diff --git a/ui.content.example/src/main/content/jcr_root/conf/acm/settings/script/manual/example/ACME-200_hello-world.groovy b/ui.content.example/src/main/content/jcr_root/conf/acm/settings/script/manual/example/ACME-200_hello-world.groovy index 3783c3685..c9b03d2b6 100644 --- a/ui.content.example/src/main/content/jcr_root/conf/acm/settings/script/manual/example/ACME-200_hello-world.groovy +++ b/ui.content.example/src/main/content/jcr_root/conf/acm/settings/script/manual/example/ACME-200_hello-world.groovy @@ -1,8 +1,5 @@ /** - * Prints "Hello World!" to the console. - * - * This is a minimal example of AEM Content Manager script. - * + * @description Prints "Hello World!" to the console. * @author Krystian Panek */ diff --git a/ui.content.example/src/main/content/jcr_root/conf/acm/settings/script/manual/example/ACME-201_inputs.groovy b/ui.content.example/src/main/content/jcr_root/conf/acm/settings/script/manual/example/ACME-201_inputs.groovy index b86aac07e..a4b67fce3 100644 --- a/ui.content.example/src/main/content/jcr_root/conf/acm/settings/script/manual/example/ACME-201_inputs.groovy +++ b/ui.content.example/src/main/content/jcr_root/conf/acm/settings/script/manual/example/ACME-201_inputs.groovy @@ -1,11 +1,8 @@ /** - * Prints animal information to the console based on user input. - * - * This is an example of AEM Content Manager script with inputs. - * + * @description Prints animal information to the console based on user input. This is an example of AEM Content Manager script with inputs. * @author Krystian Panek */ - + void describeRun() { inputs.string("animalName") { value = "Whiskers"; validator = "(v, a) => a.animalType === 'cat' ? (v && v.startsWith('W') || 'Cat name must start with W!') : true" } diff --git a/ui.content.example/src/main/content/jcr_root/conf/acm/settings/script/manual/example/ACME-203_output-csv.groovy b/ui.content.example/src/main/content/jcr_root/conf/acm/settings/script/manual/example/ACME-203_output-csv.groovy index 10be01fc1..62718999d 100644 --- a/ui.content.example/src/main/content/jcr_root/conf/acm/settings/script/manual/example/ACME-203_output-csv.groovy +++ b/ui.content.example/src/main/content/jcr_root/conf/acm/settings/script/manual/example/ACME-203_output-csv.groovy @@ -1,9 +1,10 @@ import java.time.LocalDate import java.util.Random -boolean canRun() { - return conditions.always() -} +/** + * @description Generates a CSV report of users with random names and birth dates. + * @author Krystian Panek + */ void describeRun() { inputs.integerNumber("count") { label = "Users to generate"; min = 1; value = 10000 } @@ -11,6 +12,10 @@ void describeRun() { inputs.text("lastNames") { label = "Last names"; description = "One last name per line"; value = "Doe\nSmith\nBrown\nJohnson\nWhite" } } +boolean canRun() { + return conditions.always() +} + void doRun() { out.info "Users CSV report generation started" diff --git a/ui.content.example/src/main/content/jcr_root/conf/acm/settings/script/manual/example/ACME-203_output-xls.groovy b/ui.content.example/src/main/content/jcr_root/conf/acm/settings/script/manual/example/ACME-203_output-xls.groovy index 095ecf59e..e296771d6 100644 --- a/ui.content.example/src/main/content/jcr_root/conf/acm/settings/script/manual/example/ACME-203_output-xls.groovy +++ b/ui.content.example/src/main/content/jcr_root/conf/acm/settings/script/manual/example/ACME-203_output-xls.groovy @@ -5,9 +5,10 @@ import java.time.ZoneId import java.util.Date import java.util.Random -boolean canRun() { - return conditions.always() -} +/** + * @description Generates an XLS report of users with random names and birth dates. + * @author Krystian Panek + */ void describeRun() { inputs.integerNumber("count") { label = "Users to generate"; min = 1; value = 100000 } @@ -15,6 +16,10 @@ void describeRun() { inputs.text("lastNames") { label = "Last names"; description = "One last name per line"; value = "Doe\nSmith\nBrown\nJohnson\nWhite" } } +boolean canRun() { + return conditions.always() +} + void doRun() { out.info "Users XLS report generation started" From 2e7d85de0360eef67aecaca970f0cc03422caa6d Mon Sep 17 00:00:00 2001 From: Krystian Panek Date: Fri, 21 Nov 2025 07:01:04 +0100 Subject: [PATCH 02/14] Code metadata works --- .../vml/es/acm/core/code/CodeMetadata.java | 49 ++++++++++++------- .../es/acm/core/code/CodeMetadataTest.java | 10 ++-- .../example/ACME-200_hello-world.groovy | 3 +- .../manual/example/ACME-201_inputs.groovy | 5 +- 4 files changed, 41 insertions(+), 26 deletions(-) diff --git a/core/src/main/java/dev/vml/es/acm/core/code/CodeMetadata.java b/core/src/main/java/dev/vml/es/acm/core/code/CodeMetadata.java index 2523d4202..d597975d9 100644 --- a/core/src/main/java/dev/vml/es/acm/core/code/CodeMetadata.java +++ b/core/src/main/java/dev/vml/es/acm/core/code/CodeMetadata.java @@ -52,30 +52,45 @@ public static CodeMetadata parse(String code) { private static String findFirstDocComment(String code) { Matcher matcher = DOC_COMMENT_PATTERN.matcher(code); - // Find first doc comment that's not attached to a method/class - // (i.e., appears before any method definition) + // Find first doc comment that's properly separated with newlines while (matcher.find()) { String comment = matcher.group(); + int commentStart = matcher.start(); int commentEnd = matcher.end(); - // Check if this comment is followed by typical method/class keywords - String afterComment = code.substring(commentEnd).trim(); + // Get text after comment (until next non-whitespace or end of line) + String afterComment = code.substring(commentEnd); - // If it's followed by a method signature (contains parentheses before opening brace) - // or class/interface keyword, skip it - if (afterComment.matches("^(public|private|protected|static|final|abstract|void|boolean|int|long|String|class|interface)\\s+.*")) { - // Check if it's actually a method (has parentheses) - int firstBrace = afterComment.indexOf('{'); - int firstParen = afterComment.indexOf('('); - - if (firstParen > 0 && (firstBrace < 0 || firstParen < firstBrace)) { - // This is a method/constructor, skip it - continue; - } + // Must have at least one newline after the comment + if (!afterComment.matches("^\\s*\\n[\\s\\S]*")) { + continue; + } + + // Check what follows after the newline(s) + String trimmedAfter = afterComment.trim(); + + // Skip if directly followed by describeRun (it's OK to be attached) + if (trimmedAfter.startsWith("void describeRun()")) { + return comment; } - // This is a top-level comment - return comment; + // Must have blank line after (double newline) for other cases + if (!afterComment.matches("^\\s*\\n\\s*\\n[\\s\\S]*")) { + continue; + } + + // If there's code before the comment, check for blank line before + if (commentStart > 0) { + String beforeComment = code.substring(0, commentStart); + // Should have imports or package, followed by blank line + if (beforeComment.trim().isEmpty() || + beforeComment.matches("[\\s\\S]*(import|package)[\\s\\S]*\\n\\s*\\n\\s*$")) { + return comment; + } + } else { + // Comment at the very start is OK + return comment; + } } return null; diff --git a/core/src/test/java/dev/vml/es/acm/core/code/CodeMetadataTest.java b/core/src/test/java/dev/vml/es/acm/core/code/CodeMetadataTest.java index 97109e86e..aebaa31c7 100644 --- a/core/src/test/java/dev/vml/es/acm/core/code/CodeMetadataTest.java +++ b/core/src/test/java/dev/vml/es/acm/core/code/CodeMetadataTest.java @@ -22,7 +22,7 @@ private String readScript(String relativePath) throws IOException { @Test void shouldParseEmptyCode() { CodeMetadata metadata = CodeMetadata.parse(""); - + assertTrue(metadata.getValues().isEmpty()); } @@ -40,8 +40,6 @@ void shouldParseHelloWorldScript() throws IOException { assertEquals("Prints \"Hello World!\" to the console.", metadata.getValues().get("description")); - assertEquals("Krystian Panek ", - metadata.getValues().get("author")); } @Test @@ -49,8 +47,10 @@ void shouldParseInputsScript() throws IOException { String code = readScript("manual/example/ACME-201_inputs.groovy"); CodeMetadata metadata = CodeMetadata.parse(code); - assertEquals("Prints animal information to the console based on user input. This is an example of AEM Content Manager script with inputs.", - metadata.getValues().get("description")); + String description = (String) metadata.getValues().get("description"); + assertNotNull(description); + assertTrue(description.contains("Prints animal information to the console based on user input")); + assertTrue(description.contains("This is an example of AEM Content Manager script with inputs")); assertEquals("Krystian Panek ", metadata.getValues().get("author")); } diff --git a/ui.content.example/src/main/content/jcr_root/conf/acm/settings/script/manual/example/ACME-200_hello-world.groovy b/ui.content.example/src/main/content/jcr_root/conf/acm/settings/script/manual/example/ACME-200_hello-world.groovy index c9b03d2b6..0f1fc8f1a 100644 --- a/ui.content.example/src/main/content/jcr_root/conf/acm/settings/script/manual/example/ACME-200_hello-world.groovy +++ b/ui.content.example/src/main/content/jcr_root/conf/acm/settings/script/manual/example/ACME-200_hello-world.groovy @@ -1,6 +1,5 @@ /** - * @description Prints "Hello World!" to the console. - * @author Krystian Panek + * Prints "Hello World!" to the console. */ boolean canRun() { diff --git a/ui.content.example/src/main/content/jcr_root/conf/acm/settings/script/manual/example/ACME-201_inputs.groovy b/ui.content.example/src/main/content/jcr_root/conf/acm/settings/script/manual/example/ACME-201_inputs.groovy index a4b67fce3..10d896393 100644 --- a/ui.content.example/src/main/content/jcr_root/conf/acm/settings/script/manual/example/ACME-201_inputs.groovy +++ b/ui.content.example/src/main/content/jcr_root/conf/acm/settings/script/manual/example/ACME-201_inputs.groovy @@ -1,8 +1,9 @@ /** - * @description Prints animal information to the console based on user input. This is an example of AEM Content Manager script with inputs. + * Prints animal information to the console based on user input. + * This is an example of AEM Content Manager script with inputs. + * * @author Krystian Panek */ - void describeRun() { inputs.string("animalName") { value = "Whiskers"; validator = "(v, a) => a.animalType === 'cat' ? (v && v.startsWith('W') || 'Cat name must start with W!') : true" } From 82ae7e1159c80e0a2a712ea48e22d86ebab25537 Mon Sep 17 00:00:00 2001 From: Krystian Panek Date: Fri, 21 Nov 2025 09:00:29 +0100 Subject: [PATCH 03/14] Script metadata works --- .../vml/es/acm/core/code/CodeMetadata.java | 117 ++++++++++-------- .../dev/vml/es/acm/core/script/Script.java | 5 + .../es/acm/core/code/CodeMetadataTest.java | 7 +- .../example/ACME-202_page-thumbnail.groovy | 1 - .../manual/example/ACME-203_output-csv.groovy | 1 - .../manual/example/ACME-203_output-xls.groovy | 1 - ui.frontend/src/types/script.ts | 3 + 7 files changed, 78 insertions(+), 57 deletions(-) diff --git a/core/src/main/java/dev/vml/es/acm/core/code/CodeMetadata.java b/core/src/main/java/dev/vml/es/acm/core/code/CodeMetadata.java index d597975d9..4a44b5e62 100644 --- a/core/src/main/java/dev/vml/es/acm/core/code/CodeMetadata.java +++ b/core/src/main/java/dev/vml/es/acm/core/code/CodeMetadata.java @@ -9,19 +9,25 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import com.fasterxml.jackson.annotation.JsonAnyGetter; -class CodeMetadata implements Serializable { +public class CodeMetadata implements Serializable { + + public static final CodeMetadata EMPTY = new CodeMetadata(new HashMap<>()); + + private static final Logger LOG = LoggerFactory.getLogger(CodeMetadata.class); private static final Pattern DOC_COMMENT_PATTERN = Pattern.compile( - "/\\*\\*([^*]|\\*(?!/))*\\*/", - Pattern.DOTALL - ); - + "/\\*\\*([^*]|\\*(?!/))*\\*/", + Pattern.DOTALL); + private static final Pattern TAG_PATTERN = Pattern.compile( - "(?m)^\\s*\\*?\\s*@(\\w+)\\s+(.+?)(?=(?m)^\\s*\\*?\\s*@\\w+|\\*/|$)", - Pattern.DOTALL - ); + "(?m)^\\s*\\*?\\s*@(\\w+)\\s+(.+?)(?=(?m)^\\s*\\*?\\s*@\\w+|\\*/|$)", + Pattern.DOTALL); private Map values; @@ -30,61 +36,63 @@ public CodeMetadata(Map values) { } public static CodeMetadata of(Executable executable) { - return parse(executable.getContent()); + try { + return parse(executable.getContent()); + } catch (Exception e) { + LOG.warn("Cannot parse code metadata from executable '{}'!", executable.getId(), e); + return EMPTY; + } } public static CodeMetadata parse(String code) { - if (code == null || code.trim().isEmpty()) { - return new CodeMetadata(Map.of()); - } - - Map metadata = new HashMap<>(); - - // Find the first doc comment (either at the top or after imports) - String docComment = findFirstDocComment(code); - if (docComment != null) { - parseDocComment(docComment, metadata); + if (StringUtils.isNotBlank(code)) { + String docComment = findFirstDocComment(code); + if (docComment != null) { + return new CodeMetadata(parseDocComment(docComment)); + } } - - return new CodeMetadata(metadata); + return EMPTY; } + /** + * Find the first doc comment (either at the top or after imports) + */ private static String findFirstDocComment(String code) { Matcher matcher = DOC_COMMENT_PATTERN.matcher(code); - + // Find first doc comment that's properly separated with newlines while (matcher.find()) { String comment = matcher.group(); int commentStart = matcher.start(); int commentEnd = matcher.end(); - + // Get text after comment (until next non-whitespace or end of line) String afterComment = code.substring(commentEnd); - + // Must have at least one newline after the comment if (!afterComment.matches("^\\s*\\n[\\s\\S]*")) { continue; } - + // Check what follows after the newline(s) String trimmedAfter = afterComment.trim(); - + // Skip if directly followed by describeRun (it's OK to be attached) if (trimmedAfter.startsWith("void describeRun()")) { return comment; } - + // Must have blank line after (double newline) for other cases if (!afterComment.matches("^\\s*\\n\\s*\\n[\\s\\S]*")) { continue; } - + // If there's code before the comment, check for blank line before if (commentStart > 0) { String beforeComment = code.substring(0, commentStart); // Should have imports or package, followed by blank line - if (beforeComment.trim().isEmpty() || - beforeComment.matches("[\\s\\S]*(import|package)[\\s\\S]*\\n\\s*\\n\\s*$")) { + if (beforeComment.trim().isEmpty() || + beforeComment.matches("[\\s\\S]*(import|package)[\\s\\S]*\\n\\s*\\n\\s*$")) { return comment; } } else { @@ -92,55 +100,61 @@ private static String findFirstDocComment(String code) { return comment; } } - + return null; } - private static void parseDocComment(String docComment, Map metadata) { + /** + * Parses the doc comment to extract description and tags. + */ + private static Map parseDocComment(String docComment) { + Map result = new HashMap<>(); + // Remove /** and */ markers and leading comment decorations String content = docComment.replaceAll("^/\\*\\*", "").replaceAll("\\*/$", ""); - + // Extract general description (text before first @tag) - // Look for @tag pattern (@ at start of word boundary, not in middle of text like email) + // Look for @tag pattern (@ at start of word boundary, not in middle of text + // like email) Pattern firstTagPattern = Pattern.compile("(?m)^\\s*\\*?\\s*@\\w+"); Matcher firstTagMatcher = firstTagPattern.matcher(content); - + if (firstTagMatcher.find()) { int firstTagIndex = firstTagMatcher.start(); String description = content.substring(0, firstTagIndex) - .replaceAll("(?m)^\\s*\\*\\s?", "") - .trim(); + .replaceAll("(?m)^\\s*\\*\\s?", "") + .trim(); if (!description.isEmpty()) { - metadata.put("description", description); + result.put("description", description); } } else { // No tags, just description String description = content - .replaceAll("(?m)^\\s*\\*\\s?", "") - .trim(); + .replaceAll("(?m)^\\s*\\*\\s?", "") + .trim(); if (!description.isEmpty()) { - metadata.put("description", description); + result.put("description", description); } } - + // Parse tags Matcher tagMatcher = TAG_PATTERN.matcher(content); - + while (tagMatcher.find()) { String tagName = tagMatcher.group(1); String tagValue = tagMatcher.group(2); - + if (tagValue != null) { tagValue = tagValue - .replaceAll("(?m)^\\s*\\*\\s?", "") // Remove leading * from each line - .trim(); - + .replaceAll("(?m)^\\s*\\*\\s?", "") // Remove leading * from each line + .trim(); + if (!tagValue.isEmpty()) { // Store tag value, use list for potential multiple values - Object existing = metadata.get(tagName); - + Object existing = result.get(tagName); + if (existing == null) { - metadata.put(tagName, tagValue); + result.put(tagName, tagValue); } else if (existing instanceof List) { @SuppressWarnings("unchecked") List list = (List) existing; @@ -150,11 +164,12 @@ private static void parseDocComment(String docComment, Map metad List list = new ArrayList<>(); list.add((String) existing); list.add(tagValue); - metadata.put(tagName, list); + result.put(tagName, list); } } } } + return result; } @JsonAnyGetter diff --git a/core/src/main/java/dev/vml/es/acm/core/script/Script.java b/core/src/main/java/dev/vml/es/acm/core/script/Script.java index df8e975d1..34010961b 100644 --- a/core/src/main/java/dev/vml/es/acm/core/script/Script.java +++ b/core/src/main/java/dev/vml/es/acm/core/script/Script.java @@ -2,6 +2,7 @@ import com.fasterxml.jackson.annotation.JsonIgnore; import dev.vml.es.acm.core.AcmException; +import dev.vml.es.acm.core.code.CodeMetadata; import dev.vml.es.acm.core.code.Executable; import java.io.IOException; import java.io.InputStream; @@ -74,6 +75,10 @@ protected Resource getResource() { return resource; } + public CodeMetadata getMetadata() { + return CodeMetadata.of(this); + } + @Override public int compareTo(Script other) { if (other == null) { diff --git a/core/src/test/java/dev/vml/es/acm/core/code/CodeMetadataTest.java b/core/src/test/java/dev/vml/es/acm/core/code/CodeMetadataTest.java index aebaa31c7..d6d60f819 100644 --- a/core/src/test/java/dev/vml/es/acm/core/code/CodeMetadataTest.java +++ b/core/src/test/java/dev/vml/es/acm/core/code/CodeMetadataTest.java @@ -3,6 +3,7 @@ import org.junit.jupiter.api.Test; import java.io.IOException; +import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; @@ -16,7 +17,7 @@ class CodeMetadataTest { private String readScript(String relativePath) throws IOException { Path scriptPath = SCRIPTS_BASE_PATH.resolve(relativePath); - return Files.readString(scriptPath); + return new String(Files.readAllBytes(scriptPath), StandardCharsets.UTF_8); } @Test @@ -83,7 +84,7 @@ void shouldParseMultipleAuthors() { " * @author Jane Smith\n" + " */\n" + "\n" + - "void run() {\n" + + "void doRun() {\n" + " println \"Hello\"\n" + "}"; CodeMetadata metadata = CodeMetadata.parse(code); @@ -106,7 +107,7 @@ void shouldParseCustomTags() { " * @category migration\n" + " */\n" + "\n" + - "void run() {\n" + + "void doRun() {\n" + " println \"Hello\"\n" + "}"; diff --git a/ui.content.example/src/main/content/jcr_root/conf/acm/settings/script/manual/example/ACME-202_page-thumbnail.groovy b/ui.content.example/src/main/content/jcr_root/conf/acm/settings/script/manual/example/ACME-202_page-thumbnail.groovy index 9c80b0d84..c2106a59c 100644 --- a/ui.content.example/src/main/content/jcr_root/conf/acm/settings/script/manual/example/ACME-202_page-thumbnail.groovy +++ b/ui.content.example/src/main/content/jcr_root/conf/acm/settings/script/manual/example/ACME-202_page-thumbnail.groovy @@ -5,7 +5,6 @@ * * @author Krystian Panek */ - void describeRun() { inputs.path("pagePath") { rootPathExclusive = '/' } inputs.file("pageThumbnailFile") { mimeTypes = ["image/jpeg"] } diff --git a/ui.content.example/src/main/content/jcr_root/conf/acm/settings/script/manual/example/ACME-203_output-csv.groovy b/ui.content.example/src/main/content/jcr_root/conf/acm/settings/script/manual/example/ACME-203_output-csv.groovy index 62718999d..35699b207 100644 --- a/ui.content.example/src/main/content/jcr_root/conf/acm/settings/script/manual/example/ACME-203_output-csv.groovy +++ b/ui.content.example/src/main/content/jcr_root/conf/acm/settings/script/manual/example/ACME-203_output-csv.groovy @@ -5,7 +5,6 @@ import java.util.Random * @description Generates a CSV report of users with random names and birth dates. * @author Krystian Panek */ - void describeRun() { inputs.integerNumber("count") { label = "Users to generate"; min = 1; value = 10000 } inputs.text("firstNames") { label = "First names"; description = "One first name per line"; value = "John\nJane\nJack\nAlice\nBob"} diff --git a/ui.content.example/src/main/content/jcr_root/conf/acm/settings/script/manual/example/ACME-203_output-xls.groovy b/ui.content.example/src/main/content/jcr_root/conf/acm/settings/script/manual/example/ACME-203_output-xls.groovy index e296771d6..6a4bd6255 100644 --- a/ui.content.example/src/main/content/jcr_root/conf/acm/settings/script/manual/example/ACME-203_output-xls.groovy +++ b/ui.content.example/src/main/content/jcr_root/conf/acm/settings/script/manual/example/ACME-203_output-xls.groovy @@ -9,7 +9,6 @@ import java.util.Random * @description Generates an XLS report of users with random names and birth dates. * @author Krystian Panek */ - void describeRun() { inputs.integerNumber("count") { label = "Users to generate"; min = 1; value = 100000 } inputs.text("firstNames") { label = "First names"; description = "One first name per line"; value = "John\nJane\nJack\nAlice\nBob"} diff --git a/ui.frontend/src/types/script.ts b/ui.frontend/src/types/script.ts index ecb7dc882..e8dce0ebe 100644 --- a/ui.frontend/src/types/script.ts +++ b/ui.frontend/src/types/script.ts @@ -14,8 +14,11 @@ export type Script = { path: string; name: string; content: string; + metadata: ScriptMetadata; }; +export type ScriptMetadata = Record + export type ScriptStats = { path: string; statusCount: { From f062a6969a7ce33a9471d8a22cc33d189dd33a18 Mon Sep 17 00:00:00 2001 From: Krystian Panek Date: Fri, 21 Nov 2025 09:09:40 +0100 Subject: [PATCH 04/14] Script metadata rendered --- ui.frontend/src/components/ScriptMetadata.tsx | 60 +++++++++++++++++++ ui.frontend/src/pages/ScriptView.tsx | 25 ++++---- 2 files changed, 74 insertions(+), 11 deletions(-) create mode 100644 ui.frontend/src/components/ScriptMetadata.tsx diff --git a/ui.frontend/src/components/ScriptMetadata.tsx b/ui.frontend/src/components/ScriptMetadata.tsx new file mode 100644 index 000000000..606e1867f --- /dev/null +++ b/ui.frontend/src/components/ScriptMetadata.tsx @@ -0,0 +1,60 @@ +import { Flex, LabeledValue, Text, View } from '@adobe/react-spectrum'; +import { ScriptMetadata as ScriptMetadataType } from '../types/script'; + +type ScriptMetadataProps = { + metadata: ScriptMetadataType; +}; + +const ScriptMetadata = ({ metadata }: ScriptMetadataProps) => { + const entries = Object.entries(metadata); + + if (entries.length === 0) { + return null; + } + + const renderEntries = () => { + const result: JSX.Element[] = []; + + entries.forEach(([key, value]) => { + const label = key.charAt(0).toUpperCase() + key.slice(1); + + if (Array.isArray(value)) { + value.forEach((item) => { + result.push( + {item}} + /> + ); + }); + } else { + result.push( + {value}} + /> + ); + } + }); + + return result; + }; + + return ( + + + {renderEntries()} + + + ); +}; + +export default ScriptMetadata; diff --git a/ui.frontend/src/pages/ScriptView.tsx b/ui.frontend/src/pages/ScriptView.tsx index bb78f0092..ecfc2ac66 100644 --- a/ui.frontend/src/pages/ScriptView.tsx +++ b/ui.frontend/src/pages/ScriptView.tsx @@ -1,5 +1,4 @@ import { Button, ButtonGroup, Content, Flex, IllustratedMessage, Item, LabeledValue, ProgressBar, TabList, TabPanels, Tabs, Text, View } from '@adobe/react-spectrum'; -import { Field } from '@react-spectrum/label'; import { ToastQueue } from '@react-spectrum/toast'; import NotFound from '@spectrum-icons/illustrations/NotFound'; import Copy from '@spectrum-icons/workflow/Copy'; @@ -9,6 +8,7 @@ import { useEffect, useState } from 'react'; import { useNavigate, useParams } from 'react-router-dom'; import CodeEditor from '../components/CodeEditor'; import CodeExecuteButton from '../components/CodeExecuteButton'; +import ScriptMetadata from '../components/ScriptMetadata'; import { useFeatureEnabled } from '../hooks/app.ts'; import { NavigationSearchParams, useNavigationTab } from '../hooks/navigation'; import { InputValues } from '../types/input.ts'; @@ -150,16 +150,19 @@ const ScriptView = () => { - - - -
- {script.name} -
-
- -
-
+ + + + + + + + {script.metadata && Object.keys(script.metadata).length > 0 && ( + + + + )} + From 3a0dce69ad2efcf831055188dabe1045afa57885 Mon Sep 17 00:00:00 2001 From: Krystian Panek Date: Fri, 21 Nov 2025 09:51:15 +0100 Subject: [PATCH 05/14] UI --- .../main/java/dev/vml/es/acm/core/code/CodeMetadata.java | 6 +++--- .../script/manual/example/ACM-1_classes_jms.groovy | 2 +- .../script/manual/example/ACM-1_classes_rtjar.groovy | 2 +- .../settings/script/manual/example/ACME-201_inputs.groovy | 2 +- .../script/manual/example/ACME-202_page-thumbnail.groovy | 2 +- .../script/manual/example/ACME-203_output-csv.groovy | 2 +- .../script/manual/example/ACME-203_output-xls.groovy | 2 +- ui.frontend/src/components/ScriptMetadata.tsx | 7 ++++--- 8 files changed, 13 insertions(+), 12 deletions(-) diff --git a/core/src/main/java/dev/vml/es/acm/core/code/CodeMetadata.java b/core/src/main/java/dev/vml/es/acm/core/code/CodeMetadata.java index 4a44b5e62..77e76adfc 100644 --- a/core/src/main/java/dev/vml/es/acm/core/code/CodeMetadata.java +++ b/core/src/main/java/dev/vml/es/acm/core/code/CodeMetadata.java @@ -3,7 +3,7 @@ import java.io.Serializable; import java.util.ArrayList; -import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.regex.Matcher; @@ -17,7 +17,7 @@ public class CodeMetadata implements Serializable { - public static final CodeMetadata EMPTY = new CodeMetadata(new HashMap<>()); + public static final CodeMetadata EMPTY = new CodeMetadata(new LinkedHashMap<>()); private static final Logger LOG = LoggerFactory.getLogger(CodeMetadata.class); @@ -108,7 +108,7 @@ private static String findFirstDocComment(String code) { * Parses the doc comment to extract description and tags. */ private static Map parseDocComment(String docComment) { - Map result = new HashMap<>(); + Map result = new LinkedHashMap<>(); // Remove /** and */ markers and leading comment decorations String content = docComment.replaceAll("^/\\*\\*", "").replaceAll("\\*/$", ""); diff --git a/ui.content.example/src/main/content/jcr_root/conf/acm/settings/script/manual/example/ACM-1_classes_jms.groovy b/ui.content.example/src/main/content/jcr_root/conf/acm/settings/script/manual/example/ACM-1_classes_jms.groovy index 9c413f6f9..7bf6fb338 100644 --- a/ui.content.example/src/main/content/jcr_root/conf/acm/settings/script/manual/example/ACM-1_classes_jms.groovy +++ b/ui.content.example/src/main/content/jcr_root/conf/acm/settings/script/manual/example/ACM-1_classes_jms.groovy @@ -6,7 +6,7 @@ * - print the list (for debugging purposes), * - save it directly in the repository in expected path. * - * @author Krystian Panek + * @author */ import dev.vml.es.acm.core.assist.JavaDictionary diff --git a/ui.content.example/src/main/content/jcr_root/conf/acm/settings/script/manual/example/ACM-1_classes_rtjar.groovy b/ui.content.example/src/main/content/jcr_root/conf/acm/settings/script/manual/example/ACM-1_classes_rtjar.groovy index aeab81487..f419c614e 100644 --- a/ui.content.example/src/main/content/jcr_root/conf/acm/settings/script/manual/example/ACM-1_classes_rtjar.groovy +++ b/ui.content.example/src/main/content/jcr_root/conf/acm/settings/script/manual/example/ACM-1_classes_rtjar.groovy @@ -6,7 +6,7 @@ * - print the list (for debugging purposes), * - save it directly in the repository in expected path. * - * @author Krystian Panek + * @author */ import dev.vml.es.acm.core.assist.JavaDictionary diff --git a/ui.content.example/src/main/content/jcr_root/conf/acm/settings/script/manual/example/ACME-201_inputs.groovy b/ui.content.example/src/main/content/jcr_root/conf/acm/settings/script/manual/example/ACME-201_inputs.groovy index 10d896393..9d15f0c6d 100644 --- a/ui.content.example/src/main/content/jcr_root/conf/acm/settings/script/manual/example/ACME-201_inputs.groovy +++ b/ui.content.example/src/main/content/jcr_root/conf/acm/settings/script/manual/example/ACME-201_inputs.groovy @@ -2,7 +2,7 @@ * Prints animal information to the console based on user input. * This is an example of AEM Content Manager script with inputs. * - * @author Krystian Panek + * @author */ void describeRun() { inputs.string("animalName") { value = "Whiskers"; diff --git a/ui.content.example/src/main/content/jcr_root/conf/acm/settings/script/manual/example/ACME-202_page-thumbnail.groovy b/ui.content.example/src/main/content/jcr_root/conf/acm/settings/script/manual/example/ACME-202_page-thumbnail.groovy index c2106a59c..66aa62d19 100644 --- a/ui.content.example/src/main/content/jcr_root/conf/acm/settings/script/manual/example/ACME-202_page-thumbnail.groovy +++ b/ui.content.example/src/main/content/jcr_root/conf/acm/settings/script/manual/example/ACME-202_page-thumbnail.groovy @@ -3,7 +3,7 @@ * Deletes the existing thumbnails and saves a new one. * File must be a JPEG image. * - * @author Krystian Panek + * @author */ void describeRun() { inputs.path("pagePath") { rootPathExclusive = '/' } diff --git a/ui.content.example/src/main/content/jcr_root/conf/acm/settings/script/manual/example/ACME-203_output-csv.groovy b/ui.content.example/src/main/content/jcr_root/conf/acm/settings/script/manual/example/ACME-203_output-csv.groovy index 35699b207..9fd14eae4 100644 --- a/ui.content.example/src/main/content/jcr_root/conf/acm/settings/script/manual/example/ACME-203_output-csv.groovy +++ b/ui.content.example/src/main/content/jcr_root/conf/acm/settings/script/manual/example/ACME-203_output-csv.groovy @@ -3,7 +3,7 @@ import java.util.Random /** * @description Generates a CSV report of users with random names and birth dates. - * @author Krystian Panek + * @author */ void describeRun() { inputs.integerNumber("count") { label = "Users to generate"; min = 1; value = 10000 } diff --git a/ui.content.example/src/main/content/jcr_root/conf/acm/settings/script/manual/example/ACME-203_output-xls.groovy b/ui.content.example/src/main/content/jcr_root/conf/acm/settings/script/manual/example/ACME-203_output-xls.groovy index 6a4bd6255..ba069b4db 100644 --- a/ui.content.example/src/main/content/jcr_root/conf/acm/settings/script/manual/example/ACME-203_output-xls.groovy +++ b/ui.content.example/src/main/content/jcr_root/conf/acm/settings/script/manual/example/ACME-203_output-xls.groovy @@ -7,7 +7,7 @@ import java.util.Random /** * @description Generates an XLS report of users with random names and birth dates. - * @author Krystian Panek + * @author */ void describeRun() { inputs.integerNumber("count") { label = "Users to generate"; min = 1; value = 100000 } diff --git a/ui.frontend/src/components/ScriptMetadata.tsx b/ui.frontend/src/components/ScriptMetadata.tsx index 606e1867f..220c6c76b 100644 --- a/ui.frontend/src/components/ScriptMetadata.tsx +++ b/ui.frontend/src/components/ScriptMetadata.tsx @@ -1,5 +1,6 @@ -import { Flex, LabeledValue, Text, View } from '@adobe/react-spectrum'; +import { Flex, LabeledValue, View } from '@adobe/react-spectrum'; import { ScriptMetadata as ScriptMetadataType } from '../types/script'; +import Markdown from './Markdown'; type ScriptMetadataProps = { metadata: ScriptMetadataType; @@ -24,7 +25,7 @@ const ScriptMetadata = ({ metadata }: ScriptMetadataProps) => { {item}} + value={} /> ); }); @@ -33,7 +34,7 @@ const ScriptMetadata = ({ metadata }: ScriptMetadataProps) => { {value}} + value={} /> ); } From 15a41bbd52c277eafe1ca639278376d444abbbd3 Mon Sep 17 00:00:00 2001 From: Krystian Panek Date: Fri, 21 Nov 2025 09:53:38 +0100 Subject: [PATCH 06/14] Layout --- ui.frontend/src/components/ScriptMetadata.tsx | 61 ------------------- ui.frontend/src/pages/ScriptView.tsx | 32 ++++++++-- 2 files changed, 27 insertions(+), 66 deletions(-) delete mode 100644 ui.frontend/src/components/ScriptMetadata.tsx diff --git a/ui.frontend/src/components/ScriptMetadata.tsx b/ui.frontend/src/components/ScriptMetadata.tsx deleted file mode 100644 index 220c6c76b..000000000 --- a/ui.frontend/src/components/ScriptMetadata.tsx +++ /dev/null @@ -1,61 +0,0 @@ -import { Flex, LabeledValue, View } from '@adobe/react-spectrum'; -import { ScriptMetadata as ScriptMetadataType } from '../types/script'; -import Markdown from './Markdown'; - -type ScriptMetadataProps = { - metadata: ScriptMetadataType; -}; - -const ScriptMetadata = ({ metadata }: ScriptMetadataProps) => { - const entries = Object.entries(metadata); - - if (entries.length === 0) { - return null; - } - - const renderEntries = () => { - const result: JSX.Element[] = []; - - entries.forEach(([key, value]) => { - const label = key.charAt(0).toUpperCase() + key.slice(1); - - if (Array.isArray(value)) { - value.forEach((item) => { - result.push( - } - /> - ); - }); - } else { - result.push( - } - /> - ); - } - }); - - return result; - }; - - return ( - - - {renderEntries()} - - - ); -}; - -export default ScriptMetadata; diff --git a/ui.frontend/src/pages/ScriptView.tsx b/ui.frontend/src/pages/ScriptView.tsx index ecfc2ac66..019e1194a 100644 --- a/ui.frontend/src/pages/ScriptView.tsx +++ b/ui.frontend/src/pages/ScriptView.tsx @@ -8,7 +8,7 @@ import { useEffect, useState } from 'react'; import { useNavigate, useParams } from 'react-router-dom'; import CodeEditor from '../components/CodeEditor'; import CodeExecuteButton from '../components/CodeExecuteButton'; -import ScriptMetadata from '../components/ScriptMetadata'; +import Markdown from '../components/Markdown'; import { useFeatureEnabled } from '../hooks/app.ts'; import { NavigationSearchParams, useNavigationTab } from '../hooks/navigation'; import { InputValues } from '../types/input.ts'; @@ -150,16 +150,38 @@ const ScriptView = () => { - - + + {script.metadata && Object.keys(script.metadata).length > 0 && ( - - + + + {Object.entries(script.metadata).map(([key, value]) => { + const label = key.charAt(0).toUpperCase() + key.slice(1); + + if (Array.isArray(value)) { + return value.map((item, index) => ( + } + /> + )); + } + + return ( + } + /> + ); + })} + )} From 625e58944766a0d81939597d1c73298bccf8d8ab Mon Sep 17 00:00:00 2001 From: Krystian Panek Date: Fri, 21 Nov 2025 10:54:37 +0100 Subject: [PATCH 07/14] Reusable metadata --- .../java/dev/vml/es/acm/core/code/Code.java | 7 +- .../vml/es/acm/core/code/CodeMetadata.java | 27 ++---- .../dev/vml/es/acm/core/code/Executable.java | 5 +- .../dev/vml/es/acm/core/script/Script.java | 9 +- .../es/acm/core/code/CodeMetadataTest.java | 57 ++++++------- .../src/components/ExecutableMetadata.tsx | 65 ++++++++++++++ ui.frontend/src/components/InfoCard.tsx | 25 ++++++ ui.frontend/src/pages/ExecutionView.tsx | 84 ++++++++++++------- ui.frontend/src/pages/ScriptView.tsx | 43 ++-------- ui.frontend/src/types/executable.ts | 5 ++ ui.frontend/src/types/script.ts | 8 +- 11 files changed, 210 insertions(+), 125 deletions(-) create mode 100644 ui.frontend/src/components/ExecutableMetadata.tsx create mode 100644 ui.frontend/src/components/InfoCard.tsx diff --git a/core/src/main/java/dev/vml/es/acm/core/code/Code.java b/core/src/main/java/dev/vml/es/acm/core/code/Code.java index d4d309376..b90bc2823 100644 --- a/core/src/main/java/dev/vml/es/acm/core/code/Code.java +++ b/core/src/main/java/dev/vml/es/acm/core/code/Code.java @@ -8,7 +8,7 @@ import org.apache.sling.event.jobs.Job; /** - * Represents a code that can be executed. + * Represents any code (e.g text from interactive console) that can be executed. */ public class Code implements Executable { @@ -61,6 +61,11 @@ public String getContent() { return content; } + @Override + public CodeMetadata getMetadata() { + return CodeMetadata.of(this); + } + public String toString() { return new ToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE) .append("id", id) diff --git a/core/src/main/java/dev/vml/es/acm/core/code/CodeMetadata.java b/core/src/main/java/dev/vml/es/acm/core/code/CodeMetadata.java index 77e76adfc..c08ddc42b 100644 --- a/core/src/main/java/dev/vml/es/acm/core/code/CodeMetadata.java +++ b/core/src/main/java/dev/vml/es/acm/core/code/CodeMetadata.java @@ -1,6 +1,6 @@ - package dev.vml.es.acm.core.code; +import com.fasterxml.jackson.annotation.JsonAnyGetter; import java.io.Serializable; import java.util.ArrayList; import java.util.LinkedHashMap; @@ -8,26 +8,20 @@ import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; - import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import com.fasterxml.jackson.annotation.JsonAnyGetter; - public class CodeMetadata implements Serializable { public static final CodeMetadata EMPTY = new CodeMetadata(new LinkedHashMap<>()); private static final Logger LOG = LoggerFactory.getLogger(CodeMetadata.class); - private static final Pattern DOC_COMMENT_PATTERN = Pattern.compile( - "/\\*\\*([^*]|\\*(?!/))*\\*/", - Pattern.DOTALL); + private static final Pattern DOC_COMMENT_PATTERN = Pattern.compile("/\\*\\*([^*]|\\*(?!/))*\\*/", Pattern.DOTALL); - private static final Pattern TAG_PATTERN = Pattern.compile( - "(?m)^\\s*\\*?\\s*@(\\w+)\\s+(.+?)(?=(?m)^\\s*\\*?\\s*@\\w+|\\*/|$)", - Pattern.DOTALL); + private static final Pattern TAG_PATTERN = + Pattern.compile("(?m)^\\s*\\*?\\s*@(\\w+)\\s+(.+?)(?=(?m)^\\s*\\*?\\s*@\\w+|\\*/|$)", Pattern.DOTALL); private Map values; @@ -91,8 +85,8 @@ private static String findFirstDocComment(String code) { if (commentStart > 0) { String beforeComment = code.substring(0, commentStart); // Should have imports or package, followed by blank line - if (beforeComment.trim().isEmpty() || - beforeComment.matches("[\\s\\S]*(import|package)[\\s\\S]*\\n\\s*\\n\\s*$")) { + if (beforeComment.trim().isEmpty() + || beforeComment.matches("[\\s\\S]*(import|package)[\\s\\S]*\\n\\s*\\n\\s*$")) { return comment; } } else { @@ -129,9 +123,7 @@ private static Map parseDocComment(String docComment) { } } else { // No tags, just description - String description = content - .replaceAll("(?m)^\\s*\\*\\s?", "") - .trim(); + String description = content.replaceAll("(?m)^\\s*\\*\\s?", "").trim(); if (!description.isEmpty()) { result.put("description", description); } @@ -145,8 +137,7 @@ private static Map parseDocComment(String docComment) { String tagValue = tagMatcher.group(2); if (tagValue != null) { - tagValue = tagValue - .replaceAll("(?m)^\\s*\\*\\s?", "") // Remove leading * from each line + tagValue = tagValue.replaceAll("(?m)^\\s*\\*\\s?", "") // Remove leading * from each line .trim(); if (!tagValue.isEmpty()) { @@ -176,4 +167,4 @@ private static Map parseDocComment(String docComment) { public Map getValues() { return values; } -} \ No newline at end of file +} diff --git a/core/src/main/java/dev/vml/es/acm/core/code/Executable.java b/core/src/main/java/dev/vml/es/acm/core/code/Executable.java index 9bfa0af12..925b5f745 100644 --- a/core/src/main/java/dev/vml/es/acm/core/code/Executable.java +++ b/core/src/main/java/dev/vml/es/acm/core/code/Executable.java @@ -1,6 +1,5 @@ package dev.vml.es.acm.core.code; -import dev.vml.es.acm.core.AcmException; import java.io.Serializable; public interface Executable extends Serializable { @@ -11,5 +10,7 @@ public interface Executable extends Serializable { String getId(); - String getContent() throws AcmException; + String getContent(); + + CodeMetadata getMetadata(); } diff --git a/core/src/main/java/dev/vml/es/acm/core/script/Script.java b/core/src/main/java/dev/vml/es/acm/core/script/Script.java index 34010961b..ffcdc51d2 100644 --- a/core/src/main/java/dev/vml/es/acm/core/script/Script.java +++ b/core/src/main/java/dev/vml/es/acm/core/script/Script.java @@ -56,6 +56,11 @@ public String getContent() throws AcmException { } } + @Override + public CodeMetadata getMetadata() { + return CodeMetadata.of(this); + } + @JsonIgnore public String getPath() { return resource.getPath(); @@ -75,10 +80,6 @@ protected Resource getResource() { return resource; } - public CodeMetadata getMetadata() { - return CodeMetadata.of(this); - } - @Override public int compareTo(Script other) { if (other == null) { diff --git a/core/src/test/java/dev/vml/es/acm/core/code/CodeMetadataTest.java b/core/src/test/java/dev/vml/es/acm/core/code/CodeMetadataTest.java index d6d60f819..a064ae7a6 100644 --- a/core/src/test/java/dev/vml/es/acm/core/code/CodeMetadataTest.java +++ b/core/src/test/java/dev/vml/es/acm/core/code/CodeMetadataTest.java @@ -1,6 +1,6 @@ package dev.vml.es.acm.core.code; -import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.*; import java.io.IOException; import java.nio.charset.StandardCharsets; @@ -8,12 +8,12 @@ import java.nio.file.Path; import java.nio.file.Paths; import java.util.List; - -import static org.junit.jupiter.api.Assertions.*; +import org.junit.jupiter.api.Test; class CodeMetadataTest { - private static final Path SCRIPTS_BASE_PATH = Paths.get("../ui.content.example/src/main/content/jcr_root/conf/acm/settings/script"); + private static final Path SCRIPTS_BASE_PATH = + Paths.get("../ui.content.example/src/main/content/jcr_root/conf/acm/settings/script"); private String readScript(String relativePath) throws IOException { Path scriptPath = SCRIPTS_BASE_PATH.resolve(relativePath); @@ -39,8 +39,8 @@ void shouldParseHelloWorldScript() throws IOException { String code = readScript("manual/example/ACME-200_hello-world.groovy"); CodeMetadata metadata = CodeMetadata.parse(code); - assertEquals("Prints \"Hello World!\" to the console.", - metadata.getValues().get("description")); + assertEquals( + "Prints \"Hello World!\" to the console.", metadata.getValues().get("description")); } @Test @@ -52,8 +52,7 @@ void shouldParseInputsScript() throws IOException { assertNotNull(description); assertTrue(description.contains("Prints animal information to the console based on user input")); assertTrue(description.contains("This is an example of AEM Content Manager script with inputs")); - assertEquals("Krystian Panek ", - metadata.getValues().get("author")); + assertEquals("", metadata.getValues().get("author")); } @Test @@ -65,8 +64,7 @@ void shouldParsePageThumbnailScript() throws IOException { assertNotNull(description); assertTrue(description.contains("Updates the thumbnail")); assertTrue(description.contains("File must be a JPEG image")); - assertEquals("Krystian Panek ", - metadata.getValues().get("author")); + assertEquals("", metadata.getValues().get("author")); } @Test @@ -79,14 +77,13 @@ void shouldParseScriptWithoutDocComment() throws IOException { @Test void shouldParseMultipleAuthors() { - String code = "/**\n" + - " * @author John Doe\n" + - " * @author Jane Smith\n" + - " */\n" + - "\n" + - "void doRun() {\n" + - " println \"Hello\"\n" + - "}"; + String code = "/**\n" + " * @author John Doe\n" + + " * @author Jane Smith\n" + + " */\n" + + "\n" + + "void doRun() {\n" + + " println \"Hello\"\n" + + "}"; CodeMetadata metadata = CodeMetadata.parse(code); Object authors = metadata.getValues().get("author"); assertTrue(authors instanceof List); @@ -100,23 +97,21 @@ void shouldParseMultipleAuthors() { @Test void shouldParseCustomTags() { - String code = "/**\n" + - " * @description Custom script with metadata\n" + - " * @version 1.0.0\n" + - " * @since 2025-01-01\n" + - " * @category migration\n" + - " */\n" + - "\n" + - "void doRun() {\n" + - " println \"Hello\"\n" + - "}"; - + String code = "/**\n" + " * @description Custom script with metadata\n" + + " * @version 1.0.0\n" + + " * @since 2025-01-01\n" + + " * @category migration\n" + + " */\n" + + "\n" + + "void doRun() {\n" + + " println \"Hello\"\n" + + "}"; + CodeMetadata metadata = CodeMetadata.parse(code); - + assertEquals("Custom script with metadata", metadata.getValues().get("description")); assertEquals("1.0.0", metadata.getValues().get("version")); assertEquals("2025-01-01", metadata.getValues().get("since")); assertEquals("migration", metadata.getValues().get("category")); } } - diff --git a/ui.frontend/src/components/ExecutableMetadata.tsx b/ui.frontend/src/components/ExecutableMetadata.tsx new file mode 100644 index 000000000..fd701595c --- /dev/null +++ b/ui.frontend/src/components/ExecutableMetadata.tsx @@ -0,0 +1,65 @@ +import { Badge, Content, ContextualHelp, Flex, Heading, LabeledValue, View } from '@adobe/react-spectrum'; +import { Field } from '@react-spectrum/label'; +import InfoOutline from '@spectrum-icons/workflow/InfoOutline'; +import { Text } from '@adobe/react-spectrum'; +import React from 'react'; +import { ExecutableMetadata as ExecutableMetadataType } from '../types/executable'; +import Markdown from './Markdown'; + +type ExecutableMetadataProps = { + metadata: ExecutableMetadataType | null | undefined; +}; + +const ExecutableMetadata: React.FC = ({ metadata }) => { + if (!metadata || Object.keys(metadata).length === 0) { + return ( + +
+ + + + Not available + + + Defining metadata + + + Add a JavaDoc or GroovyDoc comment block at the top of your script file: + +
+                  {`/**
+ * Explain purpose here
+ *
+ * @author Your Name 
+ * @version 1.0
+ */`}
+                
+ + The comment must be followed by a blank line. Description is extracted from text before any @tags. + +
+
+
+
+
+ ); + } + + return ( + <> + {Object.entries(metadata).map(([key, value]) => { + const label = key.charAt(0).toUpperCase() + key.slice(1); + + if (Array.isArray(value)) { + return value.map((item, index) => ( + } /> + )); + } + + return } />; + })} + + ); +}; + +export default ExecutableMetadata; diff --git a/ui.frontend/src/components/InfoCard.tsx b/ui.frontend/src/components/InfoCard.tsx new file mode 100644 index 000000000..0fe5dc9e6 --- /dev/null +++ b/ui.frontend/src/components/InfoCard.tsx @@ -0,0 +1,25 @@ +import { Flex, View } from '@adobe/react-spectrum'; +import { ReactNode } from 'react'; + +type InfoCardProps = { + children: ReactNode; +}; + +const InfoCard = ({ children }: InfoCardProps) => { + return ( + + + {children} + + + ); +}; + +export default InfoCard; diff --git a/ui.frontend/src/pages/ExecutionView.tsx b/ui.frontend/src/pages/ExecutionView.tsx index a6b7cbad2..d16c401d5 100644 --- a/ui.frontend/src/pages/ExecutionView.tsx +++ b/ui.frontend/src/pages/ExecutionView.tsx @@ -1,5 +1,4 @@ import { Badge, Button, ButtonGroup, Content, Flex, IllustratedMessage, Item, LabeledValue, ProgressBar, Switch, TabList, TabPanels, Tabs, Text, View } from '@adobe/react-spectrum'; -import { Editor } from '@monaco-editor/react'; import { Field } from '@react-spectrum/label'; import { ToastQueue } from '@react-spectrum/toast'; import NotFound from '@spectrum-icons/illustrations/NotFound'; @@ -11,13 +10,17 @@ import Print from '@spectrum-icons/workflow/Print'; import { useState } from 'react'; import { useNavigate, useParams } from 'react-router-dom'; import CodeEditor from '../components/CodeEditor.tsx'; +import CodeTextarea from '../components/CodeTextarea'; import ExecutableIdValue from '../components/ExecutableIdValue'; +import ExecutableMetadata from '../components/ExecutableMetadata'; import ExecutionAbortButton from '../components/ExecutionAbortButton'; import ExecutionCopyOutputButton from '../components/ExecutionCopyOutputButton'; import ExecutionProgressBar from '../components/ExecutionProgressBar'; import ExecutionReviewOutputsButton from '../components/ExecutionReviewOutputsButton.tsx'; import ExecutionStatusBadge from '../components/ExecutionStatusBadge'; +import InfoCard from '../components/InfoCard'; import Toggle from '../components/Toggle.tsx'; +import UserInfo from '../components/UserInfo'; import { useAppState } from '../hooks/app.ts'; import { useExecutionPolling } from '../hooks/execution'; import { useFormatter } from '../hooks/formatter'; @@ -100,49 +103,72 @@ const ExecutionView = () => {
- - - - - - -
- -
-
-
-
-
- - + {/* Row 1: Execution Info */} + + + + +
+ +
+
+ +
+ +
+
+
+ -
-
- - - -
+ + + {/* Row 2: Executable Info */} + + + +
- -
+ + + + + + + {/* Row 3: I/O */} + + + +
{Objects.isEmpty(execution.inputs) ? ( Not described ) : ( - - - + )}
-
- + + + +
+ {Objects.isEmpty(execution.outputs) ? ( + + + Not generated + + ) : ( + + )} +
+
+
+ diff --git a/ui.frontend/src/pages/ScriptView.tsx b/ui.frontend/src/pages/ScriptView.tsx index 019e1194a..02a0081b7 100644 --- a/ui.frontend/src/pages/ScriptView.tsx +++ b/ui.frontend/src/pages/ScriptView.tsx @@ -8,7 +8,8 @@ import { useEffect, useState } from 'react'; import { useNavigate, useParams } from 'react-router-dom'; import CodeEditor from '../components/CodeEditor'; import CodeExecuteButton from '../components/CodeExecuteButton'; -import Markdown from '../components/Markdown'; +import ExecutableMetadata from '../components/ExecutableMetadata'; +import InfoCard from '../components/InfoCard'; import { useFeatureEnabled } from '../hooks/app.ts'; import { NavigationSearchParams, useNavigationTab } from '../hooks/navigation'; import { InputValues } from '../types/input.ts'; @@ -151,39 +152,13 @@ const ScriptView = () => { - - - - - - - {script.metadata && Object.keys(script.metadata).length > 0 && ( - - - {Object.entries(script.metadata).map(([key, value]) => { - const label = key.charAt(0).toUpperCase() + key.slice(1); - - if (Array.isArray(value)) { - return value.map((item, index) => ( - } - /> - )); - } - - return ( - } - /> - ); - })} - - - )} + + + + + + + diff --git a/ui.frontend/src/types/executable.ts b/ui.frontend/src/types/executable.ts index 8dc52f227..23ca07a74 100644 --- a/ui.frontend/src/types/executable.ts +++ b/ui.frontend/src/types/executable.ts @@ -3,7 +3,11 @@ import { ScriptRoot } from './script'; export type Executable = { id: string; content: string; + metadata: ExecutableMetadata; }; + +export type ExecutableMetadata = Record + export const ExecutableIdConsole = 'console'; export function isExecutableConsole(id: string): boolean { @@ -13,3 +17,4 @@ export function isExecutableConsole(id: string): boolean { export function isExecutableScript(id: string): boolean { return id.startsWith(ScriptRoot); } + diff --git a/ui.frontend/src/types/script.ts b/ui.frontend/src/types/script.ts index e8dce0ebe..4cccd4917 100644 --- a/ui.frontend/src/types/script.ts +++ b/ui.frontend/src/types/script.ts @@ -1,4 +1,5 @@ import { ExecutionStatus } from './execution'; +import { Executable } from './executable'; import { ExecutionSummary } from './main'; export enum ScriptType { @@ -8,17 +9,12 @@ export enum ScriptType { MOCK = 'MOCK', } -export type Script = { - id: string; +export type Script = Executable & { type: ScriptType; path: string; name: string; - content: string; - metadata: ScriptMetadata; }; -export type ScriptMetadata = Record - export type ScriptStats = { path: string; statusCount: { From 30f59a0568b641d37d311986a47dbbca6ae4f5d8 Mon Sep 17 00:00:00 2001 From: Krystian Panek Date: Fri, 21 Nov 2025 11:15:37 +0100 Subject: [PATCH 08/14] Contextual help --- .../src/components/ExecutableMetadata.tsx | 10 +-- .../src/components/ExecutionInputs.tsx | 61 +++++++++++++++++++ .../src/components/ExecutionOutputs.tsx | 61 +++++++++++++++++++ ui.frontend/src/pages/ExecutionView.tsx | 33 ++-------- 4 files changed, 132 insertions(+), 33 deletions(-) create mode 100644 ui.frontend/src/components/ExecutionInputs.tsx create mode 100644 ui.frontend/src/components/ExecutionOutputs.tsx diff --git a/ui.frontend/src/components/ExecutableMetadata.tsx b/ui.frontend/src/components/ExecutableMetadata.tsx index fd701595c..d97662dbc 100644 --- a/ui.frontend/src/components/ExecutableMetadata.tsx +++ b/ui.frontend/src/components/ExecutableMetadata.tsx @@ -1,6 +1,6 @@ import { Badge, Content, ContextualHelp, Flex, Heading, LabeledValue, View } from '@adobe/react-spectrum'; import { Field } from '@react-spectrum/label'; -import InfoOutline from '@spectrum-icons/workflow/InfoOutline'; +import DataUnavailable from '@spectrum-icons/workflow/DataUnavailable'; import { Text } from '@adobe/react-spectrum'; import React from 'react'; import { ExecutableMetadata as ExecutableMetadataType } from '../types/executable'; @@ -17,23 +17,23 @@ const ExecutableMetadata: React.FC = ({ metadata }) =>
- + Not available Defining metadata - Add a JavaDoc or GroovyDoc comment block at the top of your script file: + Use a JavaDoc or GroovyDoc comment block at the top of your script file: -
+                

                   {`/**
  * Explain purpose here
  *
  * @author Your Name 
  * @version 1.0
  */`}
-                
+
The comment must be followed by a blank line. Description is extracted from text before any @tags. diff --git a/ui.frontend/src/components/ExecutionInputs.tsx b/ui.frontend/src/components/ExecutionInputs.tsx new file mode 100644 index 000000000..6b4240a36 --- /dev/null +++ b/ui.frontend/src/components/ExecutionInputs.tsx @@ -0,0 +1,61 @@ +import { Badge, Content, ContextualHelp, Flex, Heading, View } from '@adobe/react-spectrum'; +import { Field } from '@react-spectrum/label'; +import Download from '@spectrum-icons/workflow/Download'; +import { Text } from '@adobe/react-spectrum'; +import React from 'react'; +import CodeTextarea from './CodeTextarea'; +import { Objects } from '../utils/objects'; + +type ExecutionInputsProps = { + inputs: Record | null | undefined; +}; + +const ExecutionInputs: React.FC = ({ inputs }) => { + const isEmpty = Objects.isEmpty(inputs ?? undefined); + const count = isEmpty ? 0 : Object.keys(inputs!).length; + + return ( + +
+ {isEmpty ? ( + + + + Not described + + + Defining inputs + + + Use describeRun() method to collect values from users before execution: + +

+                  {`void describeRun() {
+  inputs.path("pagePath") { 
+    rootPathExclusive = '/' 
+  }
+  inputs.file("pageThumbnailFile") { 
+    mimeTypes = ["image/jpeg"] 
+  }
+  inputs.integerNumber("count") { 
+    label = "Users to generate"
+    min = 1
+    value = 10000 
+  }
+}`}
+                
+ + Access input values in doRun() using inputs.value("name"). Supports various types: string, integerNumber, decimalNumber, bool, date, time, path, file, and more. + +
+
+
+ ) : ( + + )} +
+
+ ); +}; + +export default ExecutionInputs; diff --git a/ui.frontend/src/components/ExecutionOutputs.tsx b/ui.frontend/src/components/ExecutionOutputs.tsx new file mode 100644 index 000000000..6028e9590 --- /dev/null +++ b/ui.frontend/src/components/ExecutionOutputs.tsx @@ -0,0 +1,61 @@ +import { Badge, Content, ContextualHelp, Flex, Heading, View } from '@adobe/react-spectrum'; +import { Field } from '@react-spectrum/label'; +import Upload from '@spectrum-icons/workflow/UploadToCloud'; +import { Text } from '@adobe/react-spectrum'; +import React from 'react'; +import CodeTextarea from './CodeTextarea'; +import { Objects } from '../utils/objects'; + +type ExecutionOutputsProps = { + outputs: Record | null | undefined; +}; + +const ExecutionOutputs: React.FC = ({ outputs }) => { + const isEmpty = Objects.isEmpty(outputs ?? undefined); + const count = isEmpty ? 0 : Object.keys(outputs!).length; + + return ( + +
+ {isEmpty ? ( + + + + Not generated + + + Generating outputs + + + Use doRun() method to return structured data, files, or summaries: + +

+                  {`void doRun() {
+  def report = outputs.file("report") {
+    label = "Report"
+    description = "Users report as CSV"
+    downloadName = "report.csv"
+  }
+  report.out.println("Name,Surname,Date")
+  report.out.println("John,Doe,2024-01-01")
+  
+  outputs.text("summary") {
+    value = "Processed \${count} users"
+  }
+}`}
+                
+ + Use outputs.file() for downloadable assets, reports or outputs.text() for summaries and documentation. + +
+
+
+ ) : ( + + )} +
+
+ ); +}; + +export default ExecutionOutputs; diff --git a/ui.frontend/src/pages/ExecutionView.tsx b/ui.frontend/src/pages/ExecutionView.tsx index d16c401d5..c89b5a024 100644 --- a/ui.frontend/src/pages/ExecutionView.tsx +++ b/ui.frontend/src/pages/ExecutionView.tsx @@ -1,20 +1,20 @@ -import { Badge, Button, ButtonGroup, Content, Flex, IllustratedMessage, Item, LabeledValue, ProgressBar, Switch, TabList, TabPanels, Tabs, Text, View } from '@adobe/react-spectrum'; +import { Button, ButtonGroup, Content, Flex, IllustratedMessage, Item, LabeledValue, ProgressBar, Switch, TabList, TabPanels, Tabs, Text, View } from '@adobe/react-spectrum'; import { Field } from '@react-spectrum/label'; import { ToastQueue } from '@react-spectrum/toast'; import NotFound from '@spectrum-icons/illustrations/NotFound'; import Copy from '@spectrum-icons/workflow/Copy'; import FileCode from '@spectrum-icons/workflow/FileCode'; import History from '@spectrum-icons/workflow/History'; -import InfoOutline from '@spectrum-icons/workflow/InfoOutline'; import Print from '@spectrum-icons/workflow/Print'; import { useState } from 'react'; import { useNavigate, useParams } from 'react-router-dom'; import CodeEditor from '../components/CodeEditor.tsx'; -import CodeTextarea from '../components/CodeTextarea'; import ExecutableIdValue from '../components/ExecutableIdValue'; import ExecutableMetadata from '../components/ExecutableMetadata'; import ExecutionAbortButton from '../components/ExecutionAbortButton'; import ExecutionCopyOutputButton from '../components/ExecutionCopyOutputButton'; +import ExecutionInputs from '../components/ExecutionInputs'; +import ExecutionOutputs from '../components/ExecutionOutputs'; import ExecutionProgressBar from '../components/ExecutionProgressBar'; import ExecutionReviewOutputsButton from '../components/ExecutionReviewOutputsButton.tsx'; import ExecutionStatusBadge from '../components/ExecutionStatusBadge'; @@ -29,7 +29,6 @@ import { isExecutableScript } from '../types/executable.ts'; import { isExecutionPending } from '../types/execution.ts'; import { GROOVY_LANGUAGE_ID } from '../utils/monaco/groovy.ts'; import { LOG_LANGUAGE_ID } from '../utils/monaco/log.ts'; -import { Objects } from '../utils/objects'; import { ToastTimeoutQuick } from '../utils/spectrum.ts'; const ExecutionView = () => { @@ -141,32 +140,10 @@ const ExecutionView = () => { {/* Row 3: I/O */} - -
- {Objects.isEmpty(execution.inputs) ? ( - - - Not described - - ) : ( - - )} -
-
+
- -
- {Objects.isEmpty(execution.outputs) ? ( - - - Not generated - - ) : ( - - )} -
-
+
From c442a3c2d9428bdd000b4115a9e86a9bffa9efaf Mon Sep 17 00:00:00 2001 From: Krystian Panek Date: Fri, 21 Nov 2025 11:53:36 +0100 Subject: [PATCH 09/14] Skip --- ui.frontend/src/components/ExecutableMetadata.tsx | 2 +- ui.frontend/src/pages/ExecutionView.tsx | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/ui.frontend/src/components/ExecutableMetadata.tsx b/ui.frontend/src/components/ExecutableMetadata.tsx index d97662dbc..d218521ac 100644 --- a/ui.frontend/src/components/ExecutableMetadata.tsx +++ b/ui.frontend/src/components/ExecutableMetadata.tsx @@ -30,7 +30,7 @@ const ExecutableMetadata: React.FC = ({ metadata }) => {`/** * Explain purpose here * - * @author Your Name + * @author * @version 1.0 */`} diff --git a/ui.frontend/src/pages/ExecutionView.tsx b/ui.frontend/src/pages/ExecutionView.tsx index c89b5a024..066c49625 100644 --- a/ui.frontend/src/pages/ExecutionView.tsx +++ b/ui.frontend/src/pages/ExecutionView.tsx @@ -25,7 +25,7 @@ import { useAppState } from '../hooks/app.ts'; import { useExecutionPolling } from '../hooks/execution'; import { useFormatter } from '../hooks/formatter'; import { useNavigationTab } from '../hooks/navigation'; -import { isExecutableScript } from '../types/executable.ts'; +import { isExecutableConsole, isExecutableScript } from '../types/executable.ts'; import { isExecutionPending } from '../types/execution.ts'; import { GROOVY_LANGUAGE_ID } from '../utils/monaco/groovy.ts'; import { LOG_LANGUAGE_ID } from '../utils/monaco/log.ts'; @@ -131,7 +131,9 @@ const ExecutionView = () => {
- + {!isExecutableConsole(execution.executable.id) && ( + + )} From 786ce1fb6a66f83670eeae88bb62235bcbaa9052 Mon Sep 17 00:00:00 2001 From: Krystian Panek Date: Fri, 21 Nov 2025 11:59:44 +0100 Subject: [PATCH 10/14] Images fix --- ui.frontend/src/components/Footer.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/ui.frontend/src/components/Footer.tsx b/ui.frontend/src/components/Footer.tsx index 4694ca6b5..35192cbc4 100644 --- a/ui.frontend/src/components/Footer.tsx +++ b/ui.frontend/src/components/Footer.tsx @@ -1,4 +1,6 @@ import { Divider, Flex, Link, Footer as SpectrumFooter, View } from '@adobe/react-spectrum'; +import githubMark from '/github-mark.svg'; +import vmlLogo from '/vml-logo.svg'; const Footer = () => { return ( @@ -8,7 +10,7 @@ const Footer = () => { - VML Logo + VML Logo @@ -19,7 +21,7 @@ const Footer = () => { - GitHub + GitHub View 'Content Manager' on GitHub From ea607a827b7bb3b37421ae924a5cb80b815397b4 Mon Sep 17 00:00:00 2001 From: Krystian Panek Date: Fri, 21 Nov 2025 12:02:48 +0100 Subject: [PATCH 11/14] Performance improvement --- .../vml/es/acm/core/code/CodeMetadata.java | 26 +++++++++++-------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/core/src/main/java/dev/vml/es/acm/core/code/CodeMetadata.java b/core/src/main/java/dev/vml/es/acm/core/code/CodeMetadata.java index c08ddc42b..564e8275d 100644 --- a/core/src/main/java/dev/vml/es/acm/core/code/CodeMetadata.java +++ b/core/src/main/java/dev/vml/es/acm/core/code/CodeMetadata.java @@ -19,9 +19,14 @@ public class CodeMetadata implements Serializable { private static final Logger LOG = LoggerFactory.getLogger(CodeMetadata.class); private static final Pattern DOC_COMMENT_PATTERN = Pattern.compile("/\\*\\*([^*]|\\*(?!/))*\\*/", Pattern.DOTALL); - private static final Pattern TAG_PATTERN = Pattern.compile("(?m)^\\s*\\*?\\s*@(\\w+)\\s+(.+?)(?=(?m)^\\s*\\*?\\s*@\\w+|\\*/|$)", Pattern.DOTALL); + private static final Pattern NEWLINE_AFTER_COMMENT = Pattern.compile("^\\s*\\n[\\s\\S]*"); + private static final Pattern BLANK_LINE_AFTER_COMMENT = Pattern.compile("^\\s*\\n\\s*\\n[\\s\\S]*"); + private static final Pattern IMPORT_OR_PACKAGE_BEFORE = Pattern.compile("[\\s\\S]*(import|package)[\\s\\S]*\\n\\s*\\n\\s*$"); + private static final Pattern FIRST_TAG_PATTERN = Pattern.compile("(?m)^\\s*\\*?\\s*@\\w+"); + private static final Pattern LEADING_ASTERISK = Pattern.compile("(?m)^\\s*\\*\\s?"); + private static final Pattern DOC_MARKERS = Pattern.compile("^/\\*\\*|\\*/$"); private Map values; @@ -64,7 +69,7 @@ private static String findFirstDocComment(String code) { String afterComment = code.substring(commentEnd); // Must have at least one newline after the comment - if (!afterComment.matches("^\\s*\\n[\\s\\S]*")) { + if (!NEWLINE_AFTER_COMMENT.matcher(afterComment).matches()) { continue; } @@ -77,7 +82,7 @@ private static String findFirstDocComment(String code) { } // Must have blank line after (double newline) for other cases - if (!afterComment.matches("^\\s*\\n\\s*\\n[\\s\\S]*")) { + if (!BLANK_LINE_AFTER_COMMENT.matcher(afterComment).matches()) { continue; } @@ -86,7 +91,7 @@ private static String findFirstDocComment(String code) { String beforeComment = code.substring(0, commentStart); // Should have imports or package, followed by blank line if (beforeComment.trim().isEmpty() - || beforeComment.matches("[\\s\\S]*(import|package)[\\s\\S]*\\n\\s*\\n\\s*$")) { + || IMPORT_OR_PACKAGE_BEFORE.matcher(beforeComment).matches()) { return comment; } } else { @@ -105,25 +110,24 @@ private static Map parseDocComment(String docComment) { Map result = new LinkedHashMap<>(); // Remove /** and */ markers and leading comment decorations - String content = docComment.replaceAll("^/\\*\\*", "").replaceAll("\\*/$", ""); + String content = DOC_MARKERS.matcher(docComment).replaceAll(""); // Extract general description (text before first @tag) // Look for @tag pattern (@ at start of word boundary, not in middle of text // like email) - Pattern firstTagPattern = Pattern.compile("(?m)^\\s*\\*?\\s*@\\w+"); - Matcher firstTagMatcher = firstTagPattern.matcher(content); + Matcher firstTagMatcher = FIRST_TAG_PATTERN.matcher(content); if (firstTagMatcher.find()) { int firstTagIndex = firstTagMatcher.start(); - String description = content.substring(0, firstTagIndex) - .replaceAll("(?m)^\\s*\\*\\s?", "") + String description = LEADING_ASTERISK.matcher(content.substring(0, firstTagIndex)) + .replaceAll("") .trim(); if (!description.isEmpty()) { result.put("description", description); } } else { // No tags, just description - String description = content.replaceAll("(?m)^\\s*\\*\\s?", "").trim(); + String description = LEADING_ASTERISK.matcher(content).replaceAll("").trim(); if (!description.isEmpty()) { result.put("description", description); } @@ -137,7 +141,7 @@ private static Map parseDocComment(String docComment) { String tagValue = tagMatcher.group(2); if (tagValue != null) { - tagValue = tagValue.replaceAll("(?m)^\\s*\\*\\s?", "") // Remove leading * from each line + tagValue = LEADING_ASTERISK.matcher(tagValue).replaceAll("") // Remove leading * from each line .trim(); if (!tagValue.isEmpty()) { From 5fccfd64d40d5bce0ec5b59e020c39870ff6d7bb Mon Sep 17 00:00:00 2001 From: Krystian Panek Date: Fri, 21 Nov 2025 12:08:05 +0100 Subject: [PATCH 12/14] Doc --- .../vml/es/acm/core/code/CodeMetadata.java | 31 +++++-------------- 1 file changed, 8 insertions(+), 23 deletions(-) diff --git a/core/src/main/java/dev/vml/es/acm/core/code/CodeMetadata.java b/core/src/main/java/dev/vml/es/acm/core/code/CodeMetadata.java index 564e8275d..0160f7402 100644 --- a/core/src/main/java/dev/vml/es/acm/core/code/CodeMetadata.java +++ b/core/src/main/java/dev/vml/es/acm/core/code/CodeMetadata.java @@ -54,48 +54,41 @@ public static CodeMetadata parse(String code) { } /** - * Find the first doc comment (either at the top or after imports) + * Finds first JavaDoc/GroovyDoc comment that's properly separated with blank lines, + * or directly attached to describeRun() method. */ private static String findFirstDocComment(String code) { Matcher matcher = DOC_COMMENT_PATTERN.matcher(code); - // Find first doc comment that's properly separated with newlines while (matcher.find()) { String comment = matcher.group(); int commentStart = matcher.start(); int commentEnd = matcher.end(); - // Get text after comment (until next non-whitespace or end of line) String afterComment = code.substring(commentEnd); - // Must have at least one newline after the comment if (!NEWLINE_AFTER_COMMENT.matcher(afterComment).matches()) { continue; } - // Check what follows after the newline(s) String trimmedAfter = afterComment.trim(); - // Skip if directly followed by describeRun (it's OK to be attached) if (trimmedAfter.startsWith("void describeRun()")) { return comment; } - // Must have blank line after (double newline) for other cases if (!BLANK_LINE_AFTER_COMMENT.matcher(afterComment).matches()) { continue; } - // If there's code before the comment, check for blank line before if (commentStart > 0) { String beforeComment = code.substring(0, commentStart); - // Should have imports or package, followed by blank line - if (beforeComment.trim().isEmpty() + String trimmedBefore = beforeComment.trim(); + if (trimmedBefore.isEmpty() || IMPORT_OR_PACKAGE_BEFORE.matcher(beforeComment).matches()) { return comment; } } else { - // Comment at the very start is OK return comment; } } @@ -104,17 +97,14 @@ private static String findFirstDocComment(String code) { } /** - * Parses the doc comment to extract description and tags. + * Extracts description and @tags from doc comment. Supports multiple values per tag. */ private static Map parseDocComment(String docComment) { Map result = new LinkedHashMap<>(); - // Remove /** and */ markers and leading comment decorations String content = DOC_MARKERS.matcher(docComment).replaceAll(""); - // Extract general description (text before first @tag) - // Look for @tag pattern (@ at start of word boundary, not in middle of text - // like email) + // @ at line start (not in email addresses) Matcher firstTagMatcher = FIRST_TAG_PATTERN.matcher(content); if (firstTagMatcher.find()) { @@ -126,26 +116,22 @@ private static Map parseDocComment(String docComment) { result.put("description", description); } } else { - // No tags, just description String description = LEADING_ASTERISK.matcher(content).replaceAll("").trim(); if (!description.isEmpty()) { result.put("description", description); } } - // Parse tags Matcher tagMatcher = TAG_PATTERN.matcher(content); while (tagMatcher.find()) { String tagName = tagMatcher.group(1); String tagValue = tagMatcher.group(2); - if (tagValue != null) { - tagValue = LEADING_ASTERISK.matcher(tagValue).replaceAll("") // Remove leading * from each line - .trim(); + if (tagValue != null && !tagValue.isEmpty()) { + tagValue = LEADING_ASTERISK.matcher(tagValue).replaceAll("").trim(); if (!tagValue.isEmpty()) { - // Store tag value, use list for potential multiple values Object existing = result.get(tagName); if (existing == null) { @@ -155,7 +141,6 @@ private static Map parseDocComment(String docComment) { List list = (List) existing; list.add(tagValue); } else { - // Convert to list if we have multiple values List list = new ArrayList<>(); list.add((String) existing); list.add(tagValue); From 2eccbaa97663000d821d8d5d369c49c97dcb090e Mon Sep 17 00:00:00 2001 From: Krystian Panek Date: Fri, 21 Nov 2025 12:25:33 +0100 Subject: [PATCH 13/14] Hardeninig --- .../vml/es/acm/core/code/CodeMetadata.java | 9 +++++--- .../automatic/example/ACME-100_acl.groovy | 7 +++++++ .../manual/example/ACM-1_classes_jms.groovy | 2 +- .../manual/example/ACM-1_classes_rtjar.groovy | 2 +- .../example/ACME-200_hello-world.groovy | 2 ++ .../manual/example/ACME-201_inputs.groovy | 5 ++--- .../example/ACME-202_page-thumbnail.groovy | 2 +- .../manual/example/ACME-203_output-csv.groovy | 2 +- .../manual/example/ACME-203_output-xls.groovy | 2 +- .../src/components/ExecutableMetadata.tsx | 19 ++++++++--------- .../src/components/ExecutionInputs.tsx | 21 ++++++++++++------- .../src/components/ExecutionOutputs.tsx | 21 ++++++++++++------- ui.frontend/src/components/InfoCard.tsx | 9 +------- .../src/components/markdown.module.css | 4 ++-- ui.frontend/src/pages/ExecutionView.tsx | 4 +--- ui.frontend/src/types/executable.ts | 3 +-- ui.frontend/src/types/script.ts | 2 +- 17 files changed, 63 insertions(+), 53 deletions(-) diff --git a/core/src/main/java/dev/vml/es/acm/core/code/CodeMetadata.java b/core/src/main/java/dev/vml/es/acm/core/code/CodeMetadata.java index 0160f7402..e93f25b76 100644 --- a/core/src/main/java/dev/vml/es/acm/core/code/CodeMetadata.java +++ b/core/src/main/java/dev/vml/es/acm/core/code/CodeMetadata.java @@ -23,7 +23,8 @@ public class CodeMetadata implements Serializable { Pattern.compile("(?m)^\\s*\\*?\\s*@(\\w+)\\s+(.+?)(?=(?m)^\\s*\\*?\\s*@\\w+|\\*/|$)", Pattern.DOTALL); private static final Pattern NEWLINE_AFTER_COMMENT = Pattern.compile("^\\s*\\n[\\s\\S]*"); private static final Pattern BLANK_LINE_AFTER_COMMENT = Pattern.compile("^\\s*\\n\\s*\\n[\\s\\S]*"); - private static final Pattern IMPORT_OR_PACKAGE_BEFORE = Pattern.compile("[\\s\\S]*(import|package)[\\s\\S]*\\n\\s*\\n\\s*$"); + private static final Pattern IMPORT_OR_PACKAGE_BEFORE = + Pattern.compile("[\\s\\S]*(import|package)[\\s\\S]*\\n\\s*\\n\\s*$"); private static final Pattern FIRST_TAG_PATTERN = Pattern.compile("(?m)^\\s*\\*?\\s*@\\w+"); private static final Pattern LEADING_ASTERISK = Pattern.compile("(?m)^\\s*\\*\\s?"); private static final Pattern DOC_MARKERS = Pattern.compile("^/\\*\\*|\\*/$"); @@ -109,14 +110,16 @@ private static Map parseDocComment(String docComment) { if (firstTagMatcher.find()) { int firstTagIndex = firstTagMatcher.start(); - String description = LEADING_ASTERISK.matcher(content.substring(0, firstTagIndex)) + String description = LEADING_ASTERISK + .matcher(content.substring(0, firstTagIndex)) .replaceAll("") .trim(); if (!description.isEmpty()) { result.put("description", description); } } else { - String description = LEADING_ASTERISK.matcher(content).replaceAll("").trim(); + String description = + LEADING_ASTERISK.matcher(content).replaceAll("").trim(); if (!description.isEmpty()) { result.put("description", description); } diff --git a/ui.content.example/src/main/content/jcr_root/conf/acm/settings/script/automatic/example/ACME-100_acl.groovy b/ui.content.example/src/main/content/jcr_root/conf/acm/settings/script/automatic/example/ACME-100_acl.groovy index 43ea688ce..63e1fe085 100644 --- a/ui.content.example/src/main/content/jcr_root/conf/acm/settings/script/automatic/example/ACME-100_acl.groovy +++ b/ui.content.example/src/main/content/jcr_root/conf/acm/settings/script/automatic/example/ACME-100_acl.groovy @@ -1,3 +1,10 @@ +/** + * This script creates content author groups for each tenant-country-language combination. + * + * The groups are named in the format: `{tenant}-{country}-{language}-content-authors`. + * Each group is granted read, write, and replicate permissions on the corresponding content and DAM paths. + */ + def scheduleRun() { return schedules.cron("0 10 * ? * * *") // every hour at minute 10 } diff --git a/ui.content.example/src/main/content/jcr_root/conf/acm/settings/script/manual/example/ACM-1_classes_jms.groovy b/ui.content.example/src/main/content/jcr_root/conf/acm/settings/script/manual/example/ACM-1_classes_jms.groovy index 7bf6fb338..267bb6c03 100644 --- a/ui.content.example/src/main/content/jcr_root/conf/acm/settings/script/manual/example/ACM-1_classes_jms.groovy +++ b/ui.content.example/src/main/content/jcr_root/conf/acm/settings/script/manual/example/ACM-1_classes_jms.groovy @@ -6,7 +6,7 @@ * - print the list (for debugging purposes), * - save it directly in the repository in expected path. * - * @author + * @author */ import dev.vml.es.acm.core.assist.JavaDictionary diff --git a/ui.content.example/src/main/content/jcr_root/conf/acm/settings/script/manual/example/ACM-1_classes_rtjar.groovy b/ui.content.example/src/main/content/jcr_root/conf/acm/settings/script/manual/example/ACM-1_classes_rtjar.groovy index f419c614e..5aa4c2a87 100644 --- a/ui.content.example/src/main/content/jcr_root/conf/acm/settings/script/manual/example/ACM-1_classes_rtjar.groovy +++ b/ui.content.example/src/main/content/jcr_root/conf/acm/settings/script/manual/example/ACM-1_classes_rtjar.groovy @@ -6,7 +6,7 @@ * - print the list (for debugging purposes), * - save it directly in the repository in expected path. * - * @author + * @author */ import dev.vml.es.acm.core.assist.JavaDictionary diff --git a/ui.content.example/src/main/content/jcr_root/conf/acm/settings/script/manual/example/ACME-200_hello-world.groovy b/ui.content.example/src/main/content/jcr_root/conf/acm/settings/script/manual/example/ACME-200_hello-world.groovy index 0f1fc8f1a..86b260d42 100644 --- a/ui.content.example/src/main/content/jcr_root/conf/acm/settings/script/manual/example/ACME-200_hello-world.groovy +++ b/ui.content.example/src/main/content/jcr_root/conf/acm/settings/script/manual/example/ACME-200_hello-world.groovy @@ -1,5 +1,7 @@ /** * Prints "Hello World!" to the console. + * + * @author */ boolean canRun() { diff --git a/ui.content.example/src/main/content/jcr_root/conf/acm/settings/script/manual/example/ACME-201_inputs.groovy b/ui.content.example/src/main/content/jcr_root/conf/acm/settings/script/manual/example/ACME-201_inputs.groovy index 9d15f0c6d..3dc55dd8f 100644 --- a/ui.content.example/src/main/content/jcr_root/conf/acm/settings/script/manual/example/ACME-201_inputs.groovy +++ b/ui.content.example/src/main/content/jcr_root/conf/acm/settings/script/manual/example/ACME-201_inputs.groovy @@ -1,8 +1,7 @@ /** - * Prints animal information to the console based on user input. - * This is an example of AEM Content Manager script with inputs. + * Prints animal information to the console based on user input. * - * @author + * @author */ void describeRun() { inputs.string("animalName") { value = "Whiskers"; diff --git a/ui.content.example/src/main/content/jcr_root/conf/acm/settings/script/manual/example/ACME-202_page-thumbnail.groovy b/ui.content.example/src/main/content/jcr_root/conf/acm/settings/script/manual/example/ACME-202_page-thumbnail.groovy index 66aa62d19..66eb2f428 100644 --- a/ui.content.example/src/main/content/jcr_root/conf/acm/settings/script/manual/example/ACME-202_page-thumbnail.groovy +++ b/ui.content.example/src/main/content/jcr_root/conf/acm/settings/script/manual/example/ACME-202_page-thumbnail.groovy @@ -3,7 +3,7 @@ * Deletes the existing thumbnails and saves a new one. * File must be a JPEG image. * - * @author + * @author */ void describeRun() { inputs.path("pagePath") { rootPathExclusive = '/' } diff --git a/ui.content.example/src/main/content/jcr_root/conf/acm/settings/script/manual/example/ACME-203_output-csv.groovy b/ui.content.example/src/main/content/jcr_root/conf/acm/settings/script/manual/example/ACME-203_output-csv.groovy index 9fd14eae4..8a3a530f5 100644 --- a/ui.content.example/src/main/content/jcr_root/conf/acm/settings/script/manual/example/ACME-203_output-csv.groovy +++ b/ui.content.example/src/main/content/jcr_root/conf/acm/settings/script/manual/example/ACME-203_output-csv.groovy @@ -3,7 +3,7 @@ import java.util.Random /** * @description Generates a CSV report of users with random names and birth dates. - * @author + * @author */ void describeRun() { inputs.integerNumber("count") { label = "Users to generate"; min = 1; value = 10000 } diff --git a/ui.content.example/src/main/content/jcr_root/conf/acm/settings/script/manual/example/ACME-203_output-xls.groovy b/ui.content.example/src/main/content/jcr_root/conf/acm/settings/script/manual/example/ACME-203_output-xls.groovy index ba069b4db..08f2a9ea5 100644 --- a/ui.content.example/src/main/content/jcr_root/conf/acm/settings/script/manual/example/ACME-203_output-xls.groovy +++ b/ui.content.example/src/main/content/jcr_root/conf/acm/settings/script/manual/example/ACME-203_output-xls.groovy @@ -7,7 +7,7 @@ import java.util.Random /** * @description Generates an XLS report of users with random names and birth dates. - * @author + * @author */ void describeRun() { inputs.integerNumber("count") { label = "Users to generate"; min = 1; value = 100000 } diff --git a/ui.frontend/src/components/ExecutableMetadata.tsx b/ui.frontend/src/components/ExecutableMetadata.tsx index d218521ac..986c7533a 100644 --- a/ui.frontend/src/components/ExecutableMetadata.tsx +++ b/ui.frontend/src/components/ExecutableMetadata.tsx @@ -1,7 +1,6 @@ -import { Badge, Content, ContextualHelp, Flex, Heading, LabeledValue, View } from '@adobe/react-spectrum'; +import { Badge, Content, ContextualHelp, Flex, Heading, LabeledValue, Text, View } from '@adobe/react-spectrum'; import { Field } from '@react-spectrum/label'; import DataUnavailable from '@spectrum-icons/workflow/DataUnavailable'; -import { Text } from '@adobe/react-spectrum'; import React from 'react'; import { ExecutableMetadata as ExecutableMetadataType } from '../types/executable'; import Markdown from './Markdown'; @@ -26,14 +25,16 @@ const ExecutableMetadata: React.FC = ({ metadata }) => Use a JavaDoc or GroovyDoc comment block at the top of your script file: -

-                  {`/**
+                
+                  
+                    {`/**
  * Explain purpose here
  *
  * @author 
  * @version 1.0
  */`}
-                
+
+
The comment must be followed by a blank line. Description is extracted from text before any @tags. @@ -46,19 +47,17 @@ const ExecutableMetadata: React.FC = ({ metadata }) => } return ( - <> + {Object.entries(metadata).map(([key, value]) => { const label = key.charAt(0).toUpperCase() + key.slice(1); if (Array.isArray(value)) { - return value.map((item, index) => ( - } /> - )); + return value.map((item, index) => } />); } return } />; })} - + ); }; diff --git a/ui.frontend/src/components/ExecutionInputs.tsx b/ui.frontend/src/components/ExecutionInputs.tsx index 6b4240a36..436638017 100644 --- a/ui.frontend/src/components/ExecutionInputs.tsx +++ b/ui.frontend/src/components/ExecutionInputs.tsx @@ -1,10 +1,9 @@ -import { Badge, Content, ContextualHelp, Flex, Heading, View } from '@adobe/react-spectrum'; +import { Badge, Content, ContextualHelp, Flex, Heading, Text, View } from '@adobe/react-spectrum'; import { Field } from '@react-spectrum/label'; import Download from '@spectrum-icons/workflow/Download'; -import { Text } from '@adobe/react-spectrum'; import React from 'react'; -import CodeTextarea from './CodeTextarea'; import { Objects } from '../utils/objects'; +import CodeTextarea from './CodeTextarea'; type ExecutionInputsProps = { inputs: Record | null | undefined; @@ -27,10 +26,13 @@ const ExecutionInputs: React.FC = ({ inputs }) => { Defining inputs - Use describeRun() method to collect values from users before execution: + + Use describeRun() method to collect values from users before execution: + -

-                  {`void describeRun() {
+                
+                  
+                    {`void describeRun() {
   inputs.path("pagePath") { 
     rootPathExclusive = '/' 
   }
@@ -43,9 +45,12 @@ const ExecutionInputs: React.FC = ({ inputs }) => {
     value = 10000 
   }
 }`}
-                
+
+
- Access input values in doRun() using inputs.value("name"). Supports various types: string, integerNumber, decimalNumber, bool, date, time, path, file, and more. + + Access input values in doRun() using inputs.value("name"). Supports various types: string, integerNumber, decimalNumber, bool, date, time, path, file, and more. +
diff --git a/ui.frontend/src/components/ExecutionOutputs.tsx b/ui.frontend/src/components/ExecutionOutputs.tsx index 6028e9590..e1f2ce858 100644 --- a/ui.frontend/src/components/ExecutionOutputs.tsx +++ b/ui.frontend/src/components/ExecutionOutputs.tsx @@ -1,10 +1,9 @@ -import { Badge, Content, ContextualHelp, Flex, Heading, View } from '@adobe/react-spectrum'; +import { Badge, Content, ContextualHelp, Flex, Heading, Text, View } from '@adobe/react-spectrum'; import { Field } from '@react-spectrum/label'; import Upload from '@spectrum-icons/workflow/UploadToCloud'; -import { Text } from '@adobe/react-spectrum'; import React from 'react'; -import CodeTextarea from './CodeTextarea'; import { Objects } from '../utils/objects'; +import CodeTextarea from './CodeTextarea'; type ExecutionOutputsProps = { outputs: Record | null | undefined; @@ -27,10 +26,13 @@ const ExecutionOutputs: React.FC = ({ outputs }) => { Generating outputs - Use doRun() method to return structured data, files, or summaries: + + Use doRun() method to return structured data, files, or summaries: + -

-                  {`void doRun() {
+                
+                  
+                    {`void doRun() {
   def report = outputs.file("report") {
     label = "Report"
     description = "Users report as CSV"
@@ -43,9 +45,12 @@ const ExecutionOutputs: React.FC = ({ outputs }) => {
     value = "Processed \${count} users"
   }
 }`}
-                
+
+
- Use outputs.file() for downloadable assets, reports or outputs.text() for summaries and documentation. + + Use outputs.file() for downloadable assets, reports or outputs.text() for summaries and documentation. +
diff --git a/ui.frontend/src/components/InfoCard.tsx b/ui.frontend/src/components/InfoCard.tsx index 0fe5dc9e6..f8fc2f0a8 100644 --- a/ui.frontend/src/components/InfoCard.tsx +++ b/ui.frontend/src/components/InfoCard.tsx @@ -7,14 +7,7 @@ type InfoCardProps = { const InfoCard = ({ children }: InfoCardProps) => { return ( - + {children} diff --git a/ui.frontend/src/components/markdown.module.css b/ui.frontend/src/components/markdown.module.css index c0ad672c2..9b4da2b1b 100644 --- a/ui.frontend/src/components/markdown.module.css +++ b/ui.frontend/src/components/markdown.module.css @@ -2,10 +2,10 @@ font-size: 12px; } -.markdown p:first-child { +.markdown p:first-child, .markdown ol:first-child, .markdown ul:first-child { margin-block-start: 0; } -.markdown p:last-child { +.markdown p:last-child, .markdown ol:last-child, .markdown ul:last-child { margin-block-end: 0; } diff --git a/ui.frontend/src/pages/ExecutionView.tsx b/ui.frontend/src/pages/ExecutionView.tsx index 066c49625..ec6b1d0dd 100644 --- a/ui.frontend/src/pages/ExecutionView.tsx +++ b/ui.frontend/src/pages/ExecutionView.tsx @@ -131,9 +131,7 @@ const ExecutionView = () => {
- {!isExecutableConsole(execution.executable.id) && ( - - )} + {!isExecutableConsole(execution.executable.id) && }
diff --git a/ui.frontend/src/types/executable.ts b/ui.frontend/src/types/executable.ts index 23ca07a74..0abfe0362 100644 --- a/ui.frontend/src/types/executable.ts +++ b/ui.frontend/src/types/executable.ts @@ -6,7 +6,7 @@ export type Executable = { metadata: ExecutableMetadata; }; -export type ExecutableMetadata = Record +export type ExecutableMetadata = Record; export const ExecutableIdConsole = 'console'; @@ -17,4 +17,3 @@ export function isExecutableConsole(id: string): boolean { export function isExecutableScript(id: string): boolean { return id.startsWith(ScriptRoot); } - diff --git a/ui.frontend/src/types/script.ts b/ui.frontend/src/types/script.ts index 4cccd4917..17afca341 100644 --- a/ui.frontend/src/types/script.ts +++ b/ui.frontend/src/types/script.ts @@ -1,5 +1,5 @@ -import { ExecutionStatus } from './execution'; import { Executable } from './executable'; +import { ExecutionStatus } from './execution'; import { ExecutionSummary } from './main'; export enum ScriptType { From ae95b2ec6fbc7fe9ddc785bc491fbb005c7c0ca3 Mon Sep 17 00:00:00 2001 From: Krystian Panek Date: Fri, 21 Nov 2025 12:33:53 +0100 Subject: [PATCH 14/14] Test fix --- .../test/java/dev/vml/es/acm/core/code/CodeMetadataTest.java | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/core/src/test/java/dev/vml/es/acm/core/code/CodeMetadataTest.java b/core/src/test/java/dev/vml/es/acm/core/code/CodeMetadataTest.java index a064ae7a6..7f21ee02e 100644 --- a/core/src/test/java/dev/vml/es/acm/core/code/CodeMetadataTest.java +++ b/core/src/test/java/dev/vml/es/acm/core/code/CodeMetadataTest.java @@ -51,8 +51,7 @@ void shouldParseInputsScript() throws IOException { String description = (String) metadata.getValues().get("description"); assertNotNull(description); assertTrue(description.contains("Prints animal information to the console based on user input")); - assertTrue(description.contains("This is an example of AEM Content Manager script with inputs")); - assertEquals("", metadata.getValues().get("author")); + assertEquals("", metadata.getValues().get("author")); } @Test @@ -64,7 +63,7 @@ void shouldParsePageThumbnailScript() throws IOException { assertNotNull(description); assertTrue(description.contains("Updates the thumbnail")); assertTrue(description.contains("File must be a JPEG image")); - assertEquals("", metadata.getValues().get("author")); + assertEquals("", metadata.getValues().get("author")); } @Test