From 6ca1534bcb7625a64d3964c58eb8271f98747fc8 Mon Sep 17 00:00:00 2001 From: Thorsten Marx Date: Thu, 27 Aug 2026 20:55:18 +0200 Subject: [PATCH 01/28] first iteration --- .../java/com/condation/cms/api/Constants.java | 1 + .../java/com/condation/cms/api/db/DB.java | 3 + .../cms/api/db/collection/Collection.java | 34 +++ .../cms/api/db/collection/CollectionItem.java | 36 +++ .../cms/api/db/collection/Collections.java | 34 +++ .../cms/filesystem/FileCollections.java | 287 ++++++++++++++++++ .../com/condation/cms/filesystem/FileDB.java | 17 +- .../persistent/CollectionMetaData.java | 280 +++++++++++++++++ .../metadata/persistent/LuceneQuery.java | 72 ++++- .../persistent/LuceneQueryPolicy.java | 31 ++ .../cms/filesystem/FileCollectionsTest.java | 241 +++++++++++++++ .../cms/templates/DynamicConfiguration.java | 8 +- ...namicConfigurationFunctionWrapperTest.java | 40 +++ .../TemplateEngineFunctionsTest.java | 26 +- .../CollectionTemplateFunctionExtensions.java | 44 +++ .../demo/collections/authors/thorsten.md | 6 + .../hosts/demo/collections/blog/item_1.md | 6 + .../hosts/demo/collections/blog/item_2.md | 6 + .../hosts/demo/collections/blog/item_3.md | 6 + .../hosts/demo/content/collections/index.md | 6 + .../themes/demo/templates/collections.html | 43 +++ 21 files changed, 1209 insertions(+), 18 deletions(-) create mode 100644 cms-api/src/main/java/com/condation/cms/api/db/collection/Collection.java create mode 100644 cms-api/src/main/java/com/condation/cms/api/db/collection/CollectionItem.java create mode 100644 cms-api/src/main/java/com/condation/cms/api/db/collection/Collections.java create mode 100644 cms-filesystem/src/main/java/com/condation/cms/filesystem/FileCollections.java create mode 100644 cms-filesystem/src/main/java/com/condation/cms/filesystem/metadata/persistent/CollectionMetaData.java create mode 100644 cms-filesystem/src/main/java/com/condation/cms/filesystem/metadata/persistent/LuceneQueryPolicy.java create mode 100644 cms-filesystem/src/test/java/com/condation/cms/filesystem/FileCollectionsTest.java create mode 100644 cms-templates/src/test/java/com/condation/cms/templates/DynamicConfigurationFunctionWrapperTest.java create mode 100644 modules/system-modules/src/main/java/com/condation/cms/modules/system/templates/CollectionTemplateFunctionExtensions.java create mode 100644 test-server/hosts/demo/collections/authors/thorsten.md create mode 100644 test-server/hosts/demo/collections/blog/item_1.md create mode 100644 test-server/hosts/demo/collections/blog/item_2.md create mode 100644 test-server/hosts/demo/collections/blog/item_3.md create mode 100644 test-server/hosts/demo/content/collections/index.md create mode 100644 test-server/themes/demo/templates/collections.html diff --git a/cms-api/src/main/java/com/condation/cms/api/Constants.java b/cms-api/src/main/java/com/condation/cms/api/Constants.java index 1efead7b5..20faf8780 100644 --- a/cms-api/src/main/java/com/condation/cms/api/Constants.java +++ b/cms-api/src/main/java/com/condation/cms/api/Constants.java @@ -75,6 +75,7 @@ public static class MetaFields { public static class Folders { public static final String CONTENT = "content/"; + public static final String COLLECTIONS = "collections/"; public static final String TEMPLATES = "templates/"; public static final String ASSETS = "assets/"; public static final String EXTENSIONS = "extensions/"; diff --git a/cms-api/src/main/java/com/condation/cms/api/db/DB.java b/cms-api/src/main/java/com/condation/cms/api/db/DB.java index 9e2d212b5..6956bfe84 100644 --- a/cms-api/src/main/java/com/condation/cms/api/db/DB.java +++ b/cms-api/src/main/java/com/condation/cms/api/db/DB.java @@ -22,6 +22,7 @@ */ import com.condation.cms.api.db.taxonomy.Taxonomies; +import com.condation.cms.api.db.collection.Collections; import com.condation.cms.api.db.cms.ReadOnlyFileSystem; @@ -40,6 +41,8 @@ public interface DB extends AutoCloseable{ public ReadOnlyFileSystem getReadOnlyFileSystem(); public Content getContent(); + + public Collections getCollections(); public Taxonomies getTaxonomies(); } diff --git a/cms-api/src/main/java/com/condation/cms/api/db/collection/Collection.java b/cms-api/src/main/java/com/condation/cms/api/db/collection/Collection.java new file mode 100644 index 000000000..b1375cec3 --- /dev/null +++ b/cms-api/src/main/java/com/condation/cms/api/db/collection/Collection.java @@ -0,0 +1,34 @@ +package com.condation.cms.api.db.collection; + +/*- + * #%L + * CMS Api + * %% + * Copyright (C) 2023 - 2026 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * #L% + */ + +import com.condation.cms.api.db.ContentQuery; + +/** + * A named, file-backed collection. + */ +public interface Collection { + + String name(); + + ContentQuery query(); +} diff --git a/cms-api/src/main/java/com/condation/cms/api/db/collection/CollectionItem.java b/cms-api/src/main/java/com/condation/cms/api/db/collection/CollectionItem.java new file mode 100644 index 000000000..45f84b9c8 --- /dev/null +++ b/cms-api/src/main/java/com/condation/cms/api/db/collection/CollectionItem.java @@ -0,0 +1,36 @@ +package com.condation.cms.api.db.collection; + +/*- + * #%L + * CMS Api + * %% + * Copyright (C) 2023 - 2026 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * #L% + */ + +import java.util.Map; + +/** + * A collection entry. The content is raw Markdown and is never rendered by + * the collection layer. + */ +public record CollectionItem( + String id, + String collection, + String path, + String content, + Map meta) { +} diff --git a/cms-api/src/main/java/com/condation/cms/api/db/collection/Collections.java b/cms-api/src/main/java/com/condation/cms/api/db/collection/Collections.java new file mode 100644 index 000000000..c634a05a4 --- /dev/null +++ b/cms-api/src/main/java/com/condation/cms/api/db/collection/Collections.java @@ -0,0 +1,34 @@ +package com.condation.cms.api.db.collection; + +/*- + * #%L + * CMS Api + * %% + * Copyright (C) 2023 - 2026 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * #L% + */ + +import java.util.Set; + +/** + * Site-scoped access to collections. + */ +public interface Collections { + + Collection collection(String name); + + Set names(); +} diff --git a/cms-filesystem/src/main/java/com/condation/cms/filesystem/FileCollections.java b/cms-filesystem/src/main/java/com/condation/cms/filesystem/FileCollections.java new file mode 100644 index 000000000..361f69f17 --- /dev/null +++ b/cms-filesystem/src/main/java/com/condation/cms/filesystem/FileCollections.java @@ -0,0 +1,287 @@ +package com.condation.cms.filesystem; + +/*- + * #%L + * CMS FileSystem + * %% + * Copyright (C) 2023 - 2026 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * #L% + */ + +import com.condation.cms.api.Constants; +import com.condation.cms.api.db.ContentNode; +import com.condation.cms.api.db.ContentQuery; +import com.condation.cms.api.db.collection.CollectionItem; +import com.condation.cms.api.db.collection.Collections; +import com.condation.cms.api.utils.PathUtil; +import com.condation.cms.filesystem.metadata.persistent.CollectionMetaData; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.time.Duration; +import java.time.LocalDate; +import java.time.ZoneId; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Function; +import java.util.regex.Pattern; +import lombok.extern.slf4j.Slf4j; + +/** + * Site-scoped, file-backed collections implementation. + */ +@Slf4j +public class FileCollections implements Collections, AutoCloseable { + + private static final Pattern COLLECTION_NAME = Pattern.compile("[a-zA-Z0-9][a-zA-Z0-9_-]*"); + private static final Duration CHANGE_QUIET_PERIOD = Duration.ofMillis(200); + + private final String siteId; + private final Path hostBase; + private final Path collectionsBase; + private final Function> metaParser; + private final Set collectionNames = ConcurrentHashMap.newKeySet(); + + private CollectionMetaData metaData; + private MultiRootRecursiveWatcher watcher; + private ContentChangeCoordinator changeCoordinator; + + public FileCollections( + String siteId, + Path hostBase, + Function> metaParser) { + this.siteId = siteId; + this.hostBase = hostBase; + this.collectionsBase = hostBase.resolve(Constants.Folders.COLLECTIONS); + this.metaParser = metaParser; + } + + public void init() throws IOException { + Files.createDirectories(collectionsBase); + metaData = new CollectionMetaData(hostBase); + metaData.open(); + changeCoordinator = new ContentChangeCoordinator( + CHANGE_QUIET_PERIOD, + this::processChanges); + rebuild(); + + watcher = new MultiRootRecursiveWatcher(siteId, List.of(collectionsBase)); + watcher.getPublisher(collectionsBase).subscribe( + new MultiRootRecursiveWatcher.AbstractFileEventSubscriber() { + @Override + public void onNext(FileEvent item) { + if (item.type() == FileEvent.Type.OVERFLOW) { + changeCoordinator.requestFullResync(); + } else { + changeCoordinator.submit(item.file().toPath()); + } + this.subscription.request(1); + } + }); + watcher.start(); + } + + @Override + public com.condation.cms.api.db.collection.Collection collection(String name) { + validateCollectionName(name); + return new FileCollection(name); + } + + @Override + public Set names() { + return Set.copyOf(collectionNames); + } + + void handleEvent(FileEvent event) { + if (event.type() == FileEvent.Type.OVERFLOW) { + changeCoordinator.requestFullResync(); + } else { + changeCoordinator.submit(event.file().toPath()); + } + } + + void flushChanges() { + changeCoordinator.flushNow(); + } + + private void processChanges(boolean fullResync, Set paths) { + try { + if (fullResync) { + rebuild(); + return; + } + for (var path : paths) { + processPath(path); + } + } catch (IOException ex) { + log.error("error processing collection changes", ex); + } + } + + private void processPath(Path path) throws IOException { + var relative = PathUtil.toRelativeEntry(path, collectionsBase); + if (relative.isBlank() || relative.startsWith("../")) { + return; + } + var parts = relative.split("/"); + if (parts.length == 1) { + if (Files.isDirectory(path)) { + scanCollection(path); + } else if (!Files.exists(path)) { + metaData.removeDirectory(parts[0]); + collectionNames.remove(parts[0]); + } + return; + } + if (parts.length != 2 || !isMarkdown(path)) { + return; + } + if (!isValidCollectionName(parts[0])) { + return; + } + if (Files.isRegularFile(path)) { + index(path); + } else if (!Files.exists(path)) { + metaData.removeFile(relative); + } + } + + private void rebuild() throws IOException { + metaData.clear(); + collectionNames.clear(); + metaData.startBatch(); + try (var collections = Files.list(collectionsBase)) { + for (var collection : collections.filter(Files::isDirectory).toList()) { + scanCollection(collection); + } + } finally { + metaData.stopBatch(); + } + } + + private void scanCollection(Path collection) throws IOException { + var name = collection.getFileName().toString(); + if (!isValidCollectionName(name)) { + log.warn("ignoring invalid collection name {}", name); + return; + } + collectionNames.add(name); + metaData.removeDirectory(name); + try (var files = Files.list(collection)) { + for (var file : files.filter(Files::isRegularFile).filter(FileCollections::isMarkdown).toList()) { + index(file); + } + } + } + + private void index(Path file) throws IOException { + var path = PathUtil.toRelativeEntry(file, collectionsBase); + collectionNames.add(path.substring(0, path.indexOf('/'))); + var modified = LocalDate.ofInstant( + Files.getLastModifiedTime(file).toInstant(), + ZoneId.systemDefault()); + metaData.addFile(path, metaParser.apply(file), modified); + } + + private CollectionItem map(ContentNode node, int ignoredExcerptLength) { + var path = node.path(); + var separator = path.indexOf('/'); + var collection = path.substring(0, separator); + var filename = path.substring(separator + 1); + var id = filename.substring(0, filename.length() - 3); + return new CollectionItem( + id, + collection, + path, + readMarkdownBody(collectionsBase.resolve(path)), + node.data()); + } + + private static String readMarkdownBody(Path file) { + try { + var lines = Files.readAllLines(file); + var body = new StringBuilder(); + var inFrontMatter = false; + var frontMatterClosed = false; + for (var line : lines) { + if (line.trim().equals("---") && !frontMatterClosed) { + if (!inFrontMatter) { + inFrontMatter = true; + } else { + inFrontMatter = false; + frontMatterClosed = true; + } + continue; + } + if (!inFrontMatter) { + body.append(line).append("\r\n"); + } + } + return body.toString(); + } catch (IOException ex) { + log.error("error reading collection item {}", file, ex); + return ""; + } + } + + private static boolean isMarkdown(Path file) { + return file.getFileName().toString().endsWith(".md"); + } + + private static boolean isValidCollectionName(String name) { + return name != null && COLLECTION_NAME.matcher(name).matches(); + } + + private static void validateCollectionName(String name) { + if (!isValidCollectionName(name)) { + throw new IllegalArgumentException("invalid collection name: " + name); + } + } + + @Override + public void close() throws IOException { + if (watcher != null) { + watcher.stop(); + } + if (changeCoordinator != null) { + changeCoordinator.close(); + } + if (metaData != null) { + metaData.close(); + } + } + + private class FileCollection implements com.condation.cms.api.db.collection.Collection { + + private final String name; + + private FileCollection(String name) { + this.name = name; + } + + @Override + public String name() { + return name; + } + + @Override + public ContentQuery query() { + return metaData.query(name, FileCollections.this::map); + } + } +} diff --git a/cms-filesystem/src/main/java/com/condation/cms/filesystem/FileDB.java b/cms-filesystem/src/main/java/com/condation/cms/filesystem/FileDB.java index f451a83e8..92ce07249 100644 --- a/cms-filesystem/src/main/java/com/condation/cms/filesystem/FileDB.java +++ b/cms-filesystem/src/main/java/com/condation/cms/filesystem/FileDB.java @@ -28,6 +28,7 @@ import com.condation.cms.api.db.DB; import com.condation.cms.api.db.DBFileSystem; import com.condation.cms.api.db.taxonomy.Taxonomies; +import com.condation.cms.api.db.collection.Collections; import com.condation.cms.api.eventbus.EventBus; import com.condation.cms.filesystem.taxonomy.FileTaxonomies; import java.io.IOException; @@ -51,6 +52,7 @@ public class FileDB implements DB { private FileSystem fileSystem; private FileContent content; + private FileCollections collections; private ReadOnlyFileSystem readOnlyFileSystem; private FileTaxonomies taxonomies; @@ -68,6 +70,8 @@ public void init () throws IOException { readOnlyFileSystem = new WrappedReadOnlyFileSystem(fileSystem); content = new FileContent(fileSystem); + collections = new FileCollections(siteProperties.id(), hostBaseDirectory, contentParser); + collections.init(); taxonomies = new FileTaxonomies(configuration, content); } @@ -97,7 +101,13 @@ public DBFileSystem getFileSystem() { @Override public void close() throws Exception { - fileSystem.shutdown(); + try { + if (collections != null) { + collections.close(); + } + } finally { + fileSystem.shutdown(); + } } @Override @@ -105,6 +115,11 @@ public Content getContent() { return content; } + @Override + public Collections getCollections() { + return collections; + } + @Override public Taxonomies getTaxonomies() { return taxonomies; diff --git a/cms-filesystem/src/main/java/com/condation/cms/filesystem/metadata/persistent/CollectionMetaData.java b/cms-filesystem/src/main/java/com/condation/cms/filesystem/metadata/persistent/CollectionMetaData.java new file mode 100644 index 000000000..ac9733f53 --- /dev/null +++ b/cms-filesystem/src/main/java/com/condation/cms/filesystem/metadata/persistent/CollectionMetaData.java @@ -0,0 +1,280 @@ +package com.condation.cms.filesystem.metadata.persistent; + +/*- + * #%L + * CMS FileSystem + * %% + * Copyright (C) 2023 - 2026 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * #L% + */ + +import com.condation.cms.api.db.ContentNode; +import com.condation.cms.api.db.ContentQuery; +import com.condation.cms.filesystem.MetaData; +import com.condation.cms.filesystem.metadata.query.ExcerptMapperFunction; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.BiFunction; +import lombok.extern.slf4j.Slf4j; +import org.apache.lucene.document.Document; +import org.apache.lucene.document.Field; +import org.apache.lucene.document.StringField; +import org.apache.lucene.index.Term; +import org.apache.lucene.search.MatchAllDocsQuery; +import org.apache.lucene.search.TermQuery; +import org.h2.mvstore.MVMap; +import org.h2.mvstore.MVStore; + +/** + * Shared metadata and Lucene index for every collection of one site. + */ +@Slf4j +public class CollectionMetaData implements MetaData { + + public static final String FIELD_COLLECTION = "_collection"; + public static final String FIELD_ID = "_id"; + + private final Path hostPath; + private LuceneIndex index; + private MVStore store; + private MVMap nodes; + + public CollectionMetaData(Path hostPath) { + this.hostPath = hostPath; + } + + @Override + public void open() throws IOException { + var dataPath = hostPath.resolve("data/collections"); + Files.createDirectories(dataPath.resolve("store")); + Files.createDirectories(dataPath.resolve("index")); + + index = new LuceneIndex(); + index.open(dataPath.resolve("index")); + store = MVStore.open(dataPath.resolve("store/data.db").toString()); + nodes = store.openMap("nodes"); + nodes.clear(); + } + + @Override + public void close() throws IOException { + try { + if (index != null) { + index.close(); + } + if (store != null) { + store.close(); + } + } catch (Exception ex) { + throw new IOException(ex); + } + } + + public void startBatch() { + index.setBatchMode(true); + } + + public void stopBatch() { + try { + index.setBatchMode(false); + index.commit(); + } catch (IOException ex) { + log.error("error committing collection index", ex); + } + } + + @Override + public synchronized void addFile(String path, Map data, LocalDate lastModified) { + var normalizedPath = normalize(path); + var separator = normalizedPath.indexOf('/'); + if (separator <= 0 || separator == normalizedPath.length() - 1) { + throw new IllegalArgumentException("collection item path must contain collection and filename"); + } + + var collection = normalizedPath.substring(0, separator); + var filename = normalizedPath.substring(separator + 1); + var id = filename.endsWith(".md") + ? filename.substring(0, filename.length() - 3) + : filename; + var node = new ContentNode( + normalizedPath, + normalizedPath, + filename, + data, + lastModified); + nodes.put(normalizedPath, node); + + var document = new Document(); + document.add(new StringField("_uri", normalizedPath, Field.Store.YES)); + document.add(new StringField(FIELD_COLLECTION, collection, Field.Store.YES)); + document.add(new StringField(FIELD_ID, id, Field.Store.YES)); + DocumentHelper.addData(document, data); + DocumentHelper.addSearchFields(document, data); + DocumentHelper.addAvailableFields(document); + try { + index.update(new Term("_uri", normalizedPath), document); + } catch (IOException ex) { + log.error("error indexing collection item {}", normalizedPath, ex); + } + } + + @Override + public synchronized void removeFile(String path) { + var normalizedPath = normalize(path); + nodes.remove(normalizedPath); + try { + index.delete(new TermQuery(new Term("_uri", normalizedPath))); + } catch (IOException ex) { + log.error("error deleting collection item {}", normalizedPath, ex); + } + } + + @Override + public synchronized void removeDirectory(String path) { + var collection = normalize(path); + var prefix = collection + "/"; + var affectedPaths = new ArrayList(); + var cursor = nodes.cursor(prefix); + while (cursor.hasNext()) { + var itemPath = cursor.next(); + if (!itemPath.startsWith(prefix)) { + break; + } + affectedPaths.add(itemPath); + } + affectedPaths.forEach(nodes::remove); + try { + index.delete(new TermQuery(new Term(FIELD_COLLECTION, collection))); + } catch (IOException ex) { + log.error("error deleting collection {}", collection, ex); + } + } + + @Override + public synchronized void removePath(String path) { + var normalizedPath = normalize(path); + if (normalizedPath.contains("/")) { + removeFile(normalizedPath); + } else { + removeDirectory(normalizedPath); + } + } + + @Override + public Optional byUri(String uri) { + return byPath(uri); + } + + @Override + public Optional byPath(String path) { + return Optional.ofNullable(nodes.get(normalize(path))); + } + + @Override + public Optional byUrl(String url) { + return Optional.empty(); + } + + @Override + public void createDirectory(String path) { + // Collections are represented by the _collection field, not directory nodes. + } + + @Override + public Optional findFolder(String path) { + return Optional.empty(); + } + + @Override + public List listChildren(String path) { + var prefix = normalize(path) + "/"; + return nodes.values().stream() + .filter(node -> node.path().startsWith(prefix)) + .toList(); + } + + @Override + public List listSectionEntries(String pagePath) { + return List.of(); + } + + @Override + public TitleQuery searchByTitle(String input) { + throw new UnsupportedOperationException("title search is not exposed for collections"); + } + + @Override + public synchronized void clear() { + nodes.clear(); + try { + index.delete(MatchAllDocsQuery.INSTANCE); + } catch (IOException ex) { + log.error("error clearing collection index", ex); + } + } + + @Override + public Map getNodes() { + return new ConcurrentHashMap<>(nodes); + } + + @Override + public Map getTree() { + return Map.of(); + } + + @Override + public ContentQuery query(BiFunction nodeMapper) { + return collectionQuery(null, nodeMapper); + } + + @Override + public ContentQuery query(String collection, BiFunction nodeMapper) { + return collectionQuery(collection, nodeMapper); + } + + private ContentQuery collectionQuery( + String collection, + BiFunction nodeMapper) { + var scope = collection == null + ? null + : new TermQuery(new Term(FIELD_COLLECTION, collection)); + return new LuceneQuery<>( + index, + this, + new ExcerptMapperFunction<>(nodeMapper), + LuceneQueryPolicy.COLLECTION, + scope); + } + + private static String normalize(String path) { + var normalized = path == null ? "" : path.replace('\\', '/'); + while (normalized.startsWith("/")) { + normalized = normalized.substring(1); + } + while (normalized.endsWith("/")) { + normalized = normalized.substring(0, normalized.length() - 1); + } + return normalized; + } +} diff --git a/cms-filesystem/src/main/java/com/condation/cms/filesystem/metadata/persistent/LuceneQuery.java b/cms-filesystem/src/main/java/com/condation/cms/filesystem/metadata/persistent/LuceneQuery.java index 3aeef04eb..995f9eafd 100644 --- a/cms-filesystem/src/main/java/com/condation/cms/filesystem/metadata/persistent/LuceneQuery.java +++ b/cms-filesystem/src/main/java/com/condation/cms/filesystem/metadata/persistent/LuceneQuery.java @@ -24,6 +24,7 @@ import com.condation.cms.api.db.ContentNode; import com.condation.cms.api.db.ContentQuery; import com.condation.cms.api.db.DistanceUnit; +import com.condation.cms.api.db.NodeVisibility; import com.condation.cms.api.db.Page; import com.condation.cms.api.db.VariantSearchMode; import com.condation.cms.api.feature.features.IsPreviewFeature; @@ -47,7 +48,6 @@ import java.util.Objects; import java.util.Optional; import java.util.function.Predicate; -import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.apache.lucene.document.LongField; import org.apache.lucene.document.LatLonPoint; @@ -65,7 +65,6 @@ * @param */ @Slf4j -@RequiredArgsConstructor public class LuceneQuery extends ExtendableQuery implements ContentQuery.Sort { private static final int SCAN_BATCH_SIZE = 128; @@ -74,6 +73,8 @@ public class LuceneQuery extends ExtendableQuery implements ContentQuery.S private final LuceneIndex index; private final MetaData metaData; private final ExcerptMapperFunction nodeMapper; + private final LuceneQueryPolicy policy; + private final Optional scopeQuery; private String contentType = Constants.DEFAULT_CONTENT_TYPE; @@ -97,10 +98,30 @@ public LuceneQuery( final LuceneIndex index, final MetaData metaData, final ExcerptMapperFunction nodeMapper) { - this(index, metaData, nodeMapper); + this(index, metaData, nodeMapper, LuceneQueryPolicy.CONTENT, null); this.startUri = Optional.ofNullable(startUri); } + public LuceneQuery( + final LuceneIndex index, + final MetaData metaData, + final ExcerptMapperFunction nodeMapper) { + this(index, metaData, nodeMapper, LuceneQueryPolicy.CONTENT, null); + } + + public LuceneQuery( + final LuceneIndex index, + final MetaData metaData, + final ExcerptMapperFunction nodeMapper, + final LuceneQueryPolicy policy, + final Query scopeQuery) { + this.index = Objects.requireNonNull(index, "index must not be null"); + this.metaData = Objects.requireNonNull(metaData, "metaData must not be null"); + this.nodeMapper = Objects.requireNonNull(nodeMapper, "nodeMapper must not be null"); + this.policy = Objects.requireNonNull(policy, "policy must not be null"); + this.scopeQuery = Optional.ofNullable(scopeQuery); + } + @Override public ContentQuery excerpt(long excerptLength) { nodeMapper.setExcerpt((int) excerptLength); @@ -268,16 +289,22 @@ private Page inMemorySortedPage(long page, long size, long offset) { private Query buildBaseQuery() { var baseQuery = new BooleanQuery.Builder(); queryBuilder.build().clauses().forEach(baseQuery::add); - baseQuery.add( - new TermQuery(new Term("content.type", contentType)), - BooleanClause.Occur.MUST); - startUri.ifPresent(uri -> baseQuery.add( - new PrefixQuery(new Term("_uri", uri)), - BooleanClause.Occur.FILTER)); + scopeQuery.ifPresent(query -> baseQuery.add(query, BooleanClause.Occur.FILTER)); + if (policy == LuceneQueryPolicy.CONTENT) { + baseQuery.add( + new TermQuery(new Term("content.type", contentType)), + BooleanClause.Occur.MUST); + startUri.ifPresent(uri -> baseQuery.add( + new PrefixQuery(new Term("_uri", uri)), + BooleanClause.Occur.FILTER)); + } return baseQuery.build(); } private Query structuralVisibilityQuery(Query query) { + if (policy == LuceneQueryPolicy.COLLECTION) { + return query; + } var visiblePages = new BooleanQuery.Builder(); visiblePages.add(query, BooleanClause.Occur.MUST); visiblePages.add( @@ -378,10 +405,17 @@ private boolean isManagerPreview() { } private boolean isAcceptedNode(ContentNode node) { - return !node.isDirectory() - && PageMetaData.isPage(node) - && PageMetaData.isVisible(node) - && extensionOperations.stream().allMatch(predicate -> predicate.test(node)); + if (node.isDirectory()) { + return false; + } + if (policy == LuceneQueryPolicy.COLLECTION && !NodeVisibility.isVisible(node)) { + return false; + } + if (policy == LuceneQueryPolicy.CONTENT + && (!PageMetaData.isPage(node) || !PageMetaData.isVisible(node))) { + return false; + } + return extensionOperations.stream().allMatch(predicate -> predicate.test(node)); } private void validatePage(long page, long size) { @@ -425,18 +459,27 @@ public Sort orderby(String field) { @Override public ContentQuery json() { + if (policy == LuceneQueryPolicy.COLLECTION) { + return this; + } this.contentType = Constants.ContentTypes.JSON; return this; } @Override public ContentQuery html() { + if (policy == LuceneQueryPolicy.COLLECTION) { + return this; + } this.contentType = Constants.ContentTypes.HTML; return this; } @Override public ContentQuery contentType(String contentType) { + if (policy == LuceneQueryPolicy.COLLECTION) { + return this; + } this.contentType = contentType; return this; } @@ -444,6 +487,9 @@ public ContentQuery contentType(String contentType) { @Override public ContentQuery variants(VariantSearchMode mode) { Objects.requireNonNull(mode, "mode must not be null"); + if (policy == LuceneQueryPolicy.COLLECTION) { + return this; + } if (mode != VariantSearchMode.ALL) { queryBuilder.add( new TermQuery(new Term("_variant", Boolean.toString(mode == VariantSearchMode.VARIANT))), diff --git a/cms-filesystem/src/main/java/com/condation/cms/filesystem/metadata/persistent/LuceneQueryPolicy.java b/cms-filesystem/src/main/java/com/condation/cms/filesystem/metadata/persistent/LuceneQueryPolicy.java new file mode 100644 index 000000000..02ca2d6a5 --- /dev/null +++ b/cms-filesystem/src/main/java/com/condation/cms/filesystem/metadata/persistent/LuceneQueryPolicy.java @@ -0,0 +1,31 @@ +package com.condation.cms.filesystem.metadata.persistent; + +/*- + * #%L + * CMS FileSystem + * %% + * Copyright (C) 2023 - 2026 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * #L% + */ + +/** + * Selects the domain rules applied on top of the shared Lucene query + * implementation. + */ +public enum LuceneQueryPolicy { + CONTENT, + COLLECTION +} diff --git a/cms-filesystem/src/test/java/com/condation/cms/filesystem/FileCollectionsTest.java b/cms-filesystem/src/test/java/com/condation/cms/filesystem/FileCollectionsTest.java new file mode 100644 index 000000000..91651a4fc --- /dev/null +++ b/cms-filesystem/src/test/java/com/condation/cms/filesystem/FileCollectionsTest.java @@ -0,0 +1,241 @@ +package com.condation.cms.filesystem; + +/*- + * #%L + * CMS FileSystem + * %% + * Copyright (C) 2023 - 2026 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * #L% + */ + +import com.condation.cms.api.db.ContentNode; +import com.condation.cms.api.db.ContentQuery; +import com.condation.cms.api.feature.features.IsPreviewFeature; +import com.condation.cms.api.feature.features.WorkflowFeature; +import com.condation.cms.api.request.RequestContext; +import com.condation.cms.api.request.RequestContextScope; +import com.condation.cms.api.workflow.WFStatusProvider; +import com.condation.cms.api.workflow.WFStatusQueryProvider; +import com.condation.cms.api.workflow.WorkflowInstance; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Map; +import java.util.Optional; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import org.yaml.snakeyaml.Yaml; + +class FileCollectionsTest { + + @TempDir + Path tempDirectory; + + @Test + void queriesCollectionsWithFilteringSortingPagingAndRawMarkdown() throws Exception { + write("blog/first.md", "title: First\nfeatured: true\nrank: 2", "# First"); + write("blog/second.md", "title: Second\nfeatured: false\nrank: 3", "**Second**"); + write("blog/third.md", "title: Third\nfeatured: true\nrank: 1", "_Third_"); + write("authors/first.md", "title: Author\nfeatured: true\nrank: 0", "Author body"); + write("blog/nested/ignored.md", "title: Ignored", "Ignored body"); + + var collections = createCollections(); + try { + Assertions.assertThat(collections.names()).containsExactlyInAnyOrder("blog", "authors"); + + var page = collections.collection("blog") + .query() + .where("featured", true) + .orderby("rank") + .asc() + .page(1, 1); + + Assertions.assertThat(page.getTotalItems()).isEqualTo(2); + Assertions.assertThat(page.getTotalPages()).isEqualTo(2); + Assertions.assertThat(page.getItems()) + .singleElement() + .satisfies(item -> { + Assertions.assertThat(item.id()).isEqualTo("third"); + Assertions.assertThat(item.collection()).isEqualTo("blog"); + Assertions.assertThat(item.path()).isEqualTo("blog/third.md"); + Assertions.assertThat(item.content()).isEqualTo("_Third_\r\n"); + Assertions.assertThat(item.meta()).containsEntry("title", "Third"); + }); + + Assertions.assertThat(collections.collection("authors").query().get()) + .extracting(item -> item.id()) + .containsExactly("first"); + } finally { + collections.close(); + } + } + + @Test + void appliesFlatFileChangesIncrementally() throws Exception { + var item = write("blog/item.md", "title: Before\nfeatured: false", "Before"); + var collections = createCollections(); + try { + write("blog/item.md", "title: After\nfeatured: true", "After"); + collections.handleEvent(new FileEvent(item.toFile(), FileEvent.Type.MODIFIED)); + collections.flushChanges(); + + Assertions.assertThat(collections.collection("blog").query().where("featured", true).get()) + .singleElement() + .satisfies(result -> { + Assertions.assertThat(result.meta()).containsEntry("title", "After"); + Assertions.assertThat(result.content()).isEqualTo("After\r\n"); + }); + + Files.delete(item); + collections.handleEvent(new FileEvent(item.toFile(), FileEvent.Type.DELETED)); + collections.flushChanges(); + Assertions.assertThat(collections.collection("blog").query().get()).isEmpty(); + } finally { + collections.close(); + } + } + + @Test + void rejectsUnsafeCollectionNames() throws Exception { + var collections = createCollections(); + try { + Assertions.assertThatIllegalArgumentException() + .isThrownBy(() -> collections.collection("../content")); + } finally { + collections.close(); + } + } + + @Test + void appliesDefaultWorkflowSchedulingAndPreview() throws Exception { + writeMeta("blog/published.md", "status: published", "Published"); + writeMeta("blog/draft.md", "status: draft", "Draft"); + writeMeta( + "blog/future.md", + "status: published\npublish_date: 2099-01-01T00:00:00Z", + "Future"); + writeMeta( + "blog/expired.md", + "status: published\nunpublish_date: 2000-01-01T00:00:00Z", + "Expired"); + + var collections = createCollections(); + try { + var publicPage = collections.collection("blog").query().page(1, 10); + Assertions.assertThat(publicPage.getTotalItems()).isEqualTo(1); + Assertions.assertThat(publicPage.getItems()) + .extracting(item -> item.id()) + .containsExactly("published"); + + var previewContext = new RequestContext(); + previewContext.add(IsPreviewFeature.class, new IsPreviewFeature(IsPreviewFeature.Mode.PREVIEW)); + var previewItems = ScopedValue.where(RequestContextScope.REQUEST_CONTEXT, previewContext) + .call(() -> collections.collection("blog").query().get()); + + Assertions.assertThat(previewItems) + .extracting(item -> item.id()) + .containsExactlyInAnyOrder("published", "draft", "future", "expired"); + } finally { + collections.close(); + } + } + + @Test + void appliesConfiguredContentWorkflow() throws Exception { + writeMeta("blog/approved.md", "status: draft\napproval: approved", "Approved"); + writeMeta("blog/blocked.md", "status: published\napproval: blocked", "Blocked"); + + var collections = createCollections(); + try { + var requestContext = new RequestContext(); + requestContext.add( + WorkflowFeature.class, + new WorkflowFeature(new WorkflowInstance("approval", "Approval", approvalStatusProvider()))); + + var result = ScopedValue.where(RequestContextScope.REQUEST_CONTEXT, requestContext) + .call(() -> collections.collection("blog").query().page(1, 10)); + + Assertions.assertThat(result.getTotalItems()).isEqualTo(1); + Assertions.assertThat(result.getItems()) + .singleElement() + .satisfies(item -> Assertions.assertThat(item.id()).isEqualTo("approved")); + } finally { + collections.close(); + } + } + + private FileCollections createCollections() throws Exception { + var collections = new FileCollections("test-site", tempDirectory, FileCollectionsTest::parseMeta); + collections.init(); + return collections; + } + + private Path write(String relativePath, String meta, String body) throws Exception { + return writeMeta(relativePath, "status: published\n" + meta, body); + } + + private Path writeMeta(String relativePath, String meta, String body) throws Exception { + var file = tempDirectory.resolve("collections").resolve(relativePath); + Files.createDirectories(file.getParent()); + Files.writeString(file, "---\n%s\n---\n%s\n".formatted(meta, body)); + return file; + } + + private static WFStatusProvider approvalStatusProvider() { + return new WFStatusQueryProvider() { + @Override + public boolean isPublished(ContentNode node) { + return "approved".equals(node.data().get("approval")); + } + + @Override + public Status status(ContentNode node) { + return new Status( + isPublished(node), + null, + null, + true, + (String) node.data().get("approval")); + } + + @Override + public String newNodeStatus() { + return "blocked"; + } + + @Override + public Optional> published(ContentQuery query) { + return Optional.of(query.where("approval", "approved")); + } + + @Override + public ContentQuery unpublished(ContentQuery query) { + return query.where("approval", "!=", "approved"); + } + }; + } + + @SuppressWarnings("unchecked") + private static Map parseMeta(Path file) { + try { + var content = Files.readString(file); + var parts = content.split("---", 3); + return parts.length == 3 ? new Yaml().load(parts[1]) : Map.of(); + } catch (Exception ex) { + throw new RuntimeException(ex); + } + } +} diff --git a/cms-templates/src/main/java/com/condation/cms/templates/DynamicConfiguration.java b/cms-templates/src/main/java/com/condation/cms/templates/DynamicConfiguration.java index 311bb1172..901ed9d77 100644 --- a/cms-templates/src/main/java/com/condation/cms/templates/DynamicConfiguration.java +++ b/cms-templates/src/main/java/com/condation/cms/templates/DynamicConfiguration.java @@ -119,7 +119,13 @@ public Object invoke(Object... params) { } else if (params.length == 1 && params[0] instanceof Map) { parameter = new Parameter((Map)params[0], requestContext); } else { - parameter = new Parameter(); + parameter = new Parameter(requestContext); + for (int index = 0; index < params.length; index++) { + parameter.put(Integer.toString(index), params[index]); + } + if (params.length == 1) { + parameter.put("value", params[0]); + } } return function.apply(parameter); } diff --git a/cms-templates/src/test/java/com/condation/cms/templates/DynamicConfigurationFunctionWrapperTest.java b/cms-templates/src/test/java/com/condation/cms/templates/DynamicConfigurationFunctionWrapperTest.java new file mode 100644 index 000000000..6a751bacd --- /dev/null +++ b/cms-templates/src/test/java/com/condation/cms/templates/DynamicConfigurationFunctionWrapperTest.java @@ -0,0 +1,40 @@ +package com.condation.cms.templates; + +/*- + * #%L + * CMS Templates + * %% + * Copyright (C) 2023 - 2026 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * #L% + */ + +import com.condation.cms.api.request.RequestContext; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; + +class DynamicConfigurationFunctionWrapperTest { + + @Test + void exposesSinglePositionalArgumentToContextStyleFunctions() { + var wrapper = new DynamicConfiguration.FunctionWrapper( + "cms", + "collection", + params -> params.get("value"), + new RequestContext()); + + Assertions.assertThat(wrapper.invoke("blog")).isEqualTo("blog"); + } +} diff --git a/cms-templates/src/test/java/com/condation/cms/templates/TemplateEngineFunctionsTest.java b/cms-templates/src/test/java/com/condation/cms/templates/TemplateEngineFunctionsTest.java index e247fc616..64e083b5f 100644 --- a/cms-templates/src/test/java/com/condation/cms/templates/TemplateEngineFunctionsTest.java +++ b/cms-templates/src/test/java/com/condation/cms/templates/TemplateEngineFunctionsTest.java @@ -46,7 +46,6 @@ import java.util.List; import java.util.Map; import java.util.Optional; -import java.util.function.Function; import org.assertj.core.api.Assertions; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; @@ -100,6 +99,7 @@ public TemplateLoader getLoader() { .add("fn_param", "{{ ext.testfn4({'name': 'World'}) }}") // explicit namespace .add("fn_namespace", "{{ ns1.shout({'text': 'hello'}) }}") + .add("fn_collection", "{{ cms.collection('blog').query() }}") // chained with filter .add("fn_chained", "{{ ext.testfn3() | upper }}"); } @@ -157,6 +157,13 @@ void test_explicit_namespace_function_renders() throws IOException { .isEqualToIgnoringWhitespace("HELLO!"); } + @Test + void test_collection_function_accepts_scalar_name_and_can_be_chained() throws IOException { + Template template = SUT.getTemplate("fn_collection"); + Assertions.assertThat(template.evaluate(Map.of(), dynamicConfiguration)) + .isEqualToIgnoringWhitespace("blog"); + } + // --- function result can be chained with filter --- @Test @@ -171,8 +178,9 @@ void test_function_result_chained_with_filter() throws IOException { @Test void all_functions_are_registered() { var tfs = dynamicConfiguration.templateFunctions(); - // testfn1 (map), testfn2 (Parameter), testfn3 (no-arg), testfn4 (@Param), shout (@Param, ns1) - Assertions.assertThat(tfs).isNotNull().hasSize(5); + // testfn1 (map), testfn2 (Parameter), testfn3 (no-arg), testfn4 (@Param), + // shout (@Param, ns1), collection (Parameter, cms) + Assertions.assertThat(tfs).isNotNull().hasSize(6); } @Test @@ -216,5 +224,17 @@ public Object testfn4(@Param("name") String name) { public Object shout(@Param("text") String text) { return text.toUpperCase() + "!"; } + + @TemplateFunction(value = "collection", namespace = "cms") + public Object collection(Parameter params) { + return new TestCollection(params.get("value").toString()); + } + } + + public record TestCollection(String name) { + + public String query() { + return name; + } } } diff --git a/modules/system-modules/src/main/java/com/condation/cms/modules/system/templates/CollectionTemplateFunctionExtensions.java b/modules/system-modules/src/main/java/com/condation/cms/modules/system/templates/CollectionTemplateFunctionExtensions.java new file mode 100644 index 000000000..0ed1f505b --- /dev/null +++ b/modules/system-modules/src/main/java/com/condation/cms/modules/system/templates/CollectionTemplateFunctionExtensions.java @@ -0,0 +1,44 @@ +package com.condation.cms.modules.system.templates; + +/*- + * #%L + * CMS System Modules + * %% + * Copyright (C) 2023 - 2026 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * #L% + */ + +import com.condation.cms.api.annotations.TemplateFunction; +import com.condation.cms.api.extensions.RegisterTemplateFunctionExtensionPoint; +import com.condation.cms.api.feature.features.DBFeature; +import com.condation.cms.api.model.Parameter; +import com.condation.modules.api.annotation.Extension; + +/** + * Makes site collections available as {@code cms.collection('name')}. + */ +@Extension(value = RegisterTemplateFunctionExtensionPoint.class, cached = Extension.Caching.TRUE) +public class CollectionTemplateFunctionExtensions extends RegisterTemplateFunctionExtensionPoint { + + @TemplateFunction(value = "collection", namespace = "cms") + public Object collection(Parameter params) { + var value = params.containsKey("name") ? params.get("name") : params.get("value"); + if (value == null) { + throw new IllegalArgumentException("collection name must not be null"); + } + return context.get(DBFeature.class).db().getCollections().collection(value.toString()); + } +} diff --git a/test-server/hosts/demo/collections/authors/thorsten.md b/test-server/hosts/demo/collections/authors/thorsten.md new file mode 100644 index 000000000..766efc1bd --- /dev/null +++ b/test-server/hosts/demo/collections/authors/thorsten.md @@ -0,0 +1,6 @@ +--- +title: Thorsten +status: published +description: CondationCMS main author +publish_date: 2026-04-07T00:00:00Z +--- \ No newline at end of file diff --git a/test-server/hosts/demo/collections/blog/item_1.md b/test-server/hosts/demo/collections/blog/item_1.md new file mode 100644 index 000000000..d6c134067 --- /dev/null +++ b/test-server/hosts/demo/collections/blog/item_1.md @@ -0,0 +1,6 @@ +--- +title: Blog item 1 +status: published +description: This is the first item +publish_date: 2026-04-07T00:00:00Z +--- \ No newline at end of file diff --git a/test-server/hosts/demo/collections/blog/item_2.md b/test-server/hosts/demo/collections/blog/item_2.md new file mode 100644 index 000000000..82ebb2baf --- /dev/null +++ b/test-server/hosts/demo/collections/blog/item_2.md @@ -0,0 +1,6 @@ +--- +title: Blog item 2 +status: published +description: This is the second item +publish_date: 2026-04-08T00:00:00Z +--- \ No newline at end of file diff --git a/test-server/hosts/demo/collections/blog/item_3.md b/test-server/hosts/demo/collections/blog/item_3.md new file mode 100644 index 000000000..8e1132c46 --- /dev/null +++ b/test-server/hosts/demo/collections/blog/item_3.md @@ -0,0 +1,6 @@ +--- +title: Blog item 3 +status: draft +description: This is the third item +publish_date: 2026-04-09T00:00:00Z +--- \ No newline at end of file diff --git a/test-server/hosts/demo/content/collections/index.md b/test-server/hosts/demo/content/collections/index.md new file mode 100644 index 000000000..391189bc0 --- /dev/null +++ b/test-server/hosts/demo/content/collections/index.md @@ -0,0 +1,6 @@ +--- +title: Collections Test +status: published +template: collections.html +--- + diff --git a/test-server/themes/demo/templates/collections.html b/test-server/themes/demo/templates/collections.html new file mode 100644 index 000000000..98716c1e2 --- /dev/null +++ b/test-server/themes/demo/templates/collections.html @@ -0,0 +1,43 @@ + + + + + + + + + {% include "libs/fragments.html" %} + + + + + + +
+
+ {{ node.content | raw }} +
+ +

Blog collection

+ {% assign items = cms.collection("blog").query().get() %} + + {% for item in items %} +
+ {{ item.meta.title | raw }} +
+ {% endfor %} + +

Author collection

+ {% assign items = cms.collection("authors").query().get() %} + + {% for item in items %} +
+ {{ item.meta.title | raw }} +
+ {% endfor %} +
+ + + + + \ No newline at end of file From c6c1c096ff3cc7b7c908ee23a7155a129fbb7616 Mon Sep 17 00:00:00 2001 From: Thorsten Marx Date: Thu, 27 Aug 2026 21:04:30 +0200 Subject: [PATCH 02/28] fix sonar issues --- .../java/com/condation/cms/filesystem/FileCollections.java | 6 +++++- .../com/condation/cms/filesystem/FileCollectionsTest.java | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/cms-filesystem/src/main/java/com/condation/cms/filesystem/FileCollections.java b/cms-filesystem/src/main/java/com/condation/cms/filesystem/FileCollections.java index 361f69f17..f5357a37d 100644 --- a/cms-filesystem/src/main/java/com/condation/cms/filesystem/FileCollections.java +++ b/cms-filesystem/src/main/java/com/condation/cms/filesystem/FileCollections.java @@ -36,6 +36,7 @@ import java.time.ZoneId; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.function.Function; @@ -81,7 +82,10 @@ public void init() throws IOException { rebuild(); watcher = new MultiRootRecursiveWatcher(siteId, List.of(collectionsBase)); - watcher.getPublisher(collectionsBase).subscribe( + var publisher = Objects.requireNonNull( + watcher.getPublisher(collectionsBase), + "collections publisher must be available"); + publisher.subscribe( new MultiRootRecursiveWatcher.AbstractFileEventSubscriber() { @Override public void onNext(FileEvent item) { diff --git a/cms-filesystem/src/test/java/com/condation/cms/filesystem/FileCollectionsTest.java b/cms-filesystem/src/test/java/com/condation/cms/filesystem/FileCollectionsTest.java index 91651a4fc..3f5908bf0 100644 --- a/cms-filesystem/src/test/java/com/condation/cms/filesystem/FileCollectionsTest.java +++ b/cms-filesystem/src/test/java/com/condation/cms/filesystem/FileCollectionsTest.java @@ -190,7 +190,7 @@ private Path write(String relativePath, String meta, String body) throws Excepti private Path writeMeta(String relativePath, String meta, String body) throws Exception { var file = tempDirectory.resolve("collections").resolve(relativePath); Files.createDirectories(file.getParent()); - Files.writeString(file, "---\n%s\n---\n%s\n".formatted(meta, body)); + Files.writeString(file, "---%n%s%n---%n%s%n".formatted(meta, body)); return file; } From cc0dc96d742b90420c098fc65dd2fb0e1d25220d Mon Sep 17 00:00:00 2001 From: Thorsten Marx Date: Thu, 27 Aug 2026 21:07:46 +0200 Subject: [PATCH 03/28] remove unused imports --- .../cms/filesystem/metadata/persistent/PersistentMetaData.java | 1 - 1 file changed, 1 deletion(-) diff --git a/cms-filesystem/src/main/java/com/condation/cms/filesystem/metadata/persistent/PersistentMetaData.java b/cms-filesystem/src/main/java/com/condation/cms/filesystem/metadata/persistent/PersistentMetaData.java index 0b79827c7..63acbfdb4 100644 --- a/cms-filesystem/src/main/java/com/condation/cms/filesystem/metadata/persistent/PersistentMetaData.java +++ b/cms-filesystem/src/main/java/com/condation/cms/filesystem/metadata/persistent/PersistentMetaData.java @@ -25,7 +25,6 @@ import com.condation.cms.api.db.ContentNode; import com.condation.cms.api.db.ContentQuery; import com.condation.cms.api.db.NodeVisibility; -import com.condation.cms.api.db.VariantSearchMode; import com.condation.cms.api.utils.PathUtil; import com.condation.cms.filesystem.metadata.AbstractMetaData; import com.condation.cms.filesystem.metadata.persistent.field.IndexFieldConfiguration; From 1a3fbbe086e613d7e8f04f5a61ae3ea791be37f7 Mon Sep 17 00:00:00 2001 From: Thorsten Marx Date: Fri, 28 Aug 2026 10:37:47 +0200 Subject: [PATCH 04/28] routing for collections, iteration 2 --- .../configs/CollectionConfiguration.java | 45 +++++ .../configs/CollectionDefinition.java | 45 +++++ .../CollectionDetailConfiguration.java | 68 ++++++++ .../condation/cms/api/db/DBFileSystem.java | 2 + .../cms/api/db/collection/Collection.java | 3 + .../events/ReloadCollectionsConfig.java | 30 ++++ .../cms/content/CollectionResolver.java | 150 ++++++++++++++++ .../cms/content/ContentRenderer.java | 8 + .../cms/content/DefaultContentRenderer.java | 43 +++++ .../template/functions/LinkFunction.java | 40 +++++ .../cms/content/CollectionResolverTest.java | 140 +++++++++++++++ .../template/functions/LinkFunctionTest.java | 111 ++++++++++++ .../core/configuration/ConfigManagement.java | 6 + .../configuration/ConfigurationFactory.java | 22 +++ .../configs/CollectionConfiguration.java | 163 ++++++++++++++++++ .../CollectionConfigurationTest.java | 73 ++++++++ .../cms/filesystem/FileCollections.java | 17 ++ .../condation/cms/filesystem/FileSystem.java | 6 + .../cms/filesystem/FileCollectionsTest.java | 11 ++ .../cms/server/configs/SiteHandlerModule.java | 2 + .../cms/server/configs/SiteModule.java | 10 ++ .../content/JettyCollectionHandler.java | 61 +++++++ .../com/condation/cms/server/host/VHost.java | 3 + .../demo/collections/authors/thorsten.md | 1 + .../hosts/demo/config/collections.yaml | 9 + .../themes/demo/templates/collections.html | 6 +- .../templates/collections/author-detail.html | 26 +++ .../templates/collections/blog-detail.html | 20 +++ 28 files changed, 1118 insertions(+), 3 deletions(-) create mode 100644 cms-api/src/main/java/com/condation/cms/api/configuration/configs/CollectionConfiguration.java create mode 100644 cms-api/src/main/java/com/condation/cms/api/configuration/configs/CollectionDefinition.java create mode 100644 cms-api/src/main/java/com/condation/cms/api/configuration/configs/CollectionDetailConfiguration.java create mode 100644 cms-api/src/main/java/com/condation/cms/api/eventbus/events/ReloadCollectionsConfig.java create mode 100644 cms-content/src/main/java/com/condation/cms/content/CollectionResolver.java create mode 100644 cms-content/src/test/java/com/condation/cms/content/CollectionResolverTest.java create mode 100644 cms-content/src/test/java/com/condation/cms/content/template/functions/LinkFunctionTest.java create mode 100644 cms-core/src/main/java/com/condation/cms/core/configuration/configs/CollectionConfiguration.java create mode 100644 cms-core/src/test/java/com/condation/cms/core/configuration/CollectionConfigurationTest.java create mode 100644 cms-server/src/main/java/com/condation/cms/server/handler/content/JettyCollectionHandler.java create mode 100644 test-server/hosts/demo/config/collections.yaml create mode 100644 test-server/themes/demo/templates/collections/author-detail.html create mode 100644 test-server/themes/demo/templates/collections/blog-detail.html diff --git a/cms-api/src/main/java/com/condation/cms/api/configuration/configs/CollectionConfiguration.java b/cms-api/src/main/java/com/condation/cms/api/configuration/configs/CollectionConfiguration.java new file mode 100644 index 000000000..a3638b71e --- /dev/null +++ b/cms-api/src/main/java/com/condation/cms/api/configuration/configs/CollectionConfiguration.java @@ -0,0 +1,45 @@ +package com.condation.cms.api.configuration.configs; + +/*- + * #%L + * CMS Api + * %% + * Copyright (C) 2023 - 2026 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * #L% + */ + +import com.condation.cms.api.configuration.Config; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.ConcurrentMap; +import lombok.RequiredArgsConstructor; + +/** + * Reloadable, site-scoped collection definitions. + */ +@RequiredArgsConstructor +public class CollectionConfiguration implements Config { + + private final ConcurrentMap collections; + + public Optional collection(String name) { + return Optional.ofNullable(collections.get(name)); + } + + public Map collections() { + return Map.copyOf(collections); + } +} diff --git a/cms-api/src/main/java/com/condation/cms/api/configuration/configs/CollectionDefinition.java b/cms-api/src/main/java/com/condation/cms/api/configuration/configs/CollectionDefinition.java new file mode 100644 index 000000000..4a8b20a6b --- /dev/null +++ b/cms-api/src/main/java/com/condation/cms/api/configuration/configs/CollectionDefinition.java @@ -0,0 +1,45 @@ +package com.condation.cms.api.configuration.configs; + +/*- + * #%L + * CMS Api + * %% + * Copyright (C) 2023 - 2026 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * #L% + */ + +import java.util.Objects; +import java.util.Optional; +import java.util.regex.Pattern; + +/** + * Configuration of one named collection. + */ +public record CollectionDefinition(String name, CollectionDetailConfiguration detail) { + + private static final Pattern NAME = Pattern.compile("[a-zA-Z0-9][a-zA-Z0-9_-]*"); + + public CollectionDefinition { + Objects.requireNonNull(name, "collection name must not be null"); + if (!NAME.matcher(name).matches()) { + throw new IllegalArgumentException("invalid collection name: " + name); + } + } + + public Optional detailPage() { + return Optional.ofNullable(detail); + } +} diff --git a/cms-api/src/main/java/com/condation/cms/api/configuration/configs/CollectionDetailConfiguration.java b/cms-api/src/main/java/com/condation/cms/api/configuration/configs/CollectionDetailConfiguration.java new file mode 100644 index 000000000..d4e3e629f --- /dev/null +++ b/cms-api/src/main/java/com/condation/cms/api/configuration/configs/CollectionDetailConfiguration.java @@ -0,0 +1,68 @@ +package com.condation.cms.api.configuration.configs; + +/*- + * #%L + * CMS Api + * %% + * Copyright (C) 2023 - 2026 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * #L% + */ + +import java.util.Objects; +import java.util.regex.Pattern; + +/** + * Route and template used for collection detail pages. + */ +public record CollectionDetailConfiguration(String route, String template) { + + private static final Pattern PARAMETER = Pattern.compile("\\{([a-zA-Z][a-zA-Z0-9_.-]*)}"); + + public CollectionDetailConfiguration { + Objects.requireNonNull(route, "collection detail route must not be null"); + Objects.requireNonNull(template, "collection detail template must not be null"); + + route = normalizeRoute(route); + template = template.trim(); + if (template.isEmpty()) { + throw new IllegalArgumentException("collection detail template must not be blank"); + } + + var matcher = PARAMETER.matcher(route); + if (!matcher.find() || matcher.find()) { + throw new IllegalArgumentException("collection detail route must contain exactly one parameter"); + } + } + + public String parameter() { + var matcher = PARAMETER.matcher(route); + if (!matcher.find()) { + throw new IllegalStateException("collection detail route has no parameter"); + } + return matcher.group(1); + } + + private static String normalizeRoute(String value) { + var normalized = value.trim(); + if (!normalized.startsWith("/")) { + normalized = "/" + normalized; + } + while (normalized.length() > 1 && normalized.endsWith("/")) { + normalized = normalized.substring(0, normalized.length() - 1); + } + return normalized; + } +} diff --git a/cms-api/src/main/java/com/condation/cms/api/db/DBFileSystem.java b/cms-api/src/main/java/com/condation/cms/api/db/DBFileSystem.java index ee970c893..26e215216 100644 --- a/cms-api/src/main/java/com/condation/cms/api/db/DBFileSystem.java +++ b/cms-api/src/main/java/com/condation/cms/api/db/DBFileSystem.java @@ -47,6 +47,8 @@ public interface DBFileSystem { ReadOnlyFile contentBase(); + ReadOnlyFile collectionsBase(); + ReadOnlyFile assetBase(); /** diff --git a/cms-api/src/main/java/com/condation/cms/api/db/collection/Collection.java b/cms-api/src/main/java/com/condation/cms/api/db/collection/Collection.java index b1375cec3..327a61d82 100644 --- a/cms-api/src/main/java/com/condation/cms/api/db/collection/Collection.java +++ b/cms-api/src/main/java/com/condation/cms/api/db/collection/Collection.java @@ -22,6 +22,7 @@ */ import com.condation.cms.api.db.ContentQuery; +import java.util.Optional; /** * A named, file-backed collection. @@ -30,5 +31,7 @@ public interface Collection { String name(); + Optional item(String id); + ContentQuery query(); } diff --git a/cms-api/src/main/java/com/condation/cms/api/eventbus/events/ReloadCollectionsConfig.java b/cms-api/src/main/java/com/condation/cms/api/eventbus/events/ReloadCollectionsConfig.java new file mode 100644 index 000000000..e249e6202 --- /dev/null +++ b/cms-api/src/main/java/com/condation/cms/api/eventbus/events/ReloadCollectionsConfig.java @@ -0,0 +1,30 @@ +package com.condation.cms.api.eventbus.events; + +/*- + * #%L + * CMS Api + * %% + * Copyright (C) 2023 - 2026 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * #L% + */ + +import com.condation.cms.api.eventbus.Event; + +/** + * Triggers reloading the site collection configuration. + */ +public record ReloadCollectionsConfig() implements Event { +} diff --git a/cms-content/src/main/java/com/condation/cms/content/CollectionResolver.java b/cms-content/src/main/java/com/condation/cms/content/CollectionResolver.java new file mode 100644 index 000000000..58ef4403a --- /dev/null +++ b/cms-content/src/main/java/com/condation/cms/content/CollectionResolver.java @@ -0,0 +1,150 @@ +package com.condation.cms.content; + +/*- + * #%L + * CMS Content + * %% + * Copyright (C) 2023 - 2026 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * #L% + */ + +import com.condation.cms.api.Constants; +import com.condation.cms.api.configuration.Configuration; +import com.condation.cms.api.configuration.configs.CollectionConfiguration; +import com.condation.cms.api.configuration.configs.CollectionDefinition; +import com.condation.cms.api.configuration.configs.CollectionDetailConfiguration; +import com.condation.cms.api.content.ContentResponse; +import com.condation.cms.api.content.DefaultContentResponse; +import com.condation.cms.api.db.ContentNode; +import com.condation.cms.api.db.DB; +import com.condation.cms.api.db.collection.CollectionItem; +import com.condation.cms.api.feature.features.CurrentNodeFeature; +import com.condation.cms.api.feature.features.RequestFeature; +import com.condation.cms.api.request.RequestContext; +import java.io.IOException; +import java.util.Comparator; +import java.util.HashMap; +import java.util.Optional; +import java.util.regex.Pattern; +import lombok.RequiredArgsConstructor; + +/** + * Resolves configured collection detail routes and renders their items. + */ +@RequiredArgsConstructor +public class CollectionResolver { + + private final ContentRenderer contentRenderer; + private final DB db; + private final Configuration configuration; + + public Optional getContent(RequestContext context) throws IOException { + var collectionConfiguration = configuration.get(CollectionConfiguration.class); + if (collectionConfiguration == null) { + return Optional.empty(); + } + + var uri = normalizeUri(context.get(RequestFeature.class).uri()); + for (var definition : collectionConfiguration.collections().values().stream() + .sorted(Comparator.comparing(CollectionDefinition::name)) + .toList()) { + var detail = definition.detailPage(); + if (detail.isEmpty()) { + continue; + } + var routeValue = match(detail.get(), uri); + if (routeValue.isEmpty()) { + continue; + } + return resolve(definition, detail.get(), routeValue.get(), uri, context); + } + return Optional.empty(); + } + + private Optional resolve( + CollectionDefinition definition, + CollectionDetailConfiguration detail, + String routeValue, + String uri, + RequestContext context) throws IOException { + var collection = db.getCollections().collection(definition.name()); + Optional item; + if ("id".equals(detail.parameter())) { + item = findById(collection, routeValue); + } else { + item = collection.query() + .where(detail.parameter(), routeValue) + .page(1, 1) + .getItems() + .stream() + .findFirst(); + } + if (item.isEmpty()) { + return Optional.empty(); + } + + var collectionItem = item.get(); + var nodeData = new HashMap<>(collectionItem.meta()); + nodeData.put("template", detail.template()); + var node = new ContentNode( + collectionItem.path(), + uri, + collectionItem.id() + ".md", + nodeData); + context.add(CurrentNodeFeature.class, new CurrentNodeFeature(node)); + + var collectionFile = db.getFileSystem().collectionsBase().resolve(collectionItem.path()); + if (!collectionFile.exists()) { + return Optional.empty(); + } + var content = contentRenderer.renderCollection( + collectionFile, + node, + collectionItem, + detail.template(), + context); + return Optional.of(new DefaultContentResponse(content, Constants.DEFAULT_CONTENT_TYPE, node)); + } + + private static Optional findById( + com.condation.cms.api.db.collection.Collection collection, + String id) { + try { + return collection.item(id); + } catch (IllegalArgumentException ex) { + return Optional.empty(); + } + } + + private static Optional match(CollectionDetailConfiguration detail, String uri) { + var token = "{" + detail.parameter() + "}"; + var tokenStart = detail.route().indexOf(token); + var prefix = detail.route().substring(0, tokenStart); + var suffix = detail.route().substring(tokenStart + token.length()); + var routePattern = Pattern.compile( + "^" + Pattern.quote(prefix) + "([^/]+)" + Pattern.quote(suffix) + "/?$"); + var matcher = routePattern.matcher(uri); + return matcher.matches() ? Optional.of(matcher.group(1)) : Optional.empty(); + } + + private static String normalizeUri(String uri) { + var normalized = uri == null ? "" : uri.trim(); + if (!normalized.startsWith("/")) { + normalized = "/" + normalized; + } + return normalized; + } +} diff --git a/cms-content/src/main/java/com/condation/cms/content/ContentRenderer.java b/cms-content/src/main/java/com/condation/cms/content/ContentRenderer.java index f4b7d5782..ccfe0e37d 100644 --- a/cms-content/src/main/java/com/condation/cms/content/ContentRenderer.java +++ b/cms-content/src/main/java/com/condation/cms/content/ContentRenderer.java @@ -23,6 +23,7 @@ import com.condation.cms.api.db.ContentNode; import com.condation.cms.api.db.Page; +import com.condation.cms.api.db.collection.CollectionItem; import com.condation.cms.api.db.cms.ReadOnlyFile; import com.condation.cms.api.db.taxonomy.Taxonomy; import com.condation.cms.api.model.ListNode; @@ -47,6 +48,13 @@ public interface ContentRenderer { String render(final ReadOnlyFile contentFile, final RequestContext context, final Map> sectionEntries, final Map meta, final String markdownContent, final Consumer modelExtending) throws IOException; + String renderCollection( + final ReadOnlyFile collectionFile, + final ContentNode collectionNode, + final CollectionItem item, + final String template, + final RequestContext context) throws IOException; + Map> renderSectionEntries(final List sectionEntryNodes, final RequestContext context) throws IOException; String renderTaxonomy(final Optional contentFileOpt, final Taxonomy taxonomy, Optional taxonomyValue, final RequestContext context, final Map meta, final Page page, Map> sectionEntries) throws IOException; diff --git a/cms-content/src/main/java/com/condation/cms/content/DefaultContentRenderer.java b/cms-content/src/main/java/com/condation/cms/content/DefaultContentRenderer.java index d14c280c5..94d1f86e1 100644 --- a/cms-content/src/main/java/com/condation/cms/content/DefaultContentRenderer.java +++ b/cms-content/src/main/java/com/condation/cms/content/DefaultContentRenderer.java @@ -27,6 +27,7 @@ import com.condation.cms.api.db.ContentNode; import com.condation.cms.api.db.DB; import com.condation.cms.api.db.Page; +import com.condation.cms.api.db.collection.CollectionItem; import com.condation.cms.api.db.cms.ReadOnlyFile; import com.condation.cms.api.db.taxonomy.Taxonomy; import com.condation.cms.api.extensions.ContentQueryOperatorExtensionPoint; @@ -137,6 +138,29 @@ private String renderContent(final String rawContent, final RequestContext conte return pipeline.process(rawContent); } + @Override + public String renderCollection( + ReadOnlyFile collectionFile, + ContentNode collectionNode, + CollectionItem item, + String template, + RequestContext context) throws IOException { + var meta = new HashMap<>(item.meta()); + meta.put("template", template); + return renderResolved( + collectionFile, + collectionNode.url(), + context, + Collections.emptyMap(), + meta, + item.content(), + Optional.of(collectionNode), + model -> { + model.values.put("collection_item", item); + model.values.put("collection", db.getCollections().collection(item.collection())); + }); + } + @Override public String render(final ReadOnlyFile contentFile, final RequestContext context, final Map> sectionEntries, @@ -145,7 +169,26 @@ public String render(final ReadOnlyFile contentFile, final RequestContext contex var uri = PathUtil.toRelativeFile(contentFile, db.getFileSystem().contentBase()); Optional contentNode = db.getContent().byUri(uri); + return renderResolved( + contentFile, + uri, + context, + sectionEntries, + meta, + rawContent, + contentNode, + modelExtending); + } + private String renderResolved( + ReadOnlyFile contentFile, + String uri, + RequestContext context, + Map> sectionEntries, + Map meta, + String rawContent, + Optional contentNode, + Consumer modelExtending) throws IOException { TemplateEngine.Model model = new TemplateEngine.Model( contentFile, contentNode.orElse(null), diff --git a/cms-content/src/main/java/com/condation/cms/content/template/functions/LinkFunction.java b/cms-content/src/main/java/com/condation/cms/content/template/functions/LinkFunction.java index 97f693ffb..86fe44500 100644 --- a/cms-content/src/main/java/com/condation/cms/content/template/functions/LinkFunction.java +++ b/cms-content/src/main/java/com/condation/cms/content/template/functions/LinkFunction.java @@ -22,8 +22,13 @@ */ +import com.condation.cms.api.configuration.configs.CollectionConfiguration; +import com.condation.cms.api.db.collection.CollectionItem; +import com.condation.cms.api.feature.features.ConfigurationFeature; import com.condation.cms.api.request.RequestContext; import com.condation.cms.api.utils.HTTPUtil; +import com.condation.cms.api.utils.MapUtil; +import java.util.Objects; import lombok.RequiredArgsConstructor; /** @@ -38,4 +43,39 @@ public class LinkFunction { public String createUrl (String url) { return HTTPUtil.modifyUrl(url, requestContext); } + + /** + * Creates the configured detail URL for a collection item. + * + * @param item collection item to link to + * @return context-aware detail URL + */ + public String collectionUrl(CollectionItem item) { + Objects.requireNonNull(item, "collection item must not be null"); + var configuration = requestContext.get(ConfigurationFeature.class) + .configuration() + .get(CollectionConfiguration.class); + if (configuration == null) { + throw new IllegalStateException("collection configuration is not available"); + } + + var definition = configuration.collection(item.collection()) + .orElseThrow(() -> new IllegalArgumentException( + "collection is not configured: " + item.collection())); + var detail = definition.detailPage() + .orElseThrow(() -> new IllegalArgumentException( + "collection has no detail route: " + item.collection())); + var parameterValue = "id".equals(detail.parameter()) + ? item.id() + : MapUtil.getValue(item.meta(), detail.parameter()); + if (parameterValue == null || parameterValue.toString().isBlank()) { + throw new IllegalArgumentException( + "collection item has no route value for: " + detail.parameter()); + } + + var route = detail.route().replace( + "{" + detail.parameter() + "}", + parameterValue.toString()); + return createUrl(route); + } } diff --git a/cms-content/src/test/java/com/condation/cms/content/CollectionResolverTest.java b/cms-content/src/test/java/com/condation/cms/content/CollectionResolverTest.java new file mode 100644 index 000000000..9f0ca6bf7 --- /dev/null +++ b/cms-content/src/test/java/com/condation/cms/content/CollectionResolverTest.java @@ -0,0 +1,140 @@ +package com.condation.cms.content; + +/*- + * #%L + * CMS Content + * %% + * Copyright (C) 2023 - 2026 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * #L% + */ + +import com.condation.cms.api.configuration.Configuration; +import com.condation.cms.api.configuration.configs.CollectionConfiguration; +import com.condation.cms.api.configuration.configs.CollectionDefinition; +import com.condation.cms.api.configuration.configs.CollectionDetailConfiguration; +import com.condation.cms.api.content.DefaultContentResponse; +import com.condation.cms.api.db.ContentQuery; +import com.condation.cms.api.db.DB; +import com.condation.cms.api.db.DBFileSystem; +import com.condation.cms.api.db.Page; +import com.condation.cms.api.db.cms.ReadOnlyFile; +import com.condation.cms.api.db.collection.Collection; +import com.condation.cms.api.db.collection.CollectionItem; +import com.condation.cms.api.feature.features.CurrentNodeFeature; +import com.condation.cms.api.feature.features.RequestFeature; +import com.condation.cms.api.request.RequestContext; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Mockito; + +class CollectionResolverTest { + + private final ContentRenderer renderer = Mockito.mock(ContentRenderer.class); + private final DB db = Mockito.mock(DB.class); + private final com.condation.cms.api.db.collection.Collections collections = + Mockito.mock(com.condation.cms.api.db.collection.Collections.class); + private final Collection collection = Mockito.mock(Collection.class); + private final DBFileSystem fileSystem = Mockito.mock(DBFileSystem.class); + private final ReadOnlyFile collectionsBase = Mockito.mock(ReadOnlyFile.class); + private final ReadOnlyFile itemFile = Mockito.mock(ReadOnlyFile.class); + private final ConcurrentHashMap definitions = new ConcurrentHashMap<>(); + private final Configuration configuration = new Configuration(); + private final CollectionItem item = new CollectionItem( + "first", + "blog", + "blog/first.md", + "# First", + Map.of("title", "First", "slug", "first-post")); + + @BeforeEach + void setUp() throws Exception { + configuration.add(CollectionConfiguration.class, new CollectionConfiguration(definitions)); + Mockito.when(db.getCollections()).thenReturn(collections); + Mockito.when(collections.collection("blog")).thenReturn(collection); + Mockito.when(db.getFileSystem()).thenReturn(fileSystem); + Mockito.when(fileSystem.collectionsBase()).thenReturn(collectionsBase); + Mockito.when(collectionsBase.resolve("blog/first.md")).thenReturn(itemFile); + Mockito.when(itemFile.exists()).thenReturn(true); + Mockito.when(renderer.renderCollection( + Mockito.eq(itemFile), + Mockito.any(), + Mockito.eq(item), + Mockito.anyString(), + Mockito.any())).thenReturn("

First

"); + } + + @Test + void resolvesAnIdRouteAndUsesReloadedDefinitions() throws Exception { + definitions.put("blog", definition("/old/{id}")); + Mockito.when(collection.item("first")).thenReturn(Optional.of(item)); + var resolver = new CollectionResolver(renderer, db, configuration); + var context = context("/blog/first"); + + Assertions.assertThat(resolver.getContent(context)).isEmpty(); + + definitions.put("blog", definition("/blog/{id}")); + var response = resolver.getContent(context); + + Assertions.assertThat(response) + .isPresent() + .get() + .isInstanceOfSatisfying(DefaultContentResponse.class, content -> + Assertions.assertThat(content.content()).isEqualTo("

First

")); + Assertions.assertThat(context.get(CurrentNodeFeature.class).node().url()).isEqualTo("/blog/first"); + var node = ArgumentCaptor.forClass(com.condation.cms.api.db.ContentNode.class); + Mockito.verify(renderer).renderCollection( + Mockito.eq(itemFile), + node.capture(), + Mockito.eq(item), + Mockito.eq("collections/detail.html"), + Mockito.eq(context)); + Assertions.assertThat(node.getValue().data()).containsEntry("template", "collections/detail.html"); + } + + @Test + void resolvesAConfiguredFrontMatterField() throws Exception { + definitions.put("blog", definition("/blog/{slug}")); + @SuppressWarnings("unchecked") + var query = (ContentQuery) Mockito.mock(ContentQuery.class); + Mockito.when(collection.query()).thenReturn(query); + Mockito.when(query.where("slug", "first-post")).thenReturn(query); + Mockito.when(query.page(1, 1)).thenReturn(new Page<>(1, 1, 1, 1, List.of(item))); + var resolver = new CollectionResolver(renderer, db, configuration); + + var response = resolver.getContent(context("/blog/first-post/")); + + Assertions.assertThat(response).isPresent(); + Mockito.verify(query).where("slug", "first-post"); + } + + private static CollectionDefinition definition(String route) { + return new CollectionDefinition( + "blog", + new CollectionDetailConfiguration(route, "collections/detail.html")); + } + + private static RequestContext context(String uri) { + var context = new RequestContext(); + context.add(RequestFeature.class, new RequestFeature(uri, Map.of())); + return context; + } +} diff --git a/cms-content/src/test/java/com/condation/cms/content/template/functions/LinkFunctionTest.java b/cms-content/src/test/java/com/condation/cms/content/template/functions/LinkFunctionTest.java new file mode 100644 index 000000000..a9efed6f7 --- /dev/null +++ b/cms-content/src/test/java/com/condation/cms/content/template/functions/LinkFunctionTest.java @@ -0,0 +1,111 @@ +package com.condation.cms.content.template.functions; + +/*- + * #%L + * CMS Content + * %% + * Copyright (C) 2023 - 2026 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * #L% + */ + +import com.condation.cms.api.SiteProperties; +import com.condation.cms.api.configuration.Configuration; +import com.condation.cms.api.configuration.configs.CollectionConfiguration; +import com.condation.cms.api.configuration.configs.CollectionDefinition; +import com.condation.cms.api.configuration.configs.CollectionDetailConfiguration; +import com.condation.cms.api.db.collection.CollectionItem; +import com.condation.cms.api.feature.features.ConfigurationFeature; +import com.condation.cms.api.feature.features.SitePropertiesFeature; +import com.condation.cms.api.request.RequestContext; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +class LinkFunctionTest { + + private final ConcurrentHashMap definitions = new ConcurrentHashMap<>(); + private final RequestContext context = new RequestContext(); + private final CollectionItem item = new CollectionItem( + "item_1", + "blog", + "blog/item_1.md", + "", + Map.of("slug", "first-post")); + + @BeforeEach + void setUp() { + var configuration = new Configuration(); + configuration.add(CollectionConfiguration.class, new CollectionConfiguration(definitions)); + context.add(ConfigurationFeature.class, new ConfigurationFeature(configuration)); + + var siteProperties = Mockito.mock(SiteProperties.class); + Mockito.when(siteProperties.contextPath()).thenReturn("/docs"); + context.add(SitePropertiesFeature.class, new SitePropertiesFeature(siteProperties)); + } + + @Test + void createsContextAwareUrlUsingTheItemId() { + definitions.put("blog", definition("/articles/{id}")); + + var url = new LinkFunction(context).collectionUrl(item); + + Assertions.assertThat(url).isEqualTo("/docs/articles/item_1"); + } + + @Test + void createsContextAwareUrlUsingConfiguredFrontMatter() { + definitions.put("blog", definition("/articles/{slug}")); + + var url = new LinkFunction(context).collectionUrl(item); + + Assertions.assertThat(url).isEqualTo("/docs/articles/first-post"); + } + + @Test + void usesReloadedCollectionRoute() { + definitions.put("blog", definition("/old/{id}")); + var links = new LinkFunction(context); + Assertions.assertThat(links.collectionUrl(item)).isEqualTo("/docs/old/item_1"); + + definitions.put("blog", definition("/new/{slug}")); + + Assertions.assertThat(links.collectionUrl(item)).isEqualTo("/docs/new/first-post"); + } + + @Test + void rejectsItemsWithoutTheConfiguredRouteValue() { + definitions.put("blog", definition("/articles/{slug}")); + var itemWithoutSlug = new CollectionItem( + "item_2", + "blog", + "blog/item_2.md", + "", + Map.of()); + + Assertions.assertThatIllegalArgumentException() + .isThrownBy(() -> new LinkFunction(context).collectionUrl(itemWithoutSlug)) + .withMessageContaining("slug"); + } + + private static CollectionDefinition definition(String route) { + return new CollectionDefinition( + "blog", + new CollectionDetailConfiguration(route, "collections/detail.html")); + } +} diff --git a/cms-core/src/main/java/com/condation/cms/core/configuration/ConfigManagement.java b/cms-core/src/main/java/com/condation/cms/core/configuration/ConfigManagement.java index cc527f981..628b985c3 100644 --- a/cms-core/src/main/java/com/condation/cms/core/configuration/ConfigManagement.java +++ b/cms-core/src/main/java/com/condation/cms/core/configuration/ConfigManagement.java @@ -72,6 +72,12 @@ public void initConfiguration (Configuration configuration) { .get()).getTaxonomies() ) ); + configuration.add( + com.condation.cms.api.configuration.configs.CollectionConfiguration.class, + new com.condation.cms.api.configuration.configs.CollectionConfiguration( + ((com.condation.cms.core.configuration.configs.CollectionConfiguration) get("collections") + .orElseThrow()).getCollections()) + ); var mediaConfig = new com.condation.cms.api.configuration.configs.MediaConfiguration( ((com.condation.cms.core.configuration.configs.MediaConfiguration) get("media") .get()).getMediaFormats() diff --git a/cms-core/src/main/java/com/condation/cms/core/configuration/ConfigurationFactory.java b/cms-core/src/main/java/com/condation/cms/core/configuration/ConfigurationFactory.java index b58afe18e..ffce263c9 100644 --- a/cms-core/src/main/java/com/condation/cms/core/configuration/ConfigurationFactory.java +++ b/cms-core/src/main/java/com/condation/cms/core/configuration/ConfigurationFactory.java @@ -25,6 +25,7 @@ import com.condation.cms.core.configuration.configs.SimpleConfiguration; import com.condation.cms.api.eventbus.EventBus; import com.condation.cms.api.eventbus.events.ReloadMediaConfig; +import com.condation.cms.api.eventbus.events.ReloadCollectionsConfig; import com.condation.cms.api.eventbus.events.ReloadParentThemeConfig; import com.condation.cms.api.eventbus.events.ReloadServerConfig; import com.condation.cms.api.eventbus.events.ReloadSiteConfig; @@ -73,6 +74,14 @@ public static ConfigManagement create(Path hostBase, EventBus eventBus, CronJobS new EventReload<>(eventBus, ReloadTaxonomyConfig.class) ) ); + final com.condation.cms.core.configuration.configs.CollectionConfiguration collectionConfiguration = collectionConfiguration( + eventBus, + hostBase, + new CompositeReload( + new CronReload("0/10 * * * * ?", cronScheduler), + new EventReload<>(eventBus, ReloadCollectionsConfig.class) + ) + ); final SimpleConfiguration themeConfiguration = themeConfiguration( "theme", @@ -97,6 +106,7 @@ public static ConfigManagement create(Path hostBase, EventBus eventBus, CronJobS management.add(serverConfiguration.id(), serverConfiguration); management.add(siteConfiguration.id(), siteConfiguration); management.add(taxonomyConfiguration.id(), taxonomyConfiguration); + management.add(collectionConfiguration.id(), collectionConfiguration); management.add(mediaConfiguration.id(), mediaConfiguration); management.add(themeConfiguration.id(), themeConfiguration); management.add(themeConfiguration.id(), parentThemeConfiguration); @@ -190,4 +200,16 @@ private static TaxonomyConfiguration taxonomyConfiguration(EventBus eventBus, Pa .addSource(TomlConfigSource.build(hostBase.resolve("config/taxonomy.toml"))) .build(); } + + private static com.condation.cms.core.configuration.configs.CollectionConfiguration collectionConfiguration( + EventBus eventBus, + Path hostBase, + ReloadStrategy reloadStrategy) throws IOException { + return com.condation.cms.core.configuration.configs.CollectionConfiguration.builder(eventBus) + .id("collections") + .reloadStrategy(reloadStrategy) + .addSource(YamlConfigSource.build(hostBase.resolve("config/collections.yaml"))) + .addSource(TomlConfigSource.build(hostBase.resolve("config/collections.toml"))) + .build(); + } } diff --git a/cms-core/src/main/java/com/condation/cms/core/configuration/configs/CollectionConfiguration.java b/cms-core/src/main/java/com/condation/cms/core/configuration/configs/CollectionConfiguration.java new file mode 100644 index 000000000..c36e07223 --- /dev/null +++ b/cms-core/src/main/java/com/condation/cms/core/configuration/configs/CollectionConfiguration.java @@ -0,0 +1,163 @@ +package com.condation.cms.core.configuration.configs; + +/*- + * #%L + * CMS Core + * %% + * Copyright (C) 2023 - 2026 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * #L% + */ + +import com.condation.cms.api.configuration.configs.CollectionDefinition; +import com.condation.cms.api.configuration.configs.CollectionDetailConfiguration; +import com.condation.cms.api.eventbus.EventBus; +import com.condation.cms.api.eventbus.events.ConfigurationReloadEvent; +import com.condation.cms.core.configuration.ConfigSource; +import com.condation.cms.core.configuration.IConfiguration; +import com.condation.cms.core.configuration.ReloadStrategy; +import com.condation.cms.core.configuration.reload.NoReload; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import lombok.extern.slf4j.Slf4j; + +/** + * Loads collection definitions from site configuration sources. + */ +@Slf4j +public class CollectionConfiguration extends AbstractConfiguration implements IConfiguration { + + private final List sources; + private final ReloadStrategy reloadStrategy; + private final EventBus eventBus; + private final String id; + private final ConcurrentMap collections = new ConcurrentHashMap<>(); + + private CollectionConfiguration(Builder builder) { + this.sources = builder.sources; + this.reloadStrategy = builder.reloadStrategy; + this.eventBus = builder.eventBus; + this.id = builder.id; + reloadStrategy.register(this); + reload(); + } + + public static Builder builder(EventBus eventBus) { + return new Builder(eventBus); + } + + @Override + protected List getSources() { + return sources; + } + + @Override + public String id() { + return id; + } + + public ConcurrentMap getCollections() { + return collections; + } + + @Override + public void reload() { + var reloaded = false; + var updatedCollections = new ConcurrentHashMap(); + for (var source : sources) { + reloaded |= source.reload(); + if (!source.exists()) { + continue; + } + for (var entry : source.getMap("collections").entrySet()) { + parse(entry.getKey(), entry.getValue()).ifPresent(definition -> + updatedCollections.put(definition.name(), definition)); + } + } + + collections.clear(); + collections.putAll(updatedCollections); + if (reloaded && eventBus != null) { + eventBus.publish(new ConfigurationReloadEvent(id)); + } + } + + private java.util.Optional parse(String name, Object value) { + try { + if (!(value instanceof Map collection)) { + throw new IllegalArgumentException("collection definition must be a map"); + } + + var detailValue = collection.get("detail"); + if (detailValue == null) { + return java.util.Optional.of(new CollectionDefinition(name, null)); + } + if (!(detailValue instanceof Map detail)) { + throw new IllegalArgumentException("collection detail definition must be a map"); + } + + var route = stringValue(detail.get("route"), "route"); + var template = stringValue(detail.get("template"), "template"); + return java.util.Optional.of(new CollectionDefinition( + name, + new CollectionDetailConfiguration(route, template))); + } catch (RuntimeException ex) { + log.error("invalid configuration for collection {}", name, ex); + return java.util.Optional.empty(); + } + } + + private static String stringValue(Object value, String field) { + if (!(value instanceof String string) || string.isBlank()) { + throw new IllegalArgumentException("collection detail " + field + " must be a non-empty string"); + } + return string; + } + + public static class Builder { + + private final List sources = new ArrayList<>(); + private ReloadStrategy reloadStrategy = new NoReload(); + private String id = UUID.randomUUID().toString(); + private final EventBus eventBus; + + private Builder(EventBus eventBus) { + this.eventBus = eventBus; + } + + public Builder id(String uniqueId) { + this.id = uniqueId; + return this; + } + + public Builder addSource(ConfigSource source) { + sources.add(source); + return this; + } + + public Builder reloadStrategy(ReloadStrategy reload) { + this.reloadStrategy = reload; + return this; + } + + public CollectionConfiguration build() { + return new CollectionConfiguration(this); + } + } +} diff --git a/cms-core/src/test/java/com/condation/cms/core/configuration/CollectionConfigurationTest.java b/cms-core/src/test/java/com/condation/cms/core/configuration/CollectionConfigurationTest.java new file mode 100644 index 000000000..3f6d6d244 --- /dev/null +++ b/cms-core/src/test/java/com/condation/cms/core/configuration/CollectionConfigurationTest.java @@ -0,0 +1,73 @@ +package com.condation.cms.core.configuration; + +/*- + * #%L + * CMS Core + * %% + * Copyright (C) 2023 - 2026 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * #L% + */ + +import com.condation.cms.api.eventbus.EventBus; +import com.condation.cms.api.eventbus.events.ConfigurationReloadEvent; +import com.condation.cms.core.configuration.configs.CollectionConfiguration; +import java.util.Map; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +class CollectionConfigurationTest { + + @Test + void updatesTheSharedConfigurationOnReload() { + var eventBus = Mockito.mock(EventBus.class); + var source = Mockito.mock(ConfigSource.class); + var initial = Map.of( + "blog", + Map.of("detail", Map.of( + "route", "/blog/{slug}", + "template", "collections/blog.html")), + "listing-only", + Map.of()); + var updated = Map.of( + "products", + Map.of("detail", Map.of( + "route", "/products/{id}", + "template", "collections/product.html"))); + + Mockito.when(source.exists()).thenReturn(true); + Mockito.when(source.reload()).thenReturn(false, true); + Mockito.when(source.getMap("collections")).thenReturn(initial, updated); + + var configuration = CollectionConfiguration.builder(eventBus) + .id("collections") + .addSource(source) + .build(); + var sharedCollections = configuration.getCollections(); + + Assertions.assertThat(sharedCollections).containsOnlyKeys("blog", "listing-only"); + Assertions.assertThat(sharedCollections.get("blog").detailPage().orElseThrow().parameter()) + .isEqualTo("slug"); + + configuration.reload(); + + Assertions.assertThat(configuration.getCollections()).isSameAs(sharedCollections); + Assertions.assertThat(sharedCollections).containsOnlyKeys("products"); + Assertions.assertThat(sharedCollections.get("products").detailPage().orElseThrow().route()) + .isEqualTo("/products/{id}"); + Mockito.verify(eventBus).publish(new ConfigurationReloadEvent("collections")); + } +} diff --git a/cms-filesystem/src/main/java/com/condation/cms/filesystem/FileCollections.java b/cms-filesystem/src/main/java/com/condation/cms/filesystem/FileCollections.java index f5357a37d..220030e07 100644 --- a/cms-filesystem/src/main/java/com/condation/cms/filesystem/FileCollections.java +++ b/cms-filesystem/src/main/java/com/condation/cms/filesystem/FileCollections.java @@ -24,6 +24,7 @@ import com.condation.cms.api.Constants; import com.condation.cms.api.db.ContentNode; import com.condation.cms.api.db.ContentQuery; +import com.condation.cms.api.db.NodeVisibility; import com.condation.cms.api.db.collection.CollectionItem; import com.condation.cms.api.db.collection.Collections; import com.condation.cms.api.utils.PathUtil; @@ -37,6 +38,7 @@ import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Optional; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.function.Function; @@ -50,6 +52,7 @@ public class FileCollections implements Collections, AutoCloseable { private static final Pattern COLLECTION_NAME = Pattern.compile("[a-zA-Z0-9][a-zA-Z0-9_-]*"); + private static final Pattern ITEM_ID = Pattern.compile("[a-zA-Z0-9][a-zA-Z0-9_.-]*"); private static final Duration CHANGE_QUIET_PERIOD = Duration.ofMillis(200); private final String siteId; @@ -257,6 +260,12 @@ private static void validateCollectionName(String name) { } } + private static void validateItemId(String id) { + if (id == null || !ITEM_ID.matcher(id).matches()) { + throw new IllegalArgumentException("invalid collection item id: " + id); + } + } + @Override public void close() throws IOException { if (watcher != null) { @@ -283,6 +292,14 @@ public String name() { return name; } + @Override + public Optional item(String id) { + validateItemId(id); + return metaData.byPath(name + "/" + id + ".md") + .filter(NodeVisibility::isVisible) + .map(node -> FileCollections.this.map(node, 0)); + } + @Override public ContentQuery query() { return metaData.query(name, FileCollections.this::map); diff --git a/cms-filesystem/src/main/java/com/condation/cms/filesystem/FileSystem.java b/cms-filesystem/src/main/java/com/condation/cms/filesystem/FileSystem.java index f1e48d57a..75e520d43 100644 --- a/cms-filesystem/src/main/java/com/condation/cms/filesystem/FileSystem.java +++ b/cms-filesystem/src/main/java/com/condation/cms/filesystem/FileSystem.java @@ -401,6 +401,12 @@ public ReadOnlyFile contentBase() { return new NIOReadOnlyFile(path, path); } + @Override + public ReadOnlyFile collectionsBase() { + var path = resolve(Constants.Folders.COLLECTIONS); + return new NIOReadOnlyFile(path, path); + } + @Override public ReadOnlyFile assetBase() { var path = resolve(Constants.Folders.ASSETS); diff --git a/cms-filesystem/src/test/java/com/condation/cms/filesystem/FileCollectionsTest.java b/cms-filesystem/src/test/java/com/condation/cms/filesystem/FileCollectionsTest.java index 3f5908bf0..8ca822034 100644 --- a/cms-filesystem/src/test/java/com/condation/cms/filesystem/FileCollectionsTest.java +++ b/cms-filesystem/src/test/java/com/condation/cms/filesystem/FileCollectionsTest.java @@ -139,15 +139,20 @@ void appliesDefaultWorkflowSchedulingAndPreview() throws Exception { Assertions.assertThat(publicPage.getItems()) .extracting(item -> item.id()) .containsExactly("published"); + Assertions.assertThat(collections.collection("blog").item("published")).isPresent(); + Assertions.assertThat(collections.collection("blog").item("draft")).isEmpty(); var previewContext = new RequestContext(); previewContext.add(IsPreviewFeature.class, new IsPreviewFeature(IsPreviewFeature.Mode.PREVIEW)); var previewItems = ScopedValue.where(RequestContextScope.REQUEST_CONTEXT, previewContext) .call(() -> collections.collection("blog").query().get()); + var previewDraft = ScopedValue.where(RequestContextScope.REQUEST_CONTEXT, previewContext) + .call(() -> collections.collection("blog").item("draft")); Assertions.assertThat(previewItems) .extracting(item -> item.id()) .containsExactlyInAnyOrder("published", "draft", "future", "expired"); + Assertions.assertThat(previewDraft).isPresent(); } finally { collections.close(); } @@ -167,11 +172,17 @@ void appliesConfiguredContentWorkflow() throws Exception { var result = ScopedValue.where(RequestContextScope.REQUEST_CONTEXT, requestContext) .call(() -> collections.collection("blog").query().page(1, 10)); + var approvedItem = ScopedValue.where(RequestContextScope.REQUEST_CONTEXT, requestContext) + .call(() -> collections.collection("blog").item("approved")); + var blockedItem = ScopedValue.where(RequestContextScope.REQUEST_CONTEXT, requestContext) + .call(() -> collections.collection("blog").item("blocked")); Assertions.assertThat(result.getTotalItems()).isEqualTo(1); Assertions.assertThat(result.getItems()) .singleElement() .satisfies(item -> Assertions.assertThat(item.id()).isEqualTo("approved")); + Assertions.assertThat(approvedItem).isPresent(); + Assertions.assertThat(blockedItem).isEmpty(); } finally { collections.close(); } diff --git a/cms-server/src/main/java/com/condation/cms/server/configs/SiteHandlerModule.java b/cms-server/src/main/java/com/condation/cms/server/configs/SiteHandlerModule.java index c9e92f0fe..ee6feb868 100644 --- a/cms-server/src/main/java/com/condation/cms/server/configs/SiteHandlerModule.java +++ b/cms-server/src/main/java/com/condation/cms/server/configs/SiteHandlerModule.java @@ -44,6 +44,7 @@ import com.condation.cms.server.handler.StaticFileHandler; import com.condation.cms.server.handler.WellKnownHandler; import com.condation.cms.server.handler.auth.JettyAuthenticationHandler; +import com.condation.cms.server.handler.content.JettyCollectionHandler; import com.condation.cms.server.handler.content.JettyContentHandler; import com.condation.cms.server.handler.content.JettyTaxonomyHandler; import com.condation.cms.server.handler.content.JettyViewHandler; @@ -73,6 +74,7 @@ public class SiteHandlerModule extends AbstractModule { protected void configure() { bind(JettyViewHandler.class).in(Singleton.class); + bind(JettyCollectionHandler.class).in(Singleton.class); bind(JettyContentHandler.class).in(Singleton.class); bind(JettyTaxonomyHandler.class).in(Singleton.class); bind(RoutesHandler.class).in(Singleton.class); diff --git a/cms-server/src/main/java/com/condation/cms/server/configs/SiteModule.java b/cms-server/src/main/java/com/condation/cms/server/configs/SiteModule.java index b35470036..f585515c6 100644 --- a/cms-server/src/main/java/com/condation/cms/server/configs/SiteModule.java +++ b/cms-server/src/main/java/com/condation/cms/server/configs/SiteModule.java @@ -59,6 +59,7 @@ import com.condation.cms.api.workflow.Workflow; import com.condation.cms.api.workflow.WorkflowInstance; import com.condation.cms.auth.services.AuthService; +import com.condation.cms.content.CollectionResolver; import com.condation.cms.content.ContentRenderer; import com.condation.cms.content.ContentResolver; import com.condation.cms.content.DefaultContentParser; @@ -376,6 +377,15 @@ public ContentResolver contentResolver(ContentRenderer contentRenderer, return new ContentResolver(contentRenderer, db, variantResolver, variantSelector); } + @Provides + @Singleton + public CollectionResolver collectionResolver( + ContentRenderer contentRenderer, + FileDB db, + Configuration configuration) { + return new CollectionResolver(contentRenderer, db, configuration); + } + @Provides @Singleton public VariantResolver variantResolver(FileDB db) { diff --git a/cms-server/src/main/java/com/condation/cms/server/handler/content/JettyCollectionHandler.java b/cms-server/src/main/java/com/condation/cms/server/handler/content/JettyCollectionHandler.java new file mode 100644 index 000000000..d880fd4a2 --- /dev/null +++ b/cms-server/src/main/java/com/condation/cms/server/handler/content/JettyCollectionHandler.java @@ -0,0 +1,61 @@ +package com.condation.cms.server.handler.content; + +/*- + * #%L + * CMS Server + * %% + * Copyright (C) 2023 - 2026 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * #L% + */ + +import com.condation.cms.api.Constants; +import com.condation.cms.api.content.DefaultContentResponse; +import com.condation.cms.api.request.RequestContext; +import com.condation.cms.content.CollectionResolver; +import com.google.inject.Inject; +import lombok.RequiredArgsConstructor; +import org.eclipse.jetty.http.HttpHeader; +import org.eclipse.jetty.io.Content; +import org.eclipse.jetty.server.Handler; +import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.server.Response; +import org.eclipse.jetty.util.Callback; + +/** + * Serves collection items whose detail routes are configured for the site. + */ +@RequiredArgsConstructor(onConstructor = @__({@Inject})) +public class JettyCollectionHandler extends Handler.Abstract { + + private final CollectionResolver collectionResolver; + + @Override + public boolean handle(Request request, Response response, Callback callback) throws Exception { + var requestContext = (RequestContext) request.getAttribute(Constants.REQUEST_CONTEXT_ATTRIBUTE_NAME); + var resolvedContent = collectionResolver.getContent(requestContext); + if (resolvedContent.isEmpty()) { + return false; + } + + var content = (DefaultContentResponse) resolvedContent.get(); + response.setStatus(200); + response.getHeaders().add( + HttpHeader.CONTENT_TYPE, + "%s; charset=utf-8".formatted(content.contentType())); + Content.Sink.write(response, true, content.content(), callback); + return true; + } +} diff --git a/cms-server/src/main/java/com/condation/cms/server/host/VHost.java b/cms-server/src/main/java/com/condation/cms/server/host/VHost.java index 9fa6a7824..b239fea25 100644 --- a/cms-server/src/main/java/com/condation/cms/server/host/VHost.java +++ b/cms-server/src/main/java/com/condation/cms/server/host/VHost.java @@ -70,6 +70,7 @@ import com.condation.cms.server.filter.PreviewFilter; import com.condation.cms.server.handler.StaticFileHandler; import com.condation.cms.server.handler.auth.JettyAuthenticationHandler; +import com.condation.cms.server.handler.content.JettyCollectionHandler; import com.condation.cms.server.handler.content.JettyContentHandler; import com.condation.cms.server.handler.content.JettyTaxonomyHandler; import com.condation.cms.server.handler.content.JettyViewHandler; @@ -402,6 +403,7 @@ private Handler createRootSequence(PreviewFilter uiPreviewFilter) { contentHandler = injector.getInstance(JettyContentHandler.class); var taxonomyHandler = injector.getInstance(JettyTaxonomyHandler.class); + var collectionHandler = injector.getInstance(JettyCollectionHandler.class); var viewHandler = injector.getInstance(JettyViewHandler.class); var routesHandler = injector.getInstance(RoutesHandler.class); var authHandler = injector.getInstance(JettyAuthenticationHandler.class); @@ -422,6 +424,7 @@ private Handler createRootSequence(PreviewFilter uiPreviewFilter) { uiPreviewFilter, viewHandler, taxonomyHandler, + collectionHandler, contentHandler ); diff --git a/test-server/hosts/demo/collections/authors/thorsten.md b/test-server/hosts/demo/collections/authors/thorsten.md index 766efc1bd..e24de93a7 100644 --- a/test-server/hosts/demo/collections/authors/thorsten.md +++ b/test-server/hosts/demo/collections/authors/thorsten.md @@ -3,4 +3,5 @@ title: Thorsten status: published description: CondationCMS main author publish_date: 2026-04-07T00:00:00Z +slug: thorsten --- \ No newline at end of file diff --git a/test-server/hosts/demo/config/collections.yaml b/test-server/hosts/demo/config/collections.yaml new file mode 100644 index 000000000..9de7da946 --- /dev/null +++ b/test-server/hosts/demo/config/collections.yaml @@ -0,0 +1,9 @@ +collections: + blog: + detail: + route: /collections/blog/{id} + template: collections/blog-detail.html + authors: + detail: + route: /collections/authors/{slug} + template: collections/author-detail.html diff --git a/test-server/themes/demo/templates/collections.html b/test-server/themes/demo/templates/collections.html index 98716c1e2..35493cf60 100644 --- a/test-server/themes/demo/templates/collections.html +++ b/test-server/themes/demo/templates/collections.html @@ -23,7 +23,7 @@

Blog collection

{% for item in items %}
- {{ item.meta.title | raw }} + {{ item.meta.title | raw }}
{% endfor %} @@ -32,7 +32,7 @@

Author collection

{% for item in items %}
- {{ item.meta.title | raw }} + {{ item.meta.title | raw }}
{% endfor %} @@ -40,4 +40,4 @@

Author collection

- \ No newline at end of file + diff --git a/test-server/themes/demo/templates/collections/author-detail.html b/test-server/themes/demo/templates/collections/author-detail.html new file mode 100644 index 000000000..6aac1e618 --- /dev/null +++ b/test-server/themes/demo/templates/collections/author-detail.html @@ -0,0 +1,26 @@ + + + + + + + + + {% include "libs/fragments.html" %} + + + + + + +
+

{{ node.meta.title }}

+
+ {{ node.content | raw }} +
+
+ + + + + \ No newline at end of file diff --git a/test-server/themes/demo/templates/collections/blog-detail.html b/test-server/themes/demo/templates/collections/blog-detail.html new file mode 100644 index 000000000..886cc2be2 --- /dev/null +++ b/test-server/themes/demo/templates/collections/blog-detail.html @@ -0,0 +1,20 @@ + + + + + + + {% include "libs/fragments.html" %} + + + +
+

{{ node.meta.title }}

+

{{ node.meta.description }}

+
+ {{ node.content | raw }} +
+
+ + + From 7f6a8faf8b8325bcf7343622d39274ef169bd20d Mon Sep 17 00:00:00 2001 From: Thorsten Marx Date: Fri, 28 Aug 2026 10:57:27 +0200 Subject: [PATCH 05/28] fix some sonar issues --- .../configs/CollectionDefinition.java | 4 +- .../cms/content/CollectionResolver.java | 28 +++++--- .../cms/content/DefaultContentRenderer.java | 41 +++++++---- .../cms/content/CollectionResolverTest.java | 68 ++++++++++--------- .../template/functions/LinkFunctionTest.java | 8 ++- .../configuration/ConfigurationFactory.java | 16 +++-- .../CollectionConfigurationTest.java | 17 +++-- .../CollectionTemplateFunctionExtensions.java | 2 +- 8 files changed, 110 insertions(+), 74 deletions(-) diff --git a/cms-api/src/main/java/com/condation/cms/api/configuration/configs/CollectionDefinition.java b/cms-api/src/main/java/com/condation/cms/api/configuration/configs/CollectionDefinition.java index 4a8b20a6b..bc1a51564 100644 --- a/cms-api/src/main/java/com/condation/cms/api/configuration/configs/CollectionDefinition.java +++ b/cms-api/src/main/java/com/condation/cms/api/configuration/configs/CollectionDefinition.java @@ -30,11 +30,11 @@ */ public record CollectionDefinition(String name, CollectionDetailConfiguration detail) { - private static final Pattern NAME = Pattern.compile("[a-zA-Z0-9][a-zA-Z0-9_-]*"); + private static final Pattern VALID_NAME_PATTERN = Pattern.compile("[a-zA-Z0-9][a-zA-Z0-9_-]*"); public CollectionDefinition { Objects.requireNonNull(name, "collection name must not be null"); - if (!NAME.matcher(name).matches()) { + if (!VALID_NAME_PATTERN.matcher(name).matches()) { throw new IllegalArgumentException("invalid collection name: " + name); } } diff --git a/cms-content/src/main/java/com/condation/cms/content/CollectionResolver.java b/cms-content/src/main/java/com/condation/cms/content/CollectionResolver.java index 58ef4403a..8099266a9 100644 --- a/cms-content/src/main/java/com/condation/cms/content/CollectionResolver.java +++ b/cms-content/src/main/java/com/condation/cms/content/CollectionResolver.java @@ -61,20 +61,30 @@ public Optional getContent(RequestContext context) throws IOExc for (var definition : collectionConfiguration.collections().values().stream() .sorted(Comparator.comparing(CollectionDefinition::name)) .toList()) { - var detail = definition.detailPage(); - if (detail.isEmpty()) { - continue; + var content = resolve(definition, uri, context); + if (content.isPresent()) { + return content; } - var routeValue = match(detail.get(), uri); - if (routeValue.isEmpty()) { - continue; - } - return resolve(definition, detail.get(), routeValue.get(), uri, context); } return Optional.empty(); } private Optional resolve( + CollectionDefinition definition, + String uri, + RequestContext context) throws IOException { + var detail = definition.detailPage(); + if (detail.isEmpty()) { + return Optional.empty(); + } + var routeValue = match(detail.get(), uri); + if (routeValue.isEmpty()) { + return Optional.empty(); + } + return resolveItem(definition, detail.get(), routeValue.get(), uri, context); + } + + private Optional resolveItem( CollectionDefinition definition, CollectionDetailConfiguration detail, String routeValue, @@ -124,7 +134,7 @@ private static Optional findById( String id) { try { return collection.item(id); - } catch (IllegalArgumentException ex) { + } catch (IllegalArgumentException _) { return Optional.empty(); } } diff --git a/cms-content/src/main/java/com/condation/cms/content/DefaultContentRenderer.java b/cms-content/src/main/java/com/condation/cms/content/DefaultContentRenderer.java index 94d1f86e1..c0d62d11d 100644 --- a/cms-content/src/main/java/com/condation/cms/content/DefaultContentRenderer.java +++ b/cms-content/src/main/java/com/condation/cms/content/DefaultContentRenderer.java @@ -90,6 +90,14 @@ public class DefaultContentRenderer implements ContentRenderer { private final SiteProperties siteProperties; private final ModuleManager moduleManager; + private record ResolvedRenderInput( + String uri, + Map> sectionEntries, + Map meta, + String rawContent, + Optional contentNode) { + } + @Override public String render(final ReadOnlyFile contentFile, final RequestContext context) throws IOException { return render(contentFile, context, Collections.emptyMap()); @@ -149,12 +157,13 @@ public String renderCollection( meta.put("template", template); return renderResolved( collectionFile, - collectionNode.url(), context, - Collections.emptyMap(), - meta, - item.content(), - Optional.of(collectionNode), + new ResolvedRenderInput( + collectionNode.url(), + Collections.emptyMap(), + meta, + item.content(), + Optional.of(collectionNode)), model -> { model.values.put("collection_item", item); model.values.put("collection", db.getCollections().collection(item.collection())); @@ -171,24 +180,26 @@ public String render(final ReadOnlyFile contentFile, final RequestContext contex Optional contentNode = db.getContent().byUri(uri); return renderResolved( contentFile, - uri, context, - sectionEntries, - meta, - rawContent, - contentNode, + new ResolvedRenderInput( + uri, + sectionEntries, + meta, + rawContent, + contentNode), modelExtending); } private String renderResolved( ReadOnlyFile contentFile, - String uri, RequestContext context, - Map> sectionEntries, - Map meta, - String rawContent, - Optional contentNode, + ResolvedRenderInput input, Consumer modelExtending) throws IOException { + var uri = input.uri(); + var sectionEntries = input.sectionEntries(); + var meta = input.meta(); + var rawContent = input.rawContent(); + var contentNode = input.contentNode(); TemplateEngine.Model model = new TemplateEngine.Model( contentFile, contentNode.orElse(null), diff --git a/cms-content/src/test/java/com/condation/cms/content/CollectionResolverTest.java b/cms-content/src/test/java/com/condation/cms/content/CollectionResolverTest.java index 9f0ca6bf7..61df6404f 100644 --- a/cms-content/src/test/java/com/condation/cms/content/CollectionResolverTest.java +++ b/cms-content/src/test/java/com/condation/cms/content/CollectionResolverTest.java @@ -21,6 +21,13 @@ * #L% */ +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.anyString; +import static org.mockito.Mockito.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + import com.condation.cms.api.configuration.Configuration; import com.condation.cms.api.configuration.configs.CollectionConfiguration; import com.condation.cms.api.configuration.configs.CollectionDefinition; @@ -44,18 +51,17 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; -import org.mockito.Mockito; class CollectionResolverTest { - private final ContentRenderer renderer = Mockito.mock(ContentRenderer.class); - private final DB db = Mockito.mock(DB.class); + private final ContentRenderer renderer = mock(ContentRenderer.class); + private final DB db = mock(DB.class); private final com.condation.cms.api.db.collection.Collections collections = - Mockito.mock(com.condation.cms.api.db.collection.Collections.class); - private final Collection collection = Mockito.mock(Collection.class); - private final DBFileSystem fileSystem = Mockito.mock(DBFileSystem.class); - private final ReadOnlyFile collectionsBase = Mockito.mock(ReadOnlyFile.class); - private final ReadOnlyFile itemFile = Mockito.mock(ReadOnlyFile.class); + mock(com.condation.cms.api.db.collection.Collections.class); + private final Collection collection = mock(Collection.class); + private final DBFileSystem fileSystem = mock(DBFileSystem.class); + private final ReadOnlyFile collectionsBase = mock(ReadOnlyFile.class); + private final ReadOnlyFile itemFile = mock(ReadOnlyFile.class); private final ConcurrentHashMap definitions = new ConcurrentHashMap<>(); private final Configuration configuration = new Configuration(); private final CollectionItem item = new CollectionItem( @@ -68,24 +74,24 @@ class CollectionResolverTest { @BeforeEach void setUp() throws Exception { configuration.add(CollectionConfiguration.class, new CollectionConfiguration(definitions)); - Mockito.when(db.getCollections()).thenReturn(collections); - Mockito.when(collections.collection("blog")).thenReturn(collection); - Mockito.when(db.getFileSystem()).thenReturn(fileSystem); - Mockito.when(fileSystem.collectionsBase()).thenReturn(collectionsBase); - Mockito.when(collectionsBase.resolve("blog/first.md")).thenReturn(itemFile); - Mockito.when(itemFile.exists()).thenReturn(true); - Mockito.when(renderer.renderCollection( - Mockito.eq(itemFile), - Mockito.any(), - Mockito.eq(item), - Mockito.anyString(), - Mockito.any())).thenReturn("

First

"); + when(db.getCollections()).thenReturn(collections); + when(collections.collection("blog")).thenReturn(collection); + when(db.getFileSystem()).thenReturn(fileSystem); + when(fileSystem.collectionsBase()).thenReturn(collectionsBase); + when(collectionsBase.resolve("blog/first.md")).thenReturn(itemFile); + when(itemFile.exists()).thenReturn(true); + when(renderer.renderCollection( + eq(itemFile), + any(), + eq(item), + anyString(), + any())).thenReturn("

First

"); } @Test void resolvesAnIdRouteAndUsesReloadedDefinitions() throws Exception { definitions.put("blog", definition("/old/{id}")); - Mockito.when(collection.item("first")).thenReturn(Optional.of(item)); + when(collection.item("first")).thenReturn(Optional.of(item)); var resolver = new CollectionResolver(renderer, db, configuration); var context = context("/blog/first"); @@ -101,12 +107,12 @@ void resolvesAnIdRouteAndUsesReloadedDefinitions() throws Exception { Assertions.assertThat(content.content()).isEqualTo("

First

")); Assertions.assertThat(context.get(CurrentNodeFeature.class).node().url()).isEqualTo("/blog/first"); var node = ArgumentCaptor.forClass(com.condation.cms.api.db.ContentNode.class); - Mockito.verify(renderer).renderCollection( - Mockito.eq(itemFile), + verify(renderer).renderCollection( + eq(itemFile), node.capture(), - Mockito.eq(item), - Mockito.eq("collections/detail.html"), - Mockito.eq(context)); + eq(item), + eq("collections/detail.html"), + eq(context)); Assertions.assertThat(node.getValue().data()).containsEntry("template", "collections/detail.html"); } @@ -114,16 +120,16 @@ void resolvesAnIdRouteAndUsesReloadedDefinitions() throws Exception { void resolvesAConfiguredFrontMatterField() throws Exception { definitions.put("blog", definition("/blog/{slug}")); @SuppressWarnings("unchecked") - var query = (ContentQuery) Mockito.mock(ContentQuery.class); - Mockito.when(collection.query()).thenReturn(query); - Mockito.when(query.where("slug", "first-post")).thenReturn(query); - Mockito.when(query.page(1, 1)).thenReturn(new Page<>(1, 1, 1, 1, List.of(item))); + var query = (ContentQuery) mock(ContentQuery.class); + when(collection.query()).thenReturn(query); + when(query.where("slug", "first-post")).thenReturn(query); + when(query.page(1, 1)).thenReturn(new Page<>(1, 1, 1, 1, List.of(item))); var resolver = new CollectionResolver(renderer, db, configuration); var response = resolver.getContent(context("/blog/first-post/")); Assertions.assertThat(response).isPresent(); - Mockito.verify(query).where("slug", "first-post"); + verify(query).where("slug", "first-post"); } private static CollectionDefinition definition(String route) { diff --git a/cms-content/src/test/java/com/condation/cms/content/template/functions/LinkFunctionTest.java b/cms-content/src/test/java/com/condation/cms/content/template/functions/LinkFunctionTest.java index a9efed6f7..23c3b6c06 100644 --- a/cms-content/src/test/java/com/condation/cms/content/template/functions/LinkFunctionTest.java +++ b/cms-content/src/test/java/com/condation/cms/content/template/functions/LinkFunctionTest.java @@ -21,6 +21,9 @@ * #L% */ +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + import com.condation.cms.api.SiteProperties; import com.condation.cms.api.configuration.Configuration; import com.condation.cms.api.configuration.configs.CollectionConfiguration; @@ -35,7 +38,6 @@ import org.assertj.core.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.mockito.Mockito; class LinkFunctionTest { @@ -54,8 +56,8 @@ void setUp() { configuration.add(CollectionConfiguration.class, new CollectionConfiguration(definitions)); context.add(ConfigurationFeature.class, new ConfigurationFeature(configuration)); - var siteProperties = Mockito.mock(SiteProperties.class); - Mockito.when(siteProperties.contextPath()).thenReturn("/docs"); + var siteProperties = mock(SiteProperties.class); + when(siteProperties.contextPath()).thenReturn("/docs"); context.add(SitePropertiesFeature.class, new SitePropertiesFeature(siteProperties)); } diff --git a/cms-core/src/main/java/com/condation/cms/core/configuration/ConfigurationFactory.java b/cms-core/src/main/java/com/condation/cms/core/configuration/ConfigurationFactory.java index ffce263c9..8b7f9e840 100644 --- a/cms-core/src/main/java/com/condation/cms/core/configuration/ConfigurationFactory.java +++ b/cms-core/src/main/java/com/condation/cms/core/configuration/ConfigurationFactory.java @@ -52,6 +52,7 @@ * @author t.marx */ public class ConfigurationFactory { + private static final String CONFIG_RELOAD_CRON = "0/10 * * * * ?"; public static ConfigManagement create(Path hostBase, EventBus eventBus, CronJobScheduler cronScheduler) throws IOException { ConfigManagement management = new ConfigManagement(); @@ -62,7 +63,7 @@ public static ConfigManagement create(Path hostBase, EventBus eventBus, CronJobS serverConfiguration.getString("env", "dev"), hostBase, new CompositeReload( - new CronReload("0/10 * * * * ?", cronScheduler), + new CronReload(CONFIG_RELOAD_CRON, cronScheduler), new EventReload<>(eventBus, ReloadSiteConfig.class) ) ); @@ -70,7 +71,7 @@ public static ConfigManagement create(Path hostBase, EventBus eventBus, CronJobS eventBus, hostBase, new CompositeReload( - new CronReload("0/10 * * * * ?", cronScheduler), + new CronReload(CONFIG_RELOAD_CRON, cronScheduler), new EventReload<>(eventBus, ReloadTaxonomyConfig.class) ) ); @@ -78,7 +79,7 @@ public static ConfigManagement create(Path hostBase, EventBus eventBus, CronJobS eventBus, hostBase, new CompositeReload( - new CronReload("0/10 * * * * ?", cronScheduler), + new CronReload(CONFIG_RELOAD_CRON, cronScheduler), new EventReload<>(eventBus, ReloadCollectionsConfig.class) ) ); @@ -119,9 +120,12 @@ public static SimpleConfiguration themeConfiguration(String id, String themePath } private static SimpleConfiguration themeConfiguration(String id, EventBus eventBus, String theme) throws IOException { var themeBase = ServerUtil.getPath(Constants.Folders.THEMES); - ReloadStrategy reloadStrategy = "parent-theme".equals(id) - ? new EventReload<>(eventBus, ReloadParentThemeConfig.class) - : new EventReload<>(eventBus, ReloadThemeConfig.class); + ReloadStrategy reloadStrategy; + if ("parent-theme".equals(id)) { + reloadStrategy = new EventReload<>(eventBus, ReloadParentThemeConfig.class); + } else { + reloadStrategy = new EventReload<>(eventBus, ReloadThemeConfig.class); + } return SimpleConfiguration.builder(eventBus) .id(id) .reloadStrategy(reloadStrategy) diff --git a/cms-core/src/test/java/com/condation/cms/core/configuration/CollectionConfigurationTest.java b/cms-core/src/test/java/com/condation/cms/core/configuration/CollectionConfigurationTest.java index 3f6d6d244..c4f574a16 100644 --- a/cms-core/src/test/java/com/condation/cms/core/configuration/CollectionConfigurationTest.java +++ b/cms-core/src/test/java/com/condation/cms/core/configuration/CollectionConfigurationTest.java @@ -21,20 +21,23 @@ * #L% */ +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + import com.condation.cms.api.eventbus.EventBus; import com.condation.cms.api.eventbus.events.ConfigurationReloadEvent; import com.condation.cms.core.configuration.configs.CollectionConfiguration; import java.util.Map; import org.assertj.core.api.Assertions; import org.junit.jupiter.api.Test; -import org.mockito.Mockito; class CollectionConfigurationTest { @Test void updatesTheSharedConfigurationOnReload() { - var eventBus = Mockito.mock(EventBus.class); - var source = Mockito.mock(ConfigSource.class); + var eventBus = mock(EventBus.class); + var source = mock(ConfigSource.class); var initial = Map.of( "blog", Map.of("detail", Map.of( @@ -48,9 +51,9 @@ void updatesTheSharedConfigurationOnReload() { "route", "/products/{id}", "template", "collections/product.html"))); - Mockito.when(source.exists()).thenReturn(true); - Mockito.when(source.reload()).thenReturn(false, true); - Mockito.when(source.getMap("collections")).thenReturn(initial, updated); + when(source.exists()).thenReturn(true); + when(source.reload()).thenReturn(false, true); + when(source.getMap("collections")).thenReturn(initial, updated); var configuration = CollectionConfiguration.builder(eventBus) .id("collections") @@ -68,6 +71,6 @@ void updatesTheSharedConfigurationOnReload() { Assertions.assertThat(sharedCollections).containsOnlyKeys("products"); Assertions.assertThat(sharedCollections.get("products").detailPage().orElseThrow().route()) .isEqualTo("/products/{id}"); - Mockito.verify(eventBus).publish(new ConfigurationReloadEvent("collections")); + verify(eventBus).publish(new ConfigurationReloadEvent("collections")); } } diff --git a/modules/system-modules/src/main/java/com/condation/cms/modules/system/templates/CollectionTemplateFunctionExtensions.java b/modules/system-modules/src/main/java/com/condation/cms/modules/system/templates/CollectionTemplateFunctionExtensions.java index 0ed1f505b..138081b73 100644 --- a/modules/system-modules/src/main/java/com/condation/cms/modules/system/templates/CollectionTemplateFunctionExtensions.java +++ b/modules/system-modules/src/main/java/com/condation/cms/modules/system/templates/CollectionTemplateFunctionExtensions.java @@ -35,7 +35,7 @@ public class CollectionTemplateFunctionExtensions extends RegisterTemplateFuncti @TemplateFunction(value = "collection", namespace = "cms") public Object collection(Parameter params) { - var value = params.containsKey("name") ? params.get("name") : params.get("value"); + var value = params.get(params.containsKey("name") ? "name" : "value"); if (value == null) { throw new IllegalArgumentException("collection name must not be null"); } From ad7b1befe6ff58dc52a3c19ea385bddff6e9a4c0 Mon Sep 17 00:00:00 2001 From: Thorsten Marx Date: Fri, 28 Aug 2026 15:24:55 +0200 Subject: [PATCH 06/28] manage collection items --- .../condation/cms/api/db/ContentQuery.java | 12 ++ .../cms/api/db/collection/Collections.java | 3 + .../CurrentCollectionItemFeature.java | 31 +++ .../cms/api/ui/elements/CollectionType.java | 53 +++++ .../cms/api/ui/elements/ContentTypes.java | 20 ++ .../cms/api/ui/elements/ContentTypesTest.java | 12 ++ .../cms/content/CollectionResolver.java | 97 ++------- .../cms/content/CollectionRouteResolver.java | 117 +++++++++++ .../cms/content/CollectionResolverTest.java | 2 + .../cms/filesystem/FileCollections.java | 15 ++ .../metadata/persistent/LuceneQuery.java | 17 +- .../cms/filesystem/FileCollectionsTest.java | 45 +++++ .../CollectionMenuExtension.java | 73 +++++++ .../UiTemplateModelExtension.java | 18 ++ .../RemoteCollectionEndpoints.java | 188 ++++++++++++++++++ .../RemoteContentEndpointsExtension.java | 119 +++++++---- .../remotemethods/RemoteManagerEnpoints.java | 10 + .../RemoteWorkflowEndpointsExtension.java | 89 ++++++--- .../cms/modules/ui/http/JSActionHandler.java | 49 ++++- .../modules/ui/http/RemoteCallHandler.java | 34 ++++ .../collection/edit-collection-item.d.ts | 28 +++ .../collection/edit-collection-item.js | 108 ++++++++++ .../actions/collection/manage-collection.d.ts | 23 +++ .../actions/collection/manage-collection.js | 152 ++++++++++++++ .../manager/manager.message.handlers.js | 10 + .../js/modules/manager/toolbar.inject.js | 19 ++ .../manager/js/modules/preview-context.d.ts | 3 + .../js/modules/rpc/rpc-collection.d.ts | 56 ++++++ .../manager/js/modules/rpc/rpc-collection.js | 39 ++++ .../manager/js/modules/rpc/rpc-manager.d.ts | 8 +- .../manager/js/modules/rpc/rpc-manager.js | 8 +- .../resources/manager/js/modules/rpc/rpc.js | 6 + .../collection/edit-collection-item.ts | 120 +++++++++++ .../actions/collection/manage-collection.ts | 156 +++++++++++++++ .../manager/manager.message.handlers.ts | 11 + .../src/js/modules/manager/toolbar.inject.ts | 20 ++ .../main/ts/src/js/modules/preview-context.ts | 3 + .../ts/src/js/modules/rpc/rpc-collection.ts | 86 ++++++++ .../main/ts/src/js/modules/rpc/rpc-manager.ts | 14 ++ .../src/main/ts/src/js/modules/rpc/rpc.ts | 6 + .../RemoteContentEndpointsExtensionTest.java | 3 + .../RemoteWorkflowEndpointsExtensionTest.java | 53 ++++- .../modules/ui/http/JSActionHandlerTest.java | 44 ++++ .../ui/http/RemoteCallHandlerTest.java | 38 ++++ .../hosts/demo/collections/blog/item_3.md | 8 +- .../themes/demo/extensions/theme.manager.js | 44 ++++ .../themes/demo/templates/collections.html | 4 +- .../templates/collections/author-detail.html | 4 +- .../templates/collections/blog-detail.html | 2 +- 49 files changed, 1915 insertions(+), 165 deletions(-) create mode 100644 cms-api/src/main/java/com/condation/cms/api/feature/features/CurrentCollectionItemFeature.java create mode 100644 cms-api/src/main/java/com/condation/cms/api/ui/elements/CollectionType.java create mode 100644 cms-content/src/main/java/com/condation/cms/content/CollectionRouteResolver.java create mode 100644 modules/ui-module/src/main/java/com/condation/cms/modules/ui/extensionpoints/CollectionMenuExtension.java create mode 100644 modules/ui-module/src/main/java/com/condation/cms/modules/ui/extensionpoints/remotemethods/RemoteCollectionEndpoints.java create mode 100644 modules/ui-module/src/main/resources/manager/actions/collection/edit-collection-item.d.ts create mode 100644 modules/ui-module/src/main/resources/manager/actions/collection/edit-collection-item.js create mode 100644 modules/ui-module/src/main/resources/manager/actions/collection/manage-collection.d.ts create mode 100644 modules/ui-module/src/main/resources/manager/actions/collection/manage-collection.js create mode 100644 modules/ui-module/src/main/resources/manager/js/modules/rpc/rpc-collection.d.ts create mode 100644 modules/ui-module/src/main/resources/manager/js/modules/rpc/rpc-collection.js create mode 100644 modules/ui-module/src/main/ts/src/actions/collection/edit-collection-item.ts create mode 100644 modules/ui-module/src/main/ts/src/actions/collection/manage-collection.ts create mode 100644 modules/ui-module/src/main/ts/src/js/modules/rpc/rpc-collection.ts create mode 100644 modules/ui-module/src/test/java/com/condation/cms/modules/ui/http/JSActionHandlerTest.java diff --git a/cms-api/src/main/java/com/condation/cms/api/db/ContentQuery.java b/cms-api/src/main/java/com/condation/cms/api/db/ContentQuery.java index e2d700d6f..a654998f9 100644 --- a/cms-api/src/main/java/com/condation/cms/api/db/ContentQuery.java +++ b/cms-api/src/main/java/com/condation/cms/api/db/ContentQuery.java @@ -22,6 +22,7 @@ */ +import com.condation.cms.api.Constants; import java.util.List; import java.util.Map; @@ -90,6 +91,17 @@ ContentQuery within( ContentQuery expression(final String expressions); + /** + * Restricts the query to items whose title matches the input. + * + * Implementations with a full-text title index should override this method. + * The default keeps existing query implementations source compatible and uses + * their regular title-field matching. + */ + default ContentQuery searchByTitle(final String input) { + return where(Constants.MetaFields.TITLE, input); + } + public static interface Sort { public ContentQuery asc(); diff --git a/cms-api/src/main/java/com/condation/cms/api/db/collection/Collections.java b/cms-api/src/main/java/com/condation/cms/api/db/collection/Collections.java index c634a05a4..5a08598f2 100644 --- a/cms-api/src/main/java/com/condation/cms/api/db/collection/Collections.java +++ b/cms-api/src/main/java/com/condation/cms/api/db/collection/Collections.java @@ -31,4 +31,7 @@ public interface Collections { Collection collection(String name); Set names(); + + /** Re-indexes one item after a synchronous manager write. */ + void refresh(String collection, String id); } diff --git a/cms-api/src/main/java/com/condation/cms/api/feature/features/CurrentCollectionItemFeature.java b/cms-api/src/main/java/com/condation/cms/api/feature/features/CurrentCollectionItemFeature.java new file mode 100644 index 000000000..f485eeff1 --- /dev/null +++ b/cms-api/src/main/java/com/condation/cms/api/feature/features/CurrentCollectionItemFeature.java @@ -0,0 +1,31 @@ +package com.condation.cms.api.feature.features; + +/*- + * #%L + * CMS Api + * %% + * Copyright (C) 2023 - 2026 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * #L% + */ + +import com.condation.cms.api.annotations.FeatureScope; +import com.condation.cms.api.db.collection.CollectionItem; +import com.condation.cms.api.feature.Feature; + +/** Identifies the collection item represented by the current request. */ +@FeatureScope({FeatureScope.Scope.REQUEST}) +public record CurrentCollectionItemFeature(CollectionItem item) implements Feature { +} diff --git a/cms-api/src/main/java/com/condation/cms/api/ui/elements/CollectionType.java b/cms-api/src/main/java/com/condation/cms/api/ui/elements/CollectionType.java new file mode 100644 index 000000000..d275134f5 --- /dev/null +++ b/cms-api/src/main/java/com/condation/cms/api/ui/elements/CollectionType.java @@ -0,0 +1,53 @@ +package com.condation.cms.api.ui.elements; + +/*- + * #%L + * CMS Api + * %% + * Copyright (C) 2023 - 2026 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * #L% + */ + +import java.util.Map; + +/** A collection editor type registered with the manager UI. */ +public record CollectionType( + String name, + String label, + Map forms) { + + public CollectionType { + name = ContentTypeDefinitionMapper.string(name, ""); + label = ContentTypeDefinitionMapper.string(label, name); + forms = ContentTypeDefinitionMapper.copyForms(forms); + } + + public CollectionType(String name, Map forms) { + this(name, name, forms); + } + + static CollectionType fromMap(Map collectionType) { + var name = ContentTypeDefinitionMapper.string(collectionType.get("name"), ""); + return new CollectionType( + name, + ContentTypeDefinitionMapper.string(collectionType.get("label"), name), + ContentTypeDefinitionMapper.forms(collectionType.get("forms"))); + } + + public FormDefinition getForm(String name) { + return forms.getOrDefault(name, FormDefinition.empty()); + } +} diff --git a/cms-api/src/main/java/com/condation/cms/api/ui/elements/ContentTypes.java b/cms-api/src/main/java/com/condation/cms/api/ui/elements/ContentTypes.java index 17a0e54cd..f2dfdc825 100644 --- a/cms-api/src/main/java/com/condation/cms/api/ui/elements/ContentTypes.java +++ b/cms-api/src/main/java/com/condation/cms/api/ui/elements/ContentTypes.java @@ -43,6 +43,26 @@ public class ContentTypes { private final Set pageTemplates = new LinkedHashSet<>(); private final Set sectionEntryTemplates = new LinkedHashSet<>(); private final Set listItemTypes = new LinkedHashSet<>(); + private final Set collectionTypes = new LinkedHashSet<>(); + + public void registerCollection(CollectionType collectionType) { + var registeredType = Objects.requireNonNull(collectionType, "collectionType"); + collectionTypes.removeIf(existing -> existing.name().equals(registeredType.name())); + collectionTypes.add(registeredType); + } + + /** JavaScript interop overload. */ + public void registerCollection(Map collectionType) { + registerCollection(CollectionType.fromMap(collectionType)); + } + + public Optional getCollection(String name) { + return collectionTypes.stream().filter(type -> type.name().equals(name)).findFirst(); + } + + public Set getCollections() { + return Collections.unmodifiableSet(new LinkedHashSet<>(collectionTypes)); + } public void registerListItemType(ListItemType listItemType) { listItemTypes.add(Objects.requireNonNull(listItemType, "listItemType")); diff --git a/cms-api/src/test/java/com/condation/cms/api/ui/elements/ContentTypesTest.java b/cms-api/src/test/java/com/condation/cms/api/ui/elements/ContentTypesTest.java index ec63fb477..fa26960ed 100644 --- a/cms-api/src/test/java/com/condation/cms/api/ui/elements/ContentTypesTest.java +++ b/cms-api/src/test/java/com/condation/cms/api/ui/elements/ContentTypesTest.java @@ -59,9 +59,14 @@ void convertsDynamicRegistrationIntoDetachedTypedDefinition() { "contentFolder", "content", "createButton", false, "forms", Map.of("settings", settings))); + Map collectionInput = new HashMap<>(Map.of( + "name", "blog", + "label", "Blog posts", + "forms", Map.of("edit", settings))); ContentTypes contentTypes = new ContentTypes(); contentTypes.registerPageTemplate(input); + contentTypes.registerCollection(collectionInput); PageTemplate pageTemplate = contentTypes.getPageTemplate("StartPage").orElseThrow(); assertThat(pageTemplate).isInstanceOf(PageTemplate.class); @@ -96,6 +101,10 @@ void convertsDynamicRegistrationIntoDetachedTypedDefinition() { assertThat(pageTemplate.getForm("settings").tabs().getFirst().title()).isEqualTo("Details"); assertThat(pageTemplate.getForm("settings").tabs().getFirst().fields().getFirst().getName()) .isEqualTo("description"); + assertThat(contentTypes.getCollection("blog")).hasValueSatisfying(collection -> { + assertThat(collection.label()).isEqualTo("Blog posts"); + assertThat(collection.getForm("edit").fields()).hasSize(1); + }); } @Test @@ -118,15 +127,18 @@ void supportsTypedJavaRegistration() { .forms(Map.of("attributes", form)) .build(); ListItemType listItemType = new ListItemType("features", form); + CollectionType collectionType = new CollectionType("blog", "Blog posts", Map.of("edit", form)); ContentTypes contentTypes = new ContentTypes(); contentTypes.registerPageTemplate(pageTemplate); contentTypes.registerSectionEntryTemplate(sectionEntryTemplate); contentTypes.registerListItemType(listItemType); + contentTypes.registerCollection(collectionType); assertThat(contentTypes.getPageTemplates()).containsExactly(pageTemplate); assertThat(contentTypes.getSectionEntryTemplates("main")).containsExactly(sectionEntryTemplate); assertThat(contentTypes.getListItemTypes()).containsExactly(listItemType); + assertThat(contentTypes.getCollections()).containsExactly(collectionType); assertThat(pageTemplate.createButton()).isTrue(); assertThat(form.fields()).allMatch(FormField.class::isInstance); assertThat(form.tabs()).singleElement() diff --git a/cms-content/src/main/java/com/condation/cms/content/CollectionResolver.java b/cms-content/src/main/java/com/condation/cms/content/CollectionResolver.java index 8099266a9..a2c9e3125 100644 --- a/cms-content/src/main/java/com/condation/cms/content/CollectionResolver.java +++ b/cms-content/src/main/java/com/condation/cms/content/CollectionResolver.java @@ -24,21 +24,17 @@ import com.condation.cms.api.Constants; import com.condation.cms.api.configuration.Configuration; import com.condation.cms.api.configuration.configs.CollectionConfiguration; -import com.condation.cms.api.configuration.configs.CollectionDefinition; -import com.condation.cms.api.configuration.configs.CollectionDetailConfiguration; import com.condation.cms.api.content.ContentResponse; import com.condation.cms.api.content.DefaultContentResponse; import com.condation.cms.api.db.ContentNode; import com.condation.cms.api.db.DB; -import com.condation.cms.api.db.collection.CollectionItem; +import com.condation.cms.api.feature.features.CurrentCollectionItemFeature; import com.condation.cms.api.feature.features.CurrentNodeFeature; import com.condation.cms.api.feature.features.RequestFeature; import com.condation.cms.api.request.RequestContext; import java.io.IOException; -import java.util.Comparator; import java.util.HashMap; import java.util.Optional; -import java.util.regex.Pattern; import lombok.RequiredArgsConstructor; /** @@ -56,65 +52,29 @@ public Optional getContent(RequestContext context) throws IOExc if (collectionConfiguration == null) { return Optional.empty(); } - - var uri = normalizeUri(context.get(RequestFeature.class).uri()); - for (var definition : collectionConfiguration.collections().values().stream() - .sorted(Comparator.comparing(CollectionDefinition::name)) - .toList()) { - var content = resolve(definition, uri, context); - if (content.isPresent()) { - return content; - } - } - return Optional.empty(); - } - - private Optional resolve( - CollectionDefinition definition, - String uri, - RequestContext context) throws IOException { - var detail = definition.detailPage(); - if (detail.isEmpty()) { + var route = new CollectionRouteResolver(db, collectionConfiguration) + .resolve(context.get(RequestFeature.class).uri()); + if (route.isEmpty()) { return Optional.empty(); } - var routeValue = match(detail.get(), uri); - if (routeValue.isEmpty()) { - return Optional.empty(); - } - return resolveItem(definition, detail.get(), routeValue.get(), uri, context); + return render(route.get(), context); } - private Optional resolveItem( - CollectionDefinition definition, - CollectionDetailConfiguration detail, - String routeValue, - String uri, + private Optional render( + CollectionRouteResolver.ResolvedRoute route, RequestContext context) throws IOException { - var collection = db.getCollections().collection(definition.name()); - Optional item; - if ("id".equals(detail.parameter())) { - item = findById(collection, routeValue); - } else { - item = collection.query() - .where(detail.parameter(), routeValue) - .page(1, 1) - .getItems() - .stream() - .findFirst(); - } - if (item.isEmpty()) { - return Optional.empty(); - } - - var collectionItem = item.get(); + var collectionItem = route.item(); var nodeData = new HashMap<>(collectionItem.meta()); - nodeData.put("template", detail.template()); + nodeData.put("template", route.detail().template()); var node = new ContentNode( collectionItem.path(), - uri, + route.uri(), collectionItem.id() + ".md", nodeData); context.add(CurrentNodeFeature.class, new CurrentNodeFeature(node)); + context.add( + CurrentCollectionItemFeature.class, + new CurrentCollectionItemFeature(collectionItem)); var collectionFile = db.getFileSystem().collectionsBase().resolve(collectionItem.path()); if (!collectionFile.exists()) { @@ -124,37 +84,8 @@ private Optional resolveItem( collectionFile, node, collectionItem, - detail.template(), + route.detail().template(), context); return Optional.of(new DefaultContentResponse(content, Constants.DEFAULT_CONTENT_TYPE, node)); } - - private static Optional findById( - com.condation.cms.api.db.collection.Collection collection, - String id) { - try { - return collection.item(id); - } catch (IllegalArgumentException _) { - return Optional.empty(); - } - } - - private static Optional match(CollectionDetailConfiguration detail, String uri) { - var token = "{" + detail.parameter() + "}"; - var tokenStart = detail.route().indexOf(token); - var prefix = detail.route().substring(0, tokenStart); - var suffix = detail.route().substring(tokenStart + token.length()); - var routePattern = Pattern.compile( - "^" + Pattern.quote(prefix) + "([^/]+)" + Pattern.quote(suffix) + "/?$"); - var matcher = routePattern.matcher(uri); - return matcher.matches() ? Optional.of(matcher.group(1)) : Optional.empty(); - } - - private static String normalizeUri(String uri) { - var normalized = uri == null ? "" : uri.trim(); - if (!normalized.startsWith("/")) { - normalized = "/" + normalized; - } - return normalized; - } } diff --git a/cms-content/src/main/java/com/condation/cms/content/CollectionRouteResolver.java b/cms-content/src/main/java/com/condation/cms/content/CollectionRouteResolver.java new file mode 100644 index 000000000..76f2096f7 --- /dev/null +++ b/cms-content/src/main/java/com/condation/cms/content/CollectionRouteResolver.java @@ -0,0 +1,117 @@ +package com.condation.cms.content; + +/*- + * #%L + * CMS Content + * %% + * Copyright (C) 2023 - 2026 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * #L% + */ + +import com.condation.cms.api.configuration.configs.CollectionConfiguration; +import com.condation.cms.api.configuration.configs.CollectionDefinition; +import com.condation.cms.api.configuration.configs.CollectionDetailConfiguration; +import com.condation.cms.api.db.DB; +import com.condation.cms.api.db.collection.CollectionItem; +import java.util.Comparator; +import java.util.Optional; +import java.util.regex.Pattern; +import lombok.RequiredArgsConstructor; + +/** Resolves configured collection detail routes without rendering them. */ +@RequiredArgsConstructor +public class CollectionRouteResolver { + + private final DB db; + private final CollectionConfiguration configuration; + + public Optional resolve(String requestUri) { + if (configuration == null) { + return Optional.empty(); + } + var uri = normalizeUri(requestUri); + for (var definition : configuration.collections().values().stream() + .sorted(Comparator.comparing(CollectionDefinition::name)) + .toList()) { + var resolved = resolve(definition, uri); + if (resolved.isPresent()) { + return resolved; + } + } + return Optional.empty(); + } + + private Optional resolve(CollectionDefinition definition, String uri) { + var detail = definition.detailPage(); + if (detail.isEmpty()) { + return Optional.empty(); + } + var routeValue = match(detail.get(), uri); + if (routeValue.isEmpty()) { + return Optional.empty(); + } + + var collection = db.getCollections().collection(definition.name()); + Optional item; + if ("id".equals(detail.get().parameter())) { + item = findById(collection, routeValue.get()); + } else { + item = collection.query() + .where(detail.get().parameter(), routeValue.get()) + .page(1, 1) + .getItems() + .stream() + .findFirst(); + } + return item.map(value -> new ResolvedRoute(definition, detail.get(), value, uri)); + } + + private static Optional findById( + com.condation.cms.api.db.collection.Collection collection, + String id) { + try { + return collection.item(id); + } catch (IllegalArgumentException _) { + return Optional.empty(); + } + } + + private static Optional match(CollectionDetailConfiguration detail, String uri) { + var token = "{" + detail.parameter() + "}"; + var tokenStart = detail.route().indexOf(token); + var prefix = detail.route().substring(0, tokenStart); + var suffix = detail.route().substring(tokenStart + token.length()); + var routePattern = Pattern.compile( + "^" + Pattern.quote(prefix) + "([^/]+)" + Pattern.quote(suffix) + "/?$"); + var matcher = routePattern.matcher(uri); + return matcher.matches() ? Optional.of(matcher.group(1)) : Optional.empty(); + } + + private static String normalizeUri(String uri) { + var normalized = uri == null ? "" : uri.trim(); + if (!normalized.startsWith("/")) { + normalized = "/" + normalized; + } + return normalized; + } + + public record ResolvedRoute( + CollectionDefinition definition, + CollectionDetailConfiguration detail, + CollectionItem item, + String uri) { + } +} diff --git a/cms-content/src/test/java/com/condation/cms/content/CollectionResolverTest.java b/cms-content/src/test/java/com/condation/cms/content/CollectionResolverTest.java index 61df6404f..2687eacae 100644 --- a/cms-content/src/test/java/com/condation/cms/content/CollectionResolverTest.java +++ b/cms-content/src/test/java/com/condation/cms/content/CollectionResolverTest.java @@ -40,6 +40,7 @@ import com.condation.cms.api.db.cms.ReadOnlyFile; import com.condation.cms.api.db.collection.Collection; import com.condation.cms.api.db.collection.CollectionItem; +import com.condation.cms.api.feature.features.CurrentCollectionItemFeature; import com.condation.cms.api.feature.features.CurrentNodeFeature; import com.condation.cms.api.feature.features.RequestFeature; import com.condation.cms.api.request.RequestContext; @@ -106,6 +107,7 @@ void resolvesAnIdRouteAndUsesReloadedDefinitions() throws Exception { .isInstanceOfSatisfying(DefaultContentResponse.class, content -> Assertions.assertThat(content.content()).isEqualTo("

First

")); Assertions.assertThat(context.get(CurrentNodeFeature.class).node().url()).isEqualTo("/blog/first"); + Assertions.assertThat(context.get(CurrentCollectionItemFeature.class).item()).isEqualTo(item); var node = ArgumentCaptor.forClass(com.condation.cms.api.db.ContentNode.class); verify(renderer).renderCollection( eq(itemFile), diff --git a/cms-filesystem/src/main/java/com/condation/cms/filesystem/FileCollections.java b/cms-filesystem/src/main/java/com/condation/cms/filesystem/FileCollections.java index 220030e07..25aec9074 100644 --- a/cms-filesystem/src/main/java/com/condation/cms/filesystem/FileCollections.java +++ b/cms-filesystem/src/main/java/com/condation/cms/filesystem/FileCollections.java @@ -114,6 +114,21 @@ public Set names() { return Set.copyOf(collectionNames); } + @Override + public void refresh(String collection, String id) { + validateCollectionName(collection); + validateItemId(id); + var file = collectionsBase.resolve(collection).resolve(id + ".md"); + try { + if (!Files.isRegularFile(file)) { + throw new IllegalArgumentException("collection item does not exist: " + collection + "/" + id); + } + index(file); + } catch (IOException ex) { + throw new IllegalStateException("could not refresh collection item", ex); + } + } + void handleEvent(FileEvent event) { if (event.type() == FileEvent.Type.OVERFLOW) { changeCoordinator.requestFullResync(); diff --git a/cms-filesystem/src/main/java/com/condation/cms/filesystem/metadata/persistent/LuceneQuery.java b/cms-filesystem/src/main/java/com/condation/cms/filesystem/metadata/persistent/LuceneQuery.java index 995f9eafd..112d77da1 100644 --- a/cms-filesystem/src/main/java/com/condation/cms/filesystem/metadata/persistent/LuceneQuery.java +++ b/cms-filesystem/src/main/java/com/condation/cms/filesystem/metadata/persistent/LuceneQuery.java @@ -58,6 +58,7 @@ import org.apache.lucene.search.PrefixQuery; import org.apache.lucene.search.Query; import org.apache.lucene.search.TermQuery; +import org.apache.lucene.queryparser.flexible.core.QueryNodeException; /** * @@ -91,7 +92,8 @@ enum Order { private List> extensionOperations = new ArrayList<>(); - private final Parser expressionsParser = new Parser(); + private final Parser expressionsParser = new Parser(); + private final TitleQueryFactory titleQueryFactory = new TitleQueryFactory(LuceneIndex.SEARCH_ANALYZER); public LuceneQuery( final String startUri, @@ -301,6 +303,19 @@ private Query buildBaseQuery() { return baseQuery.build(); } + @Override + public ContentQuery searchByTitle(String input) { + if (input == null || input.isBlank()) { + return this; + } + try { + queryBuilder.add(titleQueryFactory.createQuery(input), BooleanClause.Occur.MUST); + return this; + } catch (QueryNodeException ex) { + throw new IllegalArgumentException("invalid title search", ex); + } + } + private Query structuralVisibilityQuery(Query query) { if (policy == LuceneQueryPolicy.COLLECTION) { return query; diff --git a/cms-filesystem/src/test/java/com/condation/cms/filesystem/FileCollectionsTest.java b/cms-filesystem/src/test/java/com/condation/cms/filesystem/FileCollectionsTest.java index 8ca822034..973da8062 100644 --- a/cms-filesystem/src/test/java/com/condation/cms/filesystem/FileCollectionsTest.java +++ b/cms-filesystem/src/test/java/com/condation/cms/filesystem/FileCollectionsTest.java @@ -108,6 +108,51 @@ void appliesFlatFileChangesIncrementally() throws Exception { } } + @Test + void searchesCollectionTitlesWithPaging() throws Exception { + write("blog/first.md", "title: Collection Article One", "First"); + write("blog/second.md", "title: Collection Article Two", "Second"); + write("blog/other.md", "title: Unrelated Entry", "Other"); + + var collections = createCollections(); + try { + var page = collections.collection("blog") + .query() + .searchByTitle("Collection Article") + .orderby("title") + .asc() + .page(2, 1); + + Assertions.assertThat(page.getTotalItems()).isEqualTo(2); + Assertions.assertThat(page.getTotalPages()).isEqualTo(2); + Assertions.assertThat(page.getItems()) + .extracting(item -> item.id()) + .containsExactly("second"); + } finally { + collections.close(); + } + } + + @Test + void refreshesOneCollectionItemImmediately() throws Exception { + write("blog/item.md", "title: Before", "Before"); + var collections = createCollections(); + try { + write("blog/item.md", "title: After", "After"); + collections.refresh("blog", "item"); + + Assertions.assertThat(collections.collection("blog").item("item")) + .isPresent() + .get() + .satisfies(item -> { + Assertions.assertThat(item.meta()).containsEntry("title", "After"); + Assertions.assertThat(item.content()).isEqualTo("After\r\n"); + }); + } finally { + collections.close(); + } + } + @Test void rejectsUnsafeCollectionNames() throws Exception { var collections = createCollections(); diff --git a/modules/ui-module/src/main/java/com/condation/cms/modules/ui/extensionpoints/CollectionMenuExtension.java b/modules/ui-module/src/main/java/com/condation/cms/modules/ui/extensionpoints/CollectionMenuExtension.java new file mode 100644 index 000000000..5cd92e31b --- /dev/null +++ b/modules/ui-module/src/main/java/com/condation/cms/modules/ui/extensionpoints/CollectionMenuExtension.java @@ -0,0 +1,73 @@ +package com.condation.cms.modules.ui.extensionpoints; + +/*- + * #%L + * UI Module + * %% + * Copyright (C) 2023 - 2026 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * #L% + */ + +import com.condation.cms.api.auth.Permissions; +import com.condation.cms.api.extensions.AbstractExtensionPoint; +import com.condation.cms.api.feature.features.DBFeature; +import com.condation.cms.api.feature.features.HookSystemFeature; +import com.condation.cms.api.ui.action.UIScriptAction; +import com.condation.cms.api.ui.elements.Menu; +import com.condation.cms.api.ui.elements.MenuEntry; +import com.condation.cms.api.ui.extensions.UIActionsExtensionPoint; +import com.condation.cms.api.utils.HTTPUtil; +import com.condation.cms.modules.ui.utils.UIHooks; +import com.condation.modules.api.annotation.Extension; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.atomic.AtomicInteger; + +/** Adds one manager menu entry for every collection discovered on disk. */ +@Extension(UIActionsExtensionPoint.class) +public class CollectionMenuExtension extends AbstractExtensionPoint implements UIActionsExtensionPoint { + + @Override + public void addMenuItems(Menu menu) { + var db = getContext().get(DBFeature.class).db(); + var names = db.getCollections().names().stream().sorted().toList(); + if (names.isEmpty()) { + return; + } + + var contentTypes = new UIHooks(getRequestContext().get(HookSystemFeature.class).hookSystem()).contentTypes(); + var position = new AtomicInteger(1); + var children = names.stream().map(name -> MenuEntry.builder() + .id("collection-" + name) + .name(contentTypes.getCollection(name).map(type -> type.label()).orElse(name)) + .position(position.getAndIncrement()) + .permissions(List.of(Permissions.CONTENT_EDIT)) + .action(new UIScriptAction( + HTTPUtil.modifyUrl("/manager/actions/collection/manage-collection", getContext()), + Map.of("collection", name))) + .children(new ArrayList<>()) + .build()).toList(); + + menu.addMenuEntry(MenuEntry.builder() + .id("collections-menu") + .name("Collections") + .position(5) + .permissions(List.of(Permissions.CONTENT_EDIT)) + .children(new ArrayList<>(children)) + .build()); + } +} diff --git a/modules/ui-module/src/main/java/com/condation/cms/modules/ui/extensionpoints/UiTemplateModelExtension.java b/modules/ui-module/src/main/java/com/condation/cms/modules/ui/extensionpoints/UiTemplateModelExtension.java index ae7812697..b63beaed4 100644 --- a/modules/ui-module/src/main/java/com/condation/cms/modules/ui/extensionpoints/UiTemplateModelExtension.java +++ b/modules/ui-module/src/main/java/com/condation/cms/modules/ui/extensionpoints/UiTemplateModelExtension.java @@ -25,6 +25,7 @@ import com.condation.cms.api.feature.features.IsPreviewFeature; import com.condation.cms.api.request.RequestContext; import com.condation.cms.api.utils.JSONUtil; +import com.condation.cms.api.db.collection.CollectionItem; import com.condation.modules.api.annotation.Extension; import java.util.Collections; import java.util.HashMap; @@ -100,6 +101,23 @@ public String toolbar (String id, String type, String[] actions, Map additional) { + if (item == null) { + return ""; + } + var options = new HashMap<>(additional); + options.put("collection", item.collection()); + options.put("itemId", item.id()); + return toolbar(item.collection() + "-" + item.id(), "collectionItem", actions, options); + } public String mediaToolbar (String [] actions, Map options) { if (!requestContext.has(IsPreviewFeature.class)) { diff --git a/modules/ui-module/src/main/java/com/condation/cms/modules/ui/extensionpoints/remotemethods/RemoteCollectionEndpoints.java b/modules/ui-module/src/main/java/com/condation/cms/modules/ui/extensionpoints/remotemethods/RemoteCollectionEndpoints.java new file mode 100644 index 000000000..e9061aaf9 --- /dev/null +++ b/modules/ui-module/src/main/java/com/condation/cms/modules/ui/extensionpoints/remotemethods/RemoteCollectionEndpoints.java @@ -0,0 +1,188 @@ +package com.condation.cms.modules.ui.extensionpoints.remotemethods; + +/*- + * #%L + * UI Module + * %% + * Copyright (C) 2023 - 2026 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * #L% + */ + +import com.condation.cms.api.Constants; +import com.condation.cms.api.auth.Permissions; +import com.condation.cms.api.db.Page; +import com.condation.cms.api.db.collection.CollectionItem; +import com.condation.cms.api.eventbus.events.InvalidateContentCacheEvent; +import com.condation.cms.api.feature.features.EventBusFeature; +import com.condation.cms.api.ui.annotations.RemoteMethod; +import com.condation.cms.api.ui.extensions.UIRemoteMethodExtensionPoint; +import com.condation.cms.api.ui.rpc.RPCException; +import com.condation.cms.content.template.functions.LinkFunction; +import com.condation.cms.core.content.io.ContentFileParser; +import com.condation.cms.core.content.io.YamlHeaderUpdater; +import com.condation.cms.modules.ui.utils.FormHelper; +import com.condation.cms.modules.ui.utils.MetaConverter; +import com.condation.cms.modules.ui.utils.NumberUtils; +import com.condation.modules.api.annotation.Extension; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; +import lombok.extern.slf4j.Slf4j; + +/** Manager endpoints for listing and editing collection items. */ +@Slf4j +@Extension(UIRemoteMethodExtensionPoint.class) +public class RemoteCollectionEndpoints extends AbstractRemoteMethodeExtension { + + private static final long DEFAULT_PAGE_SIZE = 10; + private static final long MAX_PAGE_SIZE = 100; + + public record ItemDto( + String id, + String collection, + String path, + String title, + String detailUrl, + Map meta) { + } + + public record EditableItemDto( + String id, + String collection, + String path, + String content, + Map meta) { + } + + @RemoteMethod(name = "collections.items", permissions = {Permissions.CONTENT_EDIT}) + public Object items(Map parameters) throws RPCException { + var db = getDB(parameters); + var collectionName = requiredString(parameters, "collection"); + ensureCollectionExists(db.getCollections().names(), collectionName); + + long page = Math.max(1, NumberUtils.toLong(parameters.getOrDefault("page", 1L))); + long size = Math.clamp( + NumberUtils.toLong(parameters.getOrDefault("size", DEFAULT_PAGE_SIZE)), + 1, + MAX_PAGE_SIZE); + var title = optionalString(parameters, "query"); + + var query = db.getCollections().collection(collectionName).query(); + if (!title.isBlank()) { + query.searchByTitle(title); + } + query.orderby(Constants.MetaFields.TITLE).asc(); + Page result = query.page(page, size); + return new Page<>( + result.getTotalItems(), + result.getPageSize(), + result.getTotalPages(), + result.getPage(), + result.getItems().stream().map(this::itemDto).toList()); + } + + @RemoteMethod(name = "collections.item.get", permissions = {Permissions.CONTENT_EDIT}) + public Object get(Map parameters) throws RPCException { + var item = item(parameters); + return new EditableItemDto( + item.id(), + item.collection(), + item.path(), + item.content(), + item.meta()); + } + + @RemoteMethod(name = "collections.item.save", permissions = {Permissions.CONTENT_EDIT}) + public Object save(Map parameters) throws RPCException { + var db = getDB(parameters); + var item = item(parameters); + var sourceFile = db.getFileSystem().collectionsBase().resolve(item.path()); + var writableFile = db.getFileSystem().resolve(Constants.Folders.COLLECTIONS).resolve(item.path()); + try { + var parser = new ContentFileParser(sourceFile); + var meta = new HashMap<>(parser.getHeader()); + var rawMeta = typedMeta(parameters.get("meta")); + YamlHeaderUpdater.mergeFlatMapIntoNestedMap(meta, MetaConverter.convertMeta(rawMeta)); + var content = parameters.containsKey("content") + ? FormHelper.getContent(parameters.get("content")) + : parser.getContent(); + YamlHeaderUpdater.saveMarkdownFileWithHeader(writableFile, meta, content); + db.getCollections().refresh(item.collection(), item.id()); + getContext().get(EventBusFeature.class).eventBus().publish(new InvalidateContentCacheEvent()); + return Map.of("saved", true); + } catch (IOException | RuntimeException ex) { + log.error("could not save collection item {}/{}", item.collection(), item.id(), ex); + throw new RPCException(0, ex.getMessage()); + } + } + + private CollectionItem item(Map parameters) throws RPCException { + var db = getDB(parameters); + var collectionName = requiredString(parameters, "collection"); + var id = requiredString(parameters, "id"); + ensureCollectionExists(db.getCollections().names(), collectionName); + try { + return db.getCollections().collection(collectionName).item(id) + .orElseThrow(() -> new RPCException(404, "collection item not found")); + } catch (IllegalArgumentException ex) { + throw new RPCException(400, ex.getMessage()); + } + } + + private ItemDto itemDto(CollectionItem item) { + var title = item.meta().get(Constants.MetaFields.TITLE); + return new ItemDto( + item.id(), + item.collection(), + item.path(), + title == null || title.toString().isBlank() ? item.id() : title.toString(), + detailUrl(item), + item.meta()); + } + + private String detailUrl(CollectionItem item) { + try { + return new LinkFunction(getRequestContext()).collectionUrl(item); + } catch (IllegalArgumentException | IllegalStateException ex) { + return null; + } + } + + private static void ensureCollectionExists(java.util.Set names, String name) throws RPCException { + if (!names.contains(name)) { + throw new RPCException(404, "collection not found: " + name); + } + } + + private static String requiredString(Map parameters, String name) throws RPCException { + var value = optionalString(parameters, name); + if (value.isBlank()) { + throw new RPCException(400, name + " must not be blank"); + } + return value; + } + + private static String optionalString(Map parameters, String name) { + return parameters.get(name) instanceof String value ? value.trim() : ""; + } + + @SuppressWarnings("unchecked") + private static Map> typedMeta(Object value) { + return value instanceof Map map + ? (Map>) map + : Map.of(); + } +} diff --git a/modules/ui-module/src/main/java/com/condation/cms/modules/ui/extensionpoints/remotemethods/RemoteContentEndpointsExtension.java b/modules/ui-module/src/main/java/com/condation/cms/modules/ui/extensionpoints/remotemethods/RemoteContentEndpointsExtension.java index 74dda2aae..cce0ab611 100644 --- a/modules/ui-module/src/main/java/com/condation/cms/modules/ui/extensionpoints/remotemethods/RemoteContentEndpointsExtension.java +++ b/modules/ui-module/src/main/java/com/condation/cms/modules/ui/extensionpoints/remotemethods/RemoteContentEndpointsExtension.java @@ -29,9 +29,12 @@ import com.condation.cms.api.extensions.AbstractExtensionPoint; import com.condation.cms.api.feature.features.DBFeature; import com.condation.cms.api.feature.features.CurrentNodeFeature; +import com.condation.cms.api.feature.features.CurrentCollectionItemFeature; import com.condation.cms.api.feature.features.EventBusFeature; import com.condation.cms.api.feature.features.RequestFeature; import com.condation.cms.api.feature.features.SitePropertiesFeature; +import com.condation.cms.api.feature.features.ConfigurationFeature; +import com.condation.cms.api.configuration.configs.CollectionConfiguration; import com.condation.cms.api.ui.extensions.UIRemoteMethodExtensionPoint; import com.condation.cms.api.utils.PathUtil; import com.condation.cms.core.content.io.ContentFileParser; @@ -50,6 +53,7 @@ import com.condation.cms.content.SectionEntry; import com.condation.cms.content.ConfigurableVariantSelector; import com.condation.cms.content.VariantResolver; +import com.condation.cms.content.CollectionRouteResolver; import com.condation.cms.modules.ui.utils.FormHelper; import com.condation.cms.modules.ui.utils.MarkdownHelper; import com.condation.cms.modules.ui.utils.MetaConverter; @@ -70,24 +74,22 @@ public class RemoteContentEndpointsExtension extends AbstractExtensionPoint impl @RemoteMethod(name = "content.get", permissions = {Permissions.CONTENT_EDIT}) public Object getContent(Map parameters) throws RPCException { final DB db = getContext().get(DBFeature.class).db(); - var contentBase = db.getFileSystem().contentBase(); - - var uri = contentUri(parameters); - - var contentFile = contentBase.resolve(uri); + var target = editableTarget(parameters, db); Map result = new HashMap<>(); - result.put("uri", uri); - if (contentFile != null) { + result.put("uri", target.uri()); + if (target.file().exists()) { try { - ContentFileParser parser = new ContentFileParser(contentFile); + ContentFileParser parser = new ContentFileParser(target.file()); result.put("content", parser.getContent()); result.put("meta", parser.getHeader()); } catch (IOException ex) { log.error("", ex); throw new RPCException(0, ex.getMessage()); } - } + } else { + throw new RPCException(404, "content not found"); + } return result; } @@ -95,25 +97,19 @@ public Object getContent(Map parameters) throws RPCException { @RemoteMethod(name = "content.set", permissions = {Permissions.CONTENT_EDIT}) public Object setContent(Map parameters) throws RPCException { final DB db = getContext().get(DBFeature.class).db(); - var contentBase = db.getFileSystem().contentBase(); - var updatedContent = FormHelper.getContent(parameters.get("content")); - var uri = contentUri(parameters); - - var contentFile = contentBase.resolve(uri); + var target = editableTarget(parameters, db); Map result = new HashMap<>(); - result.put("uri", uri); - if (contentFile != null) { + result.put("uri", target.uri()); + if (target.file().exists()) { try { - ContentFileParser parser = new ContentFileParser(contentFile); + ContentFileParser parser = new ContentFileParser(target.file()); Map meta = parser.getHeader(); - - var filePath = db.getFileSystem().resolve(Constants.Folders.CONTENT).resolve(uri); - - YamlHeaderUpdater.saveMarkdownFileWithHeader(filePath, meta, updatedContent); - log.debug("file {} saved", uri); + YamlHeaderUpdater.saveMarkdownFileWithHeader(target.writableFile(db), meta, updatedContent); + refresh(target, db); + log.debug("file {} saved", target.uri()); } catch (IOException ex) { log.error("", ex); throw new RPCException(0, ex.getMessage()); @@ -168,29 +164,22 @@ public Object replaceContent(Map parameters) throws RPCException @RemoteMethod(name = "meta.set", permissions = {Permissions.CONTENT_EDIT}) public Object setMeta(Map parameters) throws RPCException { final DB db = getContext().get(DBFeature.class).db(); - var contentBase = db.getFileSystem().contentBase(); - var updateParam = (Map>) parameters.get("meta"); var update = MetaConverter.convertMeta(updateParam); - var uri = contentUri(parameters); - - var contentFile = contentBase.resolve(uri); + var target = editableTarget(parameters, db); Map result = new HashMap<>(); - result.put("uri", uri); - if (contentFile != null) { + result.put("uri", target.uri()); + if (target.file().exists()) { try { - ContentFileParser parser = new ContentFileParser(contentFile); + ContentFileParser parser = new ContentFileParser(target.file()); Map meta = parser.getHeader(); YamlHeaderUpdater.mergeFlatMapIntoNestedMap(meta, update); - var filePath = db.getFileSystem().resolve(Constants.Folders.CONTENT).resolve(uri); - - YamlHeaderUpdater.saveMarkdownFileWithHeader(filePath, meta, parser.getContent()); - log.debug("file {} saved", uri); - - getContext().get(EventBusFeature.class).eventBus().publish(new ReIndexContentMetaDataEvent(uri)); + YamlHeaderUpdater.saveMarkdownFileWithHeader(target.writableFile(db), meta, parser.getContent()); + refresh(target, db); + log.debug("file {} saved", target.uri()); } catch (IOException ex) { log.error("", ex); throw new RPCException(0, ex.getMessage()); @@ -370,8 +359,23 @@ public Object getContentNode (Map parameters) { Map result = new HashMap<>(); result.put("url", url); if (contentFile == null || canonicalUri == null) { + var collectionConfiguration = getContext().get(ConfigurationFeature.class) + .configuration().get(CollectionConfiguration.class); + var collectionRoute = new CollectionRouteResolver(db, collectionConfiguration).resolve(path); + if (collectionRoute.isEmpty()) { + return result; + } + var item = collectionRoute.get().item(); + result.put("uri", item.path()); + result.put("canonicalUri", item.path()); + result.put("variantId", null); + result.put("contentKind", "collection"); + result.put("collection", item.collection()); + result.put("collectionItemId", item.id()); + result.put("sections", Map.of()); return result; } + result.put("contentKind", "content"); var query = com.condation.cms.api.utils.HTTPUtil.queryParameters(requestUri.getQuery()); var variantId = query.getOrDefault( @@ -426,4 +430,47 @@ private String contentUri(Map parameters, String parameterName) } throw new RPCException(400, parameterName + " must not be blank"); } + + private EditableTarget editableTarget(Map parameters, DB db) throws RPCException { + if (!parameters.containsKey("uri") + && getRequestContext().has(CurrentCollectionItemFeature.class)) { + var item = getRequestContext().get(CurrentCollectionItemFeature.class).item(); + return new EditableTarget( + item.path(), + db.getFileSystem().collectionsBase().resolve(item.path()), + item.collection(), + item.id()); + } + var uri = contentUri(parameters); + return new EditableTarget( + uri, + db.getFileSystem().contentBase().resolve(uri), + null, + null); + } + + private void refresh(EditableTarget target, DB db) { + if (target.collectionName() != null) { + db.getCollections().refresh(target.collectionName(), target.itemId()); + } else { + getContext().get(EventBusFeature.class).eventBus() + .publish(new ReIndexContentMetaDataEvent(target.uri())); + db.getFileSystem().flushContentChanges(); + } + getContext().get(EventBusFeature.class).eventBus().publish(new InvalidateContentCacheEvent()); + } + + private record EditableTarget( + String uri, + ReadOnlyFile file, + String collectionName, + String itemId) { + + private Path writableFile(DB db) { + var folder = collectionName == null + ? Constants.Folders.CONTENT + : Constants.Folders.COLLECTIONS; + return db.getFileSystem().resolve(folder).resolve(uri); + } + } } diff --git a/modules/ui-module/src/main/java/com/condation/cms/modules/ui/extensionpoints/remotemethods/RemoteManagerEnpoints.java b/modules/ui-module/src/main/java/com/condation/cms/modules/ui/extensionpoints/remotemethods/RemoteManagerEnpoints.java index 7a0ba7d8a..70d91ec05 100644 --- a/modules/ui-module/src/main/java/com/condation/cms/modules/ui/extensionpoints/remotemethods/RemoteManagerEnpoints.java +++ b/modules/ui-module/src/main/java/com/condation/cms/modules/ui/extensionpoints/remotemethods/RemoteManagerEnpoints.java @@ -99,6 +99,16 @@ public Object getListItemTypes(Map parameters) throws RPCExcepti throw new RPCException(0, e.getMessage()); } } + + @RemoteMethod(name = "manager.contentTypes.collections", permissions = {Permissions.CONTENT_EDIT}) + public Object getCollectionTypes(Map parameters) throws RPCException { + try { + return uiHooks().contentTypes().getCollections(); + } catch (Exception ex) { + log.error("could not load collection editor definitions", ex); + throw new RPCException(0, ex.getMessage()); + } + } @RemoteMethod(name = "manager.token.createCSRF", permissions = {Permissions.CONTENT_EDIT}) public Object createCSRFToken(Map parameters) throws RPCException { diff --git a/modules/ui-module/src/main/java/com/condation/cms/modules/ui/extensionpoints/remotemethods/RemoteWorkflowEndpointsExtension.java b/modules/ui-module/src/main/java/com/condation/cms/modules/ui/extensionpoints/remotemethods/RemoteWorkflowEndpointsExtension.java index 63fd41f70..bb7aa36a1 100644 --- a/modules/ui-module/src/main/java/com/condation/cms/modules/ui/extensionpoints/remotemethods/RemoteWorkflowEndpointsExtension.java +++ b/modules/ui-module/src/main/java/com/condation/cms/modules/ui/extensionpoints/remotemethods/RemoteWorkflowEndpointsExtension.java @@ -30,7 +30,11 @@ import com.condation.cms.api.feature.features.InjectorFeature; import com.condation.cms.api.feature.features.DBFeature; import com.condation.cms.api.feature.features.CurrentNodeFeature; +import com.condation.cms.api.feature.features.CurrentCollectionItemFeature; import com.condation.cms.api.feature.features.WorkflowFeature; +import com.condation.cms.api.feature.features.EventBusFeature; +import com.condation.cms.api.eventbus.events.InvalidateContentCacheEvent; +import com.condation.cms.api.eventbus.events.ReIndexContentMetaDataEvent; import com.condation.cms.api.ui.rpc.RPCException; import com.condation.cms.api.ui.extensions.UIRemoteMethodExtensionPoint; import com.condation.cms.api.utils.HTTPUtil; @@ -57,6 +61,7 @@ import java.io.IOException; import java.util.Optional; import java.util.List; +import java.nio.file.Path; /** * @@ -69,10 +74,10 @@ public class RemoteWorkflowEndpointsExtension extends AbstractRemoteMethodeExten private static final String TRANSITIONS = "transitions"; private static final String STATUS = "status"; - private Optional getContentNode(String uri) { + private Optional getContentTarget(String uri) { final DB db = getContext().get(DBFeature.class).db(); var contentBase = db.getFileSystem().contentBase(); - var contentFile = getContentFile(uri); + var contentFile = contentBase.resolve(uri); if (!contentFile.exists()) { return Optional.empty(); @@ -85,7 +90,7 @@ private Optional getContentNode(String uri) { return Optional.empty(); } - return Optional.of( + return Optional.of(new WorkflowTarget( new ContentNode( node.get().uri(), node.get().url(), @@ -93,29 +98,55 @@ private Optional getContentNode(String uri) { node.get().data(), node.get().directory(), node.get().children(), - node.get().lastmodified() - )); + node.get().lastmodified()), + contentFile, + db.getFileSystem().resolve(Constants.Folders.CONTENT).resolve(uri), + false, + null, + null)); } - private ReadOnlyFile getContentFile(String uri) { + private Optional getWorkflowTarget(Map parameters) throws RPCException { final DB db = getContext().get(DBFeature.class).db(); - var contentBase = db.getFileSystem().contentBase(); - return contentBase.resolve(uri); + if (!parameters.containsKey("uri") + && getRequestContext().has(CurrentCollectionItemFeature.class)) { + var item = getRequestContext().get(CurrentCollectionItemFeature.class).item(); + var node = new ContentNode( + item.path(), + item.path(), + item.id() + ".md", + new HashMap<>(item.meta())); + return Optional.of(new WorkflowTarget( + node, + db.getFileSystem().collectionsBase().resolve(item.path()), + db.getFileSystem().resolve(Constants.Folders.COLLECTIONS).resolve(item.path()), + true, + item.collection(), + item.id())); + } + return getContentTarget(contentUri(parameters)); + } + + private record WorkflowTarget( + ContentNode node, + ReadOnlyFile file, + Path writableFile, + boolean collection, + String collectionName, + String itemId) { } @RemoteMethod(name = "workflow.manager.node.status", permissions = {Permissions.CONTENT_EDIT}) public Object nodeStatus(Map parameters) throws RPCException { - var uri = contentUri(parameters); Map result = new HashMap<>(); - var contentNodeOpt = getContentNode(uri); - - if (contentNodeOpt.isEmpty()) { + var target = getWorkflowTarget(parameters); + if (target.isEmpty()) { return result; } - var node = contentNodeOpt.get(); + var node = target.get().node(); final Workflow workflow = getContext().get(WorkflowFeature.class).workflow(); var status = workflow.getStatusProvider().status(node); @@ -129,18 +160,16 @@ public Object nodeStatus(Map parameters) throws RPCException { @RemoteMethod(name = "workflow.transitions.get", permissions = {Permissions.CONTENT_EDIT}) public Object getTransitions(Map parameters) throws RPCException { - var uri = contentUri(parameters); - Map result = new HashMap<>(); - var contentNodeOpt = getContentNode(uri); - if (contentNodeOpt.isEmpty()) { + var target = getWorkflowTarget(parameters); + if (target.isEmpty()) { result.put(TRANSITIONS, java.util.List.of()); return result; } Workflow workflow = getContext().get(WorkflowFeature.class).workflow(); - result.put(TRANSITIONS, transitionDtos(allowedTransitions(workflow, contentNodeOpt.get()))); + result.put(TRANSITIONS, transitionDtos(allowedTransitions(workflow, target.get().node()))); return result; } @@ -174,17 +203,16 @@ public Object unpublishedPages(Map parameters) throws RPCExcepti public Object transit(Map parameters) throws RPCException { var result = new HashMap(); try { - var uri = contentUri(parameters); var transitionId = requiredTransitionId(parameters); final DB db = getContext().get(DBFeature.class).db(); - var contentNodeOpt = getContentNode(uri); - if (contentNodeOpt.isEmpty()) { + var target = getWorkflowTarget(parameters); + if (target.isEmpty()) { throw new RPCException(404, "content node not found"); } - var contentNode = contentNodeOpt.get(); + var contentNode = target.get().node(); Workflow workflow = getContext().get(WorkflowFeature.class).workflow(); WFTransition transition = workflow.getNextTransitions(contentNode).stream() @@ -194,12 +222,17 @@ public Object transit(Map parameters) throws RPCException { ensureAllowed(transition); workflow.transit(transitionId, contentNode); - var contentFile = getContentFile(uri); - - ContentFileParser parser = new ContentFileParser(contentFile); - - var filePath = db.getFileSystem().resolve(Constants.Folders.CONTENT).resolve(uri); - YamlHeaderUpdater.saveMarkdownFileWithHeader(filePath, contentNode.data(), parser.getContent()); + ContentFileParser parser = new ContentFileParser(target.get().file()); + YamlHeaderUpdater.saveMarkdownFileWithHeader( + target.get().writableFile(), contentNode.data(), parser.getContent()); + if (target.get().collection()) { + db.getCollections().refresh(target.get().collectionName(), target.get().itemId()); + } else { + getContext().get(EventBusFeature.class).eventBus() + .publish(new ReIndexContentMetaDataEvent(contentNode.uri())); + db.getFileSystem().flushContentChanges(); + } + getContext().get(EventBusFeature.class).eventBus().publish(new InvalidateContentCacheEvent()); result.put("success", true); } catch (RPCException ex) { diff --git a/modules/ui-module/src/main/java/com/condation/cms/modules/ui/http/JSActionHandler.java b/modules/ui-module/src/main/java/com/condation/cms/modules/ui/http/JSActionHandler.java index 766e15934..7163a00a4 100644 --- a/modules/ui-module/src/main/java/com/condation/cms/modules/ui/http/JSActionHandler.java +++ b/modules/ui-module/src/main/java/com/condation/cms/modules/ui/http/JSActionHandler.java @@ -24,12 +24,15 @@ import com.condation.cms.api.module.SiteModuleContext; import com.condation.cms.api.ui.extensions.UIScriptActionSourceExtension; import com.google.common.base.Strings; +import java.io.IOException; +import java.nio.charset.StandardCharsets; import java.nio.file.FileSystem; import java.nio.file.Files; import java.util.Optional; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.eclipse.jetty.http.HttpHeader; +import org.eclipse.jetty.http.HttpStatus; import org.eclipse.jetty.io.Content; import org.eclipse.jetty.server.Request; import org.eclipse.jetty.server.Response; @@ -42,6 +45,7 @@ @Slf4j @RequiredArgsConstructor public class JSActionHandler extends JettyHandler { + private static final String JAVASCRIPT_EXTENSION = ".js"; private final FileSystem fileSystem; private final String base; @@ -62,12 +66,10 @@ public boolean handle(Request request, Response response, Callback callback) thr if (moduleContent.isPresent()) { scriptContent = moduleContent.get(); } else { - var resourceFile = resourceName + ".js"; - var files = fileSystem.getPath(base); - var path = files.resolve(resourceFile); - if (Files.exists(path)) { - scriptContent = Files.readString(path); - } + var bundledScript = getBundledScript(resourceName); + scriptContent = bundledScript.isPresent() + ? bundledScript.get() + : getScriptFromFileSystem(resourceName).orElse(""); } @@ -75,11 +77,46 @@ public boolean handle(Request request, Response response, Callback callback) thr response.getHeaders().put(HttpHeader.CONTENT_TYPE, "application/javascript; charset=UTF-8"); Content.Sink.write(response, true, scriptContent, callback); } else { + response.setStatus(HttpStatus.NOT_FOUND_404); callback.succeeded(); } return true; } + + Optional getBundledScript(String resourceName) { + var resourcePath = "%s/%s".formatted(base, scriptResourceName(resourceName)); + try (var stream = JSActionHandler.class.getResourceAsStream(resourcePath)) { + if (stream == null) { + return Optional.empty(); + } + return Optional.of(new String(stream.readAllBytes(), StandardCharsets.UTF_8)); + } catch (IOException exception) { + log.error("Could not load manager action {}", resourcePath, exception); + return Optional.empty(); + } + } + + private Optional getScriptFromFileSystem(String resourceName) { + var resourceFile = scriptResourceName(resourceName); + var files = fileSystem.getPath(base); + var path = files.resolve(resourceFile); + if (!Files.exists(path)) { + return Optional.empty(); + } + try { + return Optional.of(Files.readString(path)); + } catch (IOException exception) { + log.error("Could not load manager action {}", path, exception); + return Optional.empty(); + } + } + + private String scriptResourceName(String resourceName) { + return resourceName.endsWith(JAVASCRIPT_EXTENSION) + ? resourceName + : resourceName + JAVASCRIPT_EXTENSION; + } private Optional getScriptContentFromModules (String filename) { return context.get(ModuleManagerFeature.class).moduleManager().extensions(UIScriptActionSourceExtension.class) diff --git a/modules/ui-module/src/main/java/com/condation/cms/modules/ui/http/RemoteCallHandler.java b/modules/ui-module/src/main/java/com/condation/cms/modules/ui/http/RemoteCallHandler.java index c10b212ee..d1ac8e0ea 100644 --- a/modules/ui-module/src/main/java/com/condation/cms/modules/ui/http/RemoteCallHandler.java +++ b/modules/ui-module/src/main/java/com/condation/cms/modules/ui/http/RemoteCallHandler.java @@ -22,6 +22,8 @@ */ import com.condation.cms.api.module.SiteModuleContext; import com.condation.cms.api.feature.features.CurrentNodeFeature; +import com.condation.cms.api.feature.features.CurrentCollectionItemFeature; +import com.condation.cms.api.db.ContentNode; import com.condation.cms.api.feature.features.DBFeature; import com.condation.cms.api.request.RequestContext; import com.condation.cms.api.ui.rpc.RPCError; @@ -50,6 +52,8 @@ public class RemoteCallHandler extends JettyHandler { public static final String CONTENT_URI_HEADER = "X-CMS-Content-Uri"; + public static final String COLLECTION_HEADER = "X-CMS-Collection"; + public static final String COLLECTION_ITEM_HEADER = "X-CMS-Collection-Item"; private final RemoteMethodService remoteCallService; private final SiteModuleContext moduleContext; @@ -93,6 +97,9 @@ public boolean handle(Request request, Response response, Callback callback) thr } private void setCurrentContentNode(Request request) { + if (setCurrentCollectionItem(request)) { + return; + } var uri = request.getHeaders().get(CONTENT_URI_HEADER); if (uri == null || uri.isBlank()) { return; @@ -109,6 +116,33 @@ private void setCurrentContentNode(Request request) { )); } + private boolean setCurrentCollectionItem(Request request) { + var collectionName = request.getHeaders().get(COLLECTION_HEADER); + if (collectionName == null || collectionName.isBlank()) { + return false; + } + var itemId = request.getHeaders().get(COLLECTION_ITEM_HEADER); + if (itemId == null || itemId.isBlank()) { + return false; + } + if (!moduleContext.has(DBFeature.class)) { + return false; + } + try { + var item = moduleContext.get(DBFeature.class).db() + .getCollections().collection(collectionName.trim()).item(itemId.trim()); + item.ifPresent(value -> { + requestContext.add(CurrentCollectionItemFeature.class, new CurrentCollectionItemFeature(value)); + requestContext.add(CurrentNodeFeature.class, new CurrentNodeFeature(new ContentNode( + value.path(), value.path(), value.id() + ".md", value.meta()))); + }); + return item.isPresent(); + } catch (IllegalArgumentException ex) { + log.warn("invalid collection preview context", ex); + return false; + } + } + private RPCResult buildErrorResult(Exception e, String method) { log.error("error executing endpoint {}", method, e); if (e instanceof RPCException rpcException) { diff --git a/modules/ui-module/src/main/resources/manager/actions/collection/edit-collection-item.d.ts b/modules/ui-module/src/main/resources/manager/actions/collection/edit-collection-item.d.ts new file mode 100644 index 000000000..6ee1e9c26 --- /dev/null +++ b/modules/ui-module/src/main/resources/manager/actions/collection/edit-collection-item.d.ts @@ -0,0 +1,28 @@ +/*- + * #%L + * UI Module + * %% + * Copyright (C) 2023 - 2026 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * #L% + */ +export interface EditCollectionItemOptions { + collection: string; + id: string; + reloadAfterSave?: boolean; + onSaved?: () => void | Promise; +} +export declare const openCollectionItemEditor: (options: EditCollectionItemOptions) => Promise; +export declare const runAction: (options: EditCollectionItemOptions) => Promise; diff --git a/modules/ui-module/src/main/resources/manager/actions/collection/edit-collection-item.js b/modules/ui-module/src/main/resources/manager/actions/collection/edit-collection-item.js new file mode 100644 index 000000000..7719d0cea --- /dev/null +++ b/modules/ui-module/src/main/resources/manager/actions/collection/edit-collection-item.js @@ -0,0 +1,108 @@ +/*- + * #%L + * UI Module + * %% + * Copyright (C) 2023 - 2026 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * #L% + */ +import { createForm, getFormFields } from '@cms/modules/form/forms.js'; +import { i18n } from '@cms/modules/localization.js'; +import { openModal } from '@cms/modules/modal.js'; +import { buildValuesFromFields } from '@cms/modules/node.js'; +import { reloadPreview } from '@cms/modules/preview.utils.js'; +import { getCollectionItem, saveCollectionItem } from '@cms/modules/rpc/rpc-collection.js'; +import { getCollectionTypes } from '@cms/modules/rpc/rpc-manager.js'; +import { showToast } from '@cms/modules/toast.js'; +const CONTENT_FIELD = 'content'; +const defaultForm = { + fields: [ + { type: 'text', name: 'title', title: 'Title', required: true }, + { type: 'markdown', name: CONTENT_FIELD, title: 'Content', height: '60vh' } + ], + tabs: [] +}; +const collectionForm = (types, collection) => { + return types.find(type => type.name === collection)?.forms?.edit ?? defaultForm; +}; +export const openCollectionItemEditor = async (options) => { + try { + const [item, typeResponse] = await Promise.all([ + getCollectionItem(options.collection, options.id), + getCollectionTypes() + ]); + const definition = collectionForm(typeResponse.result, options.collection); + const fields = getFormFields(definition); + const form = createForm({ + fields: definition.fields ?? [], + tabs: definition.tabs ?? [], + values: { + ...buildValuesFromFields(fields, item.meta), + [CONTENT_FIELD]: item.content + } + }); + openModal({ + title: i18n.t('collection.item.edit.title', 'Edit collection item'), + body: '', + form, + fullscreen: true, + onCancel: () => { }, + onOk: async () => { + const data = form.getData(); + const content = data[CONTENT_FIELD]; + delete data[CONTENT_FIELD]; + try { + await saveCollectionItem({ + collection: options.collection, + id: options.id, + content, + meta: data + }); + showToast({ + title: i18n.t('collection.item.edit.success.title', 'Collection item updated'), + message: i18n.t('collection.item.edit.success.message', 'The collection item was updated successfully.'), + type: 'success', + timeout: 3000 + }); + await options.onSaved?.(); + if (options.reloadAfterSave) { + reloadPreview(); + } + return true; + } + catch (error) { + showToast({ + title: i18n.t('collection.item.edit.error.title', 'Collection item not updated'), + message: error?.message ?? String(error), + type: 'error', + timeout: 3000 + }); + return false; + } + } + }); + } + catch (error) { + showToast({ + title: i18n.t('collection.item.edit.loadError.title', 'Collection item could not be loaded'), + message: error?.message ?? String(error), + type: 'error', + timeout: 3000 + }); + } +}; +export const runAction = async (options) => { + await openCollectionItemEditor({ ...options, reloadAfterSave: true }); +}; diff --git a/modules/ui-module/src/main/resources/manager/actions/collection/manage-collection.d.ts b/modules/ui-module/src/main/resources/manager/actions/collection/manage-collection.d.ts new file mode 100644 index 000000000..4a16720aa --- /dev/null +++ b/modules/ui-module/src/main/resources/manager/actions/collection/manage-collection.d.ts @@ -0,0 +1,23 @@ +/*- + * #%L + * UI Module + * %% + * Copyright (C) 2023 - 2026 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * #L% + */ +export declare const runAction: (options: { + collection: string; +}) => Promise; diff --git a/modules/ui-module/src/main/resources/manager/actions/collection/manage-collection.js b/modules/ui-module/src/main/resources/manager/actions/collection/manage-collection.js new file mode 100644 index 000000000..d4291e9a8 --- /dev/null +++ b/modules/ui-module/src/main/resources/manager/actions/collection/manage-collection.js @@ -0,0 +1,152 @@ +/*- + * #%L + * UI Module + * %% + * Copyright (C) 2023 - 2026 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * #L% + */ +import { openCollectionItemEditor } from './edit-collection-item.js'; +import { i18n } from '@cms/modules/localization.js'; +import { openModal } from '@cms/modules/modal.js'; +import { loadPreview } from '@cms/modules/preview.utils.js'; +import { listCollectionItems } from '@cms/modules/rpc/rpc-collection.js'; +const PAGE_SIZE = 10; +const MIN_SEARCH_LENGTH = 3; +const escapeHtml = (value) => String(value ?? '') + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); +const renderItems = (items) => { + if (items.length === 0) { + return `

${i18n.t('collection.items.empty', 'No collection items found.')}

`; + } + return `
${items.map(item => ` +
+
+ ${escapeHtml(item.title)}
+ ${escapeHtml(item.id)} +
+
+ + ${item.detailUrl ? `` : ''} +
+
`).join('')}
`; +}; +export const runAction = async (options) => { + let currentPage = 1; + let currentQuery = ''; + let requestVersion = 0; + let modal; + const body = ` +
+ + +
+
+
`; + const update = async () => { + const version = ++requestVersion; + const root = document.querySelector('[data-collection-results]'); + const pagination = document.querySelector('[data-collection-pagination]'); + if (!root || !pagination) + return; + root.innerHTML = `
${i18n.t('collection.items.loading', 'Loading collection items...')}
`; + try { + const page = await listCollectionItems({ + collection: options.collection, + query: currentQuery, + page: currentPage, + size: PAGE_SIZE + }); + if (version !== requestVersion) + return; + root.innerHTML = renderItems(page.items); + pagination.innerHTML = page.totalPages > 1 ? ` + ` : ''; + root.querySelectorAll('[data-collection-edit]').forEach(button => { + button.addEventListener('click', () => openCollectionItemEditor({ + collection: options.collection, + id: button.dataset.collectionEdit ?? '', + onSaved: update + })); + }); + root.querySelectorAll('[data-collection-open]').forEach(button => { + button.addEventListener('click', () => { + modal.hide(); + loadPreview(button.dataset.collectionOpen ?? ''); + }); + }); + pagination.querySelectorAll('[data-collection-page]').forEach(button => { + button.addEventListener('click', () => { + currentPage = Number(button.dataset.collectionPage ?? 1); + update(); + }); + }); + } + catch (error) { + if (version !== requestVersion) + return; + root.innerHTML = `
${escapeHtml(error?.message ?? error)}
`; + pagination.innerHTML = ''; + } + }; + modal = openModal({ + title: options.collection, + body, + size: 'xl', + showFooter: false, + onShow: (element) => { + const input = element.querySelector('#cms-collection-search'); + let debounce; + input?.addEventListener('input', () => { + window.clearTimeout(debounce); + debounce = window.setTimeout(() => { + const value = input.value.trim(); + if (value.length > 0 && value.length < MIN_SEARCH_LENGTH) + return; + currentQuery = value; + currentPage = 1; + update(); + }, 300); + }); + update(); + } + }); +}; diff --git a/modules/ui-module/src/main/resources/manager/js/modules/manager/manager.message.handlers.js b/modules/ui-module/src/main/resources/manager/js/modules/manager/manager.message.handlers.js index 3894184d8..d0e854a3e 100644 --- a/modules/ui-module/src/main/resources/manager/js/modules/manager/manager.message.handlers.js +++ b/modules/ui-module/src/main/resources/manager/js/modules/manager/manager.message.handlers.js @@ -145,6 +145,16 @@ const initMessageHandlers = () => { } executeScriptAction(cmd); }); + frameMessenger.on('edit-collection-item', (payload) => { + executeScriptAction({ + module: window.manager.baseUrl + '/actions/collection/edit-collection-item', + function: 'runAction', + parameters: { + collection: payload.collection, + id: payload.id + } + }); + }); frameMessenger.on('add-sectionEntry', (payload) => { var cmd = { "module": window.manager.baseUrl + "/actions/page/add-section", diff --git a/modules/ui-module/src/main/resources/manager/js/modules/manager/toolbar.inject.js b/modules/ui-module/src/main/resources/manager/js/modules/manager/toolbar.inject.js index b137a68f6..a209da6f1 100644 --- a/modules/ui-module/src/main/resources/manager/js/modules/manager/toolbar.inject.js +++ b/modules/ui-module/src/main/resources/manager/js/modules/manager/toolbar.inject.js @@ -115,6 +115,17 @@ const editAttributes = (event) => { */ frameMessenger.send(window.parent, command); }; +const editCollectionItem = (event) => { + const toolbar = event.target.closest('[data-cms-toolbar]'); + const definition = JSON.parse(toolbar.dataset.cmsToolbar || '{}'); + frameMessenger.send(window.parent, { + type: 'edit-collection-item', + payload: { + collection: definition.collection, + id: definition.itemId + } + }); +}; const initDragDrop = (container) => { if (container.dataset.cmsDragDropInitialized === 'true') { return; @@ -348,6 +359,14 @@ export const initToolbar = (container) => { button.addEventListener('click', editAttributes); toolbar.appendChild(button); } + else if (action === "editCollectionItem") { + const button = document.createElement('button'); + button.setAttribute('data-cms-action', 'editCollectionItem'); + button.innerHTML = EDIT_ATTRIBUTES_ICON; + button.setAttribute('title', 'Edit collection item'); + button.addEventListener('click', editCollectionItem); + toolbar.appendChild(button); + } else if (action === "orderSectionEntries") { const button = document.createElement('button'); button.setAttribute('data-cms-action', 'editSections'); diff --git a/modules/ui-module/src/main/resources/manager/js/modules/preview-context.d.ts b/modules/ui-module/src/main/resources/manager/js/modules/preview-context.d.ts index c69c1c5c0..3422613f0 100644 --- a/modules/ui-module/src/main/resources/manager/js/modules/preview-context.d.ts +++ b/modules/ui-module/src/main/resources/manager/js/modules/preview-context.d.ts @@ -23,6 +23,9 @@ export interface ActivePreviewContent { url?: string; canonicalUri?: string; variantId?: string | null; + contentKind?: 'content' | 'collection'; + collection?: string; + collectionItemId?: string; } declare const setActivePreviewContent: (content: ActivePreviewContent | null) => void; declare const getActivePreviewContent: (currentPreviewUrl?: string) => ActivePreviewContent | null; diff --git a/modules/ui-module/src/main/resources/manager/js/modules/rpc/rpc-collection.d.ts b/modules/ui-module/src/main/resources/manager/js/modules/rpc/rpc-collection.d.ts new file mode 100644 index 000000000..d6fe5f95b --- /dev/null +++ b/modules/ui-module/src/main/resources/manager/js/modules/rpc/rpc-collection.d.ts @@ -0,0 +1,56 @@ +/*- + * #%L + * UI Module + * %% + * Copyright (C) 2023 - 2026 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * #L% + */ +export interface CollectionItemSummary { + id: string; + collection: string; + path: string; + title: string; + detailUrl?: string | null; + meta: Record; +} +export interface EditableCollectionItem { + id: string; + collection: string; + path: string; + content: string; + meta: Record; +} +export interface CollectionItemsPage { + totalItems: number; + pageSize: number; + totalPages: number; + page: number; + items: CollectionItemSummary[]; +} +export interface ListCollectionItemsOptions { + collection: string; + query?: string; + page?: number; + size?: number; +} +export declare const listCollectionItems: (options: ListCollectionItemsOptions) => Promise; +export declare const getCollectionItem: (collection: string, id: string) => Promise; +export declare const saveCollectionItem: (options: { + collection: string; + id: string; + content: any; + meta: Record; +}) => Promise; diff --git a/modules/ui-module/src/main/resources/manager/js/modules/rpc/rpc-collection.js b/modules/ui-module/src/main/resources/manager/js/modules/rpc/rpc-collection.js new file mode 100644 index 000000000..ea3884f63 --- /dev/null +++ b/modules/ui-module/src/main/resources/manager/js/modules/rpc/rpc-collection.js @@ -0,0 +1,39 @@ +/*- + * #%L + * UI Module + * %% + * Copyright (C) 2023 - 2026 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * #L% + */ +import { executeRemoteCall } from '@cms/modules/rpc/rpc.js'; +export const listCollectionItems = async (options) => { + return (await executeRemoteCall({ + method: 'collections.items', + parameters: options + })).result; +}; +export const getCollectionItem = async (collection, id) => { + return (await executeRemoteCall({ + method: 'collections.item.get', + parameters: { collection, id } + })).result; +}; +export const saveCollectionItem = async (options) => { + await executeRemoteCall({ + method: 'collections.item.save', + parameters: options + }); +}; diff --git a/modules/ui-module/src/main/resources/manager/js/modules/rpc/rpc-manager.d.ts b/modules/ui-module/src/main/resources/manager/js/modules/rpc/rpc-manager.d.ts index 9385b0d14..a5cde78ef 100644 --- a/modules/ui-module/src/main/resources/manager/js/modules/rpc/rpc-manager.d.ts +++ b/modules/ui-module/src/main/resources/manager/js/modules/rpc/rpc-manager.d.ts @@ -45,12 +45,18 @@ export interface ListItemType { name: string; form: FormDefinition; } +export interface CollectionType { + name: string; + label: string; + forms: Record; +} interface ContentTypeResponse { result: T[]; } declare const getSectionEntryTemplates: (options: any) => Promise>; declare const getPageTemplates: (options: any) => Promise>; declare const getListItemTypes: (options: any) => Promise>; +declare const getCollectionTypes: () => Promise>; declare const getMediaForm: (options: any) => Promise; declare const createCSRFToken: (options: any) => Promise; export declare enum Format { @@ -71,4 +77,4 @@ export interface MediaFormatsResponse { } declare const getMediaFormats: (options: any) => Promise; declare const getShortCodeNames: (options: any) => Promise; -export { getSectionEntryTemplates, getPageTemplates, getMediaForm, getShortCodeNames, getMediaFormats, getListItemTypes, createCSRFToken }; +export { getSectionEntryTemplates, getPageTemplates, getMediaForm, getShortCodeNames, getMediaFormats, getListItemTypes, getCollectionTypes, createCSRFToken }; diff --git a/modules/ui-module/src/main/resources/manager/js/modules/rpc/rpc-manager.js b/modules/ui-module/src/main/resources/manager/js/modules/rpc/rpc-manager.js index d9c29286f..a1c6b7b6b 100644 --- a/modules/ui-module/src/main/resources/manager/js/modules/rpc/rpc-manager.js +++ b/modules/ui-module/src/main/resources/manager/js/modules/rpc/rpc-manager.js @@ -40,6 +40,12 @@ const getListItemTypes = async (options) => { }; return await executeRemoteCall(data); }; +const getCollectionTypes = async () => { + return await executeRemoteCall({ + method: "manager.contentTypes.collections", + parameters: {} + }); +}; const getMediaForm = async (options) => { var data = { method: "manager.media.form", @@ -74,4 +80,4 @@ const getShortCodeNames = async (options) => { }; return await executeRemoteCall(data); }; -export { getSectionEntryTemplates, getPageTemplates, getMediaForm, getShortCodeNames, getMediaFormats, getListItemTypes, createCSRFToken }; +export { getSectionEntryTemplates, getPageTemplates, getMediaForm, getShortCodeNames, getMediaFormats, getListItemTypes, getCollectionTypes, createCSRFToken }; diff --git a/modules/ui-module/src/main/resources/manager/js/modules/rpc/rpc.js b/modules/ui-module/src/main/resources/manager/js/modules/rpc/rpc.js index 5724b2258..6ebcd61f3 100644 --- a/modules/ui-module/src/main/resources/manager/js/modules/rpc/rpc.js +++ b/modules/ui-module/src/main/resources/manager/js/modules/rpc/rpc.js @@ -53,6 +53,12 @@ const executeRemoteMethodCall = async (method, parameters) => { ...(csrfToken && { 'X-CSRF-Token': csrfToken }), ...(activePreviewContent?.uri && { 'X-CMS-Content-Uri': activePreviewContent.uri + }), + ...(activePreviewContent?.contentKind === 'collection' && activePreviewContent.collection && { + 'X-CMS-Collection': activePreviewContent.collection + }), + ...(activePreviewContent?.contentKind === 'collection' && activePreviewContent.collectionItemId && { + 'X-CMS-Collection-Item': activePreviewContent.collectionItemId }) }, body: JSON.stringify(data) diff --git a/modules/ui-module/src/main/ts/src/actions/collection/edit-collection-item.ts b/modules/ui-module/src/main/ts/src/actions/collection/edit-collection-item.ts new file mode 100644 index 000000000..51553cbbc --- /dev/null +++ b/modules/ui-module/src/main/ts/src/actions/collection/edit-collection-item.ts @@ -0,0 +1,120 @@ +/*- + * #%L + * UI Module + * %% + * Copyright (C) 2023 - 2026 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * #L% + */ + +import { createForm, getFormFields } from '@cms/modules/form/forms.js'; +import { i18n } from '@cms/modules/localization.js'; +import { openModal } from '@cms/modules/modal.js'; +import { buildValuesFromFields } from '@cms/modules/node.js'; +import { reloadPreview } from '@cms/modules/preview.utils.js'; +import { getCollectionItem, saveCollectionItem } from '@cms/modules/rpc/rpc-collection.js'; +import { CollectionType, getCollectionTypes } from '@cms/modules/rpc/rpc-manager.js'; +import { showToast } from '@cms/modules/toast.js'; + +const CONTENT_FIELD = 'content'; + +const defaultForm = { + fields: [ + { type: 'text', name: 'title', title: 'Title', required: true }, + { type: 'markdown', name: CONTENT_FIELD, title: 'Content', height: '60vh' } + ], + tabs: [] +}; + +const collectionForm = (types: CollectionType[], collection: string): any => { + return types.find(type => type.name === collection)?.forms?.edit ?? defaultForm; +}; + +export interface EditCollectionItemOptions { + collection: string; + id: string; + reloadAfterSave?: boolean; + onSaved?: () => void | Promise; +} + +export const openCollectionItemEditor = async (options: EditCollectionItemOptions) => { + try { + const [item, typeResponse] = await Promise.all([ + getCollectionItem(options.collection, options.id), + getCollectionTypes() + ]); + const definition = collectionForm(typeResponse.result, options.collection); + const fields = getFormFields(definition); + const form = createForm({ + fields: definition.fields ?? [], + tabs: definition.tabs ?? [], + values: { + ...buildValuesFromFields(fields, item.meta), + [CONTENT_FIELD]: item.content + } + }); + + openModal({ + title: i18n.t('collection.item.edit.title', 'Edit collection item'), + body: '', + form, + fullscreen: true, + onCancel: () => {}, + onOk: async () => { + const data = form.getData(); + const content = data[CONTENT_FIELD]; + delete data[CONTENT_FIELD]; + try { + await saveCollectionItem({ + collection: options.collection, + id: options.id, + content, + meta: data + }); + showToast({ + title: i18n.t('collection.item.edit.success.title', 'Collection item updated'), + message: i18n.t('collection.item.edit.success.message', 'The collection item was updated successfully.'), + type: 'success', + timeout: 3000 + }); + await options.onSaved?.(); + if (options.reloadAfterSave) { + reloadPreview(); + } + return true; + } catch (error: any) { + showToast({ + title: i18n.t('collection.item.edit.error.title', 'Collection item not updated'), + message: error?.message ?? String(error), + type: 'error', + timeout: 3000 + }); + return false; + } + } + }); + } catch (error: any) { + showToast({ + title: i18n.t('collection.item.edit.loadError.title', 'Collection item could not be loaded'), + message: error?.message ?? String(error), + type: 'error', + timeout: 3000 + }); + } +}; + +export const runAction = async (options: EditCollectionItemOptions) => { + await openCollectionItemEditor({ ...options, reloadAfterSave: true }); +}; diff --git a/modules/ui-module/src/main/ts/src/actions/collection/manage-collection.ts b/modules/ui-module/src/main/ts/src/actions/collection/manage-collection.ts new file mode 100644 index 000000000..eb4cae449 --- /dev/null +++ b/modules/ui-module/src/main/ts/src/actions/collection/manage-collection.ts @@ -0,0 +1,156 @@ +/*- + * #%L + * UI Module + * %% + * Copyright (C) 2023 - 2026 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * #L% + */ + +import { openCollectionItemEditor } from './edit-collection-item.js'; +import { i18n } from '@cms/modules/localization.js'; +import { openModal } from '@cms/modules/modal.js'; +import { loadPreview } from '@cms/modules/preview.utils.js'; +import { CollectionItemSummary, listCollectionItems } from '@cms/modules/rpc/rpc-collection.js'; + +const PAGE_SIZE = 10; +const MIN_SEARCH_LENGTH = 3; + +const escapeHtml = (value: any): string => String(value ?? '') + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); + +const renderItems = (items: CollectionItemSummary[]): string => { + if (items.length === 0) { + return `

${i18n.t('collection.items.empty', 'No collection items found.')}

`; + } + return `
${items.map(item => ` +
+
+ ${escapeHtml(item.title)}
+ ${escapeHtml(item.id)} +
+
+ + ${item.detailUrl ? `` : ''} +
+
`).join('')}
`; +}; + +export const runAction = async (options: { collection: string }) => { + let currentPage = 1; + let currentQuery = ''; + let requestVersion = 0; + let modal: any; + + const body = ` +
+ + +
+
+
`; + + const update = async () => { + const version = ++requestVersion; + const root = document.querySelector('[data-collection-results]') as HTMLElement | null; + const pagination = document.querySelector('[data-collection-pagination]') as HTMLElement | null; + if (!root || !pagination) return; + root.innerHTML = `
${i18n.t('collection.items.loading', 'Loading collection items...')}
`; + try { + const page = await listCollectionItems({ + collection: options.collection, + query: currentQuery, + page: currentPage, + size: PAGE_SIZE + }); + if (version !== requestVersion) return; + root.innerHTML = renderItems(page.items); + pagination.innerHTML = page.totalPages > 1 ? ` + ` : ''; + + root.querySelectorAll('[data-collection-edit]').forEach(button => { + button.addEventListener('click', () => openCollectionItemEditor({ + collection: options.collection, + id: button.dataset.collectionEdit ?? '', + onSaved: update + })); + }); + root.querySelectorAll('[data-collection-open]').forEach(button => { + button.addEventListener('click', () => { + modal.hide(); + loadPreview(button.dataset.collectionOpen ?? ''); + }); + }); + pagination.querySelectorAll('[data-collection-page]').forEach(button => { + button.addEventListener('click', () => { + currentPage = Number(button.dataset.collectionPage ?? 1); + update(); + }); + }); + } catch (error: any) { + if (version !== requestVersion) return; + root.innerHTML = `
${escapeHtml(error?.message ?? error)}
`; + pagination.innerHTML = ''; + } + }; + + modal = openModal({ + title: options.collection, + body, + size: 'xl', + showFooter: false, + onShow: (element: HTMLElement) => { + const input = element.querySelector('#cms-collection-search'); + let debounce: number | undefined; + input?.addEventListener('input', () => { + window.clearTimeout(debounce); + debounce = window.setTimeout(() => { + const value = input.value.trim(); + if (value.length > 0 && value.length < MIN_SEARCH_LENGTH) return; + currentQuery = value; + currentPage = 1; + update(); + }, 300); + }); + update(); + } + }); +}; diff --git a/modules/ui-module/src/main/ts/src/js/modules/manager/manager.message.handlers.ts b/modules/ui-module/src/main/ts/src/js/modules/manager/manager.message.handlers.ts index 86bbd17d5..381b586b8 100644 --- a/modules/ui-module/src/main/ts/src/js/modules/manager/manager.message.handlers.ts +++ b/modules/ui-module/src/main/ts/src/js/modules/manager/manager.message.handlers.ts @@ -146,6 +146,17 @@ const initMessageHandlers = () => { executeScriptAction(cmd) }); + frameMessenger.on('edit-collection-item', (payload: any) => { + executeScriptAction({ + module: window.manager.baseUrl + '/actions/collection/edit-collection-item', + function: 'runAction', + parameters: { + collection: payload.collection, + id: payload.id + } + }); + }); + frameMessenger.on('add-sectionEntry', (payload: any) => { var cmd : any = { "module": window.manager.baseUrl + "/actions/page/add-section", diff --git a/modules/ui-module/src/main/ts/src/js/modules/manager/toolbar.inject.ts b/modules/ui-module/src/main/ts/src/js/modules/manager/toolbar.inject.ts index a015b698c..43e952f3f 100644 --- a/modules/ui-module/src/main/ts/src/js/modules/manager/toolbar.inject.ts +++ b/modules/ui-module/src/main/ts/src/js/modules/manager/toolbar.inject.ts @@ -132,6 +132,18 @@ const editAttributes = (event: Event) => { frameMessenger.send(window.parent, command); } +const editCollectionItem = (event: Event) => { + const toolbar = (event.target as HTMLElement).closest('[data-cms-toolbar]') as HTMLElement; + const definition = JSON.parse(toolbar.dataset.cmsToolbar || '{}'); + frameMessenger.send(window.parent, { + type: 'edit-collection-item', + payload: { + collection: definition.collection, + id: definition.itemId + } + }); +}; + const initDragDrop = (container: HTMLElement) => { if (container.dataset.cmsDragDropInitialized === 'true') { @@ -404,6 +416,14 @@ export const initToolbar = (container: HTMLElement) => { button.setAttribute("title", "Edit attributes"); button.addEventListener('click', editAttributes); + toolbar.appendChild(button); + } else if (action === "editCollectionItem") { + const button = document.createElement('button'); + button.setAttribute('data-cms-action', 'editCollectionItem'); + button.innerHTML = EDIT_ATTRIBUTES_ICON; + button.setAttribute('title', 'Edit collection item'); + button.addEventListener('click', editCollectionItem); + toolbar.appendChild(button); } else if (action === "orderSectionEntries") { const button = document.createElement('button'); diff --git a/modules/ui-module/src/main/ts/src/js/modules/preview-context.ts b/modules/ui-module/src/main/ts/src/js/modules/preview-context.ts index c7bc68758..0d0a4297a 100644 --- a/modules/ui-module/src/main/ts/src/js/modules/preview-context.ts +++ b/modules/ui-module/src/main/ts/src/js/modules/preview-context.ts @@ -24,6 +24,9 @@ export interface ActivePreviewContent { url?: string; canonicalUri?: string; variantId?: string | null; + contentKind?: 'content' | 'collection'; + collection?: string; + collectionItemId?: string; } let activeContent: ActivePreviewContent | null = null; diff --git a/modules/ui-module/src/main/ts/src/js/modules/rpc/rpc-collection.ts b/modules/ui-module/src/main/ts/src/js/modules/rpc/rpc-collection.ts new file mode 100644 index 000000000..b98c89978 --- /dev/null +++ b/modules/ui-module/src/main/ts/src/js/modules/rpc/rpc-collection.ts @@ -0,0 +1,86 @@ +/*- + * #%L + * UI Module + * %% + * Copyright (C) 2023 - 2026 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * #L% + */ + +import { executeRemoteCall } from '@cms/modules/rpc/rpc.js'; + +export interface CollectionItemSummary { + id: string; + collection: string; + path: string; + title: string; + detailUrl?: string | null; + meta: Record; +} + + +export interface EditableCollectionItem { + id: string; + collection: string; + path: string; + content: string; + meta: Record; +} + +export interface CollectionItemsPage { + totalItems: number; + pageSize: number; + totalPages: number; + page: number; + items: CollectionItemSummary[]; +} + +export interface ListCollectionItemsOptions { + collection: string; + query?: string; + page?: number; + size?: number; +} + +export const listCollectionItems = async ( + options: ListCollectionItemsOptions +): Promise => { + return (await executeRemoteCall({ + method: 'collections.items', + parameters: options + })).result as CollectionItemsPage; +}; + +export const getCollectionItem = async ( + collection: string, + id: string +): Promise => { + return (await executeRemoteCall({ + method: 'collections.item.get', + parameters: { collection, id } + })).result as EditableCollectionItem; +}; + +export const saveCollectionItem = async (options: { + collection: string; + id: string; + content: any; + meta: Record; +}): Promise => { + await executeRemoteCall({ + method: 'collections.item.save', + parameters: options + }); +}; diff --git a/modules/ui-module/src/main/ts/src/js/modules/rpc/rpc-manager.ts b/modules/ui-module/src/main/ts/src/js/modules/rpc/rpc-manager.ts index 96d53fe7f..ae8c1ee3b 100644 --- a/modules/ui-module/src/main/ts/src/js/modules/rpc/rpc-manager.ts +++ b/modules/ui-module/src/main/ts/src/js/modules/rpc/rpc-manager.ts @@ -53,6 +53,12 @@ export interface ListItemType { form: FormDefinition; } +export interface CollectionType { + name: string; + label: string; + forms: Record; +} + interface ContentTypeResponse { result: T[]; } @@ -81,6 +87,13 @@ const getListItemTypes = async (options : any): Promise> => { + return await executeRemoteCall({ + method: "manager.contentTypes.collections", + parameters: {} + }); +}; + const getMediaForm = async (options : any) => { var data = { method: "manager.media.form", @@ -138,5 +151,6 @@ export { getShortCodeNames, getMediaFormats, getListItemTypes, + getCollectionTypes, createCSRFToken }; diff --git a/modules/ui-module/src/main/ts/src/js/modules/rpc/rpc.ts b/modules/ui-module/src/main/ts/src/js/modules/rpc/rpc.ts index 5ce381f25..65faed8e6 100644 --- a/modules/ui-module/src/main/ts/src/js/modules/rpc/rpc.ts +++ b/modules/ui-module/src/main/ts/src/js/modules/rpc/rpc.ts @@ -66,6 +66,12 @@ const executeRemoteMethodCall = async (method : string, parameters : any) => { ...(csrfToken && { 'X-CSRF-Token': csrfToken }), ...(activePreviewContent?.uri && { 'X-CMS-Content-Uri': activePreviewContent.uri + }), + ...(activePreviewContent?.contentKind === 'collection' && activePreviewContent.collection && { + 'X-CMS-Collection': activePreviewContent.collection + }), + ...(activePreviewContent?.contentKind === 'collection' && activePreviewContent.collectionItemId && { + 'X-CMS-Collection-Item': activePreviewContent.collectionItemId }) }, body: JSON.stringify(data) diff --git a/modules/ui-module/src/test/java/com/condation/cms/modules/ui/extensionpoints/remotemethods/RemoteContentEndpointsExtensionTest.java b/modules/ui-module/src/test/java/com/condation/cms/modules/ui/extensionpoints/remotemethods/RemoteContentEndpointsExtensionTest.java index 6c187be76..3b318f04a 100644 --- a/modules/ui-module/src/test/java/com/condation/cms/modules/ui/extensionpoints/remotemethods/RemoteContentEndpointsExtensionTest.java +++ b/modules/ui-module/src/test/java/com/condation/cms/modules/ui/extensionpoints/remotemethods/RemoteContentEndpointsExtensionTest.java @@ -83,6 +83,7 @@ void setUp() { @Test void getContent_throwsRPCException_whenParsingFails() throws IOException { when(contentBase.resolve("broken.md")).thenReturn(contentFile); + when(contentFile.exists()).thenReturn(true); when(contentFile.getContent()).thenThrow(new IOException("disk error")); Map params = Map.of("uri", "broken.md"); @@ -95,6 +96,7 @@ void getContent_throwsRPCException_whenParsingFails() throws IOException { @Test void setContent_throwsRPCException_whenParsingFails() throws IOException { when(contentBase.resolve("broken.md")).thenReturn(contentFile); + when(contentFile.exists()).thenReturn(true); when(contentFile.getContent()).thenThrow(new IOException("disk error")); Map params = Map.of("uri", "broken.md", "content", "hello"); @@ -113,6 +115,7 @@ void getContentUsesCurrentNodeWhenUriIsOmitted() throws Exception { new CurrentNodeFeature(new ContentNode(uri, "/about", "about.md", Map.of())) ); when(contentBase.resolve(uri)).thenReturn(contentFile); + when(contentFile.exists()).thenReturn(true); when(contentFile.getContent()).thenThrow(new IOException("variant selected")); assertThatThrownBy(() -> ScopedValue.where( diff --git a/modules/ui-module/src/test/java/com/condation/cms/modules/ui/extensionpoints/remotemethods/RemoteWorkflowEndpointsExtensionTest.java b/modules/ui-module/src/test/java/com/condation/cms/modules/ui/extensionpoints/remotemethods/RemoteWorkflowEndpointsExtensionTest.java index cf9d16f4a..1b43fa4ca 100644 --- a/modules/ui-module/src/test/java/com/condation/cms/modules/ui/extensionpoints/remotemethods/RemoteWorkflowEndpointsExtensionTest.java +++ b/modules/ui-module/src/test/java/com/condation/cms/modules/ui/extensionpoints/remotemethods/RemoteWorkflowEndpointsExtensionTest.java @@ -21,6 +21,7 @@ * #L% */ +import com.condation.cms.api.Constants; import com.condation.cms.api.db.DB; import com.condation.cms.api.db.DBFileSystem; import com.condation.cms.api.db.Content; @@ -29,14 +30,20 @@ import com.condation.cms.api.db.Page; import com.condation.cms.api.db.VariantSearchMode; import com.condation.cms.api.db.cms.ReadOnlyFile; +import com.condation.cms.api.db.collection.CollectionItem; +import com.condation.cms.api.feature.features.CurrentCollectionItemFeature; import com.condation.cms.api.feature.features.DBFeature; import com.condation.cms.api.feature.features.WorkflowFeature; import com.condation.cms.api.module.SiteModuleContext; +import com.condation.cms.api.request.RequestContext; +import com.condation.cms.api.request.RequestContextScope; import com.condation.cms.api.ui.rpc.RPCException; +import com.condation.cms.api.workflow.WFStatusProvider; import com.condation.cms.api.workflow.WFStatusQueryProvider; import com.condation.cms.api.workflow.Workflow; import java.util.List; import java.util.Map; +import java.nio.file.Path; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -70,6 +77,18 @@ class RemoteWorkflowEndpointsExtensionTest { @Mock private ReadOnlyFile contentFile; + @Mock + private ReadOnlyFile collectionsBase; + + @Mock + private ReadOnlyFile collectionFile; + + @Mock + private Path collectionsWritableBase; + + @Mock + private Path collectionWritableFile; + private RemoteWorkflowEndpointsExtension endpoints; @BeforeEach @@ -78,7 +97,11 @@ void setUp() { endpoints.setContext(moduleContext); when(moduleContext.get(DBFeature.class)).thenReturn(new DBFeature(db)); when(db.getFileSystem()).thenReturn(fileSystem); - when(fileSystem.contentBase()).thenReturn(contentBase); + lenient().when(fileSystem.contentBase()).thenReturn(contentBase); + lenient().when(fileSystem.collectionsBase()).thenReturn(collectionsBase); + lenient().when(collectionsBase.resolve("blog/first.md")).thenReturn(collectionFile); + lenient().when(fileSystem.resolve(Constants.Folders.COLLECTIONS)).thenReturn(collectionsWritableBase); + lenient().when(collectionsWritableBase.resolve("blog/first.md")).thenReturn(collectionWritableFile); lenient().when(contentBase.resolve("missing.md")).thenReturn(contentFile); lenient().when(contentFile.exists()).thenReturn(false); } @@ -114,6 +137,34 @@ void transit_throwsRPCException_whenContentNodeNotFound() { .satisfies(ex -> assertThat(((RPCException) ex).getCode()).isEqualTo(404)); } + @Test + void nodeStatus_usesCurrentCollectionItemOnDetailPage() throws Exception { + var item = new CollectionItem( + "first", + "blog", + "blog/first.md", + "Body", + Map.of("title", "First", "status", "draft")); + var requestContext = new RequestContext(); + requestContext.add( + CurrentCollectionItemFeature.class, + new CurrentCollectionItemFeature(item)); + var workflow = mock(Workflow.class); + var statusProvider = mock(WFStatusProvider.class); + var status = new WFStatusProvider.Status(false, null, null, true, "draft"); + when(workflow.getStatusProvider()).thenReturn(statusProvider); + when(statusProvider.status(any(ContentNode.class))).thenReturn(status); + when(moduleContext.get(WorkflowFeature.class)).thenReturn(new WorkflowFeature(workflow)); + + @SuppressWarnings("unchecked") + var result = (Map) ScopedValue.where( + RequestContextScope.REQUEST_CONTEXT, + requestContext).call(() -> endpoints.nodeStatus(Map.of())); + + assertThat(result).containsEntry("status", status); + assertThat(result).containsEntry("transitions", List.of()); + } + @Test void unpublishedPages_delegatesFilteringAndPaginationToWorkflowProvider() throws RPCException { Content content = mock(Content.class); diff --git a/modules/ui-module/src/test/java/com/condation/cms/modules/ui/http/JSActionHandlerTest.java b/modules/ui-module/src/test/java/com/condation/cms/modules/ui/http/JSActionHandlerTest.java new file mode 100644 index 000000000..b19083e95 --- /dev/null +++ b/modules/ui-module/src/test/java/com/condation/cms/modules/ui/http/JSActionHandlerTest.java @@ -0,0 +1,44 @@ +package com.condation.cms.modules.ui.http; + +/*- + * #%L + * UI Module + * %% + * Copyright (C) 2023 - 2026 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * #L% + */ + +import java.nio.file.FileSystems; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; + +class JSActionHandlerTest { + + @Test + void loadsNestedCollectionActionFromBundledResources() { + var handler = new JSActionHandler( + FileSystems.getDefault(), + "/manager/actions", + mock(com.condation.cms.api.module.SiteModuleContext.class)); + + assertThat(handler.getBundledScript("collection/manage-collection")) + .hasValueSatisfying(script -> assertThat(script).contains("export const runAction")); + assertThat(handler.getBundledScript("collection/edit-collection-item.js")) + .hasValueSatisfying(script -> assertThat(script).contains("openCollectionItemEditor")); + } +} diff --git a/modules/ui-module/src/test/java/com/condation/cms/modules/ui/http/RemoteCallHandlerTest.java b/modules/ui-module/src/test/java/com/condation/cms/modules/ui/http/RemoteCallHandlerTest.java index 8d5455083..d048ef67c 100644 --- a/modules/ui-module/src/test/java/com/condation/cms/modules/ui/http/RemoteCallHandlerTest.java +++ b/modules/ui-module/src/test/java/com/condation/cms/modules/ui/http/RemoteCallHandlerTest.java @@ -25,6 +25,10 @@ import com.condation.cms.api.db.Content; import com.condation.cms.api.db.ContentNode; import com.condation.cms.api.db.DB; +import com.condation.cms.api.db.collection.Collection; +import com.condation.cms.api.db.collection.CollectionItem; +import com.condation.cms.api.db.collection.Collections; +import com.condation.cms.api.feature.features.CurrentCollectionItemFeature; import com.condation.cms.api.feature.features.CurrentNodeFeature; import com.condation.cms.api.feature.features.DBFeature; import com.condation.cms.api.request.RequestContext; @@ -104,6 +108,7 @@ void contentUriHeaderAddsCurrentNodeFeature() throws Exception { when(moduleContext.get(DBFeature.class)).thenReturn(new DBFeature(db)); when(db.getContent()).thenReturn(content); when(request.getHeaders()).thenReturn(headers); + when(headers.get(RemoteCallHandler.COLLECTION_HEADER)).thenReturn(null); when(headers.get(RemoteCallHandler.CONTENT_URI_HEADER)).thenReturn(node.uri()); when(content.byUri(node.uri())).thenReturn(Optional.of(node)); @@ -126,6 +131,7 @@ void unknownContentUriHeaderDoesNotAddCurrentNodeFeature() throws Exception { when(moduleContext.get(DBFeature.class)).thenReturn(new DBFeature(db)); when(db.getContent()).thenReturn(content); when(request.getHeaders()).thenReturn(headers); + when(headers.get(RemoteCallHandler.COLLECTION_HEADER)).thenReturn(null); when(headers.get(RemoteCallHandler.CONTENT_URI_HEADER)).thenReturn("unknown.md"); when(content.byUri("unknown.md")).thenReturn(Optional.empty()); when(content.byPath("unknown.md")).thenReturn(Optional.empty()); @@ -137,4 +143,36 @@ void unknownContentUriHeaderDoesNotAddCurrentNodeFeature() throws Exception { assertThat(context.has(CurrentNodeFeature.class)).isFalse(); } + + @Test + void collectionHeadersAddCurrentCollectionItemAndNodeFeatures() throws Exception { + var context = new RequestContext(); + var db = mock(DB.class); + var collections = mock(Collections.class); + var collection = mock(Collection.class); + var request = mock(Request.class); + var headers = mock(HttpFields.class); + var item = new CollectionItem( + "first", + "blog", + "blog/first.md", + "Body", + Map.of("title", "First")); + when(moduleContext.has(DBFeature.class)).thenReturn(true); + when(moduleContext.get(DBFeature.class)).thenReturn(new DBFeature(db)); + when(db.getCollections()).thenReturn(collections); + when(collections.collection("blog")).thenReturn(collection); + when(collection.item("first")).thenReturn(Optional.of(item)); + when(request.getHeaders()).thenReturn(headers); + when(headers.get(RemoteCallHandler.COLLECTION_HEADER)).thenReturn("blog"); + when(headers.get(RemoteCallHandler.COLLECTION_ITEM_HEADER)).thenReturn("first"); + + var handler = new RemoteCallHandler(remoteMethodService, moduleContext, context); + var method = RemoteCallHandler.class.getDeclaredMethod("setCurrentContentNode", Request.class); + method.setAccessible(true); + method.invoke(handler, request); + + assertThat(context.get(CurrentCollectionItemFeature.class).item()).isEqualTo(item); + assertThat(context.get(CurrentNodeFeature.class).node().uri()).isEqualTo(item.path()); + } } diff --git a/test-server/hosts/demo/collections/blog/item_3.md b/test-server/hosts/demo/collections/blog/item_3.md index 8e1132c46..2f2ea46c6 100644 --- a/test-server/hosts/demo/collections/blog/item_3.md +++ b/test-server/hosts/demo/collections/blog/item_3.md @@ -1,6 +1,8 @@ --- -title: Blog item 3 -status: draft description: This is the third item +title: Blog item 3 publish_date: 2026-04-09T00:00:00Z ---- \ No newline at end of file +status: draft +--- + + diff --git a/test-server/themes/demo/extensions/theme.manager.js b/test-server/themes/demo/extensions/theme.manager.js index a1661d47f..6f1537865 100644 --- a/test-server/themes/demo/extensions/theme.manager.js +++ b/test-server/themes/demo/extensions/theme.manager.js @@ -40,6 +40,50 @@ const UnPublishDateField = { $hooks.registerFilter("manager/contentTypes/register", (contentTypes) => { + contentTypes.registerCollection({ + name: "blog", + label: "Blog", + forms: { + edit: { + fields: [ + TitleField, + DescriptionField, + { + type: "markdown", + name: "content", + title: "Content", + height: "60vh" + } + ] + } + } + }); + + contentTypes.registerCollection({ + name: "authors", + label: "Authors", + forms: { + edit: { + fields: [ + TitleField, + DescriptionField, + { + type: "text", + name: "slug", + title: "Slug", + required: true + }, + { + type: "markdown", + name: "content", + title: "Content", + height: "60vh" + } + ] + } + } + }); + contentTypes.registerPageTemplate({ name: "StartPage", template: "start.html", diff --git a/test-server/themes/demo/templates/collections.html b/test-server/themes/demo/templates/collections.html index 35493cf60..5b8ac667e 100644 --- a/test-server/themes/demo/templates/collections.html +++ b/test-server/themes/demo/templates/collections.html @@ -22,7 +22,7 @@

Blog collection

{% assign items = cms.collection("blog").query().get() %} {% for item in items %} -
+ {% endfor %} @@ -31,7 +31,7 @@

Author collection

{% assign items = cms.collection("authors").query().get() %} {% for item in items %} -
+ {% endfor %} diff --git a/test-server/themes/demo/templates/collections/author-detail.html b/test-server/themes/demo/templates/collections/author-detail.html index 6aac1e618..3a02af2c2 100644 --- a/test-server/themes/demo/templates/collections/author-detail.html +++ b/test-server/themes/demo/templates/collections/author-detail.html @@ -13,7 +13,7 @@ -
+

{{ node.meta.title }}

{{ node.content | raw }} @@ -23,4 +23,4 @@

{{ node.meta.title }}

- \ No newline at end of file + diff --git a/test-server/themes/demo/templates/collections/blog-detail.html b/test-server/themes/demo/templates/collections/blog-detail.html index 886cc2be2..4f373b502 100644 --- a/test-server/themes/demo/templates/collections/blog-detail.html +++ b/test-server/themes/demo/templates/collections/blog-detail.html @@ -8,7 +8,7 @@ -
+

{{ node.meta.title }}

{{ node.meta.description }}

From 2a1f365a33b09e4ad5ea493aeef21afc17dac5c4 Mon Sep 17 00:00:00 2001 From: Thorsten Marx Date: Fri, 28 Aug 2026 16:04:02 +0200 Subject: [PATCH 07/28] create items --- .../cms/api/db/collection/Collections.java | 2 +- .../cms/filesystem/FileCollections.java | 7 +- .../cms/filesystem/FileCollectionsTest.java | 14 ++ .../RemoteCollectionEndpoints.java | 90 +++++++++- .../cms/modules/ui/utils/ActionFactory.java | 17 ++ .../collection/create-collection-item.d.ts | 27 +++ .../collection/create-collection-item.js | 108 +++++++++++ .../collection/edit-collection-item.d.ts | 2 + .../collection/edit-collection-item.js | 5 +- .../actions/collection/manage-collection.js | 64 ++++++- .../js/modules/rpc/rpc-collection.d.ts | 7 + .../manager/js/modules/rpc/rpc-collection.js | 12 ++ .../collection/create-collection-item.ts | 117 ++++++++++++ .../collection/edit-collection-item.ts | 9 +- .../actions/collection/manage-collection.ts | 61 ++++++- .../ts/src/js/modules/rpc/rpc-collection.ts | 19 ++ .../RemoteCollectionEndpointsTest.java | 169 ++++++++++++++++++ .../ui/utils/ActionFactoryAppsTest.java | 49 +++++ 18 files changed, 762 insertions(+), 17 deletions(-) create mode 100644 modules/ui-module/src/main/resources/manager/actions/collection/create-collection-item.d.ts create mode 100644 modules/ui-module/src/main/resources/manager/actions/collection/create-collection-item.js create mode 100644 modules/ui-module/src/main/ts/src/actions/collection/create-collection-item.ts create mode 100644 modules/ui-module/src/test/java/com/condation/cms/modules/ui/extensionpoints/remotemethods/RemoteCollectionEndpointsTest.java diff --git a/cms-api/src/main/java/com/condation/cms/api/db/collection/Collections.java b/cms-api/src/main/java/com/condation/cms/api/db/collection/Collections.java index 5a08598f2..cab09c508 100644 --- a/cms-api/src/main/java/com/condation/cms/api/db/collection/Collections.java +++ b/cms-api/src/main/java/com/condation/cms/api/db/collection/Collections.java @@ -32,6 +32,6 @@ public interface Collections { Set names(); - /** Re-indexes one item after a synchronous manager write. */ + /** Re-indexes one item after a synchronous manager write, or removes a deleted item from the index. */ void refresh(String collection, String id); } diff --git a/cms-filesystem/src/main/java/com/condation/cms/filesystem/FileCollections.java b/cms-filesystem/src/main/java/com/condation/cms/filesystem/FileCollections.java index 25aec9074..72ef7c26e 100644 --- a/cms-filesystem/src/main/java/com/condation/cms/filesystem/FileCollections.java +++ b/cms-filesystem/src/main/java/com/condation/cms/filesystem/FileCollections.java @@ -120,10 +120,11 @@ public void refresh(String collection, String id) { validateItemId(id); var file = collectionsBase.resolve(collection).resolve(id + ".md"); try { - if (!Files.isRegularFile(file)) { - throw new IllegalArgumentException("collection item does not exist: " + collection + "/" + id); + if (Files.isRegularFile(file)) { + index(file); + } else { + metaData.removeFile(collection + "/" + id + ".md"); } - index(file); } catch (IOException ex) { throw new IllegalStateException("could not refresh collection item", ex); } diff --git a/cms-filesystem/src/test/java/com/condation/cms/filesystem/FileCollectionsTest.java b/cms-filesystem/src/test/java/com/condation/cms/filesystem/FileCollectionsTest.java index 973da8062..bc4fe951b 100644 --- a/cms-filesystem/src/test/java/com/condation/cms/filesystem/FileCollectionsTest.java +++ b/cms-filesystem/src/test/java/com/condation/cms/filesystem/FileCollectionsTest.java @@ -153,6 +153,20 @@ void refreshesOneCollectionItemImmediately() throws Exception { } } + @Test + void removesDeletedCollectionItemImmediatelyOnRefresh() throws Exception { + var item = write("blog/item.md", "title: Before", "Before"); + var collections = createCollections(); + try { + Files.delete(item); + collections.refresh("blog", "item"); + + Assertions.assertThat(collections.collection("blog").query().get()).isEmpty(); + } finally { + collections.close(); + } + } + @Test void rejectsUnsafeCollectionNames() throws Exception { var collections = createCollections(); diff --git a/modules/ui-module/src/main/java/com/condation/cms/modules/ui/extensionpoints/remotemethods/RemoteCollectionEndpoints.java b/modules/ui-module/src/main/java/com/condation/cms/modules/ui/extensionpoints/remotemethods/RemoteCollectionEndpoints.java index e9061aaf9..93c60314d 100644 --- a/modules/ui-module/src/main/java/com/condation/cms/modules/ui/extensionpoints/remotemethods/RemoteCollectionEndpoints.java +++ b/modules/ui-module/src/main/java/com/condation/cms/modules/ui/extensionpoints/remotemethods/RemoteCollectionEndpoints.java @@ -23,10 +23,12 @@ import com.condation.cms.api.Constants; import com.condation.cms.api.auth.Permissions; +import com.condation.cms.api.db.DB; import com.condation.cms.api.db.Page; import com.condation.cms.api.db.collection.CollectionItem; import com.condation.cms.api.eventbus.events.InvalidateContentCacheEvent; import com.condation.cms.api.feature.features.EventBusFeature; +import com.condation.cms.api.feature.features.WorkflowFeature; import com.condation.cms.api.ui.annotations.RemoteMethod; import com.condation.cms.api.ui.extensions.UIRemoteMethodExtensionPoint; import com.condation.cms.api.ui.rpc.RPCException; @@ -38,8 +40,13 @@ import com.condation.cms.modules.ui.utils.NumberUtils; import com.condation.modules.api.annotation.Extension; import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.time.Instant; +import java.util.Date; import java.util.HashMap; import java.util.Map; +import java.util.regex.Pattern; import lombok.extern.slf4j.Slf4j; /** Manager endpoints for listing and editing collection items. */ @@ -49,6 +56,7 @@ public class RemoteCollectionEndpoints extends AbstractRemoteMethodeExtension { private static final long DEFAULT_PAGE_SIZE = 10; private static final long MAX_PAGE_SIZE = 100; + private static final Pattern ITEM_ID = Pattern.compile("[a-zA-Z0-9][a-zA-Z0-9_.-]*"); public record ItemDto( String id, @@ -121,7 +129,7 @@ public Object save(Map parameters) throws RPCException { : parser.getContent(); YamlHeaderUpdater.saveMarkdownFileWithHeader(writableFile, meta, content); db.getCollections().refresh(item.collection(), item.id()); - getContext().get(EventBusFeature.class).eventBus().publish(new InvalidateContentCacheEvent()); + invalidateContentCache(); return Map.of("saved", true); } catch (IOException | RuntimeException ex) { log.error("could not save collection item {}/{}", item.collection(), item.id(), ex); @@ -129,6 +137,68 @@ public Object save(Map parameters) throws RPCException { } } + @RemoteMethod(name = "collections.item.create", permissions = {Permissions.CONTENT_EDIT}) + public Object create(Map parameters) throws RPCException { + var db = getDB(parameters); + var collectionName = requiredString(parameters, "collection"); + var id = requiredItemId(parameters); + ensureCollectionExists(db.getCollections().names(), collectionName); + var writableFile = writableFile(db, collectionName, id); + if (Files.exists(writableFile)) { + throw new RPCException(409, "collection item already exists"); + } + + var meta = new HashMap(); + YamlHeaderUpdater.mergeFlatMapIntoNestedMap( + meta, + MetaConverter.convertMeta(typedMeta(parameters.get("meta")))); + meta.putIfAbsent(Constants.MetaFields.TITLE, id); + meta.put("createdAt", Date.from(Instant.now())); + meta.put("createdBy", getUserName()); + meta.put( + Constants.MetaFields.STATUS, + getContext().get(WorkflowFeature.class).workflow().getStatusProvider().newNodeStatus()); + var content = FormHelper.getContent(parameters.get("content")); + + try { + Files.createDirectories(writableFile.getParent()); + YamlHeaderUpdater.saveMarkdownFileWithHeader(writableFile, meta, content); + db.getCollections().refresh(collectionName, id); + invalidateContentCache(); + return itemDto(new CollectionItem( + id, + collectionName, + collectionName + "/" + id + ".md", + content, + meta)); + } catch (IOException | RuntimeException exception) { + log.error("could not create collection item {}/{}", collectionName, id, exception); + throw new RPCException(0, exception.getMessage()); + } + } + + @RemoteMethod(name = "collections.item.delete", permissions = {Permissions.CONTENT_EDIT}) + public Object delete(Map parameters) throws RPCException { + var db = getDB(parameters); + var collectionName = requiredString(parameters, "collection"); + var id = requiredItemId(parameters); + ensureCollectionExists(db.getCollections().names(), collectionName); + var writableFile = writableFile(db, collectionName, id); + if (!Files.isRegularFile(writableFile)) { + throw new RPCException(404, "collection item not found"); + } + + try { + Files.delete(writableFile); + db.getCollections().refresh(collectionName, id); + invalidateContentCache(); + return Map.of("deleted", true); + } catch (IOException | RuntimeException exception) { + log.error("could not delete collection item {}/{}", collectionName, id, exception); + throw new RPCException(0, exception.getMessage()); + } + } + private CollectionItem item(Map parameters) throws RPCException { var db = getDB(parameters); var collectionName = requiredString(parameters, "collection"); @@ -161,6 +231,16 @@ private String detailUrl(CollectionItem item) { } } + private void invalidateContentCache() { + getContext().get(EventBusFeature.class).eventBus().publish(new InvalidateContentCacheEvent()); + } + + private static Path writableFile(DB db, String collectionName, String id) { + return db.getFileSystem().resolve(Constants.Folders.COLLECTIONS) + .resolve(collectionName) + .resolve(id + ".md"); + } + private static void ensureCollectionExists(java.util.Set names, String name) throws RPCException { if (!names.contains(name)) { throw new RPCException(404, "collection not found: " + name); @@ -175,6 +255,14 @@ private static String requiredString(Map parameters, String name return value; } + private static String requiredItemId(Map parameters) throws RPCException { + var id = requiredString(parameters, "id"); + if (!ITEM_ID.matcher(id).matches()) { + throw new RPCException(400, "invalid collection item id"); + } + return id; + } + private static String optionalString(Map parameters, String name) { return parameters.get(name) instanceof String value ? value.trim() : ""; } diff --git a/modules/ui-module/src/main/java/com/condation/cms/modules/ui/utils/ActionFactory.java b/modules/ui-module/src/main/java/com/condation/cms/modules/ui/utils/ActionFactory.java index 4f31eb458..f417427cd 100644 --- a/modules/ui-module/src/main/java/com/condation/cms/modules/ui/utils/ActionFactory.java +++ b/modules/ui-module/src/main/java/com/condation/cms/modules/ui/utils/ActionFactory.java @@ -46,6 +46,7 @@ import com.condation.cms.auth.services.User; import com.condation.cms.auth.services.RoleService; import com.condation.cms.api.feature.features.InjectorFeature; +import com.condation.cms.api.feature.features.DBFeature; import java.util.Arrays; import java.util.Comparator; import java.util.LinkedHashMap; @@ -137,6 +138,22 @@ public Menu createContentTypeMenu() { .build(); }).forEach(menu::addMenuEntry); + context.get(DBFeature.class).db().getCollections().names().stream() + .sorted() + .map(name -> MenuEntry.builder() + .id("collection-" + name) + .name(contentTypes.getCollection(name) + .map(collection -> collection.label()) + .orElse(name)) + .action(new UIScriptAction( + HTTPUtil.prependContext( + "/manager/actions/collection/create-collection-item", + siteProperties), + Map.of("collection", name))) + .children(new ArrayList<>()) + .build()) + .forEach(menu::addMenuEntry); + return menu; } diff --git a/modules/ui-module/src/main/resources/manager/actions/collection/create-collection-item.d.ts b/modules/ui-module/src/main/resources/manager/actions/collection/create-collection-item.d.ts new file mode 100644 index 000000000..9214742d6 --- /dev/null +++ b/modules/ui-module/src/main/resources/manager/actions/collection/create-collection-item.d.ts @@ -0,0 +1,27 @@ +/*- + * #%L + * UI Module + * %% + * Copyright (C) 2023 - 2026 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * #L% + */ +import { CollectionItemSummary } from '@cms/modules/rpc/rpc-collection.js'; +export interface CreateCollectionItemOptions { + collection: string; + onCreated?: (item: CollectionItemSummary) => void | Promise; +} +export declare const openCollectionItemCreator: (options: CreateCollectionItemOptions) => Promise; +export declare const runAction: (options: CreateCollectionItemOptions) => Promise; diff --git a/modules/ui-module/src/main/resources/manager/actions/collection/create-collection-item.js b/modules/ui-module/src/main/resources/manager/actions/collection/create-collection-item.js new file mode 100644 index 000000000..5ad4c2c02 --- /dev/null +++ b/modules/ui-module/src/main/resources/manager/actions/collection/create-collection-item.js @@ -0,0 +1,108 @@ +/*- + * #%L + * UI Module + * %% + * Copyright (C) 2023 - 2026 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * #L% + */ +import { collectionForm } from './edit-collection-item.js'; +import { createForm, getFormFields } from '@cms/modules/form/forms.js'; +import { i18n } from '@cms/modules/localization.js'; +import { openModal } from '@cms/modules/modal.js'; +import { loadPreview } from '@cms/modules/preview.utils.js'; +import { createCollectionItem } from '@cms/modules/rpc/rpc-collection.js'; +import { getCollectionTypes } from '@cms/modules/rpc/rpc-manager.js'; +import { showToast } from '@cms/modules/toast.js'; +const ID_FIELD = 'id'; +const CONTENT_FIELD = 'content'; +const fieldValue = (field) => String(field?.value ?? field ?? '').trim(); +export const openCollectionItemCreator = async (options) => { + try { + const typeResponse = await getCollectionTypes(); + const definition = collectionForm(typeResponse.result, options.collection, 'create'); + const hasIdField = getFormFields(definition).some(field => field.name === ID_FIELD); + const form = createForm({ + fields: [ + ...(hasIdField ? [] : [{ + type: 'text', + name: ID_FIELD, + title: i18n.t('collection.item.create.id', 'Item ID'), + required: true + }]), + ...(definition.fields ?? []) + ], + tabs: definition.tabs ?? [], + values: {} + }); + openModal({ + title: i18n.t('collection.item.create.title', 'Create collection item'), + body: '', + form, + fullscreen: true, + onCancel: () => { }, + onOk: async () => { + const data = form.getData(); + const id = fieldValue(data[ID_FIELD]); + const content = data[CONTENT_FIELD]; + delete data[ID_FIELD]; + delete data[CONTENT_FIELD]; + try { + const item = await createCollectionItem({ + collection: options.collection, + id, + content, + meta: data + }); + showToast({ + title: i18n.t('collection.item.create.success.title', 'Collection item created'), + message: i18n.t('collection.item.create.success.message', 'The collection item was created successfully.'), + type: 'success', + timeout: 3000 + }); + await options.onCreated?.(item); + return true; + } + catch (error) { + showToast({ + title: i18n.t('collection.item.create.error.title', 'Collection item not created'), + message: error?.message ?? String(error), + type: 'error', + timeout: 3000 + }); + return false; + } + } + }); + } + catch (error) { + showToast({ + title: i18n.t('collection.item.create.loadError.title', 'Collection form could not be loaded'), + message: error?.message ?? String(error), + type: 'error', + timeout: 3000 + }); + } +}; +export const runAction = async (options) => { + await openCollectionItemCreator({ + ...options, + onCreated: item => { + if (item.detailUrl) { + loadPreview(item.detailUrl); + } + } + }); +}; diff --git a/modules/ui-module/src/main/resources/manager/actions/collection/edit-collection-item.d.ts b/modules/ui-module/src/main/resources/manager/actions/collection/edit-collection-item.d.ts index 6ee1e9c26..f8f2e239b 100644 --- a/modules/ui-module/src/main/resources/manager/actions/collection/edit-collection-item.d.ts +++ b/modules/ui-module/src/main/resources/manager/actions/collection/edit-collection-item.d.ts @@ -18,6 +18,8 @@ * along with this program. If not, see . * #L% */ +import { CollectionType } from '@cms/modules/rpc/rpc-manager.js'; +export declare const collectionForm: (types: CollectionType[], collection: string, mode?: "create" | "edit") => any; export interface EditCollectionItemOptions { collection: string; id: string; diff --git a/modules/ui-module/src/main/resources/manager/actions/collection/edit-collection-item.js b/modules/ui-module/src/main/resources/manager/actions/collection/edit-collection-item.js index 7719d0cea..01e1e9a66 100644 --- a/modules/ui-module/src/main/resources/manager/actions/collection/edit-collection-item.js +++ b/modules/ui-module/src/main/resources/manager/actions/collection/edit-collection-item.js @@ -34,8 +34,9 @@ const defaultForm = { ], tabs: [] }; -const collectionForm = (types, collection) => { - return types.find(type => type.name === collection)?.forms?.edit ?? defaultForm; +export const collectionForm = (types, collection, mode = 'edit') => { + const forms = types.find(type => type.name === collection)?.forms; + return forms?.[mode] ?? forms?.edit ?? defaultForm; }; export const openCollectionItemEditor = async (options) => { try { diff --git a/modules/ui-module/src/main/resources/manager/actions/collection/manage-collection.js b/modules/ui-module/src/main/resources/manager/actions/collection/manage-collection.js index d4291e9a8..6b62e41db 100644 --- a/modules/ui-module/src/main/resources/manager/actions/collection/manage-collection.js +++ b/modules/ui-module/src/main/resources/manager/actions/collection/manage-collection.js @@ -18,11 +18,13 @@ * along with this program. If not, see . * #L% */ +import { openCollectionItemCreator } from './create-collection-item.js'; import { openCollectionItemEditor } from './edit-collection-item.js'; import { i18n } from '@cms/modules/localization.js'; import { openModal } from '@cms/modules/modal.js'; import { loadPreview } from '@cms/modules/preview.utils.js'; -import { listCollectionItems } from '@cms/modules/rpc/rpc-collection.js'; +import { deleteCollectionItem, listCollectionItems } from '@cms/modules/rpc/rpc-collection.js'; +import { showToast } from '@cms/modules/toast.js'; const PAGE_SIZE = 10; const MIN_SEARCH_LENGTH = 3; const escapeHtml = (value) => String(value ?? '') @@ -49,6 +51,10 @@ const renderItems = (items) => { data-collection-open="${escapeHtml(item.detailUrl)}"> ${i18n.t('collection.items.open', 'Open detail page')} ` : ''} +
`).join('')}
`; }; @@ -59,11 +65,18 @@ export const runAction = async (options) => { let modal; const body = `
- +
+
+ +
+ +
`; @@ -83,6 +96,11 @@ export const runAction = async (options) => { }); if (version !== requestVersion) return; + if (page.items.length === 0 && currentPage > 1) { + currentPage--; + await update(); + return; + } root.innerHTML = renderItems(page.items); pagination.innerHTML = page.totalPages > 1 ? `
`).join('')}
`; }; @@ -65,11 +71,18 @@ export const runAction = async (options: { collection: string }) => { const body = `
- +
+
+ +
+ +
`; @@ -88,6 +101,11 @@ export const runAction = async (options: { collection: string }) => { size: PAGE_SIZE }); if (version !== requestVersion) return; + if (page.items.length === 0 && currentPage > 1) { + currentPage--; + await update(); + return; + } root.innerHTML = renderItems(page.items); pagination.innerHTML = page.totalPages > 1 ? ` ` : ''; root.querySelectorAll('[data-collection-edit]').forEach(button => { - button.addEventListener('click', () => openCollectionItemEditor({ - collection: options.collection, - id: button.dataset.collectionEdit ?? '', - onSaved: update - })); + button.addEventListener('click', () => openEditor(button.dataset.collectionEdit ?? '')); }); root.querySelectorAll('[data-collection-open]').forEach(button => { button.addEventListener('click', () => { @@ -177,7 +268,16 @@ export const runAction = async (options) => { size: 'xl', showFooter: false, onShow: (element) => { + modalElement = element; + collectionSlider = element.querySelector('[data-collection-slider]'); + browsePanel = element.querySelector('[data-collection-browse-panel]'); + editorPanel = element.querySelector('[data-collection-editor-panel]'); + editorContent = element.querySelector('[data-collection-editor-content]'); + editorSaveButton = element.querySelector('[data-collection-editor-save]'); const input = element.querySelector('#cms-collection-search'); + element.querySelectorAll('[data-collection-editor-back], [data-collection-editor-cancel]') + .forEach(button => button.addEventListener('click', closeEditor)); + editorSaveButton?.addEventListener('click', saveEditor); element.querySelector('[data-collection-create]')?.addEventListener('click', () => { openCollectionItemCreator({ collection: options.collection, diff --git a/modules/ui-module/src/main/resources/manager/css/manager.css b/modules/ui-module/src/main/resources/manager/css/manager.css index 7247aa40f..a8e8b0e1d 100644 --- a/modules/ui-module/src/main/resources/manager/css/manager.css +++ b/modules/ui-module/src/main/resources/manager/css/manager.css @@ -760,6 +760,33 @@ i[data-cms-section-handle] { transform: translateX(-100%); } +.cms-collection-manager-slider { + display: grid; + grid-template-columns: 100% 100%; + overflow: hidden; +} + +.cms-collection-manager-panel { + grid-row: 1; + min-width: 0; + transition: transform 0.25s ease; +} + +.cms-collection-manager-panel--browse { + grid-column: 1; +} + +.cms-collection-manager-panel--editor { + grid-column: 2; + max-height: 70vh; + overflow-y: auto; + padding-left: 1rem; +} + +.cms-collection-manager-slider.is-editing-item .cms-collection-manager-panel { + transform: translateX(-100%); +} + .cms-media-grid { --cms-media-grid-gap: 12px; --cms-media-card-preview-height: 140px; diff --git a/modules/ui-module/src/main/ts/src/actions/collection/edit-collection-item.ts b/modules/ui-module/src/main/ts/src/actions/collection/edit-collection-item.ts index 8f19dabb3..a899a06aa 100644 --- a/modules/ui-module/src/main/ts/src/actions/collection/edit-collection-item.ts +++ b/modules/ui-module/src/main/ts/src/actions/collection/edit-collection-item.ts @@ -19,7 +19,7 @@ * #L% */ -import { createForm, getFormFields } from '@cms/modules/form/forms.js'; +import { createForm, Form, getFormFields } from '@cms/modules/form/forms.js'; import { i18n } from '@cms/modules/localization.js'; import { openModal } from '@cms/modules/modal.js'; import { buildValuesFromFields } from '@cms/modules/node.js'; @@ -54,61 +54,77 @@ export interface EditCollectionItemOptions { onSaved?: () => void | Promise; } +export interface CollectionItemEditor { + form: Form; + save: () => Promise; +} + +export const createCollectionItemEditor = async ( + options: EditCollectionItemOptions +): Promise => { + const [item, typeResponse] = await Promise.all([ + getCollectionItem(options.collection, options.id), + getCollectionTypes() + ]); + const definition = collectionForm(typeResponse.result, options.collection); + const fields = getFormFields(definition); + const form = createForm({ + fields: definition.fields ?? [], + tabs: definition.tabs ?? [], + values: { + ...buildValuesFromFields(fields, item.meta), + [CONTENT_FIELD]: item.content + } + }); + + return { + form, + save: async () => { + const data = form.getData(); + const content = data[CONTENT_FIELD]; + delete data[CONTENT_FIELD]; + try { + await saveCollectionItem({ + collection: options.collection, + id: options.id, + content, + meta: data + }); + showToast({ + title: i18n.t('collection.item.edit.success.title', 'Collection item updated'), + message: i18n.t('collection.item.edit.success.message', 'The collection item was updated successfully.'), + type: 'success', + timeout: 3000 + }); + await options.onSaved?.(); + if (options.reloadAfterSave) { + reloadPreview(); + } + return true; + } catch (error: any) { + showToast({ + title: i18n.t('collection.item.edit.error.title', 'Collection item not updated'), + message: error?.message ?? String(error), + type: 'error', + timeout: 3000 + }); + return false; + } + } + }; +}; + export const openCollectionItemEditor = async (options: EditCollectionItemOptions) => { try { - const [item, typeResponse] = await Promise.all([ - getCollectionItem(options.collection, options.id), - getCollectionTypes() - ]); - const definition = collectionForm(typeResponse.result, options.collection); - const fields = getFormFields(definition); - const form = createForm({ - fields: definition.fields ?? [], - tabs: definition.tabs ?? [], - values: { - ...buildValuesFromFields(fields, item.meta), - [CONTENT_FIELD]: item.content - } - }); + const editor = await createCollectionItemEditor(options); openModal({ title: i18n.t('collection.item.edit.title', 'Edit collection item'), body: '', - form, + form: editor.form, fullscreen: true, onCancel: () => {}, - onOk: async () => { - const data = form.getData(); - const content = data[CONTENT_FIELD]; - delete data[CONTENT_FIELD]; - try { - await saveCollectionItem({ - collection: options.collection, - id: options.id, - content, - meta: data - }); - showToast({ - title: i18n.t('collection.item.edit.success.title', 'Collection item updated'), - message: i18n.t('collection.item.edit.success.message', 'The collection item was updated successfully.'), - type: 'success', - timeout: 3000 - }); - await options.onSaved?.(); - if (options.reloadAfterSave) { - reloadPreview(); - } - return true; - } catch (error: any) { - showToast({ - title: i18n.t('collection.item.edit.error.title', 'Collection item not updated'), - message: error?.message ?? String(error), - type: 'error', - timeout: 3000 - }); - return false; - } - } + onOk: editor.save }); } catch (error: any) { showToast({ diff --git a/modules/ui-module/src/main/ts/src/actions/collection/manage-collection.ts b/modules/ui-module/src/main/ts/src/actions/collection/manage-collection.ts index ab789e6a6..6117704d8 100644 --- a/modules/ui-module/src/main/ts/src/actions/collection/manage-collection.ts +++ b/modules/ui-module/src/main/ts/src/actions/collection/manage-collection.ts @@ -20,7 +20,7 @@ */ import { openCollectionItemCreator } from './create-collection-item.js'; -import { openCollectionItemEditor } from './edit-collection-item.js'; +import { CollectionItemEditor, createCollectionItemEditor } from './edit-collection-item.js'; import { i18n } from '@cms/modules/localization.js'; import { openModal } from '@cms/modules/modal.js'; import { loadPreview } from '@cms/modules/preview.utils.js'; @@ -67,30 +67,118 @@ export const runAction = async (options: { collection: string }) => { let currentPage = 1; let currentQuery = ''; let requestVersion = 0; + let editorRequestVersion = 0; let modal: any; + let modalElement: HTMLElement | null = null; + let collectionSlider: HTMLElement | null = null; + let browsePanel: HTMLElement | null = null; + let editorPanel: HTMLElement | null = null; + let editorContent: HTMLElement | null = null; + let editorSaveButton: HTMLButtonElement | null = null; + let currentEditor: CollectionItemEditor | null = null; + let isSaving = false; const body = ` -
-
-
- - +
+
+
+
+ + +
+
- -
-
-
+
+
+ +
`; + const closeEditor = () => { + editorRequestVersion++; + currentEditor = null; + isSaving = false; + if (editorSaveButton) editorSaveButton.disabled = true; + collectionSlider?.classList.remove('is-editing-item'); + browsePanel?.setAttribute('aria-hidden', 'false'); + editorPanel?.setAttribute('aria-hidden', 'true'); + if (browsePanel) browsePanel.inert = false; + if (editorPanel) editorPanel.inert = true; + }; + + const openEditor = async (id: string) => { + if (!collectionSlider || !editorContent || !editorSaveButton) return; + const version = ++editorRequestVersion; + currentEditor = null; + editorSaveButton.disabled = true; + editorContent.innerHTML = `
${i18n.t('collection.item.edit.loading', 'Loading collection item...')}
`; + collectionSlider.classList.add('is-editing-item'); + browsePanel?.setAttribute('aria-hidden', 'true'); + editorPanel?.setAttribute('aria-hidden', 'false'); + if (browsePanel) browsePanel.inert = true; + if (editorPanel) editorPanel.inert = false; + + try { + const editor = await createCollectionItemEditor({ + collection: options.collection, + id, + onSaved: async () => { + await update(); + closeEditor(); + } + }); + if (version !== editorRequestVersion) return; + currentEditor = editor; + editorContent.innerHTML = ''; + editor.form.init(editorContent); + editorSaveButton.disabled = false; + } catch (error: any) { + if (version !== editorRequestVersion) return; + showToast({ + title: i18n.t('collection.item.edit.loadError.title', 'Collection item could not be loaded'), + message: error?.message ?? String(error), + type: 'error', + timeout: 3000 + }); + closeEditor(); + } + }; + + const saveEditor = async () => { + if (!currentEditor || !editorSaveButton || isSaving || !currentEditor.form.validate()) return; + isSaving = true; + editorSaveButton.disabled = true; + const saved = await currentEditor.save(); + isSaving = false; + if (!saved && currentEditor) editorSaveButton.disabled = false; + }; + const update = async () => { const version = ++requestVersion; - const root = document.querySelector('[data-collection-results]') as HTMLElement | null; - const pagination = document.querySelector('[data-collection-pagination]') as HTMLElement | null; + const root = modalElement?.querySelector('[data-collection-results]') as HTMLElement | null; + const pagination = modalElement?.querySelector('[data-collection-pagination]') as HTMLElement | null; if (!root || !pagination) return; root.innerHTML = `
${i18n.t('collection.items.loading', 'Loading collection items...')}
`; try { @@ -125,11 +213,7 @@ export const runAction = async (options: { collection: string }) => { ` : ''; root.querySelectorAll('[data-collection-edit]').forEach(button => { - button.addEventListener('click', () => openCollectionItemEditor({ - collection: options.collection, - id: button.dataset.collectionEdit ?? '', - onSaved: update - })); + button.addEventListener('click', () => openEditor(button.dataset.collectionEdit ?? '')); }); root.querySelectorAll('[data-collection-open]').forEach(button => { button.addEventListener('click', () => { @@ -180,7 +264,16 @@ export const runAction = async (options: { collection: string }) => { size: 'xl', showFooter: false, onShow: (element: HTMLElement) => { + modalElement = element; + collectionSlider = element.querySelector('[data-collection-slider]'); + browsePanel = element.querySelector('[data-collection-browse-panel]'); + editorPanel = element.querySelector('[data-collection-editor-panel]'); + editorContent = element.querySelector('[data-collection-editor-content]'); + editorSaveButton = element.querySelector('[data-collection-editor-save]'); const input = element.querySelector('#cms-collection-search'); + element.querySelectorAll('[data-collection-editor-back], [data-collection-editor-cancel]') + .forEach(button => button.addEventListener('click', closeEditor)); + editorSaveButton?.addEventListener('click', saveEditor); element.querySelector('[data-collection-create]')?.addEventListener('click', () => { openCollectionItemCreator({ collection: options.collection, diff --git a/modules/ui-module/src/test/java/com/condation/cms/modules/ui/extensionpoints/remotemethods/RemoteWorkflowEndpointsExtensionTest.java b/modules/ui-module/src/test/java/com/condation/cms/modules/ui/extensionpoints/remotemethods/RemoteWorkflowEndpointsExtensionTest.java index 1b43fa4ca..9359dc4bb 100644 --- a/modules/ui-module/src/test/java/com/condation/cms/modules/ui/extensionpoints/remotemethods/RemoteWorkflowEndpointsExtensionTest.java +++ b/modules/ui-module/src/test/java/com/condation/cms/modules/ui/extensionpoints/remotemethods/RemoteWorkflowEndpointsExtensionTest.java @@ -113,8 +113,8 @@ void nodeStatus_returnsResultWithoutStatus_whenContentNodeNotFound() throws RPCE @SuppressWarnings("unchecked") Map result = (Map) endpoints.nodeStatus(params); - assertThat(result).doesNotContainKey("status"); - assertThat(result).doesNotContainKey("error"); + assertThat(result).doesNotContainKey("status") + .doesNotContainKey("error"); } @Test @@ -124,8 +124,8 @@ void getTransitions_returnsEmptyTransitions_whenContentNodeNotFound() throws RPC @SuppressWarnings("unchecked") Map result = (Map) endpoints.getTransitions(params); - assertThat(result).containsEntry("transitions", java.util.List.of()); - assertThat(result).doesNotContainKey("error"); + assertThat(result).containsEntry("transitions", java.util.List.of()) + .doesNotContainKey("error"); } @Test @@ -161,8 +161,8 @@ void nodeStatus_usesCurrentCollectionItemOnDetailPage() throws Exception { RequestContextScope.REQUEST_CONTEXT, requestContext).call(() -> endpoints.nodeStatus(Map.of())); - assertThat(result).containsEntry("status", status); - assertThat(result).containsEntry("transitions", List.of()); + assertThat(result).containsEntry("status", status) + .containsEntry("transitions", List.of()); } @Test diff --git a/modules/ui-module/src/test/java/com/condation/cms/modules/ui/utils/ActionFactoryAppsTest.java b/modules/ui-module/src/test/java/com/condation/cms/modules/ui/utils/ActionFactoryAppsTest.java index 78dbac37a..4bde5e75e 100644 --- a/modules/ui-module/src/test/java/com/condation/cms/modules/ui/utils/ActionFactoryAppsTest.java +++ b/modules/ui-module/src/test/java/com/condation/cms/modules/ui/utils/ActionFactoryAppsTest.java @@ -41,20 +41,23 @@ import java.util.Set; import org.assertj.core.api.Assertions; import org.junit.jupiter.api.Test; -import org.mockito.Mockito; +import static org.mockito.Mockito.when; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.eq; +import static org.mockito.Mockito.any; class ActionFactoryAppsTest { @Test void createsAuthorizedAppsWithContextAwareIconAndScriptAction() { - SiteProperties siteProperties = Mockito.mock(SiteProperties.class); - Mockito.when(siteProperties.contextPath()).thenReturn("/de"); - SiteModuleContext context = Mockito.mock(SiteModuleContext.class); - Mockito.when(context.get(SitePropertiesFeature.class)) + SiteProperties siteProperties = mock(SiteProperties.class); + when(siteProperties.contextPath()).thenReturn("/de"); + SiteModuleContext context = mock(SiteModuleContext.class); + when(context.get(SitePropertiesFeature.class)) .thenReturn(new SitePropertiesFeature(siteProperties)); - AppExtensionPoint extension = Mockito.mock(AppExtensionPoint.class); - Mockito.when(extension.getApps()).thenReturn(List.of( + AppExtensionPoint extension = mock(AppExtensionPoint.class); + when(extension.getApps()).thenReturn(List.of( new App( "menu-manager", "Menu Manager", @@ -67,8 +70,8 @@ void createsAuthorizedAppsWithContextAwareIconAndScriptAction() { "/manager/public/apps/admin.svg", new UIScriptAction("/manager/actions/admin", Map.of()), List.of(Permissions.CACHE_INVALIDATE)))); - ModuleManager moduleManager = Mockito.mock(ModuleManager.class); - Mockito.when(moduleManager.extensions(AppExtensionPoint.class)) + ModuleManager moduleManager = mock(ModuleManager.class); + when(moduleManager.extensions(AppExtensionPoint.class)) .thenReturn(List.of(extension)); ActionFactory factory = new ActionFactory( @@ -90,19 +93,19 @@ void createsAuthorizedAppsWithContextAwareIconAndScriptAction() { @Test void addsCollectionsToCreateContentMenu() { - SiteProperties siteProperties = Mockito.mock(SiteProperties.class); - Mockito.when(siteProperties.contextPath()).thenReturn("/de"); - SiteModuleContext context = Mockito.mock(SiteModuleContext.class); - Mockito.when(context.get(SitePropertiesFeature.class)) + SiteProperties siteProperties = mock(SiteProperties.class); + when(siteProperties.contextPath()).thenReturn("/de"); + SiteModuleContext context = mock(SiteModuleContext.class); + when(context.get(SitePropertiesFeature.class)) .thenReturn(new SitePropertiesFeature(siteProperties)); - DB db = Mockito.mock(DB.class); - Collections collections = Mockito.mock(Collections.class); - Mockito.when(context.get(DBFeature.class)).thenReturn(new DBFeature(db)); - Mockito.when(db.getCollections()).thenReturn(collections); - Mockito.when(collections.names()).thenReturn(Set.of("blog")); + DB db = mock(DB.class); + Collections collections = mock(Collections.class); + when(context.get(DBFeature.class)).thenReturn(new DBFeature(db)); + when(db.getCollections()).thenReturn(collections); + when(collections.names()).thenReturn(Set.of("blog")); - HookSystem hookSystem = Mockito.mock(HookSystem.class); - Mockito.when(hookSystem.doFilter(Mockito.eq(UIHooks.HOOK_REGISTER_CONTENT_TYPES), Mockito.any(ContentTypes.class))) + HookSystem hookSystem = mock(HookSystem.class); + when(hookSystem.doFilter(eq(UIHooks.HOOK_REGISTER_CONTENT_TYPES), any(ContentTypes.class))) .thenAnswer(invocation -> { ContentTypes contentTypes = invocation.getArgument(1); contentTypes.registerCollection(new CollectionType("blog", "Blog posts", Map.of())); @@ -112,7 +115,7 @@ void addsCollectionsToCreateContentMenu() { context, siteProperties, hookSystem, - Mockito.mock(ModuleManager.class), + mock(ModuleManager.class), new User("editor", "hash", new String[]{"editor"})); Assertions.assertThat(factory.createContentTypeMenu().getMenuEntry("collection-blog")) diff --git a/test-server/hosts/demo/collections/blog/item_3.md b/test-server/hosts/demo/collections/blog/item_3.md index 2f2ea46c6..5b028790e 100644 --- a/test-server/hosts/demo/collections/blog/item_3.md +++ b/test-server/hosts/demo/collections/blog/item_3.md @@ -1,5 +1,5 @@ --- -description: This is the third item +description: This is the third item of 3 title: Blog item 3 publish_date: 2026-04-09T00:00:00Z status: draft From 583d7f44e077f29028bf6e31a8dd402b2bcfac31 Mon Sep 17 00:00:00 2001 From: Thorsten Marx Date: Fri, 28 Aug 2026 20:01:58 +0200 Subject: [PATCH 09/28] sonar --- .../ui/extensionpoints/MenuManagerUiActionExtension.java | 1 + .../cms/modules/ui/extensionpoints/PageMenuExtension.java | 5 ++++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/modules/ui-module/src/main/java/com/condation/cms/modules/ui/extensionpoints/MenuManagerUiActionExtension.java b/modules/ui-module/src/main/java/com/condation/cms/modules/ui/extensionpoints/MenuManagerUiActionExtension.java index bfd14c91e..fb934b72b 100644 --- a/modules/ui-module/src/main/java/com/condation/cms/modules/ui/extensionpoints/MenuManagerUiActionExtension.java +++ b/modules/ui-module/src/main/java/com/condation/cms/modules/ui/extensionpoints/MenuManagerUiActionExtension.java @@ -48,5 +48,6 @@ public class MenuManagerUiActionExtension extends AbstractExtensionPoint impleme scriptAction = @ScriptAction(module = "/manager/actions/menu/manage-menus") ) public void manage_menus() { + // can be empty } } diff --git a/modules/ui-module/src/main/java/com/condation/cms/modules/ui/extensionpoints/PageMenuExtension.java b/modules/ui-module/src/main/java/com/condation/cms/modules/ui/extensionpoints/PageMenuExtension.java index 2f3b59062..89b4766bf 100644 --- a/modules/ui-module/src/main/java/com/condation/cms/modules/ui/extensionpoints/PageMenuExtension.java +++ b/modules/ui-module/src/main/java/com/condation/cms/modules/ui/extensionpoints/PageMenuExtension.java @@ -67,6 +67,7 @@ public class PageMenuExtension extends HookSystemRegisterExtensionPoint implemen scriptAction = @com.condation.cms.api.ui.annotations.ScriptAction(module = "/manager/actions/page/create-page") ) public void create_page() { + // can be empty } /* @@ -85,7 +86,9 @@ public void create_page() { section = "Page", scriptAction = @com.condation.cms.api.ui.annotations.ScriptAction(module = "/manager/actions/page/edit-page-settings") ) - public void page_settings() {} + public void page_settings() { + // can be empty + } /* @com.condation.cms.api.ui.annotations.MenuEntry( parent = "pageMenu", From d9bf6c229571664a73d9c63acf8adbba58252549 Mon Sep 17 00:00:00 2001 From: Thorsten Marx Date: Sat, 29 Aug 2026 17:07:04 +0200 Subject: [PATCH 10/28] shared collections --- .../configs/CollectionDefinition.java | 17 ++- .../cms/api/db/collection/Collections.java | 5 + .../cms/content/CollectionResolver.java | 23 +++- .../cms/content/CollectionResolverTest.java | 50 +++++++++ .../configs/CollectionConfiguration.java | 17 ++- .../CollectionConfigurationTest.java | 4 +- .../com/condation/cms/filesystem/FileDB.java | 16 ++- .../cms/filesystem/ReferencedCollections.java | 101 ++++++++++++++++++ .../filesystem/ReferencedCollectionsTest.java | 97 +++++++++++++++++ .../CollectionMenuExtension.java | 5 +- .../UiTemplateModelExtension.java | 11 +- .../RemoteCollectionEndpoints.java | 9 ++ .../RemoteContentEndpointsExtension.java | 3 + .../RemoteWorkflowEndpointsExtension.java | 3 + .../cms/modules/ui/utils/ActionFactory.java | 1 + .../manager/js/manager-inject-init.js | 4 +- .../js/modules/manager/toolbar.inject.js | 20 ++-- .../src/main/ts/src/js/manager-inject-init.js | 4 +- .../src/js/modules/manager/toolbar.inject.ts | 20 ++-- .../UiTemplateModelExtensionTest.java | 62 +++++++++++ .../RemoteCollectionEndpointsTest.java | 16 ++- .../RemoteContentEndpointsExtensionTest.java | 29 ++++- .../RemoteWorkflowEndpointsExtensionTest.java | 25 ++++- .../ui/utils/ActionFactoryAppsTest.java | 4 +- .../hosts/demo_de/config/collections.yaml | 6 ++ .../hosts/demo_de/config/menus/main.yaml | 35 ++++++ .../demo_de/content/collections/index.md | 6 ++ .../demo/templates/collections-authors.html | 34 ++++++ 28 files changed, 583 insertions(+), 44 deletions(-) create mode 100644 cms-filesystem/src/main/java/com/condation/cms/filesystem/ReferencedCollections.java create mode 100644 cms-filesystem/src/test/java/com/condation/cms/filesystem/ReferencedCollectionsTest.java create mode 100644 modules/ui-module/src/test/java/com/condation/cms/modules/ui/extensionpoints/UiTemplateModelExtensionTest.java create mode 100644 test-server/hosts/demo_de/config/collections.yaml create mode 100644 test-server/hosts/demo_de/config/menus/main.yaml create mode 100644 test-server/hosts/demo_de/content/collections/index.md create mode 100644 test-server/themes/demo/templates/collections-authors.html diff --git a/cms-api/src/main/java/com/condation/cms/api/configuration/configs/CollectionDefinition.java b/cms-api/src/main/java/com/condation/cms/api/configuration/configs/CollectionDefinition.java index bc1a51564..75494ac39 100644 --- a/cms-api/src/main/java/com/condation/cms/api/configuration/configs/CollectionDefinition.java +++ b/cms-api/src/main/java/com/condation/cms/api/configuration/configs/CollectionDefinition.java @@ -28,7 +28,7 @@ /** * Configuration of one named collection. */ -public record CollectionDefinition(String name, CollectionDetailConfiguration detail) { +public record CollectionDefinition(String name, String site, CollectionDetailConfiguration detail) { private static final Pattern VALID_NAME_PATTERN = Pattern.compile("[a-zA-Z0-9][a-zA-Z0-9_-]*"); @@ -37,9 +37,24 @@ public record CollectionDefinition(String name, CollectionDetailConfiguration de if (!VALID_NAME_PATTERN.matcher(name).matches()) { throw new IllegalArgumentException("invalid collection name: " + name); } + if (site != null) { + site = site.trim(); + if (site.isEmpty()) { + throw new IllegalArgumentException("collection site must not be blank"); + } + } + } + + public CollectionDefinition(String name, CollectionDetailConfiguration detail) { + this(name, null, detail); } public Optional detailPage() { return Optional.ofNullable(detail); } + + /** Site whose collection data should be used, or empty for a local collection. */ + public Optional sourceSite() { + return Optional.ofNullable(site); + } } diff --git a/cms-api/src/main/java/com/condation/cms/api/db/collection/Collections.java b/cms-api/src/main/java/com/condation/cms/api/db/collection/Collections.java index cab09c508..ccfa0b753 100644 --- a/cms-api/src/main/java/com/condation/cms/api/db/collection/Collections.java +++ b/cms-api/src/main/java/com/condation/cms/api/db/collection/Collections.java @@ -32,6 +32,11 @@ public interface Collections { Set names(); + /** Returns whether a collection is stored by this site and may be modified here. */ + default boolean isLocal(String collection) { + return true; + } + /** Re-indexes one item after a synchronous manager write, or removes a deleted item from the index. */ void refresh(String collection, String id); } diff --git a/cms-content/src/main/java/com/condation/cms/content/CollectionResolver.java b/cms-content/src/main/java/com/condation/cms/content/CollectionResolver.java index a2c9e3125..1cb0e3a61 100644 --- a/cms-content/src/main/java/com/condation/cms/content/CollectionResolver.java +++ b/cms-content/src/main/java/com/condation/cms/content/CollectionResolver.java @@ -24,6 +24,8 @@ import com.condation.cms.api.Constants; import com.condation.cms.api.configuration.Configuration; import com.condation.cms.api.configuration.configs.CollectionConfiguration; +import com.condation.cms.api.configuration.configs.CollectionDefinition; +import com.condation.cms.api.configuration.configs.SiteConfiguration; import com.condation.cms.api.content.ContentResponse; import com.condation.cms.api.content.DefaultContentResponse; import com.condation.cms.api.db.ContentNode; @@ -32,6 +34,8 @@ import com.condation.cms.api.feature.features.CurrentNodeFeature; import com.condation.cms.api.feature.features.RequestFeature; import com.condation.cms.api.request.RequestContext; +import com.condation.cms.core.serivce.ServiceRegistry; +import com.condation.cms.core.serivce.impl.SiteDBService; import java.io.IOException; import java.util.HashMap; import java.util.Optional; @@ -76,7 +80,8 @@ private Optional render( CurrentCollectionItemFeature.class, new CurrentCollectionItemFeature(collectionItem)); - var collectionFile = db.getFileSystem().collectionsBase().resolve(collectionItem.path()); + var sourceDB = sourceDB(route.definition()); + var collectionFile = sourceDB.getFileSystem().collectionsBase().resolve(collectionItem.path()); if (!collectionFile.exists()) { return Optional.empty(); } @@ -88,4 +93,20 @@ private Optional render( context); return Optional.of(new DefaultContentResponse(content, Constants.DEFAULT_CONTENT_TYPE, node)); } + + private DB sourceDB(CollectionDefinition definition) { + var sourceSite = definition.sourceSite(); + if (sourceSite.isEmpty()) { + return db; + } + var siteConfiguration = configuration.get(SiteConfiguration.class); + if (siteConfiguration != null + && siteConfiguration.siteProperties().id().equals(sourceSite.get())) { + return db; + } + return ServiceRegistry.getInstance().get(sourceSite.get(), SiteDBService.class) + .orElseThrow(() -> new IllegalStateException( + "collection source site is not available: " + sourceSite.get())) + .db(); + } } diff --git a/cms-content/src/test/java/com/condation/cms/content/CollectionResolverTest.java b/cms-content/src/test/java/com/condation/cms/content/CollectionResolverTest.java index 2687eacae..4e7a851d7 100644 --- a/cms-content/src/test/java/com/condation/cms/content/CollectionResolverTest.java +++ b/cms-content/src/test/java/com/condation/cms/content/CollectionResolverTest.java @@ -44,12 +44,15 @@ import com.condation.cms.api.feature.features.CurrentNodeFeature; import com.condation.cms.api.feature.features.RequestFeature; import com.condation.cms.api.request.RequestContext; +import com.condation.cms.core.serivce.ServiceRegistry; +import com.condation.cms.core.serivce.impl.SiteDBService; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.concurrent.ConcurrentHashMap; import org.assertj.core.api.Assertions; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; @@ -89,6 +92,11 @@ void setUp() throws Exception { any())).thenReturn("

First

"); } + @AfterEach + void clearServices() { + ServiceRegistry.getInstance().clear(); + } + @Test void resolvesAnIdRouteAndUsesReloadedDefinitions() throws Exception { definitions.put("blog", definition("/old/{id}")); @@ -134,6 +142,48 @@ void resolvesAConfiguredFrontMatterField() throws Exception { verify(query).where("slug", "first-post"); } + @Test + void readsTheItemFileFromTheConfiguredSourceSite() throws Exception { + definitions.put( + "blog", + new CollectionDefinition( + "blog", + "content-site", + new CollectionDetailConfiguration( + "/shared/{id}", + "collections/detail.html"))); + when(collection.item("first")).thenReturn(Optional.of(item)); + var sourceDB = mock(DB.class); + var sourceFileSystem = mock(DBFileSystem.class); + var sourceCollectionsBase = mock(ReadOnlyFile.class); + var sourceItemFile = mock(ReadOnlyFile.class); + when(sourceDB.getFileSystem()).thenReturn(sourceFileSystem); + when(sourceFileSystem.collectionsBase()).thenReturn(sourceCollectionsBase); + when(sourceCollectionsBase.resolve("blog/first.md")).thenReturn(sourceItemFile); + when(sourceItemFile.exists()).thenReturn(true); + when(renderer.renderCollection( + eq(sourceItemFile), + any(), + eq(item), + anyString(), + any())).thenReturn("

Shared

"); + ServiceRegistry.getInstance().register( + "content-site", + SiteDBService.class, + new SiteDBService(sourceDB)); + + var response = new CollectionResolver(renderer, db, configuration) + .getContent(context("/shared/first")); + + Assertions.assertThat(response).isPresent(); + verify(renderer).renderCollection( + eq(sourceItemFile), + any(), + eq(item), + eq("collections/detail.html"), + any()); + } + private static CollectionDefinition definition(String route) { return new CollectionDefinition( "blog", diff --git a/cms-core/src/main/java/com/condation/cms/core/configuration/configs/CollectionConfiguration.java b/cms-core/src/main/java/com/condation/cms/core/configuration/configs/CollectionConfiguration.java index c36e07223..1d888a885 100644 --- a/cms-core/src/main/java/com/condation/cms/core/configuration/configs/CollectionConfiguration.java +++ b/cms-core/src/main/java/com/condation/cms/core/configuration/configs/CollectionConfiguration.java @@ -104,18 +104,20 @@ private java.util.Optional parse(String name, Object value throw new IllegalArgumentException("collection definition must be a map"); } + var site = optionalStringValue(collection.get("site"), "site"); var detailValue = collection.get("detail"); if (detailValue == null) { - return java.util.Optional.of(new CollectionDefinition(name, null)); + return java.util.Optional.of(new CollectionDefinition(name, site, null)); } if (!(detailValue instanceof Map detail)) { throw new IllegalArgumentException("collection detail definition must be a map"); } - var route = stringValue(detail.get("route"), "route"); - var template = stringValue(detail.get("template"), "template"); + var route = stringValue(detail.get("route"), "collection detail route"); + var template = stringValue(detail.get("template"), "collection detail template"); return java.util.Optional.of(new CollectionDefinition( name, + site, new CollectionDetailConfiguration(route, template))); } catch (RuntimeException ex) { log.error("invalid configuration for collection {}", name, ex); @@ -125,11 +127,18 @@ private java.util.Optional parse(String name, Object value private static String stringValue(Object value, String field) { if (!(value instanceof String string) || string.isBlank()) { - throw new IllegalArgumentException("collection detail " + field + " must be a non-empty string"); + throw new IllegalArgumentException(field + " must be a non-empty string"); } return string; } + private static String optionalStringValue(Object value, String field) { + if (value == null) { + return null; + } + return stringValue(value, "collection " + field); + } + public static class Builder { private final List sources = new ArrayList<>(); diff --git a/cms-core/src/test/java/com/condation/cms/core/configuration/CollectionConfigurationTest.java b/cms-core/src/test/java/com/condation/cms/core/configuration/CollectionConfigurationTest.java index c4f574a16..7b4fa3875 100644 --- a/cms-core/src/test/java/com/condation/cms/core/configuration/CollectionConfigurationTest.java +++ b/cms-core/src/test/java/com/condation/cms/core/configuration/CollectionConfigurationTest.java @@ -40,7 +40,7 @@ void updatesTheSharedConfigurationOnReload() { var source = mock(ConfigSource.class); var initial = Map.of( "blog", - Map.of("detail", Map.of( + Map.of("site", "content-site", "detail", Map.of( "route", "/blog/{slug}", "template", "collections/blog.html")), "listing-only", @@ -64,6 +64,8 @@ void updatesTheSharedConfigurationOnReload() { Assertions.assertThat(sharedCollections).containsOnlyKeys("blog", "listing-only"); Assertions.assertThat(sharedCollections.get("blog").detailPage().orElseThrow().parameter()) .isEqualTo("slug"); + Assertions.assertThat(sharedCollections.get("blog").sourceSite()).contains("content-site"); + Assertions.assertThat(sharedCollections.get("listing-only").sourceSite()).isEmpty(); configuration.reload(); diff --git a/cms-filesystem/src/main/java/com/condation/cms/filesystem/FileDB.java b/cms-filesystem/src/main/java/com/condation/cms/filesystem/FileDB.java index 92ce07249..d30efc792 100644 --- a/cms-filesystem/src/main/java/com/condation/cms/filesystem/FileDB.java +++ b/cms-filesystem/src/main/java/com/condation/cms/filesystem/FileDB.java @@ -52,7 +52,8 @@ public class FileDB implements DB { private FileSystem fileSystem; private FileContent content; - private FileCollections collections; + private FileCollections localCollections; + private Collections collections; private ReadOnlyFileSystem readOnlyFileSystem; private FileTaxonomies taxonomies; @@ -70,8 +71,13 @@ public void init () throws IOException { readOnlyFileSystem = new WrappedReadOnlyFileSystem(fileSystem); content = new FileContent(fileSystem); - collections = new FileCollections(siteProperties.id(), hostBaseDirectory, contentParser); - collections.init(); + localCollections = new FileCollections(siteProperties.id(), hostBaseDirectory, contentParser); + localCollections.init(); + var collectionConfiguration = configuration.get( + com.condation.cms.api.configuration.configs.CollectionConfiguration.class); + collections = collectionConfiguration == null + ? localCollections + : new ReferencedCollections(siteProperties.id(), localCollections, collectionConfiguration); taxonomies = new FileTaxonomies(configuration, content); } @@ -102,8 +108,8 @@ public DBFileSystem getFileSystem() { @Override public void close() throws Exception { try { - if (collections != null) { - collections.close(); + if (localCollections != null) { + localCollections.close(); } } finally { fileSystem.shutdown(); diff --git a/cms-filesystem/src/main/java/com/condation/cms/filesystem/ReferencedCollections.java b/cms-filesystem/src/main/java/com/condation/cms/filesystem/ReferencedCollections.java new file mode 100644 index 000000000..a3ab2127f --- /dev/null +++ b/cms-filesystem/src/main/java/com/condation/cms/filesystem/ReferencedCollections.java @@ -0,0 +1,101 @@ +package com.condation.cms.filesystem; + +/*- + * #%L + * CMS FileSystem + * %% + * Copyright (C) 2023 - 2026 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * #L% + */ + +import com.condation.cms.api.configuration.configs.CollectionConfiguration; +import com.condation.cms.api.db.collection.Collection; +import com.condation.cms.api.db.collection.Collections; +import com.condation.cms.core.serivce.ServiceRegistry; +import com.condation.cms.core.serivce.impl.SiteDBService; +import java.util.HashSet; +import java.util.Set; + +/** + * Adds lazy, read-only collection references to the collections stored by one + * site. Source sites are looked up for every access so configuration and site + * reloads do not leave cached cross-site references behind. + */ +final class ReferencedCollections implements Collections { + + private final String siteId; + private final Collections localCollections; + private final CollectionConfiguration configuration; + + ReferencedCollections( + String siteId, + Collections localCollections, + CollectionConfiguration configuration) { + this.siteId = siteId; + this.localCollections = localCollections; + this.configuration = configuration; + } + + @Override + public Collection collection(String name) { + var sourceSite = sourceSite(name); + if (sourceSite == null) { + return localCollections.collection(name); + } + var source = ServiceRegistry.getInstance().get(sourceSite, SiteDBService.class) + .orElseThrow(() -> new IllegalStateException( + "collection source site is not available: " + sourceSite)); + var sourceCollections = source.db().getCollections(); + if (!sourceCollections.isLocal(name)) { + throw new IllegalStateException( + "referenced collections must point to a local collection: " + sourceSite + "/" + name); + } + return sourceCollections.collection(name); + } + + @Override + public Set names() { + var names = new HashSet<>(localCollections.names()); + configuration.collections().values().stream() + .filter(definition -> definition.sourceSite() + .filter(sourceSite -> !siteId.equals(sourceSite)) + .isPresent()) + .map(definition -> definition.name()) + .forEach(names::add); + return Set.copyOf(names); + } + + @Override + public boolean isLocal(String collection) { + return sourceSite(collection) == null; + } + + @Override + public void refresh(String collection, String id) { + if (!isLocal(collection)) { + throw new UnsupportedOperationException( + "referenced collection is read-only: " + collection); + } + localCollections.refresh(collection, id); + } + + private String sourceSite(String collection) { + return configuration.collection(collection) + .flatMap(definition -> definition.sourceSite()) + .filter(source -> !siteId.equals(source)) + .orElse(null); + } +} diff --git a/cms-filesystem/src/test/java/com/condation/cms/filesystem/ReferencedCollectionsTest.java b/cms-filesystem/src/test/java/com/condation/cms/filesystem/ReferencedCollectionsTest.java new file mode 100644 index 000000000..03332fb62 --- /dev/null +++ b/cms-filesystem/src/test/java/com/condation/cms/filesystem/ReferencedCollectionsTest.java @@ -0,0 +1,97 @@ +package com.condation.cms.filesystem; + +/*- + * #%L + * CMS FileSystem + * %% + * Copyright (C) 2023 - 2026 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * #L% + */ + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.condation.cms.api.configuration.configs.CollectionConfiguration; +import com.condation.cms.api.configuration.configs.CollectionDefinition; +import com.condation.cms.api.db.DB; +import com.condation.cms.api.db.collection.Collection; +import com.condation.cms.api.db.collection.Collections; +import com.condation.cms.core.serivce.ServiceRegistry; +import com.condation.cms.core.serivce.impl.SiteDBService; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; + +class ReferencedCollectionsTest { + + @AfterEach + void clearServices() { + ServiceRegistry.getInstance().clear(); + } + + @Test + void resolvesReferencedCollectionsThroughTheSourceSite() { + var local = mock(Collections.class); + when(local.names()).thenReturn(Set.of("local")); + var sourceCollections = mock(Collections.class); + var sourceCollection = mock(Collection.class); + when(sourceCollections.isLocal("shared")).thenReturn(true); + when(sourceCollections.collection("shared")).thenReturn(sourceCollection); + var sourceDB = mock(DB.class); + when(sourceDB.getCollections()).thenReturn(sourceCollections); + ServiceRegistry.getInstance().register( + "content-site", + SiteDBService.class, + new SiteDBService(sourceDB)); + var definitions = new ConcurrentHashMap(); + definitions.put("shared", new CollectionDefinition("shared", "content-site", null)); + var collections = new ReferencedCollections( + "consumer-site", + local, + new CollectionConfiguration(definitions)); + + Assertions.assertThat(collections.names()).containsExactlyInAnyOrder("local", "shared"); + Assertions.assertThat(collections.isLocal("shared")).isFalse(); + Assertions.assertThat(collections.collection("shared")).isSameAs(sourceCollection); + Assertions.assertThatThrownBy(() -> collections.refresh("shared", "item")) + .isInstanceOf(UnsupportedOperationException.class) + .hasMessageContaining("read-only"); + } + + @Test + void usesReloadedConfigurationWithoutRecreatingTheCollectionsFacade() { + var local = mock(Collections.class); + var localCollection = mock(Collection.class); + when(local.names()).thenReturn(Set.of("shared")); + when(local.collection("shared")).thenReturn(localCollection); + var definitions = new ConcurrentHashMap(); + definitions.put("shared", new CollectionDefinition("shared", "content-site", null)); + var collections = new ReferencedCollections( + "consumer-site", + local, + new CollectionConfiguration(definitions)); + + definitions.put("shared", new CollectionDefinition("shared", null)); + + Assertions.assertThat(collections.isLocal("shared")).isTrue(); + Assertions.assertThat(collections.collection("shared")).isSameAs(localCollection); + collections.refresh("shared", "item"); + verify(local).refresh("shared", "item"); + } +} diff --git a/modules/ui-module/src/main/java/com/condation/cms/modules/ui/extensionpoints/CollectionMenuExtension.java b/modules/ui-module/src/main/java/com/condation/cms/modules/ui/extensionpoints/CollectionMenuExtension.java index 5cd92e31b..47cce840f 100644 --- a/modules/ui-module/src/main/java/com/condation/cms/modules/ui/extensionpoints/CollectionMenuExtension.java +++ b/modules/ui-module/src/main/java/com/condation/cms/modules/ui/extensionpoints/CollectionMenuExtension.java @@ -44,7 +44,10 @@ public class CollectionMenuExtension extends AbstractExtensionPoint implements U @Override public void addMenuItems(Menu menu) { var db = getContext().get(DBFeature.class).db(); - var names = db.getCollections().names().stream().sorted().toList(); + var names = db.getCollections().names().stream() + .filter(db.getCollections()::isLocal) + .sorted() + .toList(); if (names.isEmpty()) { return; } diff --git a/modules/ui-module/src/main/java/com/condation/cms/modules/ui/extensionpoints/UiTemplateModelExtension.java b/modules/ui-module/src/main/java/com/condation/cms/modules/ui/extensionpoints/UiTemplateModelExtension.java index b63beaed4..b2efcb10a 100644 --- a/modules/ui-module/src/main/java/com/condation/cms/modules/ui/extensionpoints/UiTemplateModelExtension.java +++ b/modules/ui-module/src/main/java/com/condation/cms/modules/ui/extensionpoints/UiTemplateModelExtension.java @@ -22,10 +22,12 @@ */ import com.condation.cms.api.extensions.TemplateModelExtendingExtensionPoint; +import com.condation.cms.api.feature.features.DBFeature; import com.condation.cms.api.feature.features.IsPreviewFeature; import com.condation.cms.api.request.RequestContext; import com.condation.cms.api.utils.JSONUtil; import com.condation.cms.api.db.collection.CollectionItem; +import com.condation.cms.api.module.SiteModuleContext; import com.condation.modules.api.annotation.Extension; import java.util.Collections; import java.util.HashMap; @@ -41,13 +43,14 @@ public class UiTemplateModelExtension extends TemplateModelExtendingExtensionPoi @Override public Map getModel() { - return Map.of("ui", new UIHelper(getRequestContext())); + return Map.of("ui", new UIHelper(getRequestContext(), getContext())); } @RequiredArgsConstructor public static class UIHelper { private final RequestContext requestContext; + private final SiteModuleContext siteContext; public String editMeta (String editor, String element) { return editMeta(editor, element, Collections.emptyMap()); @@ -110,9 +113,15 @@ public String collectionToolbar( CollectionItem item, String[] actions, Map additional) { + if (!requestContext.has(IsPreviewFeature.class)) { + return ""; + } if (item == null) { return ""; } + if (!siteContext.get(DBFeature.class).db().getCollections().isLocal(item.collection())) { + return ""; + } var options = new HashMap<>(additional); options.put("collection", item.collection()); options.put("itemId", item.id()); diff --git a/modules/ui-module/src/main/java/com/condation/cms/modules/ui/extensionpoints/remotemethods/RemoteCollectionEndpoints.java b/modules/ui-module/src/main/java/com/condation/cms/modules/ui/extensionpoints/remotemethods/RemoteCollectionEndpoints.java index 1d9e76e8e..96ffe328d 100644 --- a/modules/ui-module/src/main/java/com/condation/cms/modules/ui/extensionpoints/remotemethods/RemoteCollectionEndpoints.java +++ b/modules/ui-module/src/main/java/com/condation/cms/modules/ui/extensionpoints/remotemethods/RemoteCollectionEndpoints.java @@ -118,6 +118,7 @@ public Object get(Map parameters) throws RPCException { public Object save(Map parameters) throws RPCException { var db = getDB(parameters); var item = item(parameters); + ensureLocalCollection(db, item.collection()); var sourceFile = db.getFileSystem().collectionsBase().resolve(item.path()); var writableFile = db.getFileSystem().resolve(Constants.Folders.COLLECTIONS).resolve(item.path()); try { @@ -144,6 +145,7 @@ public Object create(Map parameters) throws RPCException { var collectionName = requiredString(parameters, Parameters.COLLECTION); var id = requiredItemId(parameters); ensureCollectionExists(db.getCollections().names(), collectionName); + ensureLocalCollection(db, collectionName); var writableFile = writableFile(db, collectionName, id); if (Files.exists(writableFile)) { throw new RPCException(409, "collection item already exists"); @@ -184,6 +186,7 @@ public Object delete(Map parameters) throws RPCException { var collectionName = requiredString(parameters, Parameters.COLLECTION); var id = requiredItemId(parameters); ensureCollectionExists(db.getCollections().names(), collectionName); + ensureLocalCollection(db, collectionName); var writableFile = writableFile(db, collectionName, id); if (!Files.isRegularFile(writableFile)) { throw new RPCException(404, "collection item not found"); @@ -248,6 +251,12 @@ private static void ensureCollectionExists(java.util.Set names, String n } } + private static void ensureLocalCollection(DB db, String name) throws RPCException { + if (!db.getCollections().isLocal(name)) { + throw new RPCException(403, "referenced collection is read-only: " + name); + } + } + private static String requiredString(Map parameters, String name) throws RPCException { var value = optionalString(parameters, name); if (value.isBlank()) { diff --git a/modules/ui-module/src/main/java/com/condation/cms/modules/ui/extensionpoints/remotemethods/RemoteContentEndpointsExtension.java b/modules/ui-module/src/main/java/com/condation/cms/modules/ui/extensionpoints/remotemethods/RemoteContentEndpointsExtension.java index f6124844b..16e1ccd39 100644 --- a/modules/ui-module/src/main/java/com/condation/cms/modules/ui/extensionpoints/remotemethods/RemoteContentEndpointsExtension.java +++ b/modules/ui-module/src/main/java/com/condation/cms/modules/ui/extensionpoints/remotemethods/RemoteContentEndpointsExtension.java @@ -436,6 +436,9 @@ private EditableTarget editableTarget(Map parameters, DB db) thr if (!parameters.containsKey("uri") && getRequestContext().has(CurrentCollectionItemFeature.class)) { var item = getRequestContext().get(CurrentCollectionItemFeature.class).item(); + if (!db.getCollections().isLocal(item.collection())) { + throw new RPCException(403, "referenced collection is read-only: " + item.collection()); + } return new EditableTarget( item.path(), db.getFileSystem().collectionsBase().resolve(item.path()), diff --git a/modules/ui-module/src/main/java/com/condation/cms/modules/ui/extensionpoints/remotemethods/RemoteWorkflowEndpointsExtension.java b/modules/ui-module/src/main/java/com/condation/cms/modules/ui/extensionpoints/remotemethods/RemoteWorkflowEndpointsExtension.java index bb7aa36a1..71155b161 100644 --- a/modules/ui-module/src/main/java/com/condation/cms/modules/ui/extensionpoints/remotemethods/RemoteWorkflowEndpointsExtension.java +++ b/modules/ui-module/src/main/java/com/condation/cms/modules/ui/extensionpoints/remotemethods/RemoteWorkflowEndpointsExtension.java @@ -111,6 +111,9 @@ private Optional getWorkflowTarget(Map parameter if (!parameters.containsKey("uri") && getRequestContext().has(CurrentCollectionItemFeature.class)) { var item = getRequestContext().get(CurrentCollectionItemFeature.class).item(); + if (!db.getCollections().isLocal(item.collection())) { + return Optional.empty(); + } var node = new ContentNode( item.path(), item.path(), diff --git a/modules/ui-module/src/main/java/com/condation/cms/modules/ui/utils/ActionFactory.java b/modules/ui-module/src/main/java/com/condation/cms/modules/ui/utils/ActionFactory.java index f417427cd..04ac1373f 100644 --- a/modules/ui-module/src/main/java/com/condation/cms/modules/ui/utils/ActionFactory.java +++ b/modules/ui-module/src/main/java/com/condation/cms/modules/ui/utils/ActionFactory.java @@ -139,6 +139,7 @@ public Menu createContentTypeMenu() { }).forEach(menu::addMenuEntry); context.get(DBFeature.class).db().getCollections().names().stream() + .filter(context.get(DBFeature.class).db().getCollections()::isLocal) .sorted() .map(name -> MenuEntry.builder() .id("collection-" + name) diff --git a/modules/ui-module/src/main/resources/manager/js/manager-inject-init.js b/modules/ui-module/src/main/resources/manager/js/manager-inject-init.js index 37b64ba09..ba32ddd8a 100644 --- a/modules/ui-module/src/main/resources/manager/js/manager-inject-init.js +++ b/modules/ui-module/src/main/resources/manager/js/manager-inject-init.js @@ -58,7 +58,7 @@ export function initIframe() { continue; } if (item.data.status === 'published') { - sectionContainer.setAttribute('data-cms-action', 'unpublish'); + sectionContainer.dataset.cmsAction = 'unpublish'; sectionContainer.setAttribute("title", "Unpublish"); if (isSectionPublishedExpired(item)) { sectionContainer.innerHTML = SECTION_UNPUBLISHED_ICON; @@ -75,7 +75,7 @@ export function initIframe() { } else { sectionContainer.innerHTML = SECTION_UNPUBLISHED_ICON; - sectionContainer.setAttribute('data-cms-action', 'publish'); + sectionContainer.dataset.cmsAction = 'publish'; sectionContainer.setAttribute("title", "Publish"); sectionContainer.classList.remove('cms-published'); sectionContainer.classList.remove('cms-published-expired'); diff --git a/modules/ui-module/src/main/resources/manager/js/modules/manager/toolbar.inject.js b/modules/ui-module/src/main/resources/manager/js/modules/manager/toolbar.inject.js index a209da6f1..9ae67f499 100644 --- a/modules/ui-module/src/main/resources/manager/js/modules/manager/toolbar.inject.js +++ b/modules/ui-module/src/main/resources/manager/js/modules/manager/toolbar.inject.js @@ -143,7 +143,7 @@ const initDragDrop = (container) => { const keepPlaceholderPosition = Symbol('keepPlaceholderPosition'); const createPlaceholder = (item) => { const nextPlaceholder = document.createElement('div'); - nextPlaceholder.setAttribute('data-cms-drag-placeholder', ''); + nextPlaceholder.dataset.cmsDragPlaceholder = 'true'; const cs = getComputedStyle(item); nextPlaceholder.style.width = item.offsetWidth + 'px'; nextPlaceholder.style.height = item.offsetHeight + 'px'; @@ -224,7 +224,7 @@ const initDragDrop = (container) => { if (itemToolbar && !itemToolbar.querySelector('[data-cms-drag-handle]')) { const handle = document.createElement('button'); handle.setAttribute('type', 'button'); - handle.setAttribute('data-cms-drag-handle', ''); + handle.dataset.cmsDragHandle = ''; handle.setAttribute('title', 'Drag to reorder'); handle.setAttribute('aria-label', 'Drag to reorder'); handle.innerHTML = MOVE_ICON; @@ -345,7 +345,7 @@ export const initToolbar = (container) => { toolbarDefinition.actions.forEach((action) => { if (action === "editContent") { const button = document.createElement('button'); - button.setAttribute('data-cms-action', 'edit'); + button.dataset.cmsAction = 'edit'; button.innerHTML = EDIT_PAGE_ICON; button.setAttribute("title", "Edit content"); button.addEventListener('click', editContent); @@ -353,7 +353,7 @@ export const initToolbar = (container) => { } else if (action === "editAttributes") { const button = document.createElement('button'); - button.setAttribute('data-cms-action', 'editAttributes'); + button.dataset.cmsAction = 'editAttributes'; button.innerHTML = EDIT_ATTRIBUTES_ICON; button.setAttribute("title", "Edit attributes"); button.addEventListener('click', editAttributes); @@ -361,7 +361,7 @@ export const initToolbar = (container) => { } else if (action === "editCollectionItem") { const button = document.createElement('button'); - button.setAttribute('data-cms-action', 'editCollectionItem'); + button.dataset.cmsAction = 'editCollectionItem'; button.innerHTML = EDIT_ATTRIBUTES_ICON; button.setAttribute('title', 'Edit collection item'); button.addEventListener('click', editCollectionItem); @@ -369,7 +369,7 @@ export const initToolbar = (container) => { } else if (action === "orderSectionEntries") { const button = document.createElement('button'); - button.setAttribute('data-cms-action', 'editSections'); + button.dataset.cmsAction = 'editSections'; button.innerHTML = SECTION_SORT_ICON; button.setAttribute("title", "Order"); button.addEventListener('click', orderSections); @@ -377,7 +377,7 @@ export const initToolbar = (container) => { } else if (action === "addSectionEntry") { const button = document.createElement('button'); - button.setAttribute('data-cms-action', 'addSection'); + button.dataset.cmsAction = 'addSection'; button.innerHTML = SECTION_ADD_ICON; button.setAttribute("title", "Add"); button.addEventListener('click', addSection); @@ -385,7 +385,7 @@ export const initToolbar = (container) => { } else if (action === "deleteSectionEntry") { const button = document.createElement('button'); - button.setAttribute('data-cms-action', 'deleteSection'); + button.dataset.cmsAction = 'deleteSection'; button.innerHTML = SECTION_DELETE_ICON; button.setAttribute("title", "Delete"); button.addEventListener('click', deleteSection); @@ -401,8 +401,8 @@ export const initToolbar = (container) => { }); if (toolbarDefinition.type === "sectionEntry") { const button = document.createElement('button'); - button.setAttribute('data-cms-action', 'publish'); - button.setAttribute('data-cms-section-uri', toolbarDefinition.uri); + button.dataset.cmsAction = 'publish'; + button.dataset.cmsSectionUri = toolbarDefinition.uri; button.classList.add('cms-unpublished'); button.innerHTML = SECTION_UNPUBLISHED_ICON; button.setAttribute("title", "Publish"); diff --git a/modules/ui-module/src/main/ts/src/js/manager-inject-init.js b/modules/ui-module/src/main/ts/src/js/manager-inject-init.js index 4594a141a..447d7c1b6 100644 --- a/modules/ui-module/src/main/ts/src/js/manager-inject-init.js +++ b/modules/ui-module/src/main/ts/src/js/manager-inject-init.js @@ -72,7 +72,7 @@ export function initIframe() { continue; } if (item.data.status === 'published') { - sectionContainer.setAttribute('data-cms-action', 'unpublish'); + sectionContainer.dataset.cmsAction = 'unpublish'; sectionContainer.setAttribute("title", "Unpublish"); if (isSectionPublishedExpired(item)) { @@ -91,7 +91,7 @@ export function initIframe() { } } else { sectionContainer.innerHTML = SECTION_UNPUBLISHED_ICON; - sectionContainer.setAttribute('data-cms-action', 'publish'); + sectionContainer.dataset.cmsAction = 'publish'; sectionContainer.setAttribute("title", "Publish"); sectionContainer.classList.remove('cms-published'); sectionContainer.classList.remove('cms-published-expired'); diff --git a/modules/ui-module/src/main/ts/src/js/modules/manager/toolbar.inject.ts b/modules/ui-module/src/main/ts/src/js/modules/manager/toolbar.inject.ts index 43e952f3f..1ce8e30b3 100644 --- a/modules/ui-module/src/main/ts/src/js/modules/manager/toolbar.inject.ts +++ b/modules/ui-module/src/main/ts/src/js/modules/manager/toolbar.inject.ts @@ -168,7 +168,7 @@ const initDragDrop = (container: HTMLElement) => { const createPlaceholder = (item: HTMLElement) => { const nextPlaceholder = document.createElement('div'); - nextPlaceholder.setAttribute('data-cms-drag-placeholder', ''); + nextPlaceholder.dataset.cmsDragPlaceholder = 'true'; const cs = getComputedStyle(item); nextPlaceholder.style.width = item.offsetWidth + 'px'; nextPlaceholder.style.height = item.offsetHeight + 'px'; @@ -262,7 +262,7 @@ const initDragDrop = (container: HTMLElement) => { if (itemToolbar && !itemToolbar.querySelector('[data-cms-drag-handle]')) { const handle = document.createElement('button'); handle.setAttribute('type', 'button'); - handle.setAttribute('data-cms-drag-handle', ''); + handle.dataset.cmsDragHandle = ''; handle.setAttribute('title', 'Drag to reorder'); handle.setAttribute('aria-label', 'Drag to reorder'); handle.innerHTML = MOVE_ICON; @@ -403,7 +403,7 @@ export const initToolbar = (container: HTMLElement) => { toolbarDefinition.actions.forEach((action : any) => { if (action === "editContent") { const button = document.createElement('button'); - button.setAttribute('data-cms-action', 'edit'); + button.dataset.cmsAction = 'edit'; button.innerHTML = EDIT_PAGE_ICON; button.setAttribute("title", "Edit content"); button.addEventListener('click', editContent); @@ -411,7 +411,7 @@ export const initToolbar = (container: HTMLElement) => { toolbar.appendChild(button); } else if (action === "editAttributes") { const button = document.createElement('button'); - button.setAttribute('data-cms-action', 'editAttributes'); + button.dataset.cmsAction = 'editAttributes'; button.innerHTML = EDIT_ATTRIBUTES_ICON; button.setAttribute("title", "Edit attributes"); button.addEventListener('click', editAttributes); @@ -419,7 +419,7 @@ export const initToolbar = (container: HTMLElement) => { toolbar.appendChild(button); } else if (action === "editCollectionItem") { const button = document.createElement('button'); - button.setAttribute('data-cms-action', 'editCollectionItem'); + button.dataset.cmsAction = 'editCollectionItem'; button.innerHTML = EDIT_ATTRIBUTES_ICON; button.setAttribute('title', 'Edit collection item'); button.addEventListener('click', editCollectionItem); @@ -427,7 +427,7 @@ export const initToolbar = (container: HTMLElement) => { toolbar.appendChild(button); } else if (action === "orderSectionEntries") { const button = document.createElement('button'); - button.setAttribute('data-cms-action', 'editSections'); + button.dataset.cmsAction = 'editSections'; button.innerHTML = SECTION_SORT_ICON; button.setAttribute("title", "Order"); button.addEventListener('click', orderSections); @@ -435,7 +435,7 @@ export const initToolbar = (container: HTMLElement) => { toolbar.appendChild(button); } else if (action === "addSectionEntry") { const button = document.createElement('button'); - button.setAttribute('data-cms-action', 'addSection'); + button.dataset.cmsAction = 'addSection'; button.innerHTML = SECTION_ADD_ICON; button.setAttribute("title", "Add"); button.addEventListener('click', addSection); @@ -443,7 +443,7 @@ export const initToolbar = (container: HTMLElement) => { toolbar.appendChild(button); } else if (action === "deleteSectionEntry") { const button = document.createElement('button'); - button.setAttribute('data-cms-action', 'deleteSection'); + button.dataset.cmsAction = 'deleteSection'; button.innerHTML = SECTION_DELETE_ICON; button.setAttribute("title", "Delete"); button.addEventListener('click', deleteSection); @@ -460,8 +460,8 @@ export const initToolbar = (container: HTMLElement) => { if (toolbarDefinition.type === "sectionEntry") { const button = document.createElement('button'); - button.setAttribute('data-cms-action', 'publish'); - button.setAttribute('data-cms-section-uri', toolbarDefinition.uri); + button.dataset.cmsAction = 'publish'; + button.dataset.cmsSectionUri = toolbarDefinition.uri; button.classList.add('cms-unpublished'); button.innerHTML = SECTION_UNPUBLISHED_ICON; button.setAttribute("title", "Publish"); diff --git a/modules/ui-module/src/test/java/com/condation/cms/modules/ui/extensionpoints/UiTemplateModelExtensionTest.java b/modules/ui-module/src/test/java/com/condation/cms/modules/ui/extensionpoints/UiTemplateModelExtensionTest.java new file mode 100644 index 000000000..e96dd5727 --- /dev/null +++ b/modules/ui-module/src/test/java/com/condation/cms/modules/ui/extensionpoints/UiTemplateModelExtensionTest.java @@ -0,0 +1,62 @@ +package com.condation.cms.modules.ui.extensionpoints; + +/*- + * #%L + * UI Module + * %% + * Copyright (C) 2023 - 2026 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * #L% + */ + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import com.condation.cms.api.db.DB; +import com.condation.cms.api.db.collection.CollectionItem; +import com.condation.cms.api.db.collection.Collections; +import com.condation.cms.api.feature.features.DBFeature; +import com.condation.cms.api.feature.features.IsPreviewFeature; +import com.condation.cms.api.module.SiteModuleContext; +import com.condation.cms.api.request.RequestContext; +import java.util.Map; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; + +class UiTemplateModelExtensionTest { + + @Test + void doesNotRenderCollectionToolbarForReferencedCollection() { + var db = mock(DB.class); + var collections = mock(Collections.class); + when(db.getCollections()).thenReturn(collections); + when(collections.isLocal("authors")).thenReturn(false); + var siteContext = mock(SiteModuleContext.class); + when(siteContext.get(DBFeature.class)).thenReturn(new DBFeature(db)); + var requestContext = new RequestContext(); + requestContext.add(IsPreviewFeature.class, new IsPreviewFeature()); + var helper = new UiTemplateModelExtension.UIHelper(requestContext, siteContext); + var item = new CollectionItem( + "first", + "authors", + "authors/first.md", + "content", + Map.of()); + + var toolbar = helper.collectionToolbar(item, new String[]{"edit"}); + + Assertions.assertThat(toolbar).isEmpty(); + } +} diff --git a/modules/ui-module/src/test/java/com/condation/cms/modules/ui/extensionpoints/remotemethods/RemoteCollectionEndpointsTest.java b/modules/ui-module/src/test/java/com/condation/cms/modules/ui/extensionpoints/remotemethods/RemoteCollectionEndpointsTest.java index aa1b453b6..128e484ab 100644 --- a/modules/ui-module/src/test/java/com/condation/cms/modules/ui/extensionpoints/remotemethods/RemoteCollectionEndpointsTest.java +++ b/modules/ui-module/src/test/java/com/condation/cms/modules/ui/extensionpoints/remotemethods/RemoteCollectionEndpointsTest.java @@ -56,6 +56,7 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import static org.mockito.Mockito.lenient; @ExtendWith(MockitoExtension.class) class RemoteCollectionEndpointsTest { @@ -95,10 +96,11 @@ void setUp() throws Exception { endpoints.setContext(moduleContext); when(moduleContext.get(DBFeature.class)).thenReturn(new DBFeature(db)); - when(db.getFileSystem()).thenReturn(fileSystem); + lenient().when(db.getFileSystem()).thenReturn(fileSystem); when(db.getCollections()).thenReturn(collections); when(collections.names()).thenReturn(Set.of("blog")); - when(fileSystem.resolve(Constants.Folders.COLLECTIONS)).thenReturn(collectionsDirectory); + when(collections.isLocal("blog")).thenReturn(true); + lenient().when(fileSystem.resolve(Constants.Folders.COLLECTIONS)).thenReturn(collectionsDirectory); } @Test @@ -156,6 +158,16 @@ void deletesCollectionItemAndRefreshesIndex() throws Exception { verify(collections).refresh("blog", "obsolete"); } + @Test + void rejectsWritesToReferencedCollections() { + when(collections.isLocal("blog")).thenReturn(false); + + assertThatThrownBy(() -> endpoints.create(Map.of("collection", "blog", "id", "new-item"))) + .isInstanceOfSatisfying( + RPCException.class, + exception -> assertThat(exception.getCode()).isEqualTo(403)); + } + private RequestContext requestContext() { var configuration = new Configuration(); var definitions = new ConcurrentHashMap(); diff --git a/modules/ui-module/src/test/java/com/condation/cms/modules/ui/extensionpoints/remotemethods/RemoteContentEndpointsExtensionTest.java b/modules/ui-module/src/test/java/com/condation/cms/modules/ui/extensionpoints/remotemethods/RemoteContentEndpointsExtensionTest.java index 3b318f04a..de63bf01d 100644 --- a/modules/ui-module/src/test/java/com/condation/cms/modules/ui/extensionpoints/remotemethods/RemoteContentEndpointsExtensionTest.java +++ b/modules/ui-module/src/test/java/com/condation/cms/modules/ui/extensionpoints/remotemethods/RemoteContentEndpointsExtensionTest.java @@ -25,7 +25,10 @@ import com.condation.cms.api.db.DB; import com.condation.cms.api.db.DBFileSystem; import com.condation.cms.api.db.cms.ReadOnlyFile; +import com.condation.cms.api.db.collection.CollectionItem; +import com.condation.cms.api.db.collection.Collections; import com.condation.cms.api.feature.features.DBFeature; +import com.condation.cms.api.feature.features.CurrentCollectionItemFeature; import com.condation.cms.api.feature.features.CurrentNodeFeature; import com.condation.cms.api.feature.features.RequestFeature; import com.condation.cms.api.module.SiteModuleContext; @@ -47,6 +50,7 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import static org.mockito.Mockito.lenient; @ExtendWith(MockitoExtension.class) class RemoteContentEndpointsExtensionTest { @@ -69,6 +73,9 @@ class RemoteContentEndpointsExtensionTest { @Mock private ReadOnlyFile contentFile; + @Mock + private Collections collections; + private RemoteContentEndpointsExtension endpoints; @BeforeEach @@ -76,8 +83,10 @@ void setUp() { endpoints = new RemoteContentEndpointsExtension(); endpoints.setContext(moduleContext); when(moduleContext.get(DBFeature.class)).thenReturn(new DBFeature(db)); - when(db.getFileSystem()).thenReturn(fileSystem); - when(fileSystem.contentBase()).thenReturn(contentBase); + lenient().when(db.getFileSystem()).thenReturn(fileSystem); + lenient().when(fileSystem.contentBase()).thenReturn(contentBase); + lenient().when(db.getCollections()).thenReturn(collections); + lenient().when(collections.isLocal("blog")).thenReturn(true); } @Test @@ -126,6 +135,22 @@ void getContentUsesCurrentNodeWhenUriIsOmitted() throws Exception { .hasMessage("variant selected"); } + @Test + void rejectsEditingAReferencedCollectionItem() { + when(collections.isLocal("blog")).thenReturn(false); + var requestContext = new RequestContext(); + requestContext.add( + CurrentCollectionItemFeature.class, + new CurrentCollectionItemFeature(new CollectionItem( + "first", "blog", "blog/first.md", "Body", Map.of()))); + + assertThatThrownBy(() -> ScopedValue.where( + RequestContextScope.REQUEST_CONTEXT, + requestContext).call(() -> endpoints.getContent(Map.of()))) + .isInstanceOfSatisfying(RPCException.class, exception -> + assertThat(exception.getCode()).isEqualTo(403)); + } + @Test void getContentNodeResolvesUriFromCustomPageUrl() throws Exception { var node = new ContentNode( diff --git a/modules/ui-module/src/test/java/com/condation/cms/modules/ui/extensionpoints/remotemethods/RemoteWorkflowEndpointsExtensionTest.java b/modules/ui-module/src/test/java/com/condation/cms/modules/ui/extensionpoints/remotemethods/RemoteWorkflowEndpointsExtensionTest.java index 9359dc4bb..fc8c370e1 100644 --- a/modules/ui-module/src/test/java/com/condation/cms/modules/ui/extensionpoints/remotemethods/RemoteWorkflowEndpointsExtensionTest.java +++ b/modules/ui-module/src/test/java/com/condation/cms/modules/ui/extensionpoints/remotemethods/RemoteWorkflowEndpointsExtensionTest.java @@ -31,6 +31,7 @@ import com.condation.cms.api.db.VariantSearchMode; import com.condation.cms.api.db.cms.ReadOnlyFile; import com.condation.cms.api.db.collection.CollectionItem; +import com.condation.cms.api.db.collection.Collections; import com.condation.cms.api.feature.features.CurrentCollectionItemFeature; import com.condation.cms.api.feature.features.DBFeature; import com.condation.cms.api.feature.features.WorkflowFeature; @@ -89,6 +90,9 @@ class RemoteWorkflowEndpointsExtensionTest { @Mock private Path collectionWritableFile; + @Mock + private Collections collections; + private RemoteWorkflowEndpointsExtension endpoints; @BeforeEach @@ -96,7 +100,9 @@ void setUp() { endpoints = new RemoteWorkflowEndpointsExtension(); endpoints.setContext(moduleContext); when(moduleContext.get(DBFeature.class)).thenReturn(new DBFeature(db)); - when(db.getFileSystem()).thenReturn(fileSystem); + lenient().when(db.getFileSystem()).thenReturn(fileSystem); + lenient().when(db.getCollections()).thenReturn(collections); + lenient().when(collections.isLocal("blog")).thenReturn(true); lenient().when(fileSystem.contentBase()).thenReturn(contentBase); lenient().when(fileSystem.collectionsBase()).thenReturn(collectionsBase); lenient().when(collectionsBase.resolve("blog/first.md")).thenReturn(collectionFile); @@ -165,6 +171,23 @@ void nodeStatus_usesCurrentCollectionItemOnDetailPage() throws Exception { .containsEntry("transitions", List.of()); } + @Test + void nodeStatus_ignoresReferencedCollectionItems() throws Exception { + when(collections.isLocal("blog")).thenReturn(false); + var requestContext = new RequestContext(); + requestContext.add( + CurrentCollectionItemFeature.class, + new CurrentCollectionItemFeature(new CollectionItem( + "first", "blog", "blog/first.md", "Body", Map.of()))); + + @SuppressWarnings("unchecked") + var result = (Map) ScopedValue.where( + RequestContextScope.REQUEST_CONTEXT, + requestContext).call(() -> endpoints.nodeStatus(Map.of())); + + assertThat(result).isEmpty(); + } + @Test void unpublishedPages_delegatesFilteringAndPaginationToWorkflowProvider() throws RPCException { Content content = mock(Content.class); diff --git a/modules/ui-module/src/test/java/com/condation/cms/modules/ui/utils/ActionFactoryAppsTest.java b/modules/ui-module/src/test/java/com/condation/cms/modules/ui/utils/ActionFactoryAppsTest.java index 4bde5e75e..9ac0b01d4 100644 --- a/modules/ui-module/src/test/java/com/condation/cms/modules/ui/utils/ActionFactoryAppsTest.java +++ b/modules/ui-module/src/test/java/com/condation/cms/modules/ui/utils/ActionFactoryAppsTest.java @@ -102,7 +102,8 @@ void addsCollectionsToCreateContentMenu() { Collections collections = mock(Collections.class); when(context.get(DBFeature.class)).thenReturn(new DBFeature(db)); when(db.getCollections()).thenReturn(collections); - when(collections.names()).thenReturn(Set.of("blog")); + when(collections.names()).thenReturn(Set.of("blog", "shared")); + when(collections.isLocal("blog")).thenReturn(true); HookSystem hookSystem = mock(HookSystem.class); when(hookSystem.doFilter(eq(UIHooks.HOOK_REGISTER_CONTENT_TYPES), any(ContentTypes.class))) @@ -131,5 +132,6 @@ void addsCollectionsToCreateContentMenu() { Assertions.assertThat(action.getParameters()).containsEntry("collection", "blog"); }); }); + Assertions.assertThat(factory.createContentTypeMenu().getMenuEntry("collection-shared")).isEmpty(); } } diff --git a/test-server/hosts/demo_de/config/collections.yaml b/test-server/hosts/demo_de/config/collections.yaml new file mode 100644 index 000000000..4340718b6 --- /dev/null +++ b/test-server/hosts/demo_de/config/collections.yaml @@ -0,0 +1,6 @@ +collections: + authors: + site: demo-site + detail: + route: /collections/authors/{slug} + template: collections/author-detail.html diff --git a/test-server/hosts/demo_de/config/menus/main.yaml b/test-server/hosts/demo_de/config/menus/main.yaml new file mode 100644 index 000000000..1543632f1 --- /dev/null +++ b/test-server/hosts/demo_de/config/menus/main.yaml @@ -0,0 +1,35 @@ +id: main +name: Mainmenu +items: +- id: a772de6e-66f1-4b59-9345-82f44d4f36d2 + type: link + label: Startseite + url: / + target: _self + enabled: true + children: [ + ] +- id: 07bc9ae6-d520-442e-93cb-9c53d12af13a + type: link + label: das ist eine neue seite + url: /total-other-page + target: _self + enabled: true + children: [ + ] +- id: fa5c603d-e470-434c-86b7-6f387821b9c7 + type: link + label: Content example + url: /content + target: _self + enabled: true + children: [ + ] +- id: af033b5a-33b5-4345-bc81-df78732cb7ce + type: link + label: Locations + url: /locations + target: _self + enabled: true + children: [ + ] diff --git a/test-server/hosts/demo_de/content/collections/index.md b/test-server/hosts/demo_de/content/collections/index.md new file mode 100644 index 000000000..9bf568545 --- /dev/null +++ b/test-server/hosts/demo_de/content/collections/index.md @@ -0,0 +1,6 @@ +--- +title: Collections remote Test +status: published +template: collections-authors.html +--- + diff --git a/test-server/themes/demo/templates/collections-authors.html b/test-server/themes/demo/templates/collections-authors.html new file mode 100644 index 000000000..5aa3d5414 --- /dev/null +++ b/test-server/themes/demo/templates/collections-authors.html @@ -0,0 +1,34 @@ + + + + + + + + + {% include "libs/fragments.html" %} + + + + + + +
+
+ {{ node.content | raw }} +
+ +

Remote author collection

+ {% assign items = cms.collection("authors").query().get() %} + + {% for item in items %} + + {% endfor %} +
+ + + + + From da3676713fd29027789b4a4f8d3b7ba7b0fe462b Mon Sep 17 00:00:00 2001 From: Thorsten Marx Date: Sat, 29 Aug 2026 17:18:21 +0200 Subject: [PATCH 11/28] fix some sonar issues --- .../ui/extensionpoints/PageMenuExtension.java | 237 ++++++++---------- .../RemoteCollectionEndpoints.java | 2 +- 2 files changed, 107 insertions(+), 132 deletions(-) diff --git a/modules/ui-module/src/main/java/com/condation/cms/modules/ui/extensionpoints/PageMenuExtension.java b/modules/ui-module/src/main/java/com/condation/cms/modules/ui/extensionpoints/PageMenuExtension.java index 89b4766bf..b84cc19b5 100644 --- a/modules/ui-module/src/main/java/com/condation/cms/modules/ui/extensionpoints/PageMenuExtension.java +++ b/modules/ui-module/src/main/java/com/condation/cms/modules/ui/extensionpoints/PageMenuExtension.java @@ -33,149 +33,124 @@ * * @author t.marx */ - @Extensions({ - @Extension(UIActionsExtensionPoint.class), - @Extension(HookSystemRegisterExtensionPoint.class), - @Extension(UILocalizationExtensionPoint.class) + @Extension(UIActionsExtensionPoint.class), + @Extension(HookSystemRegisterExtensionPoint.class), + @Extension(UILocalizationExtensionPoint.class) }) public class PageMenuExtension extends HookSystemRegisterExtensionPoint implements UIActionsExtensionPoint, UILocalizationExtensionPoint { -// @com.condation.cms.api.ui.annotations.MenuEntry( -// id = "pageMenu", -// name = "Page", -// position = 10 -// ) -// public void parentDefinition() { -// -// } + @ShortCut( + id = "page-create", + title = "Create new page", + permissions = {Permissions.CONTENT_EDIT}, + hotkey = "ctrl-3", + section = "Page", + scriptAction = @com.condation.cms.api.ui.annotations.ScriptAction(module = "/manager/actions/page/create-page") + ) + public void create_page() { + // can be empty + + } - /* - @com.condation.cms.api.ui.annotations.MenuEntry( - parent = "pageMenu", - id = "page-create", - name = "Create new page", - position = 1, - scriptAction = @com.condation.cms.api.ui.annotations.ScriptAction(module = "/manager/actions/page/create-page") - )*/ - @ShortCut( - id = "page-create", - title = "Create new page", - permissions = {Permissions.CONTENT_EDIT}, - hotkey = "ctrl-3", - section = "Page", - scriptAction = @com.condation.cms.api.ui.annotations.ScriptAction(module = "/manager/actions/page/create-page") - ) - public void create_page() { + @ShortCut( + id = "page-edit-meta", + title = "Edit page settings", + permissions = {Permissions.CONTENT_EDIT}, + hotkey = "ctrl-2", + section = "Page", + scriptAction = @com.condation.cms.api.ui.annotations.ScriptAction(module = "/manager/actions/page/edit-page-settings") + ) + public void page_settings() { // can be empty + } + - } - /* - @com.condation.cms.api.ui.annotations.MenuEntry( - parent = "pageMenu", - id = "page-edit-meta", - name = "Edit MetaData", - position = 3, - scriptAction = @com.condation.cms.api.ui.annotations.ScriptAction(module = "/manager/actions/page/edit-page-settings") - )*/ - @ShortCut( - id = "page-edit-meta", - title = "Edit page settings", - permissions = {Permissions.CONTENT_EDIT}, - hotkey = "ctrl-2", - section = "Page", - scriptAction = @com.condation.cms.api.ui.annotations.ScriptAction(module = "/manager/actions/page/edit-page-settings") - ) - public void page_settings() { + @ShortCut( + id = "manager-assets", + title = "Manage assets", + permissions = {Permissions.CONTENT_EDIT}, + hotkey = "ctrl-4", + section = "Assets", + scriptAction = @com.condation.cms.api.ui.annotations.ScriptAction(module = "/manager/actions/page/manage-assets") + ) + public void manage_media() { // can be empty } - /* - @com.condation.cms.api.ui.annotations.MenuEntry( - parent = "pageMenu", - id = "manage-assets", - name = "Manage assets", - position = 10, - scriptAction = @com.condation.cms.api.ui.annotations.ScriptAction(module = "/manager/actions/page/manage-assets") - ) - */ - @ShortCut( - id = "manager-assets", - title = "Manage assets", - permissions = {Permissions.CONTENT_EDIT}, - hotkey = "ctrl-4", - section = "Assets", - scriptAction = @com.condation.cms.api.ui.annotations.ScriptAction(module = "/manager/actions/page/manage-assets") - ) - public void manage_media() { - } - - @ShortCut( - id = "page-edit-translations", - title = "Edit page translations", - permissions = {Permissions.CONTENT_EDIT}, - hotkey = "ctrl-5", - section = "Page", - scriptAction = @com.condation.cms.api.ui.annotations.ScriptAction(module = "/manager/actions/page/translations") - ) - public void manage_translations() {} + @ShortCut( + id = "page-edit-translations", + title = "Edit page translations", + permissions = {Permissions.CONTENT_EDIT}, + hotkey = "ctrl-5", + section = "Page", + scriptAction = @com.condation.cms.api.ui.annotations.ScriptAction(module = "/manager/actions/page/translations") + ) + public void manage_translations() { + // can be empty + } - @ShortCut( - id = "page-variants", - title = "Page variants", - permissions = {Permissions.CONTENT_EDIT}, - hotkey = "ctrl-6", - section = "Page", - scriptAction = @com.condation.cms.api.ui.annotations.ScriptAction(module = "/manager/actions/page/variants") - ) - public void page_variants() {} + @ShortCut( + id = "page-variants", + title = "Page variants", + permissions = {Permissions.CONTENT_EDIT}, + hotkey = "ctrl-6", + section = "Page", + scriptAction = @com.condation.cms.api.ui.annotations.ScriptAction(module = "/manager/actions/page/variants") + ) + public void page_variants() { + // can be empty + } - @ShortCut( - id = "page-variant-create", - title = "Create page variant", - permissions = {Permissions.CONTENT_EDIT}, - hotkey = "ctrl-7", - section = "Page", - scriptAction = @com.condation.cms.api.ui.annotations.ScriptAction(module = "/manager/actions/page/create-variant") - ) - public void create_page_variant() {} + @ShortCut( + id = "page-variant-create", + title = "Create page variant", + permissions = {Permissions.CONTENT_EDIT}, + hotkey = "ctrl-7", + section = "Page", + scriptAction = @com.condation.cms.api.ui.annotations.ScriptAction(module = "/manager/actions/page/create-variant") + ) + public void create_page_variant() { + // can be empty + } - @ShortCut( - id = "page-variant-selector", - title = "Configure variant selection", - permissions = {Permissions.CONTENT_EDIT}, - hotkey = "ctrl-8", - section = "Page", - scriptAction = @com.condation.cms.api.ui.annotations.ScriptAction(module = "/manager/actions/page/variant-selector") - ) - public void configure_variant_selector() {} - + @ShortCut( + id = "page-variant-selector", + title = "Configure variant selection", + permissions = {Permissions.CONTENT_EDIT}, + hotkey = "ctrl-8", + section = "Page", + scriptAction = @com.condation.cms.api.ui.annotations.ScriptAction(module = "/manager/actions/page/variant-selector") + ) + public void configure_variant_selector() { + // can be empty + } - @Override - public Map> getLocalizations() { - return Map.of( - "de", Map.of( - "pageMenu", "Seite", - "page-create", "Neue Seite erstellen", - "page-edit-content", "Inhalt bearbeiten", - "page-edit-meta", "Metadaten bearbeiten", - "page-variants", "Seitenvarianten", - "page-variant-create", "Seitenvariante erstellen", - "page-variant-selector", "Variantenauswahl konfigurieren", - "language.de", "Deutsch", - "language.en", "Englisch" - ), - "en", Map.of( - "pageMenu", "Page", - "page-create", "Create new page", - "page-edit-content", "Edit content", - "page-edit-meta", "Edit metadata", - "page-variants", "Page variants", - "page-variant-create", "Create page variant", - "page-variant-selector", "Configure variant selection", - "language.de", "German", - "language.en", "English" - ) - ); - } + @Override + public Map> getLocalizations() { + return Map.of( + "de", Map.of( + "pageMenu", "Seite", + "page-create", "Neue Seite erstellen", + "page-edit-content", "Inhalt bearbeiten", + "page-edit-meta", "Metadaten bearbeiten", + "page-variants", "Seitenvarianten", + "page-variant-create", "Seitenvariante erstellen", + "page-variant-selector", "Variantenauswahl konfigurieren", + "language.de", "Deutsch", + "language.en", "Englisch" + ), + "en", Map.of( + "pageMenu", "Page", + "page-create", "Create new page", + "page-edit-content", "Edit content", + "page-edit-meta", "Edit metadata", + "page-variants", "Page variants", + "page-variant-create", "Create page variant", + "page-variant-selector", "Configure variant selection", + "language.de", "German", + "language.en", "English" + ) + ); + } } diff --git a/modules/ui-module/src/main/java/com/condation/cms/modules/ui/extensionpoints/remotemethods/RemoteCollectionEndpoints.java b/modules/ui-module/src/main/java/com/condation/cms/modules/ui/extensionpoints/remotemethods/RemoteCollectionEndpoints.java index 96ffe328d..44e8ac453 100644 --- a/modules/ui-module/src/main/java/com/condation/cms/modules/ui/extensionpoints/remotemethods/RemoteCollectionEndpoints.java +++ b/modules/ui-module/src/main/java/com/condation/cms/modules/ui/extensionpoints/remotemethods/RemoteCollectionEndpoints.java @@ -230,7 +230,7 @@ private ItemDto itemDto(CollectionItem item) { private String detailUrl(CollectionItem item) { try { return new LinkFunction(getRequestContext()).collectionUrl(item); - } catch (IllegalArgumentException | IllegalStateException ex) { + } catch (IllegalArgumentException | IllegalStateException _) { return null; } } From 8633ed8e46177722712e502c4ed8aa8df0da7f5b Mon Sep 17 00:00:00 2001 From: Thorsten Marx Date: Sat, 29 Aug 2026 17:21:52 +0200 Subject: [PATCH 12/28] fix some sonar issues --- .../ui/extensionpoints/PageMenuExtension.java | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/modules/ui-module/src/main/java/com/condation/cms/modules/ui/extensionpoints/PageMenuExtension.java b/modules/ui-module/src/main/java/com/condation/cms/modules/ui/extensionpoints/PageMenuExtension.java index b84cc19b5..712b93b3f 100644 --- a/modules/ui-module/src/main/java/com/condation/cms/modules/ui/extensionpoints/PageMenuExtension.java +++ b/modules/ui-module/src/main/java/com/condation/cms/modules/ui/extensionpoints/PageMenuExtension.java @@ -48,7 +48,7 @@ public class PageMenuExtension extends HookSystemRegisterExtensionPoint implemen section = "Page", scriptAction = @com.condation.cms.api.ui.annotations.ScriptAction(module = "/manager/actions/page/create-page") ) - public void create_page() { + public void createPage() { // can be empty } @@ -61,7 +61,7 @@ public void create_page() { section = "Page", scriptAction = @com.condation.cms.api.ui.annotations.ScriptAction(module = "/manager/actions/page/edit-page-settings") ) - public void page_settings() { + public void pageSettings() { // can be empty } @@ -74,7 +74,7 @@ public void page_settings() { section = "Assets", scriptAction = @com.condation.cms.api.ui.annotations.ScriptAction(module = "/manager/actions/page/manage-assets") ) - public void manage_media() { + public void managMedia() { // can be empty } @@ -86,7 +86,7 @@ public void manage_media() { section = "Page", scriptAction = @com.condation.cms.api.ui.annotations.ScriptAction(module = "/manager/actions/page/translations") ) - public void manage_translations() { + public void manageTranslations() { // can be empty } @@ -98,7 +98,7 @@ public void manage_translations() { section = "Page", scriptAction = @com.condation.cms.api.ui.annotations.ScriptAction(module = "/manager/actions/page/variants") ) - public void page_variants() { + public void pageVariants() { // can be empty } @@ -110,7 +110,7 @@ public void page_variants() { section = "Page", scriptAction = @com.condation.cms.api.ui.annotations.ScriptAction(module = "/manager/actions/page/create-variant") ) - public void create_page_variant() { + public void createPageVariant() { // can be empty } @@ -122,7 +122,7 @@ public void create_page_variant() { section = "Page", scriptAction = @com.condation.cms.api.ui.annotations.ScriptAction(module = "/manager/actions/page/variant-selector") ) - public void configure_variant_selector() { + public void configureVariantSelector() { // can be empty } From fa4e0c20ed936a4cbf37d2f45934828c19297a97 Mon Sep 17 00:00:00 2001 From: Thorsten Marx Date: Sun, 30 Aug 2026 21:01:48 +0200 Subject: [PATCH 13/28] merge different search ty title implementations --- .../condation/cms/filesystem/FileContent.java | 7 +- .../condation/cms/filesystem/MetaData.java | 4 - .../persistent/CollectionMetaData.java | 5 - .../metadata/persistent/LuceneIndex.java | 41 ------- .../persistent/PersistentMetaData.java | 8 -- .../metadata/persistent/TitleQuery.java | 101 ------------------ 6 files changed, 4 insertions(+), 162 deletions(-) delete mode 100644 cms-filesystem/src/main/java/com/condation/cms/filesystem/metadata/persistent/TitleQuery.java diff --git a/cms-filesystem/src/main/java/com/condation/cms/filesystem/FileContent.java b/cms-filesystem/src/main/java/com/condation/cms/filesystem/FileContent.java index 2d81e0134..fd7761860 100644 --- a/cms-filesystem/src/main/java/com/condation/cms/filesystem/FileContent.java +++ b/cms-filesystem/src/main/java/com/condation/cms/filesystem/FileContent.java @@ -104,9 +104,10 @@ public Optional> getMeta(String uri) { @Override public List searchByTitle (String input, VariantSearchMode variantSearchMode) { - var titleQuery = fileSystem.getMetaData().searchByTitle(input); - - return titleQuery.list(variantSearchMode); + return query((node, excerptLength) -> node) + .searchByTitle(input) + .variants(variantSearchMode) + .get(); } } diff --git a/cms-filesystem/src/main/java/com/condation/cms/filesystem/MetaData.java b/cms-filesystem/src/main/java/com/condation/cms/filesystem/MetaData.java index 91df27df1..a0d386d56 100644 --- a/cms-filesystem/src/main/java/com/condation/cms/filesystem/MetaData.java +++ b/cms-filesystem/src/main/java/com/condation/cms/filesystem/MetaData.java @@ -24,8 +24,6 @@ import com.condation.cms.api.db.ContentNode; import com.condation.cms.api.db.ContentQuery; -import com.condation.cms.api.db.VariantSearchMode; -import com.condation.cms.filesystem.metadata.persistent.TitleQuery; import java.io.IOException; import java.time.LocalDate; import java.util.List; @@ -65,8 +63,6 @@ public interface MetaData { List listSectionEntries(String pagePath); - TitleQuery searchByTitle(String input); - void clear (); Map getNodes(); diff --git a/cms-filesystem/src/main/java/com/condation/cms/filesystem/metadata/persistent/CollectionMetaData.java b/cms-filesystem/src/main/java/com/condation/cms/filesystem/metadata/persistent/CollectionMetaData.java index ac9733f53..b0f80fb12 100644 --- a/cms-filesystem/src/main/java/com/condation/cms/filesystem/metadata/persistent/CollectionMetaData.java +++ b/cms-filesystem/src/main/java/com/condation/cms/filesystem/metadata/persistent/CollectionMetaData.java @@ -218,11 +218,6 @@ public List listSectionEntries(String pagePath) { return List.of(); } - @Override - public TitleQuery searchByTitle(String input) { - throw new UnsupportedOperationException("title search is not exposed for collections"); - } - @Override public synchronized void clear() { nodes.clear(); diff --git a/cms-filesystem/src/main/java/com/condation/cms/filesystem/metadata/persistent/LuceneIndex.java b/cms-filesystem/src/main/java/com/condation/cms/filesystem/metadata/persistent/LuceneIndex.java index dcc90f744..1a4fcca54 100644 --- a/cms-filesystem/src/main/java/com/condation/cms/filesystem/metadata/persistent/LuceneIndex.java +++ b/cms-filesystem/src/main/java/com/condation/cms/filesystem/metadata/persistent/LuceneIndex.java @@ -27,10 +27,7 @@ import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; -import java.util.ArrayList; -import java.util.Collections; import java.util.EnumSet; -import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; @@ -124,44 +121,6 @@ void delete(Query query) throws IOException { } } - List query(Query query, Sort sort) throws IOException { - IndexSearcher searcher = nrt_manager.acquire(); - try { - var topDocs = searcher.search(query, Integer.MAX_VALUE, sort); - - List result = new ArrayList<>(); - for (var scoreDoc : topDocs.scoreDocs) { - result.add(searcher.storedFields().document(scoreDoc.doc)); - } - - return result; - } catch (IOException e) { - log.error("", e); - } finally { - nrt_manager.release(searcher); - } - return Collections.emptyList(); - } - - List query(Query query) throws IOException { - IndexSearcher searcher = nrt_manager.acquire(); - try { - var topDocs = searcher.search(query, Integer.MAX_VALUE); - - List result = new ArrayList<>(); - for (var scoreDoc : topDocs.scoreDocs) { - result.add(searcher.storedFields().document(scoreDoc.doc)); - } - - return result; - } catch (IOException e) { - log.error("", e); - } finally { - nrt_manager.release(searcher); - } - return Collections.emptyList(); - } - int count(Query query) throws IOException { IndexSearcher searcher = nrt_manager.acquire(); try { diff --git a/cms-filesystem/src/main/java/com/condation/cms/filesystem/metadata/persistent/PersistentMetaData.java b/cms-filesystem/src/main/java/com/condation/cms/filesystem/metadata/persistent/PersistentMetaData.java index 63acbfdb4..03d0a135f 100644 --- a/cms-filesystem/src/main/java/com/condation/cms/filesystem/metadata/persistent/PersistentMetaData.java +++ b/cms-filesystem/src/main/java/com/condation/cms/filesystem/metadata/persistent/PersistentMetaData.java @@ -64,8 +64,6 @@ public class PersistentMetaData extends AbstractMetaData implements AutoCloseabl private SectionIndex sectionIndex; private UrlIndex urlIndex; private MVMap nodesByPath; - - private TitleQueryFactory titleQueryFactory; public PersistentMetaData(Path hostPath) { this(hostPath, Map.of()); @@ -101,7 +99,6 @@ public void open() throws IOException { sectionIndex.clear(); urlIndex.clear(); - titleQueryFactory = new TitleQueryFactory(LuceneIndex.SEARCH_ANALYZER); } @Override @@ -307,9 +304,4 @@ public ContentQuery query(final String startURI, final BiFunction(uri, this.index, this, new ExcerptMapperFunction<>(nodeMapper)); } - @Override - public TitleQuery searchByTitle (String input) { - return new TitleQuery(titleQueryFactory, input, index, this); - - } } diff --git a/cms-filesystem/src/main/java/com/condation/cms/filesystem/metadata/persistent/TitleQuery.java b/cms-filesystem/src/main/java/com/condation/cms/filesystem/metadata/persistent/TitleQuery.java deleted file mode 100644 index ebfd15724..000000000 --- a/cms-filesystem/src/main/java/com/condation/cms/filesystem/metadata/persistent/TitleQuery.java +++ /dev/null @@ -1,101 +0,0 @@ -package com.condation.cms.filesystem.metadata.persistent; - -/*- - * #%L - * CMS FileSystem - * %% - * Copyright (C) 2023 - 2026 CondationCMS - * %% - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - * #L% - */ - -import com.condation.cms.api.Constants; -import com.condation.cms.api.db.ContentNode; -import com.condation.cms.api.db.VariantSearchMode; -import com.condation.cms.filesystem.MetaData; -import com.condation.cms.filesystem.metadata.PageMetaData; -import java.io.IOException; -import java.util.Collections; -import java.util.List; -import java.util.Optional; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.apache.lucene.document.Document; -import org.apache.lucene.index.Term; -import org.apache.lucene.queryparser.flexible.core.QueryNodeException; -import org.apache.lucene.search.BooleanClause; -import org.apache.lucene.search.BooleanQuery; -import org.apache.lucene.search.TermQuery; - -/** - * - * @author thorstenmarx - */ -@Slf4j -@RequiredArgsConstructor -public class TitleQuery { - - private final TitleQueryFactory titleQueryFactory; - private final String input; - private final LuceneIndex index; - private final MetaData metaData; - - private String contentType = Constants.DEFAULT_CONTENT_TYPE; - - public List list(VariantSearchMode variantSearchMode) { - return queryContentNodes(variantSearchMode); - } - - public List list() { - return queryContentNodes(VariantSearchMode.ALL); - } - - private List queryContentNodes(VariantSearchMode variantSearchMode) { - - try { - BooleanQuery.Builder queryBuilder = new BooleanQuery.Builder(); - queryBuilder.add(new TermQuery(new Term("content.type", contentType)), BooleanClause.Occur.MUST); - queryBuilder.add(titleQueryFactory.createQuery(input), BooleanClause.Occur.MUST); - if (variantSearchMode != VariantSearchMode.ALL) { - queryBuilder.add( - new TermQuery(new Term( - "_variant", - Boolean.toString(variantSearchMode == VariantSearchMode.VARIANT) - )), - BooleanClause.Occur.MUST - ); - } - List result = index.query(queryBuilder.build()); - - var contentNodes = result.stream() - .map(document -> document.get("_uri")) - .map(metaData::byPath) - .filter(Optional::isPresent) - .map(Optional::get) - .filter(node -> !node.isDirectory()) - .filter(PageMetaData::isPage) - .filter(PageMetaData::isVisible) - .toList(); - - return contentNodes; - } catch (IOException ex) { - log.error("", ex); - } catch (QueryNodeException ex) { - log.error("", ex); - } - return Collections.emptyList(); - - } -} From 417f0b6beb8d01e5da460ff12ff34169e882d7e2 Mon Sep 17 00:00:00 2001 From: Thorsten Marx Date: Sun, 30 Aug 2026 21:40:55 +0200 Subject: [PATCH 14/28] form field for collections --- .../elements/ContentTypeDefinitionMapper.java | 4 + .../ui/elements/fields/CollectionField.java | 50 +++++ .../cms/api/ui/elements/fields/FormField.java | 1 + .../cms/api/ui/elements/ContentTypesTest.java | 26 +++ .../manager/js/modules/collection-picker.d.ts | 28 +++ .../manager/js/modules/collection-picker.js | 153 ++++++++++++++++ .../js/modules/form/field.collection.d.ts | 27 +++ .../js/modules/form/field.collection.js | 95 ++++++++++ .../manager/js/modules/form/forms.js | 5 + .../ts/src/js/modules/collection-picker.ts | 173 ++++++++++++++++++ .../src/js/modules/form/field.collection.ts | 114 ++++++++++++ .../src/main/ts/src/js/modules/form/forms.ts | 5 + test-server/hosts/demo/content/index.md | 1 + .../themes/demo/extensions/theme.manager.js | 8 + 14 files changed, 690 insertions(+) create mode 100644 cms-api/src/main/java/com/condation/cms/api/ui/elements/fields/CollectionField.java create mode 100644 modules/ui-module/src/main/resources/manager/js/modules/collection-picker.d.ts create mode 100644 modules/ui-module/src/main/resources/manager/js/modules/collection-picker.js create mode 100644 modules/ui-module/src/main/resources/manager/js/modules/form/field.collection.d.ts create mode 100644 modules/ui-module/src/main/resources/manager/js/modules/form/field.collection.js create mode 100644 modules/ui-module/src/main/ts/src/js/modules/collection-picker.ts create mode 100644 modules/ui-module/src/main/ts/src/js/modules/form/field.collection.ts diff --git a/cms-api/src/main/java/com/condation/cms/api/ui/elements/ContentTypeDefinitionMapper.java b/cms-api/src/main/java/com/condation/cms/api/ui/elements/ContentTypeDefinitionMapper.java index 0391d4188..31e909492 100644 --- a/cms-api/src/main/java/com/condation/cms/api/ui/elements/ContentTypeDefinitionMapper.java +++ b/cms-api/src/main/java/com/condation/cms/api/ui/elements/ContentTypeDefinitionMapper.java @@ -23,6 +23,7 @@ import com.condation.cms.api.ui.elements.fields.CheckboxField; import com.condation.cms.api.ui.elements.fields.CodeField; +import com.condation.cms.api.ui.elements.fields.CollectionField; import com.condation.cms.api.ui.elements.fields.ColorField; import com.condation.cms.api.ui.elements.fields.DateField; import com.condation.cms.api.ui.elements.fields.DateTimeField; @@ -196,6 +197,9 @@ private static FormField field(Map definition) { case "reference" -> ReferenceField.builder() .name(name).title(title).required(required).requiredMessage(requiredMessage) .siteId(string(options.get("siteid"), null)).build(); + case "collection" -> CollectionField.builder() + .name(name).title(title).required(required).requiredMessage(requiredMessage) + .collection(string(options.get("collection"), null)).build(); case "tags" -> TagsField.builder() .name(name).title(title).required(required).requiredMessage(requiredMessage) .taxonomy(string(options.get("taxonomy"), null)).build(); diff --git a/cms-api/src/main/java/com/condation/cms/api/ui/elements/fields/CollectionField.java b/cms-api/src/main/java/com/condation/cms/api/ui/elements/fields/CollectionField.java new file mode 100644 index 000000000..205c0e127 --- /dev/null +++ b/cms-api/src/main/java/com/condation/cms/api/ui/elements/fields/CollectionField.java @@ -0,0 +1,50 @@ +package com.condation.cms.api.ui.elements.fields; + +/*- + * #%L + * CMS Api + * %% + * Copyright (C) 2023 - 2026 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * #L% + */ + +import lombok.Builder; +import lombok.Getter; + +/** A manager form field for selecting an item from a configured collection. */ +@Getter +public final class CollectionField extends FormField { + + private final Options options; + + @Builder + public CollectionField( + String name, + String title, + boolean required, + String requiredMessage, + String collection) { + super("collection", name, title, required, requiredMessage); + this.options = new Options(collection); + } + + public CollectionField(String name, String title, String collection) { + this(name, title, false, null, collection); + } + + public record Options(String collection) { + } +} diff --git a/cms-api/src/main/java/com/condation/cms/api/ui/elements/fields/FormField.java b/cms-api/src/main/java/com/condation/cms/api/ui/elements/fields/FormField.java index 84340a257..de9a097be 100644 --- a/cms-api/src/main/java/com/condation/cms/api/ui/elements/fields/FormField.java +++ b/cms-api/src/main/java/com/condation/cms/api/ui/elements/fields/FormField.java @@ -28,6 +28,7 @@ public abstract sealed class FormField permits CheckboxField, CodeField, + CollectionField, ColorField, DateField, DateTimeField, diff --git a/cms-api/src/test/java/com/condation/cms/api/ui/elements/ContentTypesTest.java b/cms-api/src/test/java/com/condation/cms/api/ui/elements/ContentTypesTest.java index fa26960ed..048c73f1b 100644 --- a/cms-api/src/test/java/com/condation/cms/api/ui/elements/ContentTypesTest.java +++ b/cms-api/src/test/java/com/condation/cms/api/ui/elements/ContentTypesTest.java @@ -21,6 +21,7 @@ * #L% */ +import com.condation.cms.api.ui.elements.fields.CollectionField; import com.condation.cms.api.ui.elements.fields.FormField; import com.condation.cms.api.ui.elements.fields.MarkdownField; import com.condation.cms.api.ui.elements.fields.NumberField; @@ -144,4 +145,29 @@ void supportsTypedJavaRegistration() { assertThat(form.tabs()).singleElement() .satisfies(tab -> assertThat(tab.fields()).allMatch(FormField.class::isInstance)); } + + @Test + void mapsCollectionFieldAndItsConfiguredCollection() { + Map collectionField = Map.of( + "type", "collection", + "name", "author", + "title", "Author", + "required", true, + "options", Map.of("collection", "authors")); + Map input = Map.of( + "name", "Article", + "template", "article.html", + "forms", Map.of("settings", Map.of("fields", List.of(collectionField)))); + + ContentTypes contentTypes = new ContentTypes(); + contentTypes.registerPageTemplate(input); + + assertThat(contentTypes.getPageTemplate("Article").orElseThrow().getForm("settings").fields()) + .singleElement() + .isInstanceOfSatisfying(CollectionField.class, field -> { + assertThat(field.getName()).isEqualTo("author"); + assertThat(field.isRequired()).isTrue(); + assertThat(field.getOptions().collection()).isEqualTo("authors"); + }); + } } diff --git a/modules/ui-module/src/main/resources/manager/js/modules/collection-picker.d.ts b/modules/ui-module/src/main/resources/manager/js/modules/collection-picker.d.ts new file mode 100644 index 000000000..c0eba907b --- /dev/null +++ b/modules/ui-module/src/main/resources/manager/js/modules/collection-picker.d.ts @@ -0,0 +1,28 @@ +/*- + * #%L + * UI Module + * %% + * Copyright (C) 2023 - 2026 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * #L% + */ +import { CollectionItemSummary } from '@cms/modules/rpc/rpc-collection.js'; +export interface CollectionItemPickerOptions { + collection: string; + title?: string; + selectText?: string; + onSelect: (item: CollectionItemSummary) => void | Promise; +} +export declare const openCollectionItemPicker: (options: CollectionItemPickerOptions) => void; diff --git a/modules/ui-module/src/main/resources/manager/js/modules/collection-picker.js b/modules/ui-module/src/main/resources/manager/js/modules/collection-picker.js new file mode 100644 index 000000000..2beb6ee40 --- /dev/null +++ b/modules/ui-module/src/main/resources/manager/js/modules/collection-picker.js @@ -0,0 +1,153 @@ +/*- + * #%L + * UI Module + * %% + * Copyright (C) 2023 - 2026 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * #L% + */ +import { i18n } from '@cms/modules/localization.js'; +import { openModal } from '@cms/modules/modal.js'; +import { listCollectionItems } from '@cms/modules/rpc/rpc-collection.js'; +const PAGE_SIZE = 10; +const escapeHtml = (value) => String(value ?? '') + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); +const renderMessage = (message) => `

${escapeHtml(message)}

`; +const renderResults = (response, selectText) => { + if (response.items.length === 0) { + return renderMessage(i18n.t('collection.picker.noResults', 'No collection items found.')); + } + const page = Math.max(1, response.page); + const totalPages = Math.max(1, response.totalPages); + return ` +
+ ${response.items.map((item, index) => ` + `).join('')} +
+
+ + + ${escapeHtml(i18n.t('collection.picker.page', 'Page'))} ${page} / ${totalPages} + + +
`; +}; +export const openCollectionItemPicker = (options) => { + let modal; + let requestVersion = 0; + let debounceTimer; + let currentPage = 1; + const inputId = `cms-collection-picker-${Date.now()}`; + const selectText = options.selectText || i18n.t('collection.picker.select', 'Select'); + modal = openModal({ + title: options.title || i18n.t('collection.picker.title', 'Select collection item'), + body: ` + + +
`, + fullscreen: false, + size: 'lg', + showFooter: false, + onShow: (modalElement) => { + const input = modalElement.querySelector(`#${inputId}`); + const resultsElement = modalElement.querySelector('[data-collection-picker-results]'); + const loadPage = async () => { + const version = ++requestVersion; + resultsElement.innerHTML = ` +
+ + ${escapeHtml(i18n.t('collection.picker.loading', 'Loading collection items...'))} +
`; + try { + const response = await listCollectionItems({ + collection: options.collection, + query: input.value.trim() || undefined, + page: currentPage, + size: PAGE_SIZE + }); + if (version !== requestVersion) + return; + currentPage = response.page; + resultsElement.innerHTML = renderResults(response, selectText); + resultsElement.querySelectorAll('[data-collection-picker-index]').forEach(button => { + button.addEventListener('click', async () => { + const item = response.items[Number(button.dataset.collectionPickerIndex)]; + if (!item) + return; + await options.onSelect(item); + modal.hide(); + }); + }); + resultsElement.querySelector('[data-collection-picker-previous]') + ?.addEventListener('click', () => { + currentPage--; + loadPage(); + }); + resultsElement.querySelector('[data-collection-picker-next]') + ?.addEventListener('click', () => { + currentPage++; + loadPage(); + }); + } + catch (error) { + if (version !== requestVersion) + return; + resultsElement.innerHTML = ` +
+ ${escapeHtml(i18n.t('collection.picker.loadError', 'Could not load collection items.'))} +
`; + } + }; + input.addEventListener('input', () => { + window.clearTimeout(debounceTimer); + debounceTimer = window.setTimeout(() => { + currentPage = 1; + loadPage(); + }, 300); + }); + input.addEventListener('keydown', event => { + if (event.key === 'Enter') { + event.preventDefault(); + window.clearTimeout(debounceTimer); + currentPage = 1; + loadPage(); + } + }); + loadPage(); + input.focus(); + } + }); +}; diff --git a/modules/ui-module/src/main/resources/manager/js/modules/form/field.collection.d.ts b/modules/ui-module/src/main/resources/manager/js/modules/form/field.collection.d.ts new file mode 100644 index 000000000..bd0aabe1c --- /dev/null +++ b/modules/ui-module/src/main/resources/manager/js/modules/form/field.collection.d.ts @@ -0,0 +1,27 @@ +/*- + * #%L + * UI Module + * %% + * Copyright (C) 2023 - 2026 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * #L% + */ +import { FieldOptions, FormField } from '@cms/modules/form/forms.js'; +export interface CollectionFieldOptions extends FieldOptions { + options?: { + collection?: string; + }; +} +export declare const CollectionField: FormField; diff --git a/modules/ui-module/src/main/resources/manager/js/modules/form/field.collection.js b/modules/ui-module/src/main/resources/manager/js/modules/form/field.collection.js new file mode 100644 index 000000000..da6997444 --- /dev/null +++ b/modules/ui-module/src/main/resources/manager/js/modules/form/field.collection.js @@ -0,0 +1,95 @@ +/*- + * #%L + * UI Module + * %% + * Copyright (C) 2023 - 2026 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * #L% + */ +import { openCollectionItemPicker } from '@cms/modules/collection-picker.js'; +import { createID } from '@cms/modules/form/utils.js'; +import { i18n } from '@cms/modules/localization.js'; +const escapeHtml = (value) => String(value ?? '') + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); +const createCollectionField = (options, value = '') => { + const id = createID(); + const key = `field.${options.name}`; + const title = i18n.t(key, options.title); + const collection = options.options?.collection || ''; + const disabled = collection ? '' : ' disabled'; + return ` +
+ +
+ + + +
+ ${collection ? '' : `
${escapeHtml(i18n.t('collection.field.missingCollection', 'No collection is configured for this field.'))}
`} +
`; +}; +const init = (context) => { + const formElement = context.formElement; + if (!formElement) + return; + formElement.querySelectorAll("[data-cms-form-field-type='collection']").forEach(wrapper => { + const input = wrapper.querySelector('.cms-collection-input-value'); + const selectButton = wrapper.querySelector('.cms-collection-select'); + const clearButton = wrapper.querySelector('.cms-collection-clear'); + const collection = wrapper.dataset.cmsCollection; + if (!input || !selectButton || !clearButton || !collection) + return; + selectButton.addEventListener('click', () => { + openCollectionItemPicker({ + collection, + title: i18n.t('collection.field.dialogTitle', `Select item from ${collection}`), + onSelect: item => { + input.value = item.id; + input.dispatchEvent(new Event('change', { bubbles: true })); + } + }); + }); + clearButton.addEventListener('click', () => { + input.value = ''; + input.dispatchEvent(new Event('change', { bubbles: true })); + }); + }); +}; +const getData = (context) => { + const data = {}; + context.formElement?.querySelectorAll("[data-cms-form-field-type='collection'] input[name]").forEach(input => { + data[input.name] = { + type: 'collection', + value: input.value + }; + }); + return data; +}; +export const CollectionField = { + markup: createCollectionField, + init, + data: getData +}; diff --git a/modules/ui-module/src/main/resources/manager/js/modules/form/forms.js b/modules/ui-module/src/main/resources/manager/js/modules/form/forms.js index a65313914..ba94036ab 100644 --- a/modules/ui-module/src/main/resources/manager/js/modules/form/forms.js +++ b/modules/ui-module/src/main/resources/manager/js/modules/form/forms.js @@ -37,6 +37,7 @@ import { MediaField } from "@cms/modules/form/field.media.js"; import { ListField } from "@cms/modules/form/field.list.js"; import { TextAreaField } from "@cms/modules/form/field.textarea.js"; import { ReferenceField } from "@cms/modules/form/field.reference.js"; +import { CollectionField } from "@cms/modules/form/field.collection.js"; import { TagsField } from "@cms/modules/form/field.tags.js"; import { i18n } from "@cms/modules/localization.js"; const getFormFields = (definition) => { @@ -101,6 +102,8 @@ const createForm = (options) => { return TextAreaField.markup(field, val); case 'reference': return ReferenceField.markup(field, val); + case 'collection': + return CollectionField.markup(field, val); case 'tags': return TagsField.markup(field, val); default: @@ -227,6 +230,7 @@ const createForm = (options) => { MediaField.init(context); ListField.init(context); ReferenceField.init(context); + CollectionField.init(context); TagsField.init(context); markRequiredFields(); }; @@ -253,6 +257,7 @@ const createForm = (options) => { ...ListField.data(context), ...TextAreaField.data(context), ...ReferenceField.data(context), + ...CollectionField.data(context), ...TagsField.data(context) }; return data; diff --git a/modules/ui-module/src/main/ts/src/js/modules/collection-picker.ts b/modules/ui-module/src/main/ts/src/js/modules/collection-picker.ts new file mode 100644 index 000000000..d23f2bff7 --- /dev/null +++ b/modules/ui-module/src/main/ts/src/js/modules/collection-picker.ts @@ -0,0 +1,173 @@ +/*- + * #%L + * UI Module + * %% + * Copyright (C) 2023 - 2026 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * #L% + */ + +import { i18n } from '@cms/modules/localization.js'; +import { openModal } from '@cms/modules/modal.js'; +import { + CollectionItemSummary, + CollectionItemsPage, + listCollectionItems +} from '@cms/modules/rpc/rpc-collection.js'; + +const PAGE_SIZE = 10; + +export interface CollectionItemPickerOptions { + collection: string; + title?: string; + selectText?: string; + onSelect: (item: CollectionItemSummary) => void | Promise; +} + +const escapeHtml = (value: unknown): string => String(value ?? '') + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); + +const renderMessage = (message: string): string => + `

${escapeHtml(message)}

`; + +const renderResults = (response: CollectionItemsPage, selectText: string): string => { + if (response.items.length === 0) { + return renderMessage(i18n.t('collection.picker.noResults', 'No collection items found.')); + } + + const page = Math.max(1, response.page); + const totalPages = Math.max(1, response.totalPages); + return ` +
+ ${response.items.map((item, index) => ` + `).join('')} +
+
+ + + ${escapeHtml(i18n.t('collection.picker.page', 'Page'))} ${page} / ${totalPages} + + +
`; +}; + +export const openCollectionItemPicker = (options: CollectionItemPickerOptions): void => { + let modal: any; + let requestVersion = 0; + let debounceTimer: number | undefined; + let currentPage = 1; + const inputId = `cms-collection-picker-${Date.now()}`; + const selectText = options.selectText || i18n.t('collection.picker.select', 'Select'); + + modal = openModal({ + title: options.title || i18n.t('collection.picker.title', 'Select collection item'), + body: ` + + +
`, + fullscreen: false, + size: 'lg', + showFooter: false, + onShow: (modalElement: HTMLElement) => { + const input = modalElement.querySelector(`#${inputId}`) as HTMLInputElement; + const resultsElement = modalElement.querySelector('[data-collection-picker-results]') as HTMLElement; + + const loadPage = async () => { + const version = ++requestVersion; + resultsElement.innerHTML = ` +
+ + ${escapeHtml(i18n.t('collection.picker.loading', 'Loading collection items...'))} +
`; + try { + const response = await listCollectionItems({ + collection: options.collection, + query: input.value.trim() || undefined, + page: currentPage, + size: PAGE_SIZE + }); + if (version !== requestVersion) return; + + currentPage = response.page; + resultsElement.innerHTML = renderResults(response, selectText); + resultsElement.querySelectorAll('[data-collection-picker-index]').forEach(button => { + button.addEventListener('click', async () => { + const item = response.items[Number(button.dataset.collectionPickerIndex)]; + if (!item) return; + await options.onSelect(item); + modal.hide(); + }); + }); + resultsElement.querySelector('[data-collection-picker-previous]') + ?.addEventListener('click', () => { + currentPage--; + loadPage(); + }); + resultsElement.querySelector('[data-collection-picker-next]') + ?.addEventListener('click', () => { + currentPage++; + loadPage(); + }); + } catch (error) { + if (version !== requestVersion) return; + resultsElement.innerHTML = ` +
+ ${escapeHtml(i18n.t('collection.picker.loadError', 'Could not load collection items.'))} +
`; + } + }; + + input.addEventListener('input', () => { + window.clearTimeout(debounceTimer); + debounceTimer = window.setTimeout(() => { + currentPage = 1; + loadPage(); + }, 300); + }); + input.addEventListener('keydown', event => { + if (event.key === 'Enter') { + event.preventDefault(); + window.clearTimeout(debounceTimer); + currentPage = 1; + loadPage(); + } + }); + + loadPage(); + input.focus(); + } + }); +}; diff --git a/modules/ui-module/src/main/ts/src/js/modules/form/field.collection.ts b/modules/ui-module/src/main/ts/src/js/modules/form/field.collection.ts new file mode 100644 index 000000000..5e5656906 --- /dev/null +++ b/modules/ui-module/src/main/ts/src/js/modules/form/field.collection.ts @@ -0,0 +1,114 @@ +/*- + * #%L + * UI Module + * %% + * Copyright (C) 2023 - 2026 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * #L% + */ + +import { openCollectionItemPicker } from '@cms/modules/collection-picker.js'; +import { FieldOptions, FormContext, FormField } from '@cms/modules/form/forms.js'; +import { createID } from '@cms/modules/form/utils.js'; +import { i18n } from '@cms/modules/localization.js'; + +export interface CollectionFieldOptions extends FieldOptions { + options?: { + collection?: string; + }; +} + +const escapeHtml = (value: unknown): string => String(value ?? '') + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); + +const createCollectionField = (options: CollectionFieldOptions, value: string = ''): string => { + const id = createID(); + const key = `field.${options.name}`; + const title = i18n.t(key, options.title); + const collection = options.options?.collection || ''; + const disabled = collection ? '' : ' disabled'; + + return ` +
+ +
+ + + +
+ ${collection ? '' : `
${escapeHtml(i18n.t( + 'collection.field.missingCollection', + 'No collection is configured for this field.' + ))}
`} +
`; +}; + +const init = (context: FormContext): void => { + const formElement = context.formElement; + if (!formElement) return; + + formElement.querySelectorAll("[data-cms-form-field-type='collection']").forEach(wrapper => { + const input = wrapper.querySelector('.cms-collection-input-value') as HTMLInputElement | null; + const selectButton = wrapper.querySelector('.cms-collection-select') as HTMLButtonElement | null; + const clearButton = wrapper.querySelector('.cms-collection-clear') as HTMLButtonElement | null; + const collection = wrapper.dataset.cmsCollection; + if (!input || !selectButton || !clearButton || !collection) return; + + selectButton.addEventListener('click', () => { + openCollectionItemPicker({ + collection, + title: i18n.t('collection.field.dialogTitle', `Select item from ${collection}`), + onSelect: item => { + input.value = item.id; + input.dispatchEvent(new Event('change', { bubbles: true })); + } + }); + }); + clearButton.addEventListener('click', () => { + input.value = ''; + input.dispatchEvent(new Event('change', { bubbles: true })); + }); + }); +}; + +const getData = (context: FormContext): Record => { + const data: Record = {}; + context.formElement?.querySelectorAll( + "[data-cms-form-field-type='collection'] input[name]" + ).forEach(input => { + data[input.name] = { + type: 'collection', + value: input.value + }; + }); + return data; +}; + +export const CollectionField = { + markup: createCollectionField, + init, + data: getData +} as FormField; diff --git a/modules/ui-module/src/main/ts/src/js/modules/form/forms.ts b/modules/ui-module/src/main/ts/src/js/modules/form/forms.ts index 914d9fe5f..33237642a 100644 --- a/modules/ui-module/src/main/ts/src/js/modules/form/forms.ts +++ b/modules/ui-module/src/main/ts/src/js/modules/form/forms.ts @@ -37,6 +37,7 @@ import { MediaField } from "@cms/modules/form/field.media.js"; import { ListField } from "@cms/modules/form/field.list.js"; import { TextAreaField } from "@cms/modules/form/field.textarea.js"; import { ReferenceField } from "@cms/modules/form/field.reference.js"; +import { CollectionField } from "@cms/modules/form/field.collection.js"; import { TagsField } from "@cms/modules/form/field.tags.js"; import { i18n } from "@cms/modules/localization.js"; @@ -107,6 +108,8 @@ const createForm = (options : any) : Form => { return TextAreaField.markup(field, val) case 'reference': return ReferenceField.markup(field, val) + case 'collection': + return CollectionField.markup(field, val) case 'tags': return TagsField.markup(field, val) default: @@ -239,6 +242,7 @@ const createForm = (options : any) : Form => { MediaField.init(context) ListField.init(context) ReferenceField.init(context) + CollectionField.init(context) TagsField.init(context) markRequiredFields(); }; @@ -266,6 +270,7 @@ const createForm = (options : any) : Form => { ...ListField.data(context), ...TextAreaField.data(context), ...ReferenceField.data(context), + ...CollectionField.data(context), ...TagsField.data(context) }; return data diff --git a/test-server/hosts/demo/content/index.md b/test-server/hosts/demo/content/index.md index 7c70d3360..e36bb7312 100644 --- a/test-server/hosts/demo/content/index.md +++ b/test-server/hosts/demo/content/index.md @@ -41,6 +41,7 @@ taxonomy: - kinderkleidung - Small Test - New tag +author: thorsten --- # Demo Project diff --git a/test-server/themes/demo/extensions/theme.manager.js b/test-server/themes/demo/extensions/theme.manager.js index 6f1537865..e923102d9 100644 --- a/test-server/themes/demo/extensions/theme.manager.js +++ b/test-server/themes/demo/extensions/theme.manager.js @@ -98,6 +98,14 @@ $hooks.registerFilter("manager/contentTypes/register", (contentTypes) => { name: "linked_page", title: "Verlinkte Seite" }, + { + type: "collection", + name: "author", + title: "Autor", + options: { + collection: "authors" + } + }, { type: "media", name: "media_url", From 34b7075e13b05ec2f0c43a50ddb06dc0b7f227aa Mon Sep 17 00:00:00 2001 From: Thorsten Marx Date: Sun, 30 Aug 2026 21:46:17 +0200 Subject: [PATCH 15/28] update typescript module --- .../manager/actions/access/manage-roles.d.ts | 20 ---------------- .../manager/actions/access/manage-users.d.ts | 20 ---------------- .../collection/create-collection-item.d.ts | 20 ---------------- .../collection/edit-collection-item.d.ts | 20 ---------------- .../actions/collection/manage-collection.d.ts | 20 ---------------- .../actions/media/edit-focal-point.d.ts | 20 ---------------- .../actions/media/edit-media-form.d.ts | 20 ---------------- .../actions/media/select-content-media.d.ts | 20 ---------------- .../manager/actions/media/select-media.d.ts | 20 ---------------- .../manager/actions/menu/manage-menus.d.ts | 20 ---------------- .../manager/actions/page/add-section.d.ts | 20 ---------------- .../manager/actions/page/create-node.d.ts | 20 ---------------- .../manager/actions/page/create-page.d.ts | 20 ---------------- .../manager/actions/page/create-variant.d.ts | 20 ---------------- .../manager/actions/page/delete-section.d.ts | 20 ---------------- .../manager/actions/page/edit-content.d.ts | 20 ---------------- .../actions/page/edit-metaattribute-form.d.ts | 20 ---------------- .../actions/page/edit-metaattribute-list.d.ts | 20 ---------------- .../actions/page/edit-metaattribute.d.ts | 20 ---------------- .../actions/page/edit-page-settings.d.ts | 20 ---------------- .../manager/actions/page/edit-schedule.d.ts | 20 ---------------- .../manager/actions/page/edit-sections.d.ts | 20 ---------------- .../actions/page/list-unpublished-pages.d.ts | 20 ---------------- .../manager/actions/page/manage-assets.d.ts | 20 ---------------- .../manager/actions/page/search-pages.d.ts | 20 ---------------- .../actions/page/section-set-published.d.ts | 20 ---------------- .../manager/actions/page/translations.d.ts | 20 ---------------- .../actions/page/variant-selector.d.ts | 20 ---------------- .../manager/actions/page/variants.d.ts | 20 ---------------- .../actions/page/wf-run-transition.d.ts | 20 ---------------- .../manager/actions/reload-preview.d.ts | 20 ---------------- .../manager/actions/site-change.d.ts | 20 ---------------- .../manager/actions/test-command.d.ts | 20 ---------------- .../resources/manager/js/manager-globals.d.ts | 20 ---------------- .../manager/js/manager-inject-init.d.ts | 20 ---------------- .../resources/manager/js/manager-inject.d.ts | 20 ---------------- .../main/resources/manager/js/manager.d.ts | 20 ---------------- .../resources/manager/js/modules/alerts.d.ts | 20 ---------------- .../resources/manager/js/modules/apps.d.ts | 20 ---------------- .../manager/js/modules/collection-picker.d.ts | 20 ---------------- .../manager/js/modules/collection-picker.js | 24 ++++++++++--------- .../manager/js/modules/event-bus.d.ts | 20 ---------------- .../filebrowser/filebrowser.actions.d.ts | 20 ---------------- .../filebrowser/filebrowser.create.d.ts | 20 ---------------- .../js/modules/filebrowser/filebrowser.d.ts | 20 ---------------- .../filebrowser/filebrowser.template.d.ts | 20 ---------------- .../js/modules/form/field.checkbox.d.ts | 20 ---------------- .../manager/js/modules/form/field.checkbox.js | 20 ---------------- .../manager/js/modules/form/field.code.d.ts | 20 ---------------- .../js/modules/form/field.collection.d.ts | 20 ---------------- .../manager/js/modules/form/field.color.d.ts | 20 ---------------- .../manager/js/modules/form/field.date.d.ts | 20 ---------------- .../js/modules/form/field.datetime.d.ts | 20 ---------------- .../js/modules/form/field.divider.d.ts | 20 ---------------- .../js/modules/form/field.easymde.d.ts | 20 ---------------- .../manager/js/modules/form/field.list.d.ts | 20 ---------------- .../manager/js/modules/form/field.mail.d.ts | 20 ---------------- .../js/modules/form/field.markdown.d.ts | 20 ---------------- .../manager/js/modules/form/field.media.d.ts | 20 ---------------- .../manager/js/modules/form/field.number.d.ts | 20 ---------------- .../manager/js/modules/form/field.radio.d.ts | 20 ---------------- .../manager/js/modules/form/field.range.d.ts | 20 ---------------- .../js/modules/form/field.reference.d.ts | 20 ---------------- .../manager/js/modules/form/field.select.d.ts | 20 ---------------- .../manager/js/modules/form/field.tags.d.ts | 20 ---------------- .../manager/js/modules/form/field.text.d.ts | 20 ---------------- .../js/modules/form/field.textarea.d.ts | 20 ---------------- .../manager/js/modules/form/forms.d.ts | 20 ---------------- .../manager/js/modules/form/utils.d.ts | 20 ---------------- .../manager/js/modules/frameMessenger.d.ts | 20 ---------------- .../manager/js/modules/locale-utils.d.ts | 20 ---------------- .../js/modules/localization-actions.d.ts | 20 ---------------- .../js/modules/localization-loader.d.ts | 20 ---------------- .../js/modules/localization-modules.d.ts | 20 ---------------- .../manager/js/modules/localization.d.ts | 20 ---------------- .../manager/js/modules/manager-ui.d.ts | 20 ---------------- .../manager/manager.message.handlers.d.ts | 20 ---------------- .../js/modules/manager/media.inject.d.ts | 20 ---------------- .../js/modules/manager/toolbar-icons.d.ts | 20 ---------------- .../js/modules/manager/toolbar.inject.d.ts | 20 ---------------- .../modules/media/mediabrowser.actions.d.ts | 20 ---------------- .../js/modules/media/mediabrowser.create.d.ts | 20 ---------------- .../js/modules/media/mediabrowser.d.ts | 20 ---------------- .../modules/media/mediabrowser.template.d.ts | 20 ---------------- .../js/modules/media/mediabrowser.upload.d.ts | 20 ---------------- .../resources/manager/js/modules/modal.d.ts | 20 ---------------- .../resources/manager/js/modules/node.d.ts | 20 ---------------- .../manager/js/modules/page-picker.d.ts | 20 ---------------- .../manager/js/modules/preview-context.d.ts | 20 ---------------- .../manager/js/modules/preview.history.d.ts | 20 ---------------- .../manager/js/modules/preview.utils.d.ts | 20 ---------------- .../manager/js/modules/rpc/rpc-access.d.ts | 20 ---------------- .../js/modules/rpc/rpc-collection.d.ts | 20 ---------------- .../manager/js/modules/rpc/rpc-content.d.ts | 20 ---------------- .../manager/js/modules/rpc/rpc-files.d.ts | 20 ---------------- .../manager/js/modules/rpc/rpc-i18n.d.ts | 20 ---------------- .../manager/js/modules/rpc/rpc-manager.d.ts | 20 ---------------- .../manager/js/modules/rpc/rpc-media.d.ts | 20 ---------------- .../manager/js/modules/rpc/rpc-menu.d.ts | 20 ---------------- .../manager/js/modules/rpc/rpc-page.d.ts | 20 ---------------- .../manager/js/modules/rpc/rpc-taxonomy.d.ts | 20 ---------------- .../js/modules/rpc/rpc-translation.d.ts | 20 ---------------- .../manager/js/modules/rpc/rpc-variant.d.ts | 20 ---------------- .../manager/js/modules/rpc/rpc-workflow.d.ts | 20 ---------------- .../resources/manager/js/modules/rpc/rpc.d.ts | 20 ---------------- .../resources/manager/js/modules/sidebar.d.ts | 20 ---------------- .../resources/manager/js/modules/state.d.ts | 20 ---------------- .../resources/manager/js/modules/toast.d.ts | 20 ---------------- .../manager/js/modules/ui-state.d.ts | 20 ---------------- .../resources/manager/js/modules/upload.d.ts | 20 ---------------- .../resources/manager/js/modules/utils.d.ts | 20 ---------------- .../resources/manager/js/modules/wizard.d.ts | 20 ---------------- .../main/resources/manager/js/ui-actions.d.ts | 20 ---------------- .../manager/public/manager-login.d.ts | 20 ---------------- .../ts/src/js/modules/collection-picker.ts | 24 ++++++++++--------- 115 files changed, 26 insertions(+), 2282 deletions(-) diff --git a/modules/ui-module/src/main/resources/manager/actions/access/manage-roles.d.ts b/modules/ui-module/src/main/resources/manager/actions/access/manage-roles.d.ts index a83d0b4a2..cd3d5445c 100644 --- a/modules/ui-module/src/main/resources/manager/actions/access/manage-roles.d.ts +++ b/modules/ui-module/src/main/resources/manager/actions/access/manage-roles.d.ts @@ -1,21 +1 @@ -/*- - * #%L - * UI Module - * %% - * Copyright (C) 2023 - 2026 CondationCMS - * %% - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - * #L% - */ export declare const runAction: () => Promise; diff --git a/modules/ui-module/src/main/resources/manager/actions/access/manage-users.d.ts b/modules/ui-module/src/main/resources/manager/actions/access/manage-users.d.ts index a83d0b4a2..cd3d5445c 100644 --- a/modules/ui-module/src/main/resources/manager/actions/access/manage-users.d.ts +++ b/modules/ui-module/src/main/resources/manager/actions/access/manage-users.d.ts @@ -1,21 +1 @@ -/*- - * #%L - * UI Module - * %% - * Copyright (C) 2023 - 2026 CondationCMS - * %% - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - * #L% - */ export declare const runAction: () => Promise; diff --git a/modules/ui-module/src/main/resources/manager/actions/collection/create-collection-item.d.ts b/modules/ui-module/src/main/resources/manager/actions/collection/create-collection-item.d.ts index 9214742d6..5a2a46bb9 100644 --- a/modules/ui-module/src/main/resources/manager/actions/collection/create-collection-item.d.ts +++ b/modules/ui-module/src/main/resources/manager/actions/collection/create-collection-item.d.ts @@ -1,23 +1,3 @@ -/*- - * #%L - * UI Module - * %% - * Copyright (C) 2023 - 2026 CondationCMS - * %% - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - * #L% - */ import { CollectionItemSummary } from '@cms/modules/rpc/rpc-collection.js'; export interface CreateCollectionItemOptions { collection: string; diff --git a/modules/ui-module/src/main/resources/manager/actions/collection/edit-collection-item.d.ts b/modules/ui-module/src/main/resources/manager/actions/collection/edit-collection-item.d.ts index 57358ee23..3c6789f99 100644 --- a/modules/ui-module/src/main/resources/manager/actions/collection/edit-collection-item.d.ts +++ b/modules/ui-module/src/main/resources/manager/actions/collection/edit-collection-item.d.ts @@ -1,23 +1,3 @@ -/*- - * #%L - * UI Module - * %% - * Copyright (C) 2023 - 2026 CondationCMS - * %% - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - * #L% - */ import { Form } from '@cms/modules/form/forms.js'; import { CollectionType } from '@cms/modules/rpc/rpc-manager.js'; export declare const collectionForm: (types: CollectionType[], collection: string, mode?: "create" | "edit") => any; diff --git a/modules/ui-module/src/main/resources/manager/actions/collection/manage-collection.d.ts b/modules/ui-module/src/main/resources/manager/actions/collection/manage-collection.d.ts index 4a16720aa..a317e2de0 100644 --- a/modules/ui-module/src/main/resources/manager/actions/collection/manage-collection.d.ts +++ b/modules/ui-module/src/main/resources/manager/actions/collection/manage-collection.d.ts @@ -1,23 +1,3 @@ -/*- - * #%L - * UI Module - * %% - * Copyright (C) 2023 - 2026 CondationCMS - * %% - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - * #L% - */ export declare const runAction: (options: { collection: string; }) => Promise; diff --git a/modules/ui-module/src/main/resources/manager/actions/media/edit-focal-point.d.ts b/modules/ui-module/src/main/resources/manager/actions/media/edit-focal-point.d.ts index 6dc2ed246..85620d3ab 100644 --- a/modules/ui-module/src/main/resources/manager/actions/media/edit-focal-point.d.ts +++ b/modules/ui-module/src/main/resources/manager/actions/media/edit-focal-point.d.ts @@ -1,21 +1 @@ -/*- - * #%L - * UI Module - * %% - * Copyright (C) 2023 - 2026 CondationCMS - * %% - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - * #L% - */ export declare function runAction(params: any): Promise; diff --git a/modules/ui-module/src/main/resources/manager/actions/media/edit-media-form.d.ts b/modules/ui-module/src/main/resources/manager/actions/media/edit-media-form.d.ts index 1dc39e464..6abc321a1 100644 --- a/modules/ui-module/src/main/resources/manager/actions/media/edit-media-form.d.ts +++ b/modules/ui-module/src/main/resources/manager/actions/media/edit-media-form.d.ts @@ -1,21 +1 @@ -/*- - * #%L - * UI Module - * %% - * Copyright (C) 2023 - 2026 CondationCMS - * %% - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - * #L% - */ export function runAction(params: any): Promise; diff --git a/modules/ui-module/src/main/resources/manager/actions/media/select-content-media.d.ts b/modules/ui-module/src/main/resources/manager/actions/media/select-content-media.d.ts index 6dc2ed246..85620d3ab 100644 --- a/modules/ui-module/src/main/resources/manager/actions/media/select-content-media.d.ts +++ b/modules/ui-module/src/main/resources/manager/actions/media/select-content-media.d.ts @@ -1,21 +1 @@ -/*- - * #%L - * UI Module - * %% - * Copyright (C) 2023 - 2026 CondationCMS - * %% - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - * #L% - */ export declare function runAction(params: any): Promise; diff --git a/modules/ui-module/src/main/resources/manager/actions/media/select-media.d.ts b/modules/ui-module/src/main/resources/manager/actions/media/select-media.d.ts index 6dc2ed246..85620d3ab 100644 --- a/modules/ui-module/src/main/resources/manager/actions/media/select-media.d.ts +++ b/modules/ui-module/src/main/resources/manager/actions/media/select-media.d.ts @@ -1,21 +1 @@ -/*- - * #%L - * UI Module - * %% - * Copyright (C) 2023 - 2026 CondationCMS - * %% - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - * #L% - */ export declare function runAction(params: any): Promise; diff --git a/modules/ui-module/src/main/resources/manager/actions/menu/manage-menus.d.ts b/modules/ui-module/src/main/resources/manager/actions/menu/manage-menus.d.ts index a83d0b4a2..cd3d5445c 100644 --- a/modules/ui-module/src/main/resources/manager/actions/menu/manage-menus.d.ts +++ b/modules/ui-module/src/main/resources/manager/actions/menu/manage-menus.d.ts @@ -1,21 +1 @@ -/*- - * #%L - * UI Module - * %% - * Copyright (C) 2023 - 2026 CondationCMS - * %% - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - * #L% - */ export declare const runAction: () => Promise; diff --git a/modules/ui-module/src/main/resources/manager/actions/page/add-section.d.ts b/modules/ui-module/src/main/resources/manager/actions/page/add-section.d.ts index 1dc39e464..6abc321a1 100644 --- a/modules/ui-module/src/main/resources/manager/actions/page/add-section.d.ts +++ b/modules/ui-module/src/main/resources/manager/actions/page/add-section.d.ts @@ -1,21 +1 @@ -/*- - * #%L - * UI Module - * %% - * Copyright (C) 2023 - 2026 CondationCMS - * %% - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - * #L% - */ export function runAction(params: any): Promise; diff --git a/modules/ui-module/src/main/resources/manager/actions/page/create-node.d.ts b/modules/ui-module/src/main/resources/manager/actions/page/create-node.d.ts index 1dc39e464..6abc321a1 100644 --- a/modules/ui-module/src/main/resources/manager/actions/page/create-node.d.ts +++ b/modules/ui-module/src/main/resources/manager/actions/page/create-node.d.ts @@ -1,21 +1 @@ -/*- - * #%L - * UI Module - * %% - * Copyright (C) 2023 - 2026 CondationCMS - * %% - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - * #L% - */ export function runAction(params: any): Promise; diff --git a/modules/ui-module/src/main/resources/manager/actions/page/create-page.d.ts b/modules/ui-module/src/main/resources/manager/actions/page/create-page.d.ts index 1dc39e464..6abc321a1 100644 --- a/modules/ui-module/src/main/resources/manager/actions/page/create-page.d.ts +++ b/modules/ui-module/src/main/resources/manager/actions/page/create-page.d.ts @@ -1,21 +1 @@ -/*- - * #%L - * UI Module - * %% - * Copyright (C) 2023 - 2026 CondationCMS - * %% - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - * #L% - */ export function runAction(params: any): Promise; diff --git a/modules/ui-module/src/main/resources/manager/actions/page/create-variant.d.ts b/modules/ui-module/src/main/resources/manager/actions/page/create-variant.d.ts index a83d0b4a2..cd3d5445c 100644 --- a/modules/ui-module/src/main/resources/manager/actions/page/create-variant.d.ts +++ b/modules/ui-module/src/main/resources/manager/actions/page/create-variant.d.ts @@ -1,21 +1 @@ -/*- - * #%L - * UI Module - * %% - * Copyright (C) 2023 - 2026 CondationCMS - * %% - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - * #L% - */ export declare const runAction: () => Promise; diff --git a/modules/ui-module/src/main/resources/manager/actions/page/delete-section.d.ts b/modules/ui-module/src/main/resources/manager/actions/page/delete-section.d.ts index 1dc39e464..6abc321a1 100644 --- a/modules/ui-module/src/main/resources/manager/actions/page/delete-section.d.ts +++ b/modules/ui-module/src/main/resources/manager/actions/page/delete-section.d.ts @@ -1,21 +1 @@ -/*- - * #%L - * UI Module - * %% - * Copyright (C) 2023 - 2026 CondationCMS - * %% - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - * #L% - */ export function runAction(params: any): Promise; diff --git a/modules/ui-module/src/main/resources/manager/actions/page/edit-content.d.ts b/modules/ui-module/src/main/resources/manager/actions/page/edit-content.d.ts index 1dc39e464..6abc321a1 100644 --- a/modules/ui-module/src/main/resources/manager/actions/page/edit-content.d.ts +++ b/modules/ui-module/src/main/resources/manager/actions/page/edit-content.d.ts @@ -1,21 +1 @@ -/*- - * #%L - * UI Module - * %% - * Copyright (C) 2023 - 2026 CondationCMS - * %% - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - * #L% - */ export function runAction(params: any): Promise; diff --git a/modules/ui-module/src/main/resources/manager/actions/page/edit-metaattribute-form.d.ts b/modules/ui-module/src/main/resources/manager/actions/page/edit-metaattribute-form.d.ts index 1dc39e464..6abc321a1 100644 --- a/modules/ui-module/src/main/resources/manager/actions/page/edit-metaattribute-form.d.ts +++ b/modules/ui-module/src/main/resources/manager/actions/page/edit-metaattribute-form.d.ts @@ -1,21 +1 @@ -/*- - * #%L - * UI Module - * %% - * Copyright (C) 2023 - 2026 CondationCMS - * %% - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - * #L% - */ export function runAction(params: any): Promise; diff --git a/modules/ui-module/src/main/resources/manager/actions/page/edit-metaattribute-list.d.ts b/modules/ui-module/src/main/resources/manager/actions/page/edit-metaattribute-list.d.ts index 1dc39e464..6abc321a1 100644 --- a/modules/ui-module/src/main/resources/manager/actions/page/edit-metaattribute-list.d.ts +++ b/modules/ui-module/src/main/resources/manager/actions/page/edit-metaattribute-list.d.ts @@ -1,21 +1 @@ -/*- - * #%L - * UI Module - * %% - * Copyright (C) 2023 - 2026 CondationCMS - * %% - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - * #L% - */ export function runAction(params: any): Promise; diff --git a/modules/ui-module/src/main/resources/manager/actions/page/edit-metaattribute.d.ts b/modules/ui-module/src/main/resources/manager/actions/page/edit-metaattribute.d.ts index 1dc39e464..6abc321a1 100644 --- a/modules/ui-module/src/main/resources/manager/actions/page/edit-metaattribute.d.ts +++ b/modules/ui-module/src/main/resources/manager/actions/page/edit-metaattribute.d.ts @@ -1,21 +1 @@ -/*- - * #%L - * UI Module - * %% - * Copyright (C) 2023 - 2026 CondationCMS - * %% - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - * #L% - */ export function runAction(params: any): Promise; diff --git a/modules/ui-module/src/main/resources/manager/actions/page/edit-page-settings.d.ts b/modules/ui-module/src/main/resources/manager/actions/page/edit-page-settings.d.ts index 1dc39e464..6abc321a1 100644 --- a/modules/ui-module/src/main/resources/manager/actions/page/edit-page-settings.d.ts +++ b/modules/ui-module/src/main/resources/manager/actions/page/edit-page-settings.d.ts @@ -1,21 +1 @@ -/*- - * #%L - * UI Module - * %% - * Copyright (C) 2023 - 2026 CondationCMS - * %% - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - * #L% - */ export function runAction(params: any): Promise; diff --git a/modules/ui-module/src/main/resources/manager/actions/page/edit-schedule.d.ts b/modules/ui-module/src/main/resources/manager/actions/page/edit-schedule.d.ts index 1dc39e464..6abc321a1 100644 --- a/modules/ui-module/src/main/resources/manager/actions/page/edit-schedule.d.ts +++ b/modules/ui-module/src/main/resources/manager/actions/page/edit-schedule.d.ts @@ -1,21 +1 @@ -/*- - * #%L - * UI Module - * %% - * Copyright (C) 2023 - 2026 CondationCMS - * %% - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - * #L% - */ export function runAction(params: any): Promise; diff --git a/modules/ui-module/src/main/resources/manager/actions/page/edit-sections.d.ts b/modules/ui-module/src/main/resources/manager/actions/page/edit-sections.d.ts index 1dc39e464..6abc321a1 100644 --- a/modules/ui-module/src/main/resources/manager/actions/page/edit-sections.d.ts +++ b/modules/ui-module/src/main/resources/manager/actions/page/edit-sections.d.ts @@ -1,21 +1 @@ -/*- - * #%L - * UI Module - * %% - * Copyright (C) 2023 - 2026 CondationCMS - * %% - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - * #L% - */ export function runAction(params: any): Promise; diff --git a/modules/ui-module/src/main/resources/manager/actions/page/list-unpublished-pages.d.ts b/modules/ui-module/src/main/resources/manager/actions/page/list-unpublished-pages.d.ts index 44d1f6c2a..8052ef2c6 100644 --- a/modules/ui-module/src/main/resources/manager/actions/page/list-unpublished-pages.d.ts +++ b/modules/ui-module/src/main/resources/manager/actions/page/list-unpublished-pages.d.ts @@ -1,23 +1,3 @@ -/*- - * #%L - * UI Module - * %% - * Copyright (C) 2023 - 2026 CondationCMS - * %% - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - * #L% - */ interface ListUnpublishedPagesOptions { page?: number; } diff --git a/modules/ui-module/src/main/resources/manager/actions/page/manage-assets.d.ts b/modules/ui-module/src/main/resources/manager/actions/page/manage-assets.d.ts index 1dc39e464..6abc321a1 100644 --- a/modules/ui-module/src/main/resources/manager/actions/page/manage-assets.d.ts +++ b/modules/ui-module/src/main/resources/manager/actions/page/manage-assets.d.ts @@ -1,21 +1 @@ -/*- - * #%L - * UI Module - * %% - * Copyright (C) 2023 - 2026 CondationCMS - * %% - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - * #L% - */ export function runAction(params: any): Promise; diff --git a/modules/ui-module/src/main/resources/manager/actions/page/search-pages.d.ts b/modules/ui-module/src/main/resources/manager/actions/page/search-pages.d.ts index 053bf8a56..fdb7a4ee9 100644 --- a/modules/ui-module/src/main/resources/manager/actions/page/search-pages.d.ts +++ b/modules/ui-module/src/main/resources/manager/actions/page/search-pages.d.ts @@ -1,23 +1,3 @@ -/*- - * #%L - * UI Module - * %% - * Copyright (C) 2023 - 2026 CondationCMS - * %% - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - * #L% - */ interface SearchPagesActionOptions { } export declare const runAction: (options?: SearchPagesActionOptions) => Promise; diff --git a/modules/ui-module/src/main/resources/manager/actions/page/section-set-published.d.ts b/modules/ui-module/src/main/resources/manager/actions/page/section-set-published.d.ts index 6dc2ed246..85620d3ab 100644 --- a/modules/ui-module/src/main/resources/manager/actions/page/section-set-published.d.ts +++ b/modules/ui-module/src/main/resources/manager/actions/page/section-set-published.d.ts @@ -1,21 +1 @@ -/*- - * #%L - * UI Module - * %% - * Copyright (C) 2023 - 2026 CondationCMS - * %% - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - * #L% - */ export declare function runAction(params: any): Promise; diff --git a/modules/ui-module/src/main/resources/manager/actions/page/translations.d.ts b/modules/ui-module/src/main/resources/manager/actions/page/translations.d.ts index 6dc2ed246..85620d3ab 100644 --- a/modules/ui-module/src/main/resources/manager/actions/page/translations.d.ts +++ b/modules/ui-module/src/main/resources/manager/actions/page/translations.d.ts @@ -1,21 +1 @@ -/*- - * #%L - * UI Module - * %% - * Copyright (C) 2023 - 2026 CondationCMS - * %% - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - * #L% - */ export declare function runAction(params: any): Promise; diff --git a/modules/ui-module/src/main/resources/manager/actions/page/variant-selector.d.ts b/modules/ui-module/src/main/resources/manager/actions/page/variant-selector.d.ts index a83d0b4a2..cd3d5445c 100644 --- a/modules/ui-module/src/main/resources/manager/actions/page/variant-selector.d.ts +++ b/modules/ui-module/src/main/resources/manager/actions/page/variant-selector.d.ts @@ -1,21 +1 @@ -/*- - * #%L - * UI Module - * %% - * Copyright (C) 2023 - 2026 CondationCMS - * %% - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - * #L% - */ export declare const runAction: () => Promise; diff --git a/modules/ui-module/src/main/resources/manager/actions/page/variants.d.ts b/modules/ui-module/src/main/resources/manager/actions/page/variants.d.ts index a83d0b4a2..cd3d5445c 100644 --- a/modules/ui-module/src/main/resources/manager/actions/page/variants.d.ts +++ b/modules/ui-module/src/main/resources/manager/actions/page/variants.d.ts @@ -1,21 +1 @@ -/*- - * #%L - * UI Module - * %% - * Copyright (C) 2023 - 2026 CondationCMS - * %% - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - * #L% - */ export declare const runAction: () => Promise; diff --git a/modules/ui-module/src/main/resources/manager/actions/page/wf-run-transition.d.ts b/modules/ui-module/src/main/resources/manager/actions/page/wf-run-transition.d.ts index 6dc2ed246..85620d3ab 100644 --- a/modules/ui-module/src/main/resources/manager/actions/page/wf-run-transition.d.ts +++ b/modules/ui-module/src/main/resources/manager/actions/page/wf-run-transition.d.ts @@ -1,21 +1 @@ -/*- - * #%L - * UI Module - * %% - * Copyright (C) 2023 - 2026 CondationCMS - * %% - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - * #L% - */ export declare function runAction(params: any): Promise; diff --git a/modules/ui-module/src/main/resources/manager/actions/reload-preview.d.ts b/modules/ui-module/src/main/resources/manager/actions/reload-preview.d.ts index 6dc2ed246..85620d3ab 100644 --- a/modules/ui-module/src/main/resources/manager/actions/reload-preview.d.ts +++ b/modules/ui-module/src/main/resources/manager/actions/reload-preview.d.ts @@ -1,21 +1 @@ -/*- - * #%L - * UI Module - * %% - * Copyright (C) 2023 - 2026 CondationCMS - * %% - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - * #L% - */ export declare function runAction(params: any): Promise; diff --git a/modules/ui-module/src/main/resources/manager/actions/site-change.d.ts b/modules/ui-module/src/main/resources/manager/actions/site-change.d.ts index 672d1f3de..ba8a4fff2 100644 --- a/modules/ui-module/src/main/resources/manager/actions/site-change.d.ts +++ b/modules/ui-module/src/main/resources/manager/actions/site-change.d.ts @@ -1,21 +1 @@ -/*- - * #%L - * UI Module - * %% - * Copyright (C) 2023 - 2026 CondationCMS - * %% - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - * #L% - */ export function runAction(parameters: any): Promise; diff --git a/modules/ui-module/src/main/resources/manager/actions/test-command.d.ts b/modules/ui-module/src/main/resources/manager/actions/test-command.d.ts index 1dc39e464..6abc321a1 100644 --- a/modules/ui-module/src/main/resources/manager/actions/test-command.d.ts +++ b/modules/ui-module/src/main/resources/manager/actions/test-command.d.ts @@ -1,21 +1 @@ -/*- - * #%L - * UI Module - * %% - * Copyright (C) 2023 - 2026 CondationCMS - * %% - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - * #L% - */ export function runAction(params: any): Promise; diff --git a/modules/ui-module/src/main/resources/manager/js/manager-globals.d.ts b/modules/ui-module/src/main/resources/manager/js/manager-globals.d.ts index 7818ef73c..255f59f32 100644 --- a/modules/ui-module/src/main/resources/manager/js/manager-globals.d.ts +++ b/modules/ui-module/src/main/resources/manager/js/manager-globals.d.ts @@ -1,23 +1,3 @@ -/*- - * #%L - * UI Module - * %% - * Copyright (C) 2023 - 2026 CondationCMS - * %% - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - * #L% - */ export function executeScriptAction(action: any): Promise; export function executeHookAction(action: any): Promise; /** diff --git a/modules/ui-module/src/main/resources/manager/js/manager-inject-init.d.ts b/modules/ui-module/src/main/resources/manager/js/manager-inject-init.d.ts index 5f66dedac..de2be067b 100644 --- a/modules/ui-module/src/main/resources/manager/js/manager-inject-init.d.ts +++ b/modules/ui-module/src/main/resources/manager/js/manager-inject-init.d.ts @@ -1,22 +1,2 @@ -/*- - * #%L - * UI Module - * %% - * Copyright (C) 2023 - 2026 CondationCMS - * %% - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - * #L% - */ export function initIframe(): void; export function isSectionPublishedExpired(section: any): boolean; diff --git a/modules/ui-module/src/main/resources/manager/js/manager-inject.d.ts b/modules/ui-module/src/main/resources/manager/js/manager-inject.d.ts index 802d6a315..e69de29bb 100644 --- a/modules/ui-module/src/main/resources/manager/js/manager-inject.d.ts +++ b/modules/ui-module/src/main/resources/manager/js/manager-inject.d.ts @@ -1,20 +0,0 @@ -/*- - * #%L - * UI Module - * %% - * Copyright (C) 2023 - 2026 CondationCMS - * %% - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - * #L% - */ diff --git a/modules/ui-module/src/main/resources/manager/js/manager.d.ts b/modules/ui-module/src/main/resources/manager/js/manager.d.ts index 4562057ed..cb0ff5c3b 100644 --- a/modules/ui-module/src/main/resources/manager/js/manager.d.ts +++ b/modules/ui-module/src/main/resources/manager/js/manager.d.ts @@ -1,21 +1 @@ -/*- - * #%L - * UI Module - * %% - * Copyright (C) 2023 - 2026 CondationCMS - * %% - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - * #L% - */ export {}; diff --git a/modules/ui-module/src/main/resources/manager/js/modules/alerts.d.ts b/modules/ui-module/src/main/resources/manager/js/modules/alerts.d.ts index c25caa88d..629a61d8f 100644 --- a/modules/ui-module/src/main/resources/manager/js/modules/alerts.d.ts +++ b/modules/ui-module/src/main/resources/manager/js/modules/alerts.d.ts @@ -1,23 +1,3 @@ -/*- - * #%L - * UI Module - * %% - * Copyright (C) 2023 - 2026 CondationCMS - * %% - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - * #L% - */ export function alertSelect(options: any): Promise; export function alertError(options: any): void; export function alertConfirm(options: any): Promise; diff --git a/modules/ui-module/src/main/resources/manager/js/modules/apps.d.ts b/modules/ui-module/src/main/resources/manager/js/modules/apps.d.ts index e864f9c21..081198908 100644 --- a/modules/ui-module/src/main/resources/manager/js/modules/apps.d.ts +++ b/modules/ui-module/src/main/resources/manager/js/modules/apps.d.ts @@ -1,21 +1 @@ -/*- - * #%L - * UI Module - * %% - * Copyright (C) 2023 - 2026 CondationCMS - * %% - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - * #L% - */ export function initApps(): void; diff --git a/modules/ui-module/src/main/resources/manager/js/modules/collection-picker.d.ts b/modules/ui-module/src/main/resources/manager/js/modules/collection-picker.d.ts index c0eba907b..2e1adce04 100644 --- a/modules/ui-module/src/main/resources/manager/js/modules/collection-picker.d.ts +++ b/modules/ui-module/src/main/resources/manager/js/modules/collection-picker.d.ts @@ -1,23 +1,3 @@ -/*- - * #%L - * UI Module - * %% - * Copyright (C) 2023 - 2026 CondationCMS - * %% - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - * #L% - */ import { CollectionItemSummary } from '@cms/modules/rpc/rpc-collection.js'; export interface CollectionItemPickerOptions { collection: string; diff --git a/modules/ui-module/src/main/resources/manager/js/modules/collection-picker.js b/modules/ui-module/src/main/resources/manager/js/modules/collection-picker.js index 2beb6ee40..0dcf1cc66 100644 --- a/modules/ui-module/src/main/resources/manager/js/modules/collection-picker.js +++ b/modules/ui-module/src/main/resources/manager/js/modules/collection-picker.js @@ -48,14 +48,16 @@ const renderResults = (response, selectText) => { `).join('')}
- ${escapeHtml(i18n.t('collection.picker.page', 'Page'))} ${page} / ${totalPages} - @@ -111,16 +113,16 @@ export const openCollectionItemPicker = (options) => { modal.hide(); }); }); - resultsElement.querySelector('[data-collection-picker-previous]') - ?.addEventListener('click', () => { - currentPage--; + resultsElement.querySelectorAll('[data-collection-picker-page]') + .forEach(button => button.addEventListener('click', () => { + const targetPage = Number(button.dataset.collectionPickerPage); + if (button.disabled || !Number.isInteger(targetPage) || targetPage < 1 + || targetPage > response.totalPages || targetPage === currentPage) { + return; + } + currentPage = targetPage; loadPage(); - }); - resultsElement.querySelector('[data-collection-picker-next]') - ?.addEventListener('click', () => { - currentPage++; - loadPage(); - }); + })); } catch (error) { if (version !== requestVersion) diff --git a/modules/ui-module/src/main/resources/manager/js/modules/event-bus.d.ts b/modules/ui-module/src/main/resources/manager/js/modules/event-bus.d.ts index 462f38dae..d44295919 100644 --- a/modules/ui-module/src/main/resources/manager/js/modules/event-bus.d.ts +++ b/modules/ui-module/src/main/resources/manager/js/modules/event-bus.d.ts @@ -1,23 +1,3 @@ -/*- - * #%L - * UI Module - * %% - * Copyright (C) 2023 - 2026 CondationCMS - * %% - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - * #L% - */ export namespace EventBus { function on(event: any, handler: any): void; function emit(event: any, payload: any): void; diff --git a/modules/ui-module/src/main/resources/manager/js/modules/filebrowser/filebrowser.actions.d.ts b/modules/ui-module/src/main/resources/manager/js/modules/filebrowser/filebrowser.actions.d.ts index e1c7c16d4..31df668e4 100644 --- a/modules/ui-module/src/main/resources/manager/js/modules/filebrowser/filebrowser.actions.d.ts +++ b/modules/ui-module/src/main/resources/manager/js/modules/filebrowser/filebrowser.actions.d.ts @@ -1,23 +1,3 @@ -/*- - * #%L - * UI Module - * %% - * Copyright (C) 2023 - 2026 CondationCMS - * %% - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - * #L% - */ export function renameFileAction({ state, getTargetFolder, filename, title, content }: { state: any; getTargetFolder: any; diff --git a/modules/ui-module/src/main/resources/manager/js/modules/filebrowser/filebrowser.create.d.ts b/modules/ui-module/src/main/resources/manager/js/modules/filebrowser/filebrowser.create.d.ts index db898301c..fb627bf7e 100644 --- a/modules/ui-module/src/main/resources/manager/js/modules/filebrowser/filebrowser.create.d.ts +++ b/modules/ui-module/src/main/resources/manager/js/modules/filebrowser/filebrowser.create.d.ts @@ -1,21 +1 @@ -/*- - * #%L - * UI Module - * %% - * Copyright (C) 2023 - 2026 CondationCMS - * %% - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - * #L% - */ export function openCreateContentBrowser(params: any): Promise; diff --git a/modules/ui-module/src/main/resources/manager/js/modules/filebrowser/filebrowser.d.ts b/modules/ui-module/src/main/resources/manager/js/modules/filebrowser/filebrowser.d.ts index 0869c773e..e10842a8d 100644 --- a/modules/ui-module/src/main/resources/manager/js/modules/filebrowser/filebrowser.d.ts +++ b/modules/ui-module/src/main/resources/manager/js/modules/filebrowser/filebrowser.d.ts @@ -1,23 +1,3 @@ -/*- - * #%L - * UI Module - * %% - * Copyright (C) 2023 - 2026 CondationCMS - * %% - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - * #L% - */ export function openFileBrowser(optionsParam: any): Promise; export namespace state { let options: null; diff --git a/modules/ui-module/src/main/resources/manager/js/modules/filebrowser/filebrowser.template.d.ts b/modules/ui-module/src/main/resources/manager/js/modules/filebrowser/filebrowser.template.d.ts index e21fdb20e..72c47da10 100644 --- a/modules/ui-module/src/main/resources/manager/js/modules/filebrowser/filebrowser.template.d.ts +++ b/modules/ui-module/src/main/resources/manager/js/modules/filebrowser/filebrowser.template.d.ts @@ -1,22 +1,2 @@ -/*- - * #%L - * UI Module - * %% - * Copyright (C) 2023 - 2026 CondationCMS - * %% - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - * #L% - */ export { template as filebrowserTemplate }; declare const template: any; diff --git a/modules/ui-module/src/main/resources/manager/js/modules/form/field.checkbox.d.ts b/modules/ui-module/src/main/resources/manager/js/modules/form/field.checkbox.d.ts index a930d60c6..d2a0c9280 100644 --- a/modules/ui-module/src/main/resources/manager/js/modules/form/field.checkbox.d.ts +++ b/modules/ui-module/src/main/resources/manager/js/modules/form/field.checkbox.d.ts @@ -1,23 +1,3 @@ -/*- - * #%L - * UI Module - * %% - * Copyright (C) 2023 - 2026 CondationCMS - * %% - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - * #L% - */ import { FieldOptions, FormField } from "@cms/modules/form/forms.js"; export interface CheckboxOptions extends FieldOptions { key?: string; diff --git a/modules/ui-module/src/main/resources/manager/js/modules/form/field.checkbox.js b/modules/ui-module/src/main/resources/manager/js/modules/form/field.checkbox.js index 6bbe7a57b..3efa4721d 100644 --- a/modules/ui-module/src/main/resources/manager/js/modules/form/field.checkbox.js +++ b/modules/ui-module/src/main/resources/manager/js/modules/form/field.checkbox.js @@ -1,23 +1,3 @@ -/*- - * #%L - * UI Module - * %% - * Copyright (C) 2023 - 2026 CondationCMS - * %% - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - * #L% - */ import { createID } from "@cms/modules/form/utils.js"; const createCheckboxField = (options, value = []) => { const id = createID(); diff --git a/modules/ui-module/src/main/resources/manager/js/modules/form/field.code.d.ts b/modules/ui-module/src/main/resources/manager/js/modules/form/field.code.d.ts index 4118facce..ada1f20f1 100644 --- a/modules/ui-module/src/main/resources/manager/js/modules/form/field.code.d.ts +++ b/modules/ui-module/src/main/resources/manager/js/modules/form/field.code.d.ts @@ -1,23 +1,3 @@ -/*- - * #%L - * UI Module - * %% - * Copyright (C) 2023 - 2026 CondationCMS - * %% - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - * #L% - */ export namespace CodeField { export { createCodeField as markup }; export { init }; diff --git a/modules/ui-module/src/main/resources/manager/js/modules/form/field.collection.d.ts b/modules/ui-module/src/main/resources/manager/js/modules/form/field.collection.d.ts index bd0aabe1c..4e23bacfb 100644 --- a/modules/ui-module/src/main/resources/manager/js/modules/form/field.collection.d.ts +++ b/modules/ui-module/src/main/resources/manager/js/modules/form/field.collection.d.ts @@ -1,23 +1,3 @@ -/*- - * #%L - * UI Module - * %% - * Copyright (C) 2023 - 2026 CondationCMS - * %% - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - * #L% - */ import { FieldOptions, FormField } from '@cms/modules/form/forms.js'; export interface CollectionFieldOptions extends FieldOptions { options?: { diff --git a/modules/ui-module/src/main/resources/manager/js/modules/form/field.color.d.ts b/modules/ui-module/src/main/resources/manager/js/modules/form/field.color.d.ts index 0225e9768..cab0d2e87 100644 --- a/modules/ui-module/src/main/resources/manager/js/modules/form/field.color.d.ts +++ b/modules/ui-module/src/main/resources/manager/js/modules/form/field.color.d.ts @@ -1,23 +1,3 @@ -/*- - * #%L - * UI Module - * %% - * Copyright (C) 2023 - 2026 CondationCMS - * %% - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - * #L% - */ import { FieldOptions, FormField } from "@cms/modules/form/forms.js"; export interface ColorFieldOptions extends FieldOptions { } diff --git a/modules/ui-module/src/main/resources/manager/js/modules/form/field.date.d.ts b/modules/ui-module/src/main/resources/manager/js/modules/form/field.date.d.ts index e45cc358c..f07e3cc6e 100644 --- a/modules/ui-module/src/main/resources/manager/js/modules/form/field.date.d.ts +++ b/modules/ui-module/src/main/resources/manager/js/modules/form/field.date.d.ts @@ -1,23 +1,3 @@ -/*- - * #%L - * UI Module - * %% - * Copyright (C) 2023 - 2026 CondationCMS - * %% - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - * #L% - */ import { FieldOptions, FormField } from "@cms/modules/form/forms.js"; export interface DateFieldOptions extends FieldOptions { placeholder?: string; diff --git a/modules/ui-module/src/main/resources/manager/js/modules/form/field.datetime.d.ts b/modules/ui-module/src/main/resources/manager/js/modules/form/field.datetime.d.ts index 13130d6a8..272202696 100644 --- a/modules/ui-module/src/main/resources/manager/js/modules/form/field.datetime.d.ts +++ b/modules/ui-module/src/main/resources/manager/js/modules/form/field.datetime.d.ts @@ -1,23 +1,3 @@ -/*- - * #%L - * UI Module - * %% - * Copyright (C) 2023 - 2026 CondationCMS - * %% - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - * #L% - */ import { FieldOptions, FormField } from "@cms/modules/form/forms.js"; export interface DateTimeFieldOptions extends FieldOptions { placeholder?: string; diff --git a/modules/ui-module/src/main/resources/manager/js/modules/form/field.divider.d.ts b/modules/ui-module/src/main/resources/manager/js/modules/form/field.divider.d.ts index d52208432..f57944c90 100644 --- a/modules/ui-module/src/main/resources/manager/js/modules/form/field.divider.d.ts +++ b/modules/ui-module/src/main/resources/manager/js/modules/form/field.divider.d.ts @@ -1,23 +1,3 @@ -/*- - * #%L - * UI Module - * %% - * Copyright (C) 2023 - 2026 CondationCMS - * %% - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - * #L% - */ import { FieldOptions, FormField } from "@cms/modules/form/forms.js"; export interface DividerOptions extends FieldOptions { } diff --git a/modules/ui-module/src/main/resources/manager/js/modules/form/field.easymde.d.ts b/modules/ui-module/src/main/resources/manager/js/modules/form/field.easymde.d.ts index 8b94a8e15..9ac807575 100644 --- a/modules/ui-module/src/main/resources/manager/js/modules/form/field.easymde.d.ts +++ b/modules/ui-module/src/main/resources/manager/js/modules/form/field.easymde.d.ts @@ -1,23 +1,3 @@ -/*- - * #%L - * UI Module - * %% - * Copyright (C) 2023 - 2026 CondationCMS - * %% - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - * #L% - */ import { FieldOptions, FormField } from "@cms/modules/form/forms.js"; export interface EasyMDEFieldOptions extends FieldOptions { } diff --git a/modules/ui-module/src/main/resources/manager/js/modules/form/field.list.d.ts b/modules/ui-module/src/main/resources/manager/js/modules/form/field.list.d.ts index eac48f3db..7bfcc69a9 100644 --- a/modules/ui-module/src/main/resources/manager/js/modules/form/field.list.d.ts +++ b/modules/ui-module/src/main/resources/manager/js/modules/form/field.list.d.ts @@ -1,23 +1,3 @@ -/*- - * #%L - * UI Module - * %% - * Copyright (C) 2023 - 2026 CondationCMS - * %% - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - * #L% - */ import { FieldOptions, FormField } from "@cms/modules/form/forms.js"; export interface ListFieldOptions extends FieldOptions { options: { diff --git a/modules/ui-module/src/main/resources/manager/js/modules/form/field.mail.d.ts b/modules/ui-module/src/main/resources/manager/js/modules/form/field.mail.d.ts index df6a6ab49..36f178178 100644 --- a/modules/ui-module/src/main/resources/manager/js/modules/form/field.mail.d.ts +++ b/modules/ui-module/src/main/resources/manager/js/modules/form/field.mail.d.ts @@ -1,23 +1,3 @@ -/*- - * #%L - * UI Module - * %% - * Copyright (C) 2023 - 2026 CondationCMS - * %% - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - * #L% - */ import { FieldOptions, FormField } from "@cms/modules/form/forms.js"; export interface MailFieldOptions extends FieldOptions { placeholder?: string; diff --git a/modules/ui-module/src/main/resources/manager/js/modules/form/field.markdown.d.ts b/modules/ui-module/src/main/resources/manager/js/modules/form/field.markdown.d.ts index 21c39aafe..32fc0b8bf 100644 --- a/modules/ui-module/src/main/resources/manager/js/modules/form/field.markdown.d.ts +++ b/modules/ui-module/src/main/resources/manager/js/modules/form/field.markdown.d.ts @@ -1,23 +1,3 @@ -/*- - * #%L - * UI Module - * %% - * Copyright (C) 2023 - 2026 CondationCMS - * %% - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - * #L% - */ import { FieldOptions, FormField } from "@cms/modules/form/forms.js"; declare global { interface HTMLInputElement { diff --git a/modules/ui-module/src/main/resources/manager/js/modules/form/field.media.d.ts b/modules/ui-module/src/main/resources/manager/js/modules/form/field.media.d.ts index 1350f89f2..b359b73f0 100644 --- a/modules/ui-module/src/main/resources/manager/js/modules/form/field.media.d.ts +++ b/modules/ui-module/src/main/resources/manager/js/modules/form/field.media.d.ts @@ -1,23 +1,3 @@ -/*- - * #%L - * UI Module - * %% - * Copyright (C) 2023 - 2026 CondationCMS - * %% - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - * #L% - */ import { FieldOptions, FormField } from "@cms/modules/form/forms.js"; export interface MediaFieldOptions extends FieldOptions { } diff --git a/modules/ui-module/src/main/resources/manager/js/modules/form/field.number.d.ts b/modules/ui-module/src/main/resources/manager/js/modules/form/field.number.d.ts index 50b700cb0..c034ebbc7 100644 --- a/modules/ui-module/src/main/resources/manager/js/modules/form/field.number.d.ts +++ b/modules/ui-module/src/main/resources/manager/js/modules/form/field.number.d.ts @@ -1,23 +1,3 @@ -/*- - * #%L - * UI Module - * %% - * Copyright (C) 2023 - 2026 CondationCMS - * %% - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - * #L% - */ import { FieldOptions, FormField } from "@cms/modules/form/forms.js"; export interface NumberFieldOptions extends FieldOptions { options: { diff --git a/modules/ui-module/src/main/resources/manager/js/modules/form/field.radio.d.ts b/modules/ui-module/src/main/resources/manager/js/modules/form/field.radio.d.ts index 344bb4fda..a238a4826 100644 --- a/modules/ui-module/src/main/resources/manager/js/modules/form/field.radio.d.ts +++ b/modules/ui-module/src/main/resources/manager/js/modules/form/field.radio.d.ts @@ -1,23 +1,3 @@ -/*- - * #%L - * UI Module - * %% - * Copyright (C) 2023 - 2026 CondationCMS - * %% - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - * #L% - */ import { FieldOptions, FormField } from "@cms/modules/form/forms.js"; export interface RadioFieldOptions extends FieldOptions { options?: { diff --git a/modules/ui-module/src/main/resources/manager/js/modules/form/field.range.d.ts b/modules/ui-module/src/main/resources/manager/js/modules/form/field.range.d.ts index e57f403cf..47e6c847c 100644 --- a/modules/ui-module/src/main/resources/manager/js/modules/form/field.range.d.ts +++ b/modules/ui-module/src/main/resources/manager/js/modules/form/field.range.d.ts @@ -1,23 +1,3 @@ -/*- - * #%L - * UI Module - * %% - * Copyright (C) 2023 - 2026 CondationCMS - * %% - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - * #L% - */ import { FieldOptions, FormField } from "@cms/modules/form/forms.js"; export interface RangeFieldOptions extends FieldOptions { options?: { diff --git a/modules/ui-module/src/main/resources/manager/js/modules/form/field.reference.d.ts b/modules/ui-module/src/main/resources/manager/js/modules/form/field.reference.d.ts index fc0de6ab4..7f1b0b8c4 100644 --- a/modules/ui-module/src/main/resources/manager/js/modules/form/field.reference.d.ts +++ b/modules/ui-module/src/main/resources/manager/js/modules/form/field.reference.d.ts @@ -1,23 +1,3 @@ -/*- - * #%L - * UI Module - * %% - * Copyright (C) 2023 - 2026 CondationCMS - * %% - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - * #L% - */ import { FieldOptions, FormField } from "@cms/modules/form/forms.js"; export interface ReferenceFieldOptions extends FieldOptions { options?: { diff --git a/modules/ui-module/src/main/resources/manager/js/modules/form/field.select.d.ts b/modules/ui-module/src/main/resources/manager/js/modules/form/field.select.d.ts index 5ae22bd43..c781deccb 100644 --- a/modules/ui-module/src/main/resources/manager/js/modules/form/field.select.d.ts +++ b/modules/ui-module/src/main/resources/manager/js/modules/form/field.select.d.ts @@ -1,23 +1,3 @@ -/*- - * #%L - * UI Module - * %% - * Copyright (C) 2023 - 2026 CondationCMS - * %% - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - * #L% - */ import { FieldOptions, FormField } from "@cms/modules/form/forms.js"; export interface SelectFieldOptions extends FieldOptions { options?: { diff --git a/modules/ui-module/src/main/resources/manager/js/modules/form/field.tags.d.ts b/modules/ui-module/src/main/resources/manager/js/modules/form/field.tags.d.ts index 634fb842a..50f7e6d01 100644 --- a/modules/ui-module/src/main/resources/manager/js/modules/form/field.tags.d.ts +++ b/modules/ui-module/src/main/resources/manager/js/modules/form/field.tags.d.ts @@ -1,23 +1,3 @@ -/*- - * #%L - * UI Module - * %% - * Copyright (C) 2023 - 2026 CondationCMS - * %% - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - * #L% - */ import { FieldOptions, FormField } from "@cms/modules/form/forms.js"; export interface TagsFieldOptions extends FieldOptions { options?: { diff --git a/modules/ui-module/src/main/resources/manager/js/modules/form/field.text.d.ts b/modules/ui-module/src/main/resources/manager/js/modules/form/field.text.d.ts index 7a72faae8..b406c7cf8 100644 --- a/modules/ui-module/src/main/resources/manager/js/modules/form/field.text.d.ts +++ b/modules/ui-module/src/main/resources/manager/js/modules/form/field.text.d.ts @@ -1,23 +1,3 @@ -/*- - * #%L - * UI Module - * %% - * Copyright (C) 2023 - 2026 CondationCMS - * %% - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - * #L% - */ import { FieldOptions, FormField } from "@cms/modules/form/forms.js"; export interface TextFieldOptions extends FieldOptions { placeholder?: string; diff --git a/modules/ui-module/src/main/resources/manager/js/modules/form/field.textarea.d.ts b/modules/ui-module/src/main/resources/manager/js/modules/form/field.textarea.d.ts index 4b4659e7b..f83d21361 100644 --- a/modules/ui-module/src/main/resources/manager/js/modules/form/field.textarea.d.ts +++ b/modules/ui-module/src/main/resources/manager/js/modules/form/field.textarea.d.ts @@ -1,23 +1,3 @@ -/*- - * #%L - * UI Module - * %% - * Copyright (C) 2023 - 2026 CondationCMS - * %% - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - * #L% - */ import { FieldOptions, FormField } from "@cms/modules/form/forms.js"; export interface TextAreaFieldOptions extends FieldOptions { rows?: number; diff --git a/modules/ui-module/src/main/resources/manager/js/modules/form/forms.d.ts b/modules/ui-module/src/main/resources/manager/js/modules/form/forms.d.ts index 66d90daed..869e84f19 100644 --- a/modules/ui-module/src/main/resources/manager/js/modules/form/forms.d.ts +++ b/modules/ui-module/src/main/resources/manager/js/modules/form/forms.d.ts @@ -1,23 +1,3 @@ -/*- - * #%L - * UI Module - * %% - * Copyright (C) 2023 - 2026 CondationCMS - * %% - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - * #L% - */ declare const getFormFields: (definition: any) => any[]; declare const createForm: (options: any) => Form; export { createForm, getFormFields }; diff --git a/modules/ui-module/src/main/resources/manager/js/modules/form/utils.d.ts b/modules/ui-module/src/main/resources/manager/js/modules/form/utils.d.ts index 23eeb2258..525b147b7 100644 --- a/modules/ui-module/src/main/resources/manager/js/modules/form/utils.d.ts +++ b/modules/ui-module/src/main/resources/manager/js/modules/form/utils.d.ts @@ -1,23 +1,3 @@ -/*- - * #%L - * UI Module - * %% - * Copyright (C) 2023 - 2026 CondationCMS - * %% - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - * #L% - */ declare const createID: () => string; declare const utcToLocalDateTimeInputValue: (utcString: string) => string; declare function getUTCDateTimeFromInput(inputElement: HTMLInputElement): string | null; diff --git a/modules/ui-module/src/main/resources/manager/js/modules/frameMessenger.d.ts b/modules/ui-module/src/main/resources/manager/js/modules/frameMessenger.d.ts index cbd1acc3c..024ff6c30 100644 --- a/modules/ui-module/src/main/resources/manager/js/modules/frameMessenger.d.ts +++ b/modules/ui-module/src/main/resources/manager/js/modules/frameMessenger.d.ts @@ -1,23 +1,3 @@ -/*- - * #%L - * UI Module - * %% - * Copyright (C) 2023 - 2026 CondationCMS - * %% - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - * #L% - */ export interface FrameMessage { type: string; payload?: T; diff --git a/modules/ui-module/src/main/resources/manager/js/modules/locale-utils.d.ts b/modules/ui-module/src/main/resources/manager/js/modules/locale-utils.d.ts index aca04a87b..98374290b 100644 --- a/modules/ui-module/src/main/resources/manager/js/modules/locale-utils.d.ts +++ b/modules/ui-module/src/main/resources/manager/js/modules/locale-utils.d.ts @@ -1,22 +1,2 @@ -/*- - * #%L - * UI Module - * %% - * Copyright (C) 2023 - 2026 CondationCMS - * %% - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - * #L% - */ export function getLocale(): any; export function setLocale(locale: any): void; diff --git a/modules/ui-module/src/main/resources/manager/js/modules/localization-actions.d.ts b/modules/ui-module/src/main/resources/manager/js/modules/localization-actions.d.ts index 6db9e8af0..43acad38a 100644 --- a/modules/ui-module/src/main/resources/manager/js/modules/localization-actions.d.ts +++ b/modules/ui-module/src/main/resources/manager/js/modules/localization-actions.d.ts @@ -1,23 +1,3 @@ -/*- - * #%L - * UI Module - * %% - * Copyright (C) 2023 - 2026 CondationCMS - * %% - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - * #L% - */ export namespace ACTION_LOCALIZATIONS { let en: {}; let de: { diff --git a/modules/ui-module/src/main/resources/manager/js/modules/localization-loader.d.ts b/modules/ui-module/src/main/resources/manager/js/modules/localization-loader.d.ts index 5cca57e0f..ea5b0a075 100644 --- a/modules/ui-module/src/main/resources/manager/js/modules/localization-loader.d.ts +++ b/modules/ui-module/src/main/resources/manager/js/modules/localization-loader.d.ts @@ -1,23 +1,3 @@ -/*- - * #%L - * UI Module - * %% - * Copyright (C) 2023 - 2026 CondationCMS - * %% - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - * #L% - */ export function loadLocalizationsWithDefaults(): Promise<{ en: { "ui.filebrowser.filename": string; diff --git a/modules/ui-module/src/main/resources/manager/js/modules/localization-modules.d.ts b/modules/ui-module/src/main/resources/manager/js/modules/localization-modules.d.ts index 94c485674..0afd1b6ed 100644 --- a/modules/ui-module/src/main/resources/manager/js/modules/localization-modules.d.ts +++ b/modules/ui-module/src/main/resources/manager/js/modules/localization-modules.d.ts @@ -1,23 +1,3 @@ -/*- - * #%L - * UI Module - * %% - * Copyright (C) 2023 - 2026 CondationCMS - * %% - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - * #L% - */ export namespace MODULE_LOCALIZATIONS { let en: {}; let de: { diff --git a/modules/ui-module/src/main/resources/manager/js/modules/localization.d.ts b/modules/ui-module/src/main/resources/manager/js/modules/localization.d.ts index e91051759..9eeed5a15 100644 --- a/modules/ui-module/src/main/resources/manager/js/modules/localization.d.ts +++ b/modules/ui-module/src/main/resources/manager/js/modules/localization.d.ts @@ -1,23 +1,3 @@ -/*- - * #%L - * UI Module - * %% - * Copyright (C) 2023 - 2026 CondationCMS - * %% - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - * #L% - */ export function localizeUi(): Promise; export namespace i18n { let _locale: any; diff --git a/modules/ui-module/src/main/resources/manager/js/modules/manager-ui.d.ts b/modules/ui-module/src/main/resources/manager/js/modules/manager-ui.d.ts index 247c12e7c..d25e9cb32 100644 --- a/modules/ui-module/src/main/resources/manager/js/modules/manager-ui.d.ts +++ b/modules/ui-module/src/main/resources/manager/js/modules/manager-ui.d.ts @@ -1,21 +1 @@ -/*- - * #%L - * UI Module - * %% - * Copyright (C) 2023 - 2026 CondationCMS - * %% - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - * #L% - */ export function updateStateButton(): void; diff --git a/modules/ui-module/src/main/resources/manager/js/modules/manager/manager.message.handlers.d.ts b/modules/ui-module/src/main/resources/manager/js/modules/manager/manager.message.handlers.d.ts index 5b7c71b35..dd88b9fe0 100644 --- a/modules/ui-module/src/main/resources/manager/js/modules/manager/manager.message.handlers.d.ts +++ b/modules/ui-module/src/main/resources/manager/js/modules/manager/manager.message.handlers.d.ts @@ -1,22 +1,2 @@ -/*- - * #%L - * UI Module - * %% - * Copyright (C) 2023 - 2026 CondationCMS - * %% - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - * #L% - */ declare const initMessageHandlers: () => void; export { initMessageHandlers }; diff --git a/modules/ui-module/src/main/resources/manager/js/modules/manager/media.inject.d.ts b/modules/ui-module/src/main/resources/manager/js/modules/manager/media.inject.d.ts index 067619660..ebd61d27b 100644 --- a/modules/ui-module/src/main/resources/manager/js/modules/manager/media.inject.d.ts +++ b/modules/ui-module/src/main/resources/manager/js/modules/manager/media.inject.d.ts @@ -1,23 +1,3 @@ -/*- - * #%L - * UI Module - * %% - * Copyright (C) 2023 - 2026 CondationCMS - * %% - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - * #L% - */ export declare const initMediaUploadOverlay: (img: HTMLImageElement) => void; export declare const initContentMediaToolbar: (img: HTMLImageElement) => void; export declare const initMediaToolbar: (img: HTMLImageElement) => void; diff --git a/modules/ui-module/src/main/resources/manager/js/modules/manager/toolbar-icons.d.ts b/modules/ui-module/src/main/resources/manager/js/modules/manager/toolbar-icons.d.ts index 3ae76b89a..570269ca5 100644 --- a/modules/ui-module/src/main/resources/manager/js/modules/manager/toolbar-icons.d.ts +++ b/modules/ui-module/src/main/resources/manager/js/modules/manager/toolbar-icons.d.ts @@ -1,23 +1,3 @@ -/*- - * #%L - * UI Module - * %% - * Copyright (C) 2023 - 2026 CondationCMS - * %% - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - * #L% - */ export declare const EDIT_PAGE_ICON = "\n\n \n \n"; export declare const EDIT_ATTRIBUTES_ICON = "\n\n \n\n"; export declare const SECTION_SORT_ICON = "\n\n \n \n\n"; diff --git a/modules/ui-module/src/main/resources/manager/js/modules/manager/toolbar.inject.d.ts b/modules/ui-module/src/main/resources/manager/js/modules/manager/toolbar.inject.d.ts index 8a082a5df..6e3a61bf4 100644 --- a/modules/ui-module/src/main/resources/manager/js/modules/manager/toolbar.inject.d.ts +++ b/modules/ui-module/src/main/resources/manager/js/modules/manager/toolbar.inject.d.ts @@ -1,21 +1 @@ -/*- - * #%L - * UI Module - * %% - * Copyright (C) 2023 - 2026 CondationCMS - * %% - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - * #L% - */ export declare const initToolbar: (container: HTMLElement) => void; diff --git a/modules/ui-module/src/main/resources/manager/js/modules/media/mediabrowser.actions.d.ts b/modules/ui-module/src/main/resources/manager/js/modules/media/mediabrowser.actions.d.ts index 688d73a00..8d9d29343 100644 --- a/modules/ui-module/src/main/resources/manager/js/modules/media/mediabrowser.actions.d.ts +++ b/modules/ui-module/src/main/resources/manager/js/modules/media/mediabrowser.actions.d.ts @@ -1,23 +1,3 @@ -/*- - * #%L - * UI Module - * %% - * Copyright (C) 2023 - 2026 CondationCMS - * %% - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - * #L% - */ export function renameMediaAction({ state, getTargetFolder, filename }: { state: any; getTargetFolder: any; diff --git a/modules/ui-module/src/main/resources/manager/js/modules/media/mediabrowser.create.d.ts b/modules/ui-module/src/main/resources/manager/js/modules/media/mediabrowser.create.d.ts index db898301c..fb627bf7e 100644 --- a/modules/ui-module/src/main/resources/manager/js/modules/media/mediabrowser.create.d.ts +++ b/modules/ui-module/src/main/resources/manager/js/modules/media/mediabrowser.create.d.ts @@ -1,21 +1 @@ -/*- - * #%L - * UI Module - * %% - * Copyright (C) 2023 - 2026 CondationCMS - * %% - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - * #L% - */ export function openCreateContentBrowser(params: any): Promise; diff --git a/modules/ui-module/src/main/resources/manager/js/modules/media/mediabrowser.d.ts b/modules/ui-module/src/main/resources/manager/js/modules/media/mediabrowser.d.ts index 125ee5274..af236ca32 100644 --- a/modules/ui-module/src/main/resources/manager/js/modules/media/mediabrowser.d.ts +++ b/modules/ui-module/src/main/resources/manager/js/modules/media/mediabrowser.d.ts @@ -1,23 +1,3 @@ -/*- - * #%L - * UI Module - * %% - * Copyright (C) 2023 - 2026 CondationCMS - * %% - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - * #L% - */ export function openMediaBrowser(optionsParam: any): Promise; export namespace state { let options: null; diff --git a/modules/ui-module/src/main/resources/manager/js/modules/media/mediabrowser.template.d.ts b/modules/ui-module/src/main/resources/manager/js/modules/media/mediabrowser.template.d.ts index 2caff5efc..2c8ff55fe 100644 --- a/modules/ui-module/src/main/resources/manager/js/modules/media/mediabrowser.template.d.ts +++ b/modules/ui-module/src/main/resources/manager/js/modules/media/mediabrowser.template.d.ts @@ -1,22 +1,2 @@ -/*- - * #%L - * UI Module - * %% - * Copyright (C) 2023 - 2026 CondationCMS - * %% - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - * #L% - */ export { template as mediabrowserTemplate }; declare const template: any; diff --git a/modules/ui-module/src/main/resources/manager/js/modules/media/mediabrowser.upload.d.ts b/modules/ui-module/src/main/resources/manager/js/modules/media/mediabrowser.upload.d.ts index 954f1b3fc..5b1bd46d8 100644 --- a/modules/ui-module/src/main/resources/manager/js/modules/media/mediabrowser.upload.d.ts +++ b/modules/ui-module/src/main/resources/manager/js/modules/media/mediabrowser.upload.d.ts @@ -1,22 +1,2 @@ -/*- - * #%L - * UI Module - * %% - * Copyright (C) 2023 - 2026 CondationCMS - * %% - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - * #L% - */ export function initDragAndDropUpload(): void; export function handleFileUpload(): Promise; diff --git a/modules/ui-module/src/main/resources/manager/js/modules/modal.d.ts b/modules/ui-module/src/main/resources/manager/js/modules/modal.d.ts index 4f6c59a03..a6bc8574c 100644 --- a/modules/ui-module/src/main/resources/manager/js/modules/modal.d.ts +++ b/modules/ui-module/src/main/resources/manager/js/modules/modal.d.ts @@ -1,21 +1 @@ -/*- - * #%L - * UI Module - * %% - * Copyright (C) 2023 - 2026 CondationCMS - * %% - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - * #L% - */ export function openModal(optionsParam: any): any; diff --git a/modules/ui-module/src/main/resources/manager/js/modules/node.d.ts b/modules/ui-module/src/main/resources/manager/js/modules/node.d.ts index 81527fd56..ade17f827 100644 --- a/modules/ui-module/src/main/resources/manager/js/modules/node.d.ts +++ b/modules/ui-module/src/main/resources/manager/js/modules/node.d.ts @@ -1,23 +1,3 @@ -/*- - * #%L - * UI Module - * %% - * Copyright (C) 2023 - 2026 CondationCMS - * %% - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - * #L% - */ /** * Retrieves a nested value from an object using a dot-notated path like "meta.title" * @param {object} sourceObj - The object to retrieve the value from diff --git a/modules/ui-module/src/main/resources/manager/js/modules/page-picker.d.ts b/modules/ui-module/src/main/resources/manager/js/modules/page-picker.d.ts index 69fe549fa..c3187feac 100644 --- a/modules/ui-module/src/main/resources/manager/js/modules/page-picker.d.ts +++ b/modules/ui-module/src/main/resources/manager/js/modules/page-picker.d.ts @@ -1,23 +1,3 @@ -/*- - * #%L - * UI Module - * %% - * Copyright (C) 2023 - 2026 CondationCMS - * %% - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - * #L% - */ import { SearchResultDto } from '@cms/modules/rpc/rpc-page.js'; export interface PagePickerOptions { title?: string; diff --git a/modules/ui-module/src/main/resources/manager/js/modules/preview-context.d.ts b/modules/ui-module/src/main/resources/manager/js/modules/preview-context.d.ts index 3422613f0..c1732a951 100644 --- a/modules/ui-module/src/main/resources/manager/js/modules/preview-context.d.ts +++ b/modules/ui-module/src/main/resources/manager/js/modules/preview-context.d.ts @@ -1,23 +1,3 @@ -/*- - * #%L - * UI Module - * %% - * Copyright (C) 2023 - 2026 CondationCMS - * %% - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - * #L% - */ export interface ActivePreviewContent { uri: string; url?: string; diff --git a/modules/ui-module/src/main/resources/manager/js/modules/preview.history.d.ts b/modules/ui-module/src/main/resources/manager/js/modules/preview.history.d.ts index 6cfeb16ad..19f953414 100644 --- a/modules/ui-module/src/main/resources/manager/js/modules/preview.history.d.ts +++ b/modules/ui-module/src/main/resources/manager/js/modules/preview.history.d.ts @@ -1,23 +1,3 @@ -/*- - * #%L - * UI Module - * %% - * Copyright (C) 2023 - 2026 CondationCMS - * %% - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - * #L% - */ export namespace PreviewHistory { export { init }; export { navigatePreview }; diff --git a/modules/ui-module/src/main/resources/manager/js/modules/preview.utils.d.ts b/modules/ui-module/src/main/resources/manager/js/modules/preview.utils.d.ts index c2899f668..a6bfee843 100644 --- a/modules/ui-module/src/main/resources/manager/js/modules/preview.utils.d.ts +++ b/modules/ui-module/src/main/resources/manager/js/modules/preview.utils.d.ts @@ -1,23 +1,3 @@ -/*- - * #%L - * UI Module - * %% - * Copyright (C) 2023 - 2026 CondationCMS - * %% - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - * #L% - */ export function activatePreviewOverlay(): void; export function deActivatePreviewOverlay(): void; export function getPreviewUrl(): any; diff --git a/modules/ui-module/src/main/resources/manager/js/modules/rpc/rpc-access.d.ts b/modules/ui-module/src/main/resources/manager/js/modules/rpc/rpc-access.d.ts index 00ef59607..c5fd921f8 100644 --- a/modules/ui-module/src/main/resources/manager/js/modules/rpc/rpc-access.d.ts +++ b/modules/ui-module/src/main/resources/manager/js/modules/rpc/rpc-access.d.ts @@ -1,23 +1,3 @@ -/*- - * #%L - * UI Module - * %% - * Copyright (C) 2023 - 2026 CondationCMS - * %% - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - * #L% - */ export interface Permission { key: string; description: string; diff --git a/modules/ui-module/src/main/resources/manager/js/modules/rpc/rpc-collection.d.ts b/modules/ui-module/src/main/resources/manager/js/modules/rpc/rpc-collection.d.ts index f7bf92d98..b57aa9c35 100644 --- a/modules/ui-module/src/main/resources/manager/js/modules/rpc/rpc-collection.d.ts +++ b/modules/ui-module/src/main/resources/manager/js/modules/rpc/rpc-collection.d.ts @@ -1,23 +1,3 @@ -/*- - * #%L - * UI Module - * %% - * Copyright (C) 2023 - 2026 CondationCMS - * %% - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - * #L% - */ export interface CollectionItemSummary { id: string; collection: string; diff --git a/modules/ui-module/src/main/resources/manager/js/modules/rpc/rpc-content.d.ts b/modules/ui-module/src/main/resources/manager/js/modules/rpc/rpc-content.d.ts index 0955b8b86..6bb73c1dc 100644 --- a/modules/ui-module/src/main/resources/manager/js/modules/rpc/rpc-content.d.ts +++ b/modules/ui-module/src/main/resources/manager/js/modules/rpc/rpc-content.d.ts @@ -1,23 +1,3 @@ -/*- - * #%L - * UI Module - * %% - * Copyright (C) 2023 - 2026 CondationCMS - * %% - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - * #L% - */ import { RPCResponse } from '@cms/modules/rpc/rpc.js'; declare const getContentNode: (options: any) => Promise; declare const getContent: (options: any) => Promise; diff --git a/modules/ui-module/src/main/resources/manager/js/modules/rpc/rpc-files.d.ts b/modules/ui-module/src/main/resources/manager/js/modules/rpc/rpc-files.d.ts index 4b1d33075..19350eb0f 100644 --- a/modules/ui-module/src/main/resources/manager/js/modules/rpc/rpc-files.d.ts +++ b/modules/ui-module/src/main/resources/manager/js/modules/rpc/rpc-files.d.ts @@ -1,23 +1,3 @@ -/*- - * #%L - * UI Module - * %% - * Copyright (C) 2023 - 2026 CondationCMS - * %% - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - * #L% - */ declare const listFiles: (options: any) => Promise; declare const deleteFile: (options: any) => Promise; declare const deleteFolder: (options: any) => Promise; diff --git a/modules/ui-module/src/main/resources/manager/js/modules/rpc/rpc-i18n.d.ts b/modules/ui-module/src/main/resources/manager/js/modules/rpc/rpc-i18n.d.ts index e28dcc439..97184c7b3 100644 --- a/modules/ui-module/src/main/resources/manager/js/modules/rpc/rpc-i18n.d.ts +++ b/modules/ui-module/src/main/resources/manager/js/modules/rpc/rpc-i18n.d.ts @@ -1,22 +1,2 @@ -/*- - * #%L - * UI Module - * %% - * Copyright (C) 2023 - 2026 CondationCMS - * %% - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - * #L% - */ declare const loadLocalizations: (options: any) => Promise; export { loadLocalizations }; diff --git a/modules/ui-module/src/main/resources/manager/js/modules/rpc/rpc-manager.d.ts b/modules/ui-module/src/main/resources/manager/js/modules/rpc/rpc-manager.d.ts index a5cde78ef..df9f3252a 100644 --- a/modules/ui-module/src/main/resources/manager/js/modules/rpc/rpc-manager.d.ts +++ b/modules/ui-module/src/main/resources/manager/js/modules/rpc/rpc-manager.d.ts @@ -1,23 +1,3 @@ -/*- - * #%L - * UI Module - * %% - * Copyright (C) 2023 - 2026 CondationCMS - * %% - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - * #L% - */ export interface FormDefinition { fields: FormFieldDefinition[]; } diff --git a/modules/ui-module/src/main/resources/manager/js/modules/rpc/rpc-media.d.ts b/modules/ui-module/src/main/resources/manager/js/modules/rpc/rpc-media.d.ts index cdd474bef..e333405cb 100644 --- a/modules/ui-module/src/main/resources/manager/js/modules/rpc/rpc-media.d.ts +++ b/modules/ui-module/src/main/resources/manager/js/modules/rpc/rpc-media.d.ts @@ -1,23 +1,3 @@ -/*- - * #%L - * UI Module - * %% - * Copyright (C) 2023 - 2026 CondationCMS - * %% - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - * #L% - */ declare const getMediaMetaData: (options: any) => Promise; declare const setMediaMetaData: (options: any) => Promise; declare const renameMedia: (options: any) => Promise; diff --git a/modules/ui-module/src/main/resources/manager/js/modules/rpc/rpc-menu.d.ts b/modules/ui-module/src/main/resources/manager/js/modules/rpc/rpc-menu.d.ts index 7f7c57968..9c037e964 100644 --- a/modules/ui-module/src/main/resources/manager/js/modules/rpc/rpc-menu.d.ts +++ b/modules/ui-module/src/main/resources/manager/js/modules/rpc/rpc-menu.d.ts @@ -1,23 +1,3 @@ -/*- - * #%L - * UI Module - * %% - * Copyright (C) 2023 - 2026 CondationCMS - * %% - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - * #L% - */ export type MenuItemType = 'link' | 'heading' | 'divider'; export interface MenuItem { id: string; diff --git a/modules/ui-module/src/main/resources/manager/js/modules/rpc/rpc-page.d.ts b/modules/ui-module/src/main/resources/manager/js/modules/rpc/rpc-page.d.ts index 59ea7b081..d075394df 100644 --- a/modules/ui-module/src/main/resources/manager/js/modules/rpc/rpc-page.d.ts +++ b/modules/ui-module/src/main/resources/manager/js/modules/rpc/rpc-page.d.ts @@ -1,23 +1,3 @@ -/*- - * #%L - * UI Module - * %% - * Copyright (C) 2023 - 2026 CondationCMS - * %% - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - * #L% - */ export interface CreatePageOptions { uri: string; name: string; diff --git a/modules/ui-module/src/main/resources/manager/js/modules/rpc/rpc-taxonomy.d.ts b/modules/ui-module/src/main/resources/manager/js/modules/rpc/rpc-taxonomy.d.ts index 9b308e05b..04f282d16 100644 --- a/modules/ui-module/src/main/resources/manager/js/modules/rpc/rpc-taxonomy.d.ts +++ b/modules/ui-module/src/main/resources/manager/js/modules/rpc/rpc-taxonomy.d.ts @@ -1,23 +1,3 @@ -/*- - * #%L - * UI Module - * %% - * Copyright (C) 2023 - 2026 CondationCMS - * %% - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - * #L% - */ export interface TaxonomyValue { id: string; title: string; diff --git a/modules/ui-module/src/main/resources/manager/js/modules/rpc/rpc-translation.d.ts b/modules/ui-module/src/main/resources/manager/js/modules/rpc/rpc-translation.d.ts index d9b97e548..9003180b3 100644 --- a/modules/ui-module/src/main/resources/manager/js/modules/rpc/rpc-translation.d.ts +++ b/modules/ui-module/src/main/resources/manager/js/modules/rpc/rpc-translation.d.ts @@ -1,23 +1,3 @@ -/*- - * #%L - * UI Module - * %% - * Copyright (C) 2023 - 2026 CondationCMS - * %% - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - * #L% - */ export interface GetTranslationsOptions { uri: string; } diff --git a/modules/ui-module/src/main/resources/manager/js/modules/rpc/rpc-variant.d.ts b/modules/ui-module/src/main/resources/manager/js/modules/rpc/rpc-variant.d.ts index 6fde1add6..136870ec1 100644 --- a/modules/ui-module/src/main/resources/manager/js/modules/rpc/rpc-variant.d.ts +++ b/modules/ui-module/src/main/resources/manager/js/modules/rpc/rpc-variant.d.ts @@ -1,23 +1,3 @@ -/*- - * #%L - * UI Module - * %% - * Copyright (C) 2023 - 2026 CondationCMS - * %% - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - * #L% - */ export interface VariantDto { id: string; uri: string; diff --git a/modules/ui-module/src/main/resources/manager/js/modules/rpc/rpc-workflow.d.ts b/modules/ui-module/src/main/resources/manager/js/modules/rpc/rpc-workflow.d.ts index 921599a38..7c1b5d18e 100644 --- a/modules/ui-module/src/main/resources/manager/js/modules/rpc/rpc-workflow.d.ts +++ b/modules/ui-module/src/main/resources/manager/js/modules/rpc/rpc-workflow.d.ts @@ -1,23 +1,3 @@ -/*- - * #%L - * UI Module - * %% - * Copyright (C) 2023 - 2026 CondationCMS - * %% - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - * #L% - */ export interface GetTransitionsRequest { uri?: string; } diff --git a/modules/ui-module/src/main/resources/manager/js/modules/rpc/rpc.d.ts b/modules/ui-module/src/main/resources/manager/js/modules/rpc/rpc.d.ts index 3a97e3941..485de8fe8 100644 --- a/modules/ui-module/src/main/resources/manager/js/modules/rpc/rpc.d.ts +++ b/modules/ui-module/src/main/resources/manager/js/modules/rpc/rpc.d.ts @@ -1,23 +1,3 @@ -/*- - * #%L - * UI Module - * %% - * Copyright (C) 2023 - 2026 CondationCMS - * %% - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - * #L% - */ interface Options { method: string; parameters?: any; diff --git a/modules/ui-module/src/main/resources/manager/js/modules/sidebar.d.ts b/modules/ui-module/src/main/resources/manager/js/modules/sidebar.d.ts index cdfd08d7f..268aae235 100644 --- a/modules/ui-module/src/main/resources/manager/js/modules/sidebar.d.ts +++ b/modules/ui-module/src/main/resources/manager/js/modules/sidebar.d.ts @@ -1,21 +1 @@ -/*- - * #%L - * UI Module - * %% - * Copyright (C) 2023 - 2026 CondationCMS - * %% - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - * #L% - */ export function openSidebar(options: any): void; diff --git a/modules/ui-module/src/main/resources/manager/js/modules/state.d.ts b/modules/ui-module/src/main/resources/manager/js/modules/state.d.ts index 80f03d1b9..7d224a238 100644 --- a/modules/ui-module/src/main/resources/manager/js/modules/state.d.ts +++ b/modules/ui-module/src/main/resources/manager/js/modules/state.d.ts @@ -1,23 +1,3 @@ -/*- - * #%L - * UI Module - * %% - * Copyright (C) 2023 - 2026 CondationCMS - * %% - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - * #L% - */ declare class State { constructor(initialState?: {}); state: {}; diff --git a/modules/ui-module/src/main/resources/manager/js/modules/toast.d.ts b/modules/ui-module/src/main/resources/manager/js/modules/toast.d.ts index f5a574c37..0fcd8a129 100644 --- a/modules/ui-module/src/main/resources/manager/js/modules/toast.d.ts +++ b/modules/ui-module/src/main/resources/manager/js/modules/toast.d.ts @@ -1,21 +1 @@ -/*- - * #%L - * UI Module - * %% - * Copyright (C) 2023 - 2026 CondationCMS - * %% - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - * #L% - */ export function showToast(options: any): void; diff --git a/modules/ui-module/src/main/resources/manager/js/modules/ui-state.d.ts b/modules/ui-module/src/main/resources/manager/js/modules/ui-state.d.ts index 30e36cd4d..d064b5d45 100644 --- a/modules/ui-module/src/main/resources/manager/js/modules/ui-state.d.ts +++ b/modules/ui-module/src/main/resources/manager/js/modules/ui-state.d.ts @@ -1,23 +1,3 @@ -/*- - * #%L - * UI Module - * %% - * Copyright (C) 2023 - 2026 CondationCMS - * %% - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - * #L% - */ export namespace UIStateManager { function setTabState(key: any, value: any): void; function getTabState(key: any, defaultValue?: null): any; diff --git a/modules/ui-module/src/main/resources/manager/js/modules/upload.d.ts b/modules/ui-module/src/main/resources/manager/js/modules/upload.d.ts index 757755054..760c8f380 100644 --- a/modules/ui-module/src/main/resources/manager/js/modules/upload.d.ts +++ b/modules/ui-module/src/main/resources/manager/js/modules/upload.d.ts @@ -1,23 +1,3 @@ -/*- - * #%L - * UI Module - * %% - * Copyright (C) 2023 - 2026 CondationCMS - * %% - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - * #L% - */ export function uploadFileWithProgress({ uploadEndpoint, file, uri, onProgress, onSuccess, onError }: { uploadEndpoint: any; file: any; diff --git a/modules/ui-module/src/main/resources/manager/js/modules/utils.d.ts b/modules/ui-module/src/main/resources/manager/js/modules/utils.d.ts index 2c2466d62..249b7baa8 100644 --- a/modules/ui-module/src/main/resources/manager/js/modules/utils.d.ts +++ b/modules/ui-module/src/main/resources/manager/js/modules/utils.d.ts @@ -1,23 +1,3 @@ -/*- - * #%L - * UI Module - * %% - * Copyright (C) 2023 - 2026 CondationCMS - * %% - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - * #L% - */ export declare function getCSRFToken(): string | null; export declare function setCSRFToken(token: string): void; export declare function uuid(): string; diff --git a/modules/ui-module/src/main/resources/manager/js/modules/wizard.d.ts b/modules/ui-module/src/main/resources/manager/js/modules/wizard.d.ts index bb3a3b4b0..13b63e736 100644 --- a/modules/ui-module/src/main/resources/manager/js/modules/wizard.d.ts +++ b/modules/ui-module/src/main/resources/manager/js/modules/wizard.d.ts @@ -1,23 +1,3 @@ -/*- - * #%L - * UI Module - * %% - * Copyright (C) 2023 - 2026 CondationCMS - * %% - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - * #L% - */ export function openWizard(optionsParam: any): { wizardId: string; modalInstance: any; diff --git a/modules/ui-module/src/main/resources/manager/js/ui-actions.d.ts b/modules/ui-module/src/main/resources/manager/js/ui-actions.d.ts index 4562057ed..cb0ff5c3b 100644 --- a/modules/ui-module/src/main/resources/manager/js/ui-actions.d.ts +++ b/modules/ui-module/src/main/resources/manager/js/ui-actions.d.ts @@ -1,21 +1 @@ -/*- - * #%L - * UI Module - * %% - * Copyright (C) 2023 - 2026 CondationCMS - * %% - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - * #L% - */ export {}; diff --git a/modules/ui-module/src/main/resources/manager/public/manager-login.d.ts b/modules/ui-module/src/main/resources/manager/public/manager-login.d.ts index ce810c5cb..5b5817d8a 100644 --- a/modules/ui-module/src/main/resources/manager/public/manager-login.d.ts +++ b/modules/ui-module/src/main/resources/manager/public/manager-login.d.ts @@ -1,23 +1,3 @@ -/*- - * #%L - * UI Module - * %% - * Copyright (C) 2023 - 2026 CondationCMS - * %% - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - * #L% - */ declare const ui: { state: string; }; diff --git a/modules/ui-module/src/main/ts/src/js/modules/collection-picker.ts b/modules/ui-module/src/main/ts/src/js/modules/collection-picker.ts index d23f2bff7..5ac94275f 100644 --- a/modules/ui-module/src/main/ts/src/js/modules/collection-picker.ts +++ b/modules/ui-module/src/main/ts/src/js/modules/collection-picker.ts @@ -66,14 +66,16 @@ const renderResults = (response: CollectionItemsPage, selectText: string): strin `).join('')}
- ${escapeHtml(i18n.t('collection.picker.page', 'Page'))} ${page} / ${totalPages} - @@ -131,16 +133,16 @@ export const openCollectionItemPicker = (options: CollectionItemPickerOptions): modal.hide(); }); }); - resultsElement.querySelector('[data-collection-picker-previous]') - ?.addEventListener('click', () => { - currentPage--; + resultsElement.querySelectorAll('[data-collection-picker-page]') + .forEach(button => button.addEventListener('click', () => { + const targetPage = Number(button.dataset.collectionPickerPage); + if (button.disabled || !Number.isInteger(targetPage) || targetPage < 1 + || targetPage > response.totalPages || targetPage === currentPage) { + return; + } + currentPage = targetPage; loadPage(); - }); - resultsElement.querySelector('[data-collection-picker-next]') - ?.addEventListener('click', () => { - currentPage++; - loadPage(); - }); + })); } catch (error) { if (version !== requestVersion) return; resultsElement.innerHTML = ` From 3c99b4943810ddeb8c05420304b5053b3cfc7a70 Mon Sep 17 00:00:00 2001 From: Thorsten Marx Date: Mon, 31 Aug 2026 08:45:09 +0200 Subject: [PATCH 16/28] add soem badges --- README.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/README.md b/README.md index 1385a0028..78acf3525 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,13 @@ +[![Quality gate status](https://sonarcloud.io/api/project_badges/measure?project=CondationCMS_cms-server&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=CondationCMS_cms-server) + +![Maven Central Version](https://img.shields.io/maven-central/v/com.condation.cms/cms-api) + +![NPM Version](https://img.shields.io/npm/v/condation-cms-ui) + + # CondationCMS + CondationCMS is a fast, flexible, and developer-friendly content management system built with Java. Content is stored in Markdown files with YAML front matter instead of a database. The file and directory structure remains transparent, easy to version with Git, and directly represents the structure of the website. From 39ae42853264cd339b30178a157327de61ec19d1 Mon Sep 17 00:00:00 2001 From: Thorsten Marx Date: Mon, 31 Aug 2026 11:21:24 +0200 Subject: [PATCH 17/28] fix some sonar issues, mini refactoring --- .../cms/modules/example/ui/example-action.js | 2 +- .../src/main/ts/dist/example-action.js | 22 +++++++++- .../src/main/ts/package-lock.json | 38 ++++++++++++++++++ .../example-module/src/main/ts/package.json | 2 +- .../src/main/ts/src/example-action.ts | 2 +- .../example-module/src/main/ts/tsconfig.json | 5 ++- .../manager/actions/access/manage-roles.d.ts | 20 +++++++++ .../manager/actions/access/manage-users.d.ts | 20 +++++++++ .../collection/create-collection-item.d.ts | 20 +++++++++ .../collection/edit-collection-item.d.ts | 20 +++++++++ .../actions/collection/manage-collection.d.ts | 20 +++++++++ .../actions/media/edit-focal-point.d.ts | 20 +++++++++ .../actions/media/edit-media-form.d.ts | 20 +++++++++ .../actions/media/select-content-media.d.ts | 20 +++++++++ .../manager/actions/media/select-media.d.ts | 20 +++++++++ .../manager/actions/menu/manage-menus.d.ts | 20 +++++++++ .../manager/actions/page/add-section.d.ts | 20 +++++++++ .../manager/actions/page/create-node.d.ts | 20 +++++++++ .../manager/actions/page/create-page.d.ts | 20 +++++++++ .../manager/actions/page/create-variant.d.ts | 20 +++++++++ .../manager/actions/page/delete-section.d.ts | 20 +++++++++ .../manager/actions/page/edit-content.d.ts | 20 +++++++++ .../actions/page/edit-metaattribute-form.d.ts | 20 +++++++++ .../actions/page/edit-metaattribute-list.d.ts | 20 +++++++++ .../actions/page/edit-metaattribute.d.ts | 20 +++++++++ .../actions/page/edit-page-settings.d.ts | 20 +++++++++ .../manager/actions/page/edit-schedule.d.ts | 20 +++++++++ .../manager/actions/page/edit-sections.d.ts | 20 +++++++++ .../actions/page/list-unpublished-pages.d.ts | 20 +++++++++ .../manager/actions/page/manage-assets.d.ts | 20 +++++++++ .../manager/actions/page/search-pages.d.ts | 20 +++++++++ .../actions/page/section-set-published.d.ts | 20 +++++++++ .../manager/actions/page/translations.d.ts | 20 +++++++++ .../actions/page/variant-selector.d.ts | 20 +++++++++ .../manager/actions/page/variants.d.ts | 20 +++++++++ .../actions/page/wf-run-transition.d.ts | 20 +++++++++ .../manager/actions/reload-preview.d.ts | 20 +++++++++ .../manager/actions/site-change.d.ts | 20 +++++++++ .../manager/actions/test-command.d.ts | 20 +++++++++ .../src/main/resources/manager/index.html | 3 +- .../resources/manager/js/manager-globals.d.ts | 20 +++++++++ .../manager/js/manager-inject-init.d.ts | 20 +++++++++ .../resources/manager/js/manager-inject.d.ts | 20 +++++++++ .../main/resources/manager/js/manager.d.ts | 20 +++++++++ .../resources/manager/js/modules/alerts.d.ts | 20 +++++++++ .../resources/manager/js/modules/apps.d.ts | 20 +++++++++ .../manager/js/modules/collection-picker.d.ts | 20 +++++++++ .../manager/js/modules/collection-picker.js | 11 ++--- .../manager/js/modules/event-bus.d.ts | 20 +++++++++ .../resources/manager/js/modules/event-bus.js | 2 +- .../filebrowser/filebrowser.actions.d.ts | 20 +++++++++ .../filebrowser/filebrowser.create.d.ts | 20 +++++++++ .../js/modules/filebrowser/filebrowser.d.ts | 20 +++++++++ .../filebrowser/filebrowser.template.d.ts | 20 +++++++++ .../js/modules/form/field.checkbox.d.ts | 20 +++++++++ .../manager/js/modules/form/field.checkbox.js | 20 +++++++++ .../manager/js/modules/form/field.code.d.ts | 20 +++++++++ .../js/modules/form/field.collection.d.ts | 20 +++++++++ .../js/modules/form/field.collection.js | 10 ++--- .../manager/js/modules/form/field.color.d.ts | 20 +++++++++ .../manager/js/modules/form/field.date.d.ts | 20 +++++++++ .../js/modules/form/field.datetime.d.ts | 20 +++++++++ .../js/modules/form/field.divider.d.ts | 20 +++++++++ .../js/modules/form/field.easymde.d.ts | 20 +++++++++ .../manager/js/modules/form/field.list.d.ts | 20 +++++++++ .../manager/js/modules/form/field.mail.d.ts | 20 +++++++++ .../js/modules/form/field.markdown.d.ts | 20 +++++++++ .../manager/js/modules/form/field.media.d.ts | 20 +++++++++ .../manager/js/modules/form/field.number.d.ts | 20 +++++++++ .../manager/js/modules/form/field.radio.d.ts | 20 +++++++++ .../manager/js/modules/form/field.range.d.ts | 20 +++++++++ .../js/modules/form/field.reference.d.ts | 20 +++++++++ .../manager/js/modules/form/field.select.d.ts | 20 +++++++++ .../manager/js/modules/form/field.tags.d.ts | 20 +++++++++ .../manager/js/modules/form/field.text.d.ts | 20 +++++++++ .../js/modules/form/field.textarea.d.ts | 20 +++++++++ .../manager/js/modules/form/forms.d.ts | 20 +++++++++ .../manager/js/modules/form/utils.d.ts | 20 +++++++++ .../manager/js/modules/frameMessenger.d.ts | 20 +++++++++ .../manager/js/modules/locale-utils.d.ts | 20 +++++++++ .../js/modules/localization-actions.d.ts | 20 +++++++++ .../js/modules/localization-loader.d.ts | 20 +++++++++ .../js/modules/localization-modules.d.ts | 20 +++++++++ .../manager/js/modules/localization.d.ts | 20 +++++++++ .../manager/js/modules/manager-ui.d.ts | 20 +++++++++ .../manager/manager.message.handlers.d.ts | 20 +++++++++ .../js/modules/manager/media.inject.d.ts | 20 +++++++++ .../js/modules/manager/toolbar-icons.d.ts | 20 +++++++++ .../js/modules/manager/toolbar.inject.d.ts | 20 +++++++++ .../modules/media/mediabrowser.actions.d.ts | 20 +++++++++ .../js/modules/media/mediabrowser.create.d.ts | 20 +++++++++ .../js/modules/media/mediabrowser.d.ts | 20 +++++++++ .../modules/media/mediabrowser.template.d.ts | 20 +++++++++ .../js/modules/media/mediabrowser.upload.d.ts | 20 +++++++++ .../resources/manager/js/modules/modal.d.ts | 20 +++++++++ .../resources/manager/js/modules/node.d.ts | 20 +++++++++ .../manager/js/modules/page-picker.d.ts | 20 +++++++++ .../manager/js/modules/preview-context.d.ts | 20 +++++++++ .../manager/js/modules/preview.history.d.ts | 20 +++++++++ .../manager/js/modules/preview.utils.d.ts | 20 +++++++++ .../manager/js/modules/rpc/rpc-access.d.ts | 20 +++++++++ .../js/modules/rpc/rpc-collection.d.ts | 20 +++++++++ .../manager/js/modules/rpc/rpc-content.d.ts | 20 +++++++++ .../manager/js/modules/rpc/rpc-files.d.ts | 20 +++++++++ .../manager/js/modules/rpc/rpc-i18n.d.ts | 20 +++++++++ .../manager/js/modules/rpc/rpc-manager.d.ts | 20 +++++++++ .../manager/js/modules/rpc/rpc-media.d.ts | 20 +++++++++ .../manager/js/modules/rpc/rpc-menu.d.ts | 20 +++++++++ .../manager/js/modules/rpc/rpc-page.d.ts | 20 +++++++++ .../manager/js/modules/rpc/rpc-taxonomy.d.ts | 20 +++++++++ .../js/modules/rpc/rpc-translation.d.ts | 20 +++++++++ .../manager/js/modules/rpc/rpc-variant.d.ts | 20 +++++++++ .../manager/js/modules/rpc/rpc-workflow.d.ts | 20 +++++++++ .../resources/manager/js/modules/rpc/rpc.d.ts | 20 +++++++++ .../resources/manager/js/modules/sidebar.d.ts | 20 +++++++++ .../resources/manager/js/modules/state.d.ts | 20 +++++++++ .../resources/manager/js/modules/toast.d.ts | 20 +++++++++ .../manager/js/modules/ui-state.d.ts | 20 +++++++++ .../resources/manager/js/modules/upload.d.ts | 20 +++++++++ .../resources/manager/js/modules/utils.d.ts | 20 +++++++++ .../resources/manager/js/modules/wizard.d.ts | 20 +++++++++ .../main/resources/manager/js/ui-actions.d.ts | 20 +++++++++ .../manager/public/manager-login.d.ts | 20 +++++++++ modules/ui-module/src/main/ts/package.json | 1 + .../ts/src/js/modules/collection-picker.ts | 14 ++++--- .../src/js/modules/form/field.collection.ts | 13 +++--- modules/ui-module/src/main/ts/tsconfig.json | 4 +- .../libs/example-module-8.2.0.jar | Bin 19411 -> 0 bytes .../libs/example-module-8.3.0.jar | Bin 0 -> 20179 bytes .../modules/example-module/module.properties | 2 +- 130 files changed, 2358 insertions(+), 33 deletions(-) create mode 100644 modules/example-module/src/main/ts/package-lock.json delete mode 100644 test-server/modules/example-module/libs/example-module-8.2.0.jar create mode 100644 test-server/modules/example-module/libs/example-module-8.3.0.jar diff --git a/modules/example-module/src/main/resources/com/condation/cms/modules/example/ui/example-action.js b/modules/example-module/src/main/resources/com/condation/cms/modules/example/ui/example-action.js index 41a9cbe38..b9a278f56 100644 --- a/modules/example-module/src/main/resources/com/condation/cms/modules/example/ui/example-action.js +++ b/modules/example-module/src/main/resources/com/condation/cms/modules/example/ui/example-action.js @@ -18,7 +18,7 @@ * along with this program. If not, see . * #L% */ -import { showToast } from 'condation-cms-ui/dist/js/modules/toast.js'; +import { showToast } from '@cms/modules/toast.js'; export async function runAction(parameters) { console.log("This is an example action"); showToast({ diff --git a/modules/example-module/src/main/ts/dist/example-action.js b/modules/example-module/src/main/ts/dist/example-action.js index 7c04ab322..950877bb7 100644 --- a/modules/example-module/src/main/ts/dist/example-action.js +++ b/modules/example-module/src/main/ts/dist/example-action.js @@ -1,4 +1,24 @@ -import { showToast } from 'condation-cms-ui/dist/js/modules/toast.js'; +/*- + * #%L + * CMS Example Module + * %% + * Copyright (C) 2023 - 2026 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * #L% + */ +import { showToast } from '@cms/modules/toast.js'; export async function runAction(parameters) { console.log("This is an example action"); showToast({ diff --git a/modules/example-module/src/main/ts/package-lock.json b/modules/example-module/src/main/ts/package-lock.json new file mode 100644 index 000000000..cefbd4c77 --- /dev/null +++ b/modules/example-module/src/main/ts/package-lock.json @@ -0,0 +1,38 @@ +{ + "name": "ts", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "ts", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "condation-cms-ui": "^0.2.0", + "typescript": "^5.9.3" + } + }, + "node_modules/condation-cms-ui": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/condation-cms-ui/-/condation-cms-ui-0.2.0.tgz", + "integrity": "sha512-IY5byzVFjN92oM4MCk6ZM64Lwu1Ql34iA02R3cAOiTEcJG5/jRuTySBsVGhvS6rEeIjfO9WrKV0SmRO0Qc7l5A==", + "dependencies": { + "typescript": "next" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + } + } +} diff --git a/modules/example-module/src/main/ts/package.json b/modules/example-module/src/main/ts/package.json index 20a5aeeba..2917e607d 100644 --- a/modules/example-module/src/main/ts/package.json +++ b/modules/example-module/src/main/ts/package.json @@ -4,7 +4,7 @@ "description": "", "main": "index.js", "scripts": { - "compile": "tsc" + "build": "tsc" }, "keywords": [], "author": "", diff --git a/modules/example-module/src/main/ts/src/example-action.ts b/modules/example-module/src/main/ts/src/example-action.ts index 1d44ae9c7..486364b60 100644 --- a/modules/example-module/src/main/ts/src/example-action.ts +++ b/modules/example-module/src/main/ts/src/example-action.ts @@ -19,7 +19,7 @@ * #L% */ -import { showToast } from 'condation-cms-ui/dist/js/modules/toast.js'; +import { showToast } from '@cms/modules/toast.js'; export async function runAction(parameters : any) : Promise { console.log("This is an example action"); diff --git a/modules/example-module/src/main/ts/tsconfig.json b/modules/example-module/src/main/ts/tsconfig.json index 757f2b4e9..4a0c3b762 100644 --- a/modules/example-module/src/main/ts/tsconfig.json +++ b/modules/example-module/src/main/ts/tsconfig.json @@ -9,7 +9,10 @@ "allowJs": true, "checkJs": false, "lib": ["dom", "es2020"], - "sourceMap": false + "sourceMap": false, + "paths": { + "@cms/modules/*": ["./node_modules/condation-cms-ui/dist/js/modules/*"] + } }, "include": [ "src/**/*", diff --git a/modules/ui-module/src/main/resources/manager/actions/access/manage-roles.d.ts b/modules/ui-module/src/main/resources/manager/actions/access/manage-roles.d.ts index cd3d5445c..a83d0b4a2 100644 --- a/modules/ui-module/src/main/resources/manager/actions/access/manage-roles.d.ts +++ b/modules/ui-module/src/main/resources/manager/actions/access/manage-roles.d.ts @@ -1 +1,21 @@ +/*- + * #%L + * UI Module + * %% + * Copyright (C) 2023 - 2026 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * #L% + */ export declare const runAction: () => Promise; diff --git a/modules/ui-module/src/main/resources/manager/actions/access/manage-users.d.ts b/modules/ui-module/src/main/resources/manager/actions/access/manage-users.d.ts index cd3d5445c..a83d0b4a2 100644 --- a/modules/ui-module/src/main/resources/manager/actions/access/manage-users.d.ts +++ b/modules/ui-module/src/main/resources/manager/actions/access/manage-users.d.ts @@ -1 +1,21 @@ +/*- + * #%L + * UI Module + * %% + * Copyright (C) 2023 - 2026 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * #L% + */ export declare const runAction: () => Promise; diff --git a/modules/ui-module/src/main/resources/manager/actions/collection/create-collection-item.d.ts b/modules/ui-module/src/main/resources/manager/actions/collection/create-collection-item.d.ts index 5a2a46bb9..9214742d6 100644 --- a/modules/ui-module/src/main/resources/manager/actions/collection/create-collection-item.d.ts +++ b/modules/ui-module/src/main/resources/manager/actions/collection/create-collection-item.d.ts @@ -1,3 +1,23 @@ +/*- + * #%L + * UI Module + * %% + * Copyright (C) 2023 - 2026 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * #L% + */ import { CollectionItemSummary } from '@cms/modules/rpc/rpc-collection.js'; export interface CreateCollectionItemOptions { collection: string; diff --git a/modules/ui-module/src/main/resources/manager/actions/collection/edit-collection-item.d.ts b/modules/ui-module/src/main/resources/manager/actions/collection/edit-collection-item.d.ts index 3c6789f99..57358ee23 100644 --- a/modules/ui-module/src/main/resources/manager/actions/collection/edit-collection-item.d.ts +++ b/modules/ui-module/src/main/resources/manager/actions/collection/edit-collection-item.d.ts @@ -1,3 +1,23 @@ +/*- + * #%L + * UI Module + * %% + * Copyright (C) 2023 - 2026 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * #L% + */ import { Form } from '@cms/modules/form/forms.js'; import { CollectionType } from '@cms/modules/rpc/rpc-manager.js'; export declare const collectionForm: (types: CollectionType[], collection: string, mode?: "create" | "edit") => any; diff --git a/modules/ui-module/src/main/resources/manager/actions/collection/manage-collection.d.ts b/modules/ui-module/src/main/resources/manager/actions/collection/manage-collection.d.ts index a317e2de0..4a16720aa 100644 --- a/modules/ui-module/src/main/resources/manager/actions/collection/manage-collection.d.ts +++ b/modules/ui-module/src/main/resources/manager/actions/collection/manage-collection.d.ts @@ -1,3 +1,23 @@ +/*- + * #%L + * UI Module + * %% + * Copyright (C) 2023 - 2026 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * #L% + */ export declare const runAction: (options: { collection: string; }) => Promise; diff --git a/modules/ui-module/src/main/resources/manager/actions/media/edit-focal-point.d.ts b/modules/ui-module/src/main/resources/manager/actions/media/edit-focal-point.d.ts index 85620d3ab..6dc2ed246 100644 --- a/modules/ui-module/src/main/resources/manager/actions/media/edit-focal-point.d.ts +++ b/modules/ui-module/src/main/resources/manager/actions/media/edit-focal-point.d.ts @@ -1 +1,21 @@ +/*- + * #%L + * UI Module + * %% + * Copyright (C) 2023 - 2026 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * #L% + */ export declare function runAction(params: any): Promise; diff --git a/modules/ui-module/src/main/resources/manager/actions/media/edit-media-form.d.ts b/modules/ui-module/src/main/resources/manager/actions/media/edit-media-form.d.ts index 6abc321a1..1dc39e464 100644 --- a/modules/ui-module/src/main/resources/manager/actions/media/edit-media-form.d.ts +++ b/modules/ui-module/src/main/resources/manager/actions/media/edit-media-form.d.ts @@ -1 +1,21 @@ +/*- + * #%L + * UI Module + * %% + * Copyright (C) 2023 - 2026 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * #L% + */ export function runAction(params: any): Promise; diff --git a/modules/ui-module/src/main/resources/manager/actions/media/select-content-media.d.ts b/modules/ui-module/src/main/resources/manager/actions/media/select-content-media.d.ts index 85620d3ab..6dc2ed246 100644 --- a/modules/ui-module/src/main/resources/manager/actions/media/select-content-media.d.ts +++ b/modules/ui-module/src/main/resources/manager/actions/media/select-content-media.d.ts @@ -1 +1,21 @@ +/*- + * #%L + * UI Module + * %% + * Copyright (C) 2023 - 2026 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * #L% + */ export declare function runAction(params: any): Promise; diff --git a/modules/ui-module/src/main/resources/manager/actions/media/select-media.d.ts b/modules/ui-module/src/main/resources/manager/actions/media/select-media.d.ts index 85620d3ab..6dc2ed246 100644 --- a/modules/ui-module/src/main/resources/manager/actions/media/select-media.d.ts +++ b/modules/ui-module/src/main/resources/manager/actions/media/select-media.d.ts @@ -1 +1,21 @@ +/*- + * #%L + * UI Module + * %% + * Copyright (C) 2023 - 2026 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * #L% + */ export declare function runAction(params: any): Promise; diff --git a/modules/ui-module/src/main/resources/manager/actions/menu/manage-menus.d.ts b/modules/ui-module/src/main/resources/manager/actions/menu/manage-menus.d.ts index cd3d5445c..a83d0b4a2 100644 --- a/modules/ui-module/src/main/resources/manager/actions/menu/manage-menus.d.ts +++ b/modules/ui-module/src/main/resources/manager/actions/menu/manage-menus.d.ts @@ -1 +1,21 @@ +/*- + * #%L + * UI Module + * %% + * Copyright (C) 2023 - 2026 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * #L% + */ export declare const runAction: () => Promise; diff --git a/modules/ui-module/src/main/resources/manager/actions/page/add-section.d.ts b/modules/ui-module/src/main/resources/manager/actions/page/add-section.d.ts index 6abc321a1..1dc39e464 100644 --- a/modules/ui-module/src/main/resources/manager/actions/page/add-section.d.ts +++ b/modules/ui-module/src/main/resources/manager/actions/page/add-section.d.ts @@ -1 +1,21 @@ +/*- + * #%L + * UI Module + * %% + * Copyright (C) 2023 - 2026 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * #L% + */ export function runAction(params: any): Promise; diff --git a/modules/ui-module/src/main/resources/manager/actions/page/create-node.d.ts b/modules/ui-module/src/main/resources/manager/actions/page/create-node.d.ts index 6abc321a1..1dc39e464 100644 --- a/modules/ui-module/src/main/resources/manager/actions/page/create-node.d.ts +++ b/modules/ui-module/src/main/resources/manager/actions/page/create-node.d.ts @@ -1 +1,21 @@ +/*- + * #%L + * UI Module + * %% + * Copyright (C) 2023 - 2026 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * #L% + */ export function runAction(params: any): Promise; diff --git a/modules/ui-module/src/main/resources/manager/actions/page/create-page.d.ts b/modules/ui-module/src/main/resources/manager/actions/page/create-page.d.ts index 6abc321a1..1dc39e464 100644 --- a/modules/ui-module/src/main/resources/manager/actions/page/create-page.d.ts +++ b/modules/ui-module/src/main/resources/manager/actions/page/create-page.d.ts @@ -1 +1,21 @@ +/*- + * #%L + * UI Module + * %% + * Copyright (C) 2023 - 2026 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * #L% + */ export function runAction(params: any): Promise; diff --git a/modules/ui-module/src/main/resources/manager/actions/page/create-variant.d.ts b/modules/ui-module/src/main/resources/manager/actions/page/create-variant.d.ts index cd3d5445c..a83d0b4a2 100644 --- a/modules/ui-module/src/main/resources/manager/actions/page/create-variant.d.ts +++ b/modules/ui-module/src/main/resources/manager/actions/page/create-variant.d.ts @@ -1 +1,21 @@ +/*- + * #%L + * UI Module + * %% + * Copyright (C) 2023 - 2026 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * #L% + */ export declare const runAction: () => Promise; diff --git a/modules/ui-module/src/main/resources/manager/actions/page/delete-section.d.ts b/modules/ui-module/src/main/resources/manager/actions/page/delete-section.d.ts index 6abc321a1..1dc39e464 100644 --- a/modules/ui-module/src/main/resources/manager/actions/page/delete-section.d.ts +++ b/modules/ui-module/src/main/resources/manager/actions/page/delete-section.d.ts @@ -1 +1,21 @@ +/*- + * #%L + * UI Module + * %% + * Copyright (C) 2023 - 2026 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * #L% + */ export function runAction(params: any): Promise; diff --git a/modules/ui-module/src/main/resources/manager/actions/page/edit-content.d.ts b/modules/ui-module/src/main/resources/manager/actions/page/edit-content.d.ts index 6abc321a1..1dc39e464 100644 --- a/modules/ui-module/src/main/resources/manager/actions/page/edit-content.d.ts +++ b/modules/ui-module/src/main/resources/manager/actions/page/edit-content.d.ts @@ -1 +1,21 @@ +/*- + * #%L + * UI Module + * %% + * Copyright (C) 2023 - 2026 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * #L% + */ export function runAction(params: any): Promise; diff --git a/modules/ui-module/src/main/resources/manager/actions/page/edit-metaattribute-form.d.ts b/modules/ui-module/src/main/resources/manager/actions/page/edit-metaattribute-form.d.ts index 6abc321a1..1dc39e464 100644 --- a/modules/ui-module/src/main/resources/manager/actions/page/edit-metaattribute-form.d.ts +++ b/modules/ui-module/src/main/resources/manager/actions/page/edit-metaattribute-form.d.ts @@ -1 +1,21 @@ +/*- + * #%L + * UI Module + * %% + * Copyright (C) 2023 - 2026 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * #L% + */ export function runAction(params: any): Promise; diff --git a/modules/ui-module/src/main/resources/manager/actions/page/edit-metaattribute-list.d.ts b/modules/ui-module/src/main/resources/manager/actions/page/edit-metaattribute-list.d.ts index 6abc321a1..1dc39e464 100644 --- a/modules/ui-module/src/main/resources/manager/actions/page/edit-metaattribute-list.d.ts +++ b/modules/ui-module/src/main/resources/manager/actions/page/edit-metaattribute-list.d.ts @@ -1 +1,21 @@ +/*- + * #%L + * UI Module + * %% + * Copyright (C) 2023 - 2026 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * #L% + */ export function runAction(params: any): Promise; diff --git a/modules/ui-module/src/main/resources/manager/actions/page/edit-metaattribute.d.ts b/modules/ui-module/src/main/resources/manager/actions/page/edit-metaattribute.d.ts index 6abc321a1..1dc39e464 100644 --- a/modules/ui-module/src/main/resources/manager/actions/page/edit-metaattribute.d.ts +++ b/modules/ui-module/src/main/resources/manager/actions/page/edit-metaattribute.d.ts @@ -1 +1,21 @@ +/*- + * #%L + * UI Module + * %% + * Copyright (C) 2023 - 2026 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * #L% + */ export function runAction(params: any): Promise; diff --git a/modules/ui-module/src/main/resources/manager/actions/page/edit-page-settings.d.ts b/modules/ui-module/src/main/resources/manager/actions/page/edit-page-settings.d.ts index 6abc321a1..1dc39e464 100644 --- a/modules/ui-module/src/main/resources/manager/actions/page/edit-page-settings.d.ts +++ b/modules/ui-module/src/main/resources/manager/actions/page/edit-page-settings.d.ts @@ -1 +1,21 @@ +/*- + * #%L + * UI Module + * %% + * Copyright (C) 2023 - 2026 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * #L% + */ export function runAction(params: any): Promise; diff --git a/modules/ui-module/src/main/resources/manager/actions/page/edit-schedule.d.ts b/modules/ui-module/src/main/resources/manager/actions/page/edit-schedule.d.ts index 6abc321a1..1dc39e464 100644 --- a/modules/ui-module/src/main/resources/manager/actions/page/edit-schedule.d.ts +++ b/modules/ui-module/src/main/resources/manager/actions/page/edit-schedule.d.ts @@ -1 +1,21 @@ +/*- + * #%L + * UI Module + * %% + * Copyright (C) 2023 - 2026 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * #L% + */ export function runAction(params: any): Promise; diff --git a/modules/ui-module/src/main/resources/manager/actions/page/edit-sections.d.ts b/modules/ui-module/src/main/resources/manager/actions/page/edit-sections.d.ts index 6abc321a1..1dc39e464 100644 --- a/modules/ui-module/src/main/resources/manager/actions/page/edit-sections.d.ts +++ b/modules/ui-module/src/main/resources/manager/actions/page/edit-sections.d.ts @@ -1 +1,21 @@ +/*- + * #%L + * UI Module + * %% + * Copyright (C) 2023 - 2026 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * #L% + */ export function runAction(params: any): Promise; diff --git a/modules/ui-module/src/main/resources/manager/actions/page/list-unpublished-pages.d.ts b/modules/ui-module/src/main/resources/manager/actions/page/list-unpublished-pages.d.ts index 8052ef2c6..44d1f6c2a 100644 --- a/modules/ui-module/src/main/resources/manager/actions/page/list-unpublished-pages.d.ts +++ b/modules/ui-module/src/main/resources/manager/actions/page/list-unpublished-pages.d.ts @@ -1,3 +1,23 @@ +/*- + * #%L + * UI Module + * %% + * Copyright (C) 2023 - 2026 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * #L% + */ interface ListUnpublishedPagesOptions { page?: number; } diff --git a/modules/ui-module/src/main/resources/manager/actions/page/manage-assets.d.ts b/modules/ui-module/src/main/resources/manager/actions/page/manage-assets.d.ts index 6abc321a1..1dc39e464 100644 --- a/modules/ui-module/src/main/resources/manager/actions/page/manage-assets.d.ts +++ b/modules/ui-module/src/main/resources/manager/actions/page/manage-assets.d.ts @@ -1 +1,21 @@ +/*- + * #%L + * UI Module + * %% + * Copyright (C) 2023 - 2026 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * #L% + */ export function runAction(params: any): Promise; diff --git a/modules/ui-module/src/main/resources/manager/actions/page/search-pages.d.ts b/modules/ui-module/src/main/resources/manager/actions/page/search-pages.d.ts index fdb7a4ee9..053bf8a56 100644 --- a/modules/ui-module/src/main/resources/manager/actions/page/search-pages.d.ts +++ b/modules/ui-module/src/main/resources/manager/actions/page/search-pages.d.ts @@ -1,3 +1,23 @@ +/*- + * #%L + * UI Module + * %% + * Copyright (C) 2023 - 2026 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * #L% + */ interface SearchPagesActionOptions { } export declare const runAction: (options?: SearchPagesActionOptions) => Promise; diff --git a/modules/ui-module/src/main/resources/manager/actions/page/section-set-published.d.ts b/modules/ui-module/src/main/resources/manager/actions/page/section-set-published.d.ts index 85620d3ab..6dc2ed246 100644 --- a/modules/ui-module/src/main/resources/manager/actions/page/section-set-published.d.ts +++ b/modules/ui-module/src/main/resources/manager/actions/page/section-set-published.d.ts @@ -1 +1,21 @@ +/*- + * #%L + * UI Module + * %% + * Copyright (C) 2023 - 2026 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * #L% + */ export declare function runAction(params: any): Promise; diff --git a/modules/ui-module/src/main/resources/manager/actions/page/translations.d.ts b/modules/ui-module/src/main/resources/manager/actions/page/translations.d.ts index 85620d3ab..6dc2ed246 100644 --- a/modules/ui-module/src/main/resources/manager/actions/page/translations.d.ts +++ b/modules/ui-module/src/main/resources/manager/actions/page/translations.d.ts @@ -1 +1,21 @@ +/*- + * #%L + * UI Module + * %% + * Copyright (C) 2023 - 2026 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * #L% + */ export declare function runAction(params: any): Promise; diff --git a/modules/ui-module/src/main/resources/manager/actions/page/variant-selector.d.ts b/modules/ui-module/src/main/resources/manager/actions/page/variant-selector.d.ts index cd3d5445c..a83d0b4a2 100644 --- a/modules/ui-module/src/main/resources/manager/actions/page/variant-selector.d.ts +++ b/modules/ui-module/src/main/resources/manager/actions/page/variant-selector.d.ts @@ -1 +1,21 @@ +/*- + * #%L + * UI Module + * %% + * Copyright (C) 2023 - 2026 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * #L% + */ export declare const runAction: () => Promise; diff --git a/modules/ui-module/src/main/resources/manager/actions/page/variants.d.ts b/modules/ui-module/src/main/resources/manager/actions/page/variants.d.ts index cd3d5445c..a83d0b4a2 100644 --- a/modules/ui-module/src/main/resources/manager/actions/page/variants.d.ts +++ b/modules/ui-module/src/main/resources/manager/actions/page/variants.d.ts @@ -1 +1,21 @@ +/*- + * #%L + * UI Module + * %% + * Copyright (C) 2023 - 2026 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * #L% + */ export declare const runAction: () => Promise; diff --git a/modules/ui-module/src/main/resources/manager/actions/page/wf-run-transition.d.ts b/modules/ui-module/src/main/resources/manager/actions/page/wf-run-transition.d.ts index 85620d3ab..6dc2ed246 100644 --- a/modules/ui-module/src/main/resources/manager/actions/page/wf-run-transition.d.ts +++ b/modules/ui-module/src/main/resources/manager/actions/page/wf-run-transition.d.ts @@ -1 +1,21 @@ +/*- + * #%L + * UI Module + * %% + * Copyright (C) 2023 - 2026 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * #L% + */ export declare function runAction(params: any): Promise; diff --git a/modules/ui-module/src/main/resources/manager/actions/reload-preview.d.ts b/modules/ui-module/src/main/resources/manager/actions/reload-preview.d.ts index 85620d3ab..6dc2ed246 100644 --- a/modules/ui-module/src/main/resources/manager/actions/reload-preview.d.ts +++ b/modules/ui-module/src/main/resources/manager/actions/reload-preview.d.ts @@ -1 +1,21 @@ +/*- + * #%L + * UI Module + * %% + * Copyright (C) 2023 - 2026 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * #L% + */ export declare function runAction(params: any): Promise; diff --git a/modules/ui-module/src/main/resources/manager/actions/site-change.d.ts b/modules/ui-module/src/main/resources/manager/actions/site-change.d.ts index ba8a4fff2..672d1f3de 100644 --- a/modules/ui-module/src/main/resources/manager/actions/site-change.d.ts +++ b/modules/ui-module/src/main/resources/manager/actions/site-change.d.ts @@ -1 +1,21 @@ +/*- + * #%L + * UI Module + * %% + * Copyright (C) 2023 - 2026 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * #L% + */ export function runAction(parameters: any): Promise; diff --git a/modules/ui-module/src/main/resources/manager/actions/test-command.d.ts b/modules/ui-module/src/main/resources/manager/actions/test-command.d.ts index 6abc321a1..1dc39e464 100644 --- a/modules/ui-module/src/main/resources/manager/actions/test-command.d.ts +++ b/modules/ui-module/src/main/resources/manager/actions/test-command.d.ts @@ -1 +1,21 @@ +/*- + * #%L + * UI Module + * %% + * Copyright (C) 2023 - 2026 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * #L% + */ export function runAction(params: any): Promise; diff --git a/modules/ui-module/src/main/resources/manager/index.html b/modules/ui-module/src/main/resources/manager/index.html index 76419af7e..6a1475af2 100644 --- a/modules/ui-module/src/main/resources/manager/index.html +++ b/modules/ui-module/src/main/resources/manager/index.html @@ -60,8 +60,7 @@ "@cms/js/": "{{ managerBaseURL }}/js/", "@cms/libs/": "{{ managerBaseURL }}/js/libs/", "@cms/manager/": "{{ managerBaseURL }}/js/manager/", - "@cms/modules/": "{{ managerBaseURL }}/js/modules/", - "condation-cms-ui/dist/js/modules/" : "{{ managerBaseURL }}/js/modules/" + "@cms/modules/": "{{ managerBaseURL }}/js/modules/" } } diff --git a/modules/ui-module/src/main/resources/manager/js/manager-globals.d.ts b/modules/ui-module/src/main/resources/manager/js/manager-globals.d.ts index 255f59f32..7818ef73c 100644 --- a/modules/ui-module/src/main/resources/manager/js/manager-globals.d.ts +++ b/modules/ui-module/src/main/resources/manager/js/manager-globals.d.ts @@ -1,3 +1,23 @@ +/*- + * #%L + * UI Module + * %% + * Copyright (C) 2023 - 2026 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * #L% + */ export function executeScriptAction(action: any): Promise; export function executeHookAction(action: any): Promise; /** diff --git a/modules/ui-module/src/main/resources/manager/js/manager-inject-init.d.ts b/modules/ui-module/src/main/resources/manager/js/manager-inject-init.d.ts index de2be067b..5f66dedac 100644 --- a/modules/ui-module/src/main/resources/manager/js/manager-inject-init.d.ts +++ b/modules/ui-module/src/main/resources/manager/js/manager-inject-init.d.ts @@ -1,2 +1,22 @@ +/*- + * #%L + * UI Module + * %% + * Copyright (C) 2023 - 2026 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * #L% + */ export function initIframe(): void; export function isSectionPublishedExpired(section: any): boolean; diff --git a/modules/ui-module/src/main/resources/manager/js/manager-inject.d.ts b/modules/ui-module/src/main/resources/manager/js/manager-inject.d.ts index e69de29bb..802d6a315 100644 --- a/modules/ui-module/src/main/resources/manager/js/manager-inject.d.ts +++ b/modules/ui-module/src/main/resources/manager/js/manager-inject.d.ts @@ -0,0 +1,20 @@ +/*- + * #%L + * UI Module + * %% + * Copyright (C) 2023 - 2026 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * #L% + */ diff --git a/modules/ui-module/src/main/resources/manager/js/manager.d.ts b/modules/ui-module/src/main/resources/manager/js/manager.d.ts index cb0ff5c3b..4562057ed 100644 --- a/modules/ui-module/src/main/resources/manager/js/manager.d.ts +++ b/modules/ui-module/src/main/resources/manager/js/manager.d.ts @@ -1 +1,21 @@ +/*- + * #%L + * UI Module + * %% + * Copyright (C) 2023 - 2026 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * #L% + */ export {}; diff --git a/modules/ui-module/src/main/resources/manager/js/modules/alerts.d.ts b/modules/ui-module/src/main/resources/manager/js/modules/alerts.d.ts index 629a61d8f..c25caa88d 100644 --- a/modules/ui-module/src/main/resources/manager/js/modules/alerts.d.ts +++ b/modules/ui-module/src/main/resources/manager/js/modules/alerts.d.ts @@ -1,3 +1,23 @@ +/*- + * #%L + * UI Module + * %% + * Copyright (C) 2023 - 2026 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * #L% + */ export function alertSelect(options: any): Promise; export function alertError(options: any): void; export function alertConfirm(options: any): Promise; diff --git a/modules/ui-module/src/main/resources/manager/js/modules/apps.d.ts b/modules/ui-module/src/main/resources/manager/js/modules/apps.d.ts index 081198908..e864f9c21 100644 --- a/modules/ui-module/src/main/resources/manager/js/modules/apps.d.ts +++ b/modules/ui-module/src/main/resources/manager/js/modules/apps.d.ts @@ -1 +1,21 @@ +/*- + * #%L + * UI Module + * %% + * Copyright (C) 2023 - 2026 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * #L% + */ export function initApps(): void; diff --git a/modules/ui-module/src/main/resources/manager/js/modules/collection-picker.d.ts b/modules/ui-module/src/main/resources/manager/js/modules/collection-picker.d.ts index 2e1adce04..c0eba907b 100644 --- a/modules/ui-module/src/main/resources/manager/js/modules/collection-picker.d.ts +++ b/modules/ui-module/src/main/resources/manager/js/modules/collection-picker.d.ts @@ -1,3 +1,23 @@ +/*- + * #%L + * UI Module + * %% + * Copyright (C) 2023 - 2026 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * #L% + */ import { CollectionItemSummary } from '@cms/modules/rpc/rpc-collection.js'; export interface CollectionItemPickerOptions { collection: string; diff --git a/modules/ui-module/src/main/resources/manager/js/modules/collection-picker.js b/modules/ui-module/src/main/resources/manager/js/modules/collection-picker.js index 0dcf1cc66..4924b7031 100644 --- a/modules/ui-module/src/main/resources/manager/js/modules/collection-picker.js +++ b/modules/ui-module/src/main/resources/manager/js/modules/collection-picker.js @@ -23,11 +23,11 @@ import { openModal } from '@cms/modules/modal.js'; import { listCollectionItems } from '@cms/modules/rpc/rpc-collection.js'; const PAGE_SIZE = 10; const escapeHtml = (value) => String(value ?? '') - .replace(/&/g, '&') - .replace(//g, '>') - .replace(/"/g, '"') - .replace(/'/g, '''); + .replaceAll('&', '&') + .replaceAll('<', '<') + .replaceAll('>', '>') + .replaceAll('"', '"') + .replaceAll("'", '''); const renderMessage = (message) => `

${escapeHtml(message)}

`; const renderResults = (response, selectText) => { if (response.items.length === 0) { @@ -125,6 +125,7 @@ export const openCollectionItemPicker = (options) => { })); } catch (error) { + // exception is ignored, message to user is displayed in the modal if (version !== requestVersion) return; resultsElement.innerHTML = ` diff --git a/modules/ui-module/src/main/resources/manager/js/modules/event-bus.d.ts b/modules/ui-module/src/main/resources/manager/js/modules/event-bus.d.ts index d44295919..462f38dae 100644 --- a/modules/ui-module/src/main/resources/manager/js/modules/event-bus.d.ts +++ b/modules/ui-module/src/main/resources/manager/js/modules/event-bus.d.ts @@ -1,3 +1,23 @@ +/*- + * #%L + * UI Module + * %% + * Copyright (C) 2023 - 2026 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * #L% + */ export namespace EventBus { function on(event: any, handler: any): void; function emit(event: any, payload: any): void; diff --git a/modules/ui-module/src/main/resources/manager/js/modules/event-bus.js b/modules/ui-module/src/main/resources/manager/js/modules/event-bus.js index 16ee9dff6..55a6111a2 100644 --- a/modules/ui-module/src/main/resources/manager/js/modules/event-bus.js +++ b/modules/ui-module/src/main/resources/manager/js/modules/event-bus.js @@ -21,7 +21,7 @@ const listeners = {}; export const EventBus = { on(event, handler) { - (listeners[event] || (listeners[event] = [])).push(handler); + (listeners[event] ||= []).push(handler); }, emit(event, payload) { (listeners[event] || []).forEach(fn => fn(payload)); diff --git a/modules/ui-module/src/main/resources/manager/js/modules/filebrowser/filebrowser.actions.d.ts b/modules/ui-module/src/main/resources/manager/js/modules/filebrowser/filebrowser.actions.d.ts index 31df668e4..e1c7c16d4 100644 --- a/modules/ui-module/src/main/resources/manager/js/modules/filebrowser/filebrowser.actions.d.ts +++ b/modules/ui-module/src/main/resources/manager/js/modules/filebrowser/filebrowser.actions.d.ts @@ -1,3 +1,23 @@ +/*- + * #%L + * UI Module + * %% + * Copyright (C) 2023 - 2026 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * #L% + */ export function renameFileAction({ state, getTargetFolder, filename, title, content }: { state: any; getTargetFolder: any; diff --git a/modules/ui-module/src/main/resources/manager/js/modules/filebrowser/filebrowser.create.d.ts b/modules/ui-module/src/main/resources/manager/js/modules/filebrowser/filebrowser.create.d.ts index fb627bf7e..db898301c 100644 --- a/modules/ui-module/src/main/resources/manager/js/modules/filebrowser/filebrowser.create.d.ts +++ b/modules/ui-module/src/main/resources/manager/js/modules/filebrowser/filebrowser.create.d.ts @@ -1 +1,21 @@ +/*- + * #%L + * UI Module + * %% + * Copyright (C) 2023 - 2026 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * #L% + */ export function openCreateContentBrowser(params: any): Promise; diff --git a/modules/ui-module/src/main/resources/manager/js/modules/filebrowser/filebrowser.d.ts b/modules/ui-module/src/main/resources/manager/js/modules/filebrowser/filebrowser.d.ts index e10842a8d..0869c773e 100644 --- a/modules/ui-module/src/main/resources/manager/js/modules/filebrowser/filebrowser.d.ts +++ b/modules/ui-module/src/main/resources/manager/js/modules/filebrowser/filebrowser.d.ts @@ -1,3 +1,23 @@ +/*- + * #%L + * UI Module + * %% + * Copyright (C) 2023 - 2026 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * #L% + */ export function openFileBrowser(optionsParam: any): Promise; export namespace state { let options: null; diff --git a/modules/ui-module/src/main/resources/manager/js/modules/filebrowser/filebrowser.template.d.ts b/modules/ui-module/src/main/resources/manager/js/modules/filebrowser/filebrowser.template.d.ts index 72c47da10..e21fdb20e 100644 --- a/modules/ui-module/src/main/resources/manager/js/modules/filebrowser/filebrowser.template.d.ts +++ b/modules/ui-module/src/main/resources/manager/js/modules/filebrowser/filebrowser.template.d.ts @@ -1,2 +1,22 @@ +/*- + * #%L + * UI Module + * %% + * Copyright (C) 2023 - 2026 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * #L% + */ export { template as filebrowserTemplate }; declare const template: any; diff --git a/modules/ui-module/src/main/resources/manager/js/modules/form/field.checkbox.d.ts b/modules/ui-module/src/main/resources/manager/js/modules/form/field.checkbox.d.ts index d2a0c9280..a930d60c6 100644 --- a/modules/ui-module/src/main/resources/manager/js/modules/form/field.checkbox.d.ts +++ b/modules/ui-module/src/main/resources/manager/js/modules/form/field.checkbox.d.ts @@ -1,3 +1,23 @@ +/*- + * #%L + * UI Module + * %% + * Copyright (C) 2023 - 2026 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * #L% + */ import { FieldOptions, FormField } from "@cms/modules/form/forms.js"; export interface CheckboxOptions extends FieldOptions { key?: string; diff --git a/modules/ui-module/src/main/resources/manager/js/modules/form/field.checkbox.js b/modules/ui-module/src/main/resources/manager/js/modules/form/field.checkbox.js index 3efa4721d..6bbe7a57b 100644 --- a/modules/ui-module/src/main/resources/manager/js/modules/form/field.checkbox.js +++ b/modules/ui-module/src/main/resources/manager/js/modules/form/field.checkbox.js @@ -1,3 +1,23 @@ +/*- + * #%L + * UI Module + * %% + * Copyright (C) 2023 - 2026 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * #L% + */ import { createID } from "@cms/modules/form/utils.js"; const createCheckboxField = (options, value = []) => { const id = createID(); diff --git a/modules/ui-module/src/main/resources/manager/js/modules/form/field.code.d.ts b/modules/ui-module/src/main/resources/manager/js/modules/form/field.code.d.ts index ada1f20f1..4118facce 100644 --- a/modules/ui-module/src/main/resources/manager/js/modules/form/field.code.d.ts +++ b/modules/ui-module/src/main/resources/manager/js/modules/form/field.code.d.ts @@ -1,3 +1,23 @@ +/*- + * #%L + * UI Module + * %% + * Copyright (C) 2023 - 2026 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * #L% + */ export namespace CodeField { export { createCodeField as markup }; export { init }; diff --git a/modules/ui-module/src/main/resources/manager/js/modules/form/field.collection.d.ts b/modules/ui-module/src/main/resources/manager/js/modules/form/field.collection.d.ts index 4e23bacfb..bd0aabe1c 100644 --- a/modules/ui-module/src/main/resources/manager/js/modules/form/field.collection.d.ts +++ b/modules/ui-module/src/main/resources/manager/js/modules/form/field.collection.d.ts @@ -1,3 +1,23 @@ +/*- + * #%L + * UI Module + * %% + * Copyright (C) 2023 - 2026 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * #L% + */ import { FieldOptions, FormField } from '@cms/modules/form/forms.js'; export interface CollectionFieldOptions extends FieldOptions { options?: { diff --git a/modules/ui-module/src/main/resources/manager/js/modules/form/field.collection.js b/modules/ui-module/src/main/resources/manager/js/modules/form/field.collection.js index da6997444..d51c8f955 100644 --- a/modules/ui-module/src/main/resources/manager/js/modules/form/field.collection.js +++ b/modules/ui-module/src/main/resources/manager/js/modules/form/field.collection.js @@ -22,11 +22,11 @@ import { openCollectionItemPicker } from '@cms/modules/collection-picker.js'; import { createID } from '@cms/modules/form/utils.js'; import { i18n } from '@cms/modules/localization.js'; const escapeHtml = (value) => String(value ?? '') - .replace(/&/g, '&') - .replace(//g, '>') - .replace(/"/g, '"') - .replace(/'/g, '''); + .replaceAll('&', '&') + .replaceAll('<', '<') + .replaceAll('>', '>') + .replaceAll('"', '"') + .replaceAll("'", '''); const createCollectionField = (options, value = '') => { const id = createID(); const key = `field.${options.name}`; diff --git a/modules/ui-module/src/main/resources/manager/js/modules/form/field.color.d.ts b/modules/ui-module/src/main/resources/manager/js/modules/form/field.color.d.ts index cab0d2e87..0225e9768 100644 --- a/modules/ui-module/src/main/resources/manager/js/modules/form/field.color.d.ts +++ b/modules/ui-module/src/main/resources/manager/js/modules/form/field.color.d.ts @@ -1,3 +1,23 @@ +/*- + * #%L + * UI Module + * %% + * Copyright (C) 2023 - 2026 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * #L% + */ import { FieldOptions, FormField } from "@cms/modules/form/forms.js"; export interface ColorFieldOptions extends FieldOptions { } diff --git a/modules/ui-module/src/main/resources/manager/js/modules/form/field.date.d.ts b/modules/ui-module/src/main/resources/manager/js/modules/form/field.date.d.ts index f07e3cc6e..e45cc358c 100644 --- a/modules/ui-module/src/main/resources/manager/js/modules/form/field.date.d.ts +++ b/modules/ui-module/src/main/resources/manager/js/modules/form/field.date.d.ts @@ -1,3 +1,23 @@ +/*- + * #%L + * UI Module + * %% + * Copyright (C) 2023 - 2026 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * #L% + */ import { FieldOptions, FormField } from "@cms/modules/form/forms.js"; export interface DateFieldOptions extends FieldOptions { placeholder?: string; diff --git a/modules/ui-module/src/main/resources/manager/js/modules/form/field.datetime.d.ts b/modules/ui-module/src/main/resources/manager/js/modules/form/field.datetime.d.ts index 272202696..13130d6a8 100644 --- a/modules/ui-module/src/main/resources/manager/js/modules/form/field.datetime.d.ts +++ b/modules/ui-module/src/main/resources/manager/js/modules/form/field.datetime.d.ts @@ -1,3 +1,23 @@ +/*- + * #%L + * UI Module + * %% + * Copyright (C) 2023 - 2026 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * #L% + */ import { FieldOptions, FormField } from "@cms/modules/form/forms.js"; export interface DateTimeFieldOptions extends FieldOptions { placeholder?: string; diff --git a/modules/ui-module/src/main/resources/manager/js/modules/form/field.divider.d.ts b/modules/ui-module/src/main/resources/manager/js/modules/form/field.divider.d.ts index f57944c90..d52208432 100644 --- a/modules/ui-module/src/main/resources/manager/js/modules/form/field.divider.d.ts +++ b/modules/ui-module/src/main/resources/manager/js/modules/form/field.divider.d.ts @@ -1,3 +1,23 @@ +/*- + * #%L + * UI Module + * %% + * Copyright (C) 2023 - 2026 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * #L% + */ import { FieldOptions, FormField } from "@cms/modules/form/forms.js"; export interface DividerOptions extends FieldOptions { } diff --git a/modules/ui-module/src/main/resources/manager/js/modules/form/field.easymde.d.ts b/modules/ui-module/src/main/resources/manager/js/modules/form/field.easymde.d.ts index 9ac807575..8b94a8e15 100644 --- a/modules/ui-module/src/main/resources/manager/js/modules/form/field.easymde.d.ts +++ b/modules/ui-module/src/main/resources/manager/js/modules/form/field.easymde.d.ts @@ -1,3 +1,23 @@ +/*- + * #%L + * UI Module + * %% + * Copyright (C) 2023 - 2026 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * #L% + */ import { FieldOptions, FormField } from "@cms/modules/form/forms.js"; export interface EasyMDEFieldOptions extends FieldOptions { } diff --git a/modules/ui-module/src/main/resources/manager/js/modules/form/field.list.d.ts b/modules/ui-module/src/main/resources/manager/js/modules/form/field.list.d.ts index 7bfcc69a9..eac48f3db 100644 --- a/modules/ui-module/src/main/resources/manager/js/modules/form/field.list.d.ts +++ b/modules/ui-module/src/main/resources/manager/js/modules/form/field.list.d.ts @@ -1,3 +1,23 @@ +/*- + * #%L + * UI Module + * %% + * Copyright (C) 2023 - 2026 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * #L% + */ import { FieldOptions, FormField } from "@cms/modules/form/forms.js"; export interface ListFieldOptions extends FieldOptions { options: { diff --git a/modules/ui-module/src/main/resources/manager/js/modules/form/field.mail.d.ts b/modules/ui-module/src/main/resources/manager/js/modules/form/field.mail.d.ts index 36f178178..df6a6ab49 100644 --- a/modules/ui-module/src/main/resources/manager/js/modules/form/field.mail.d.ts +++ b/modules/ui-module/src/main/resources/manager/js/modules/form/field.mail.d.ts @@ -1,3 +1,23 @@ +/*- + * #%L + * UI Module + * %% + * Copyright (C) 2023 - 2026 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * #L% + */ import { FieldOptions, FormField } from "@cms/modules/form/forms.js"; export interface MailFieldOptions extends FieldOptions { placeholder?: string; diff --git a/modules/ui-module/src/main/resources/manager/js/modules/form/field.markdown.d.ts b/modules/ui-module/src/main/resources/manager/js/modules/form/field.markdown.d.ts index 32fc0b8bf..21c39aafe 100644 --- a/modules/ui-module/src/main/resources/manager/js/modules/form/field.markdown.d.ts +++ b/modules/ui-module/src/main/resources/manager/js/modules/form/field.markdown.d.ts @@ -1,3 +1,23 @@ +/*- + * #%L + * UI Module + * %% + * Copyright (C) 2023 - 2026 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * #L% + */ import { FieldOptions, FormField } from "@cms/modules/form/forms.js"; declare global { interface HTMLInputElement { diff --git a/modules/ui-module/src/main/resources/manager/js/modules/form/field.media.d.ts b/modules/ui-module/src/main/resources/manager/js/modules/form/field.media.d.ts index b359b73f0..1350f89f2 100644 --- a/modules/ui-module/src/main/resources/manager/js/modules/form/field.media.d.ts +++ b/modules/ui-module/src/main/resources/manager/js/modules/form/field.media.d.ts @@ -1,3 +1,23 @@ +/*- + * #%L + * UI Module + * %% + * Copyright (C) 2023 - 2026 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * #L% + */ import { FieldOptions, FormField } from "@cms/modules/form/forms.js"; export interface MediaFieldOptions extends FieldOptions { } diff --git a/modules/ui-module/src/main/resources/manager/js/modules/form/field.number.d.ts b/modules/ui-module/src/main/resources/manager/js/modules/form/field.number.d.ts index c034ebbc7..50b700cb0 100644 --- a/modules/ui-module/src/main/resources/manager/js/modules/form/field.number.d.ts +++ b/modules/ui-module/src/main/resources/manager/js/modules/form/field.number.d.ts @@ -1,3 +1,23 @@ +/*- + * #%L + * UI Module + * %% + * Copyright (C) 2023 - 2026 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * #L% + */ import { FieldOptions, FormField } from "@cms/modules/form/forms.js"; export interface NumberFieldOptions extends FieldOptions { options: { diff --git a/modules/ui-module/src/main/resources/manager/js/modules/form/field.radio.d.ts b/modules/ui-module/src/main/resources/manager/js/modules/form/field.radio.d.ts index a238a4826..344bb4fda 100644 --- a/modules/ui-module/src/main/resources/manager/js/modules/form/field.radio.d.ts +++ b/modules/ui-module/src/main/resources/manager/js/modules/form/field.radio.d.ts @@ -1,3 +1,23 @@ +/*- + * #%L + * UI Module + * %% + * Copyright (C) 2023 - 2026 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * #L% + */ import { FieldOptions, FormField } from "@cms/modules/form/forms.js"; export interface RadioFieldOptions extends FieldOptions { options?: { diff --git a/modules/ui-module/src/main/resources/manager/js/modules/form/field.range.d.ts b/modules/ui-module/src/main/resources/manager/js/modules/form/field.range.d.ts index 47e6c847c..e57f403cf 100644 --- a/modules/ui-module/src/main/resources/manager/js/modules/form/field.range.d.ts +++ b/modules/ui-module/src/main/resources/manager/js/modules/form/field.range.d.ts @@ -1,3 +1,23 @@ +/*- + * #%L + * UI Module + * %% + * Copyright (C) 2023 - 2026 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * #L% + */ import { FieldOptions, FormField } from "@cms/modules/form/forms.js"; export interface RangeFieldOptions extends FieldOptions { options?: { diff --git a/modules/ui-module/src/main/resources/manager/js/modules/form/field.reference.d.ts b/modules/ui-module/src/main/resources/manager/js/modules/form/field.reference.d.ts index 7f1b0b8c4..fc0de6ab4 100644 --- a/modules/ui-module/src/main/resources/manager/js/modules/form/field.reference.d.ts +++ b/modules/ui-module/src/main/resources/manager/js/modules/form/field.reference.d.ts @@ -1,3 +1,23 @@ +/*- + * #%L + * UI Module + * %% + * Copyright (C) 2023 - 2026 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * #L% + */ import { FieldOptions, FormField } from "@cms/modules/form/forms.js"; export interface ReferenceFieldOptions extends FieldOptions { options?: { diff --git a/modules/ui-module/src/main/resources/manager/js/modules/form/field.select.d.ts b/modules/ui-module/src/main/resources/manager/js/modules/form/field.select.d.ts index c781deccb..5ae22bd43 100644 --- a/modules/ui-module/src/main/resources/manager/js/modules/form/field.select.d.ts +++ b/modules/ui-module/src/main/resources/manager/js/modules/form/field.select.d.ts @@ -1,3 +1,23 @@ +/*- + * #%L + * UI Module + * %% + * Copyright (C) 2023 - 2026 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * #L% + */ import { FieldOptions, FormField } from "@cms/modules/form/forms.js"; export interface SelectFieldOptions extends FieldOptions { options?: { diff --git a/modules/ui-module/src/main/resources/manager/js/modules/form/field.tags.d.ts b/modules/ui-module/src/main/resources/manager/js/modules/form/field.tags.d.ts index 50f7e6d01..634fb842a 100644 --- a/modules/ui-module/src/main/resources/manager/js/modules/form/field.tags.d.ts +++ b/modules/ui-module/src/main/resources/manager/js/modules/form/field.tags.d.ts @@ -1,3 +1,23 @@ +/*- + * #%L + * UI Module + * %% + * Copyright (C) 2023 - 2026 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * #L% + */ import { FieldOptions, FormField } from "@cms/modules/form/forms.js"; export interface TagsFieldOptions extends FieldOptions { options?: { diff --git a/modules/ui-module/src/main/resources/manager/js/modules/form/field.text.d.ts b/modules/ui-module/src/main/resources/manager/js/modules/form/field.text.d.ts index b406c7cf8..7a72faae8 100644 --- a/modules/ui-module/src/main/resources/manager/js/modules/form/field.text.d.ts +++ b/modules/ui-module/src/main/resources/manager/js/modules/form/field.text.d.ts @@ -1,3 +1,23 @@ +/*- + * #%L + * UI Module + * %% + * Copyright (C) 2023 - 2026 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * #L% + */ import { FieldOptions, FormField } from "@cms/modules/form/forms.js"; export interface TextFieldOptions extends FieldOptions { placeholder?: string; diff --git a/modules/ui-module/src/main/resources/manager/js/modules/form/field.textarea.d.ts b/modules/ui-module/src/main/resources/manager/js/modules/form/field.textarea.d.ts index f83d21361..4b4659e7b 100644 --- a/modules/ui-module/src/main/resources/manager/js/modules/form/field.textarea.d.ts +++ b/modules/ui-module/src/main/resources/manager/js/modules/form/field.textarea.d.ts @@ -1,3 +1,23 @@ +/*- + * #%L + * UI Module + * %% + * Copyright (C) 2023 - 2026 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * #L% + */ import { FieldOptions, FormField } from "@cms/modules/form/forms.js"; export interface TextAreaFieldOptions extends FieldOptions { rows?: number; diff --git a/modules/ui-module/src/main/resources/manager/js/modules/form/forms.d.ts b/modules/ui-module/src/main/resources/manager/js/modules/form/forms.d.ts index 869e84f19..66d90daed 100644 --- a/modules/ui-module/src/main/resources/manager/js/modules/form/forms.d.ts +++ b/modules/ui-module/src/main/resources/manager/js/modules/form/forms.d.ts @@ -1,3 +1,23 @@ +/*- + * #%L + * UI Module + * %% + * Copyright (C) 2023 - 2026 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * #L% + */ declare const getFormFields: (definition: any) => any[]; declare const createForm: (options: any) => Form; export { createForm, getFormFields }; diff --git a/modules/ui-module/src/main/resources/manager/js/modules/form/utils.d.ts b/modules/ui-module/src/main/resources/manager/js/modules/form/utils.d.ts index 525b147b7..23eeb2258 100644 --- a/modules/ui-module/src/main/resources/manager/js/modules/form/utils.d.ts +++ b/modules/ui-module/src/main/resources/manager/js/modules/form/utils.d.ts @@ -1,3 +1,23 @@ +/*- + * #%L + * UI Module + * %% + * Copyright (C) 2023 - 2026 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * #L% + */ declare const createID: () => string; declare const utcToLocalDateTimeInputValue: (utcString: string) => string; declare function getUTCDateTimeFromInput(inputElement: HTMLInputElement): string | null; diff --git a/modules/ui-module/src/main/resources/manager/js/modules/frameMessenger.d.ts b/modules/ui-module/src/main/resources/manager/js/modules/frameMessenger.d.ts index 024ff6c30..cbd1acc3c 100644 --- a/modules/ui-module/src/main/resources/manager/js/modules/frameMessenger.d.ts +++ b/modules/ui-module/src/main/resources/manager/js/modules/frameMessenger.d.ts @@ -1,3 +1,23 @@ +/*- + * #%L + * UI Module + * %% + * Copyright (C) 2023 - 2026 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * #L% + */ export interface FrameMessage { type: string; payload?: T; diff --git a/modules/ui-module/src/main/resources/manager/js/modules/locale-utils.d.ts b/modules/ui-module/src/main/resources/manager/js/modules/locale-utils.d.ts index 98374290b..aca04a87b 100644 --- a/modules/ui-module/src/main/resources/manager/js/modules/locale-utils.d.ts +++ b/modules/ui-module/src/main/resources/manager/js/modules/locale-utils.d.ts @@ -1,2 +1,22 @@ +/*- + * #%L + * UI Module + * %% + * Copyright (C) 2023 - 2026 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * #L% + */ export function getLocale(): any; export function setLocale(locale: any): void; diff --git a/modules/ui-module/src/main/resources/manager/js/modules/localization-actions.d.ts b/modules/ui-module/src/main/resources/manager/js/modules/localization-actions.d.ts index 43acad38a..6db9e8af0 100644 --- a/modules/ui-module/src/main/resources/manager/js/modules/localization-actions.d.ts +++ b/modules/ui-module/src/main/resources/manager/js/modules/localization-actions.d.ts @@ -1,3 +1,23 @@ +/*- + * #%L + * UI Module + * %% + * Copyright (C) 2023 - 2026 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * #L% + */ export namespace ACTION_LOCALIZATIONS { let en: {}; let de: { diff --git a/modules/ui-module/src/main/resources/manager/js/modules/localization-loader.d.ts b/modules/ui-module/src/main/resources/manager/js/modules/localization-loader.d.ts index ea5b0a075..5cca57e0f 100644 --- a/modules/ui-module/src/main/resources/manager/js/modules/localization-loader.d.ts +++ b/modules/ui-module/src/main/resources/manager/js/modules/localization-loader.d.ts @@ -1,3 +1,23 @@ +/*- + * #%L + * UI Module + * %% + * Copyright (C) 2023 - 2026 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * #L% + */ export function loadLocalizationsWithDefaults(): Promise<{ en: { "ui.filebrowser.filename": string; diff --git a/modules/ui-module/src/main/resources/manager/js/modules/localization-modules.d.ts b/modules/ui-module/src/main/resources/manager/js/modules/localization-modules.d.ts index 0afd1b6ed..94c485674 100644 --- a/modules/ui-module/src/main/resources/manager/js/modules/localization-modules.d.ts +++ b/modules/ui-module/src/main/resources/manager/js/modules/localization-modules.d.ts @@ -1,3 +1,23 @@ +/*- + * #%L + * UI Module + * %% + * Copyright (C) 2023 - 2026 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * #L% + */ export namespace MODULE_LOCALIZATIONS { let en: {}; let de: { diff --git a/modules/ui-module/src/main/resources/manager/js/modules/localization.d.ts b/modules/ui-module/src/main/resources/manager/js/modules/localization.d.ts index 9eeed5a15..e91051759 100644 --- a/modules/ui-module/src/main/resources/manager/js/modules/localization.d.ts +++ b/modules/ui-module/src/main/resources/manager/js/modules/localization.d.ts @@ -1,3 +1,23 @@ +/*- + * #%L + * UI Module + * %% + * Copyright (C) 2023 - 2026 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * #L% + */ export function localizeUi(): Promise; export namespace i18n { let _locale: any; diff --git a/modules/ui-module/src/main/resources/manager/js/modules/manager-ui.d.ts b/modules/ui-module/src/main/resources/manager/js/modules/manager-ui.d.ts index d25e9cb32..247c12e7c 100644 --- a/modules/ui-module/src/main/resources/manager/js/modules/manager-ui.d.ts +++ b/modules/ui-module/src/main/resources/manager/js/modules/manager-ui.d.ts @@ -1 +1,21 @@ +/*- + * #%L + * UI Module + * %% + * Copyright (C) 2023 - 2026 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * #L% + */ export function updateStateButton(): void; diff --git a/modules/ui-module/src/main/resources/manager/js/modules/manager/manager.message.handlers.d.ts b/modules/ui-module/src/main/resources/manager/js/modules/manager/manager.message.handlers.d.ts index dd88b9fe0..5b7c71b35 100644 --- a/modules/ui-module/src/main/resources/manager/js/modules/manager/manager.message.handlers.d.ts +++ b/modules/ui-module/src/main/resources/manager/js/modules/manager/manager.message.handlers.d.ts @@ -1,2 +1,22 @@ +/*- + * #%L + * UI Module + * %% + * Copyright (C) 2023 - 2026 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * #L% + */ declare const initMessageHandlers: () => void; export { initMessageHandlers }; diff --git a/modules/ui-module/src/main/resources/manager/js/modules/manager/media.inject.d.ts b/modules/ui-module/src/main/resources/manager/js/modules/manager/media.inject.d.ts index ebd61d27b..067619660 100644 --- a/modules/ui-module/src/main/resources/manager/js/modules/manager/media.inject.d.ts +++ b/modules/ui-module/src/main/resources/manager/js/modules/manager/media.inject.d.ts @@ -1,3 +1,23 @@ +/*- + * #%L + * UI Module + * %% + * Copyright (C) 2023 - 2026 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * #L% + */ export declare const initMediaUploadOverlay: (img: HTMLImageElement) => void; export declare const initContentMediaToolbar: (img: HTMLImageElement) => void; export declare const initMediaToolbar: (img: HTMLImageElement) => void; diff --git a/modules/ui-module/src/main/resources/manager/js/modules/manager/toolbar-icons.d.ts b/modules/ui-module/src/main/resources/manager/js/modules/manager/toolbar-icons.d.ts index 570269ca5..3ae76b89a 100644 --- a/modules/ui-module/src/main/resources/manager/js/modules/manager/toolbar-icons.d.ts +++ b/modules/ui-module/src/main/resources/manager/js/modules/manager/toolbar-icons.d.ts @@ -1,3 +1,23 @@ +/*- + * #%L + * UI Module + * %% + * Copyright (C) 2023 - 2026 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * #L% + */ export declare const EDIT_PAGE_ICON = "\n\n \n \n"; export declare const EDIT_ATTRIBUTES_ICON = "\n\n \n\n"; export declare const SECTION_SORT_ICON = "\n\n \n \n\n"; diff --git a/modules/ui-module/src/main/resources/manager/js/modules/manager/toolbar.inject.d.ts b/modules/ui-module/src/main/resources/manager/js/modules/manager/toolbar.inject.d.ts index 6e3a61bf4..8a082a5df 100644 --- a/modules/ui-module/src/main/resources/manager/js/modules/manager/toolbar.inject.d.ts +++ b/modules/ui-module/src/main/resources/manager/js/modules/manager/toolbar.inject.d.ts @@ -1 +1,21 @@ +/*- + * #%L + * UI Module + * %% + * Copyright (C) 2023 - 2026 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * #L% + */ export declare const initToolbar: (container: HTMLElement) => void; diff --git a/modules/ui-module/src/main/resources/manager/js/modules/media/mediabrowser.actions.d.ts b/modules/ui-module/src/main/resources/manager/js/modules/media/mediabrowser.actions.d.ts index 8d9d29343..688d73a00 100644 --- a/modules/ui-module/src/main/resources/manager/js/modules/media/mediabrowser.actions.d.ts +++ b/modules/ui-module/src/main/resources/manager/js/modules/media/mediabrowser.actions.d.ts @@ -1,3 +1,23 @@ +/*- + * #%L + * UI Module + * %% + * Copyright (C) 2023 - 2026 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * #L% + */ export function renameMediaAction({ state, getTargetFolder, filename }: { state: any; getTargetFolder: any; diff --git a/modules/ui-module/src/main/resources/manager/js/modules/media/mediabrowser.create.d.ts b/modules/ui-module/src/main/resources/manager/js/modules/media/mediabrowser.create.d.ts index fb627bf7e..db898301c 100644 --- a/modules/ui-module/src/main/resources/manager/js/modules/media/mediabrowser.create.d.ts +++ b/modules/ui-module/src/main/resources/manager/js/modules/media/mediabrowser.create.d.ts @@ -1 +1,21 @@ +/*- + * #%L + * UI Module + * %% + * Copyright (C) 2023 - 2026 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * #L% + */ export function openCreateContentBrowser(params: any): Promise; diff --git a/modules/ui-module/src/main/resources/manager/js/modules/media/mediabrowser.d.ts b/modules/ui-module/src/main/resources/manager/js/modules/media/mediabrowser.d.ts index af236ca32..125ee5274 100644 --- a/modules/ui-module/src/main/resources/manager/js/modules/media/mediabrowser.d.ts +++ b/modules/ui-module/src/main/resources/manager/js/modules/media/mediabrowser.d.ts @@ -1,3 +1,23 @@ +/*- + * #%L + * UI Module + * %% + * Copyright (C) 2023 - 2026 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * #L% + */ export function openMediaBrowser(optionsParam: any): Promise; export namespace state { let options: null; diff --git a/modules/ui-module/src/main/resources/manager/js/modules/media/mediabrowser.template.d.ts b/modules/ui-module/src/main/resources/manager/js/modules/media/mediabrowser.template.d.ts index 2c8ff55fe..2caff5efc 100644 --- a/modules/ui-module/src/main/resources/manager/js/modules/media/mediabrowser.template.d.ts +++ b/modules/ui-module/src/main/resources/manager/js/modules/media/mediabrowser.template.d.ts @@ -1,2 +1,22 @@ +/*- + * #%L + * UI Module + * %% + * Copyright (C) 2023 - 2026 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * #L% + */ export { template as mediabrowserTemplate }; declare const template: any; diff --git a/modules/ui-module/src/main/resources/manager/js/modules/media/mediabrowser.upload.d.ts b/modules/ui-module/src/main/resources/manager/js/modules/media/mediabrowser.upload.d.ts index 5b1bd46d8..954f1b3fc 100644 --- a/modules/ui-module/src/main/resources/manager/js/modules/media/mediabrowser.upload.d.ts +++ b/modules/ui-module/src/main/resources/manager/js/modules/media/mediabrowser.upload.d.ts @@ -1,2 +1,22 @@ +/*- + * #%L + * UI Module + * %% + * Copyright (C) 2023 - 2026 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * #L% + */ export function initDragAndDropUpload(): void; export function handleFileUpload(): Promise; diff --git a/modules/ui-module/src/main/resources/manager/js/modules/modal.d.ts b/modules/ui-module/src/main/resources/manager/js/modules/modal.d.ts index a6bc8574c..4f6c59a03 100644 --- a/modules/ui-module/src/main/resources/manager/js/modules/modal.d.ts +++ b/modules/ui-module/src/main/resources/manager/js/modules/modal.d.ts @@ -1 +1,21 @@ +/*- + * #%L + * UI Module + * %% + * Copyright (C) 2023 - 2026 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * #L% + */ export function openModal(optionsParam: any): any; diff --git a/modules/ui-module/src/main/resources/manager/js/modules/node.d.ts b/modules/ui-module/src/main/resources/manager/js/modules/node.d.ts index ade17f827..81527fd56 100644 --- a/modules/ui-module/src/main/resources/manager/js/modules/node.d.ts +++ b/modules/ui-module/src/main/resources/manager/js/modules/node.d.ts @@ -1,3 +1,23 @@ +/*- + * #%L + * UI Module + * %% + * Copyright (C) 2023 - 2026 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * #L% + */ /** * Retrieves a nested value from an object using a dot-notated path like "meta.title" * @param {object} sourceObj - The object to retrieve the value from diff --git a/modules/ui-module/src/main/resources/manager/js/modules/page-picker.d.ts b/modules/ui-module/src/main/resources/manager/js/modules/page-picker.d.ts index c3187feac..69fe549fa 100644 --- a/modules/ui-module/src/main/resources/manager/js/modules/page-picker.d.ts +++ b/modules/ui-module/src/main/resources/manager/js/modules/page-picker.d.ts @@ -1,3 +1,23 @@ +/*- + * #%L + * UI Module + * %% + * Copyright (C) 2023 - 2026 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * #L% + */ import { SearchResultDto } from '@cms/modules/rpc/rpc-page.js'; export interface PagePickerOptions { title?: string; diff --git a/modules/ui-module/src/main/resources/manager/js/modules/preview-context.d.ts b/modules/ui-module/src/main/resources/manager/js/modules/preview-context.d.ts index c1732a951..3422613f0 100644 --- a/modules/ui-module/src/main/resources/manager/js/modules/preview-context.d.ts +++ b/modules/ui-module/src/main/resources/manager/js/modules/preview-context.d.ts @@ -1,3 +1,23 @@ +/*- + * #%L + * UI Module + * %% + * Copyright (C) 2023 - 2026 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * #L% + */ export interface ActivePreviewContent { uri: string; url?: string; diff --git a/modules/ui-module/src/main/resources/manager/js/modules/preview.history.d.ts b/modules/ui-module/src/main/resources/manager/js/modules/preview.history.d.ts index 19f953414..6cfeb16ad 100644 --- a/modules/ui-module/src/main/resources/manager/js/modules/preview.history.d.ts +++ b/modules/ui-module/src/main/resources/manager/js/modules/preview.history.d.ts @@ -1,3 +1,23 @@ +/*- + * #%L + * UI Module + * %% + * Copyright (C) 2023 - 2026 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * #L% + */ export namespace PreviewHistory { export { init }; export { navigatePreview }; diff --git a/modules/ui-module/src/main/resources/manager/js/modules/preview.utils.d.ts b/modules/ui-module/src/main/resources/manager/js/modules/preview.utils.d.ts index a6bfee843..c2899f668 100644 --- a/modules/ui-module/src/main/resources/manager/js/modules/preview.utils.d.ts +++ b/modules/ui-module/src/main/resources/manager/js/modules/preview.utils.d.ts @@ -1,3 +1,23 @@ +/*- + * #%L + * UI Module + * %% + * Copyright (C) 2023 - 2026 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * #L% + */ export function activatePreviewOverlay(): void; export function deActivatePreviewOverlay(): void; export function getPreviewUrl(): any; diff --git a/modules/ui-module/src/main/resources/manager/js/modules/rpc/rpc-access.d.ts b/modules/ui-module/src/main/resources/manager/js/modules/rpc/rpc-access.d.ts index c5fd921f8..00ef59607 100644 --- a/modules/ui-module/src/main/resources/manager/js/modules/rpc/rpc-access.d.ts +++ b/modules/ui-module/src/main/resources/manager/js/modules/rpc/rpc-access.d.ts @@ -1,3 +1,23 @@ +/*- + * #%L + * UI Module + * %% + * Copyright (C) 2023 - 2026 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * #L% + */ export interface Permission { key: string; description: string; diff --git a/modules/ui-module/src/main/resources/manager/js/modules/rpc/rpc-collection.d.ts b/modules/ui-module/src/main/resources/manager/js/modules/rpc/rpc-collection.d.ts index b57aa9c35..f7bf92d98 100644 --- a/modules/ui-module/src/main/resources/manager/js/modules/rpc/rpc-collection.d.ts +++ b/modules/ui-module/src/main/resources/manager/js/modules/rpc/rpc-collection.d.ts @@ -1,3 +1,23 @@ +/*- + * #%L + * UI Module + * %% + * Copyright (C) 2023 - 2026 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * #L% + */ export interface CollectionItemSummary { id: string; collection: string; diff --git a/modules/ui-module/src/main/resources/manager/js/modules/rpc/rpc-content.d.ts b/modules/ui-module/src/main/resources/manager/js/modules/rpc/rpc-content.d.ts index 6bb73c1dc..0955b8b86 100644 --- a/modules/ui-module/src/main/resources/manager/js/modules/rpc/rpc-content.d.ts +++ b/modules/ui-module/src/main/resources/manager/js/modules/rpc/rpc-content.d.ts @@ -1,3 +1,23 @@ +/*- + * #%L + * UI Module + * %% + * Copyright (C) 2023 - 2026 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * #L% + */ import { RPCResponse } from '@cms/modules/rpc/rpc.js'; declare const getContentNode: (options: any) => Promise; declare const getContent: (options: any) => Promise; diff --git a/modules/ui-module/src/main/resources/manager/js/modules/rpc/rpc-files.d.ts b/modules/ui-module/src/main/resources/manager/js/modules/rpc/rpc-files.d.ts index 19350eb0f..4b1d33075 100644 --- a/modules/ui-module/src/main/resources/manager/js/modules/rpc/rpc-files.d.ts +++ b/modules/ui-module/src/main/resources/manager/js/modules/rpc/rpc-files.d.ts @@ -1,3 +1,23 @@ +/*- + * #%L + * UI Module + * %% + * Copyright (C) 2023 - 2026 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * #L% + */ declare const listFiles: (options: any) => Promise; declare const deleteFile: (options: any) => Promise; declare const deleteFolder: (options: any) => Promise; diff --git a/modules/ui-module/src/main/resources/manager/js/modules/rpc/rpc-i18n.d.ts b/modules/ui-module/src/main/resources/manager/js/modules/rpc/rpc-i18n.d.ts index 97184c7b3..e28dcc439 100644 --- a/modules/ui-module/src/main/resources/manager/js/modules/rpc/rpc-i18n.d.ts +++ b/modules/ui-module/src/main/resources/manager/js/modules/rpc/rpc-i18n.d.ts @@ -1,2 +1,22 @@ +/*- + * #%L + * UI Module + * %% + * Copyright (C) 2023 - 2026 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * #L% + */ declare const loadLocalizations: (options: any) => Promise; export { loadLocalizations }; diff --git a/modules/ui-module/src/main/resources/manager/js/modules/rpc/rpc-manager.d.ts b/modules/ui-module/src/main/resources/manager/js/modules/rpc/rpc-manager.d.ts index df9f3252a..a5cde78ef 100644 --- a/modules/ui-module/src/main/resources/manager/js/modules/rpc/rpc-manager.d.ts +++ b/modules/ui-module/src/main/resources/manager/js/modules/rpc/rpc-manager.d.ts @@ -1,3 +1,23 @@ +/*- + * #%L + * UI Module + * %% + * Copyright (C) 2023 - 2026 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * #L% + */ export interface FormDefinition { fields: FormFieldDefinition[]; } diff --git a/modules/ui-module/src/main/resources/manager/js/modules/rpc/rpc-media.d.ts b/modules/ui-module/src/main/resources/manager/js/modules/rpc/rpc-media.d.ts index e333405cb..cdd474bef 100644 --- a/modules/ui-module/src/main/resources/manager/js/modules/rpc/rpc-media.d.ts +++ b/modules/ui-module/src/main/resources/manager/js/modules/rpc/rpc-media.d.ts @@ -1,3 +1,23 @@ +/*- + * #%L + * UI Module + * %% + * Copyright (C) 2023 - 2026 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * #L% + */ declare const getMediaMetaData: (options: any) => Promise; declare const setMediaMetaData: (options: any) => Promise; declare const renameMedia: (options: any) => Promise; diff --git a/modules/ui-module/src/main/resources/manager/js/modules/rpc/rpc-menu.d.ts b/modules/ui-module/src/main/resources/manager/js/modules/rpc/rpc-menu.d.ts index 9c037e964..7f7c57968 100644 --- a/modules/ui-module/src/main/resources/manager/js/modules/rpc/rpc-menu.d.ts +++ b/modules/ui-module/src/main/resources/manager/js/modules/rpc/rpc-menu.d.ts @@ -1,3 +1,23 @@ +/*- + * #%L + * UI Module + * %% + * Copyright (C) 2023 - 2026 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * #L% + */ export type MenuItemType = 'link' | 'heading' | 'divider'; export interface MenuItem { id: string; diff --git a/modules/ui-module/src/main/resources/manager/js/modules/rpc/rpc-page.d.ts b/modules/ui-module/src/main/resources/manager/js/modules/rpc/rpc-page.d.ts index d075394df..59ea7b081 100644 --- a/modules/ui-module/src/main/resources/manager/js/modules/rpc/rpc-page.d.ts +++ b/modules/ui-module/src/main/resources/manager/js/modules/rpc/rpc-page.d.ts @@ -1,3 +1,23 @@ +/*- + * #%L + * UI Module + * %% + * Copyright (C) 2023 - 2026 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * #L% + */ export interface CreatePageOptions { uri: string; name: string; diff --git a/modules/ui-module/src/main/resources/manager/js/modules/rpc/rpc-taxonomy.d.ts b/modules/ui-module/src/main/resources/manager/js/modules/rpc/rpc-taxonomy.d.ts index 04f282d16..9b308e05b 100644 --- a/modules/ui-module/src/main/resources/manager/js/modules/rpc/rpc-taxonomy.d.ts +++ b/modules/ui-module/src/main/resources/manager/js/modules/rpc/rpc-taxonomy.d.ts @@ -1,3 +1,23 @@ +/*- + * #%L + * UI Module + * %% + * Copyright (C) 2023 - 2026 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * #L% + */ export interface TaxonomyValue { id: string; title: string; diff --git a/modules/ui-module/src/main/resources/manager/js/modules/rpc/rpc-translation.d.ts b/modules/ui-module/src/main/resources/manager/js/modules/rpc/rpc-translation.d.ts index 9003180b3..d9b97e548 100644 --- a/modules/ui-module/src/main/resources/manager/js/modules/rpc/rpc-translation.d.ts +++ b/modules/ui-module/src/main/resources/manager/js/modules/rpc/rpc-translation.d.ts @@ -1,3 +1,23 @@ +/*- + * #%L + * UI Module + * %% + * Copyright (C) 2023 - 2026 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * #L% + */ export interface GetTranslationsOptions { uri: string; } diff --git a/modules/ui-module/src/main/resources/manager/js/modules/rpc/rpc-variant.d.ts b/modules/ui-module/src/main/resources/manager/js/modules/rpc/rpc-variant.d.ts index 136870ec1..6fde1add6 100644 --- a/modules/ui-module/src/main/resources/manager/js/modules/rpc/rpc-variant.d.ts +++ b/modules/ui-module/src/main/resources/manager/js/modules/rpc/rpc-variant.d.ts @@ -1,3 +1,23 @@ +/*- + * #%L + * UI Module + * %% + * Copyright (C) 2023 - 2026 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * #L% + */ export interface VariantDto { id: string; uri: string; diff --git a/modules/ui-module/src/main/resources/manager/js/modules/rpc/rpc-workflow.d.ts b/modules/ui-module/src/main/resources/manager/js/modules/rpc/rpc-workflow.d.ts index 7c1b5d18e..921599a38 100644 --- a/modules/ui-module/src/main/resources/manager/js/modules/rpc/rpc-workflow.d.ts +++ b/modules/ui-module/src/main/resources/manager/js/modules/rpc/rpc-workflow.d.ts @@ -1,3 +1,23 @@ +/*- + * #%L + * UI Module + * %% + * Copyright (C) 2023 - 2026 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * #L% + */ export interface GetTransitionsRequest { uri?: string; } diff --git a/modules/ui-module/src/main/resources/manager/js/modules/rpc/rpc.d.ts b/modules/ui-module/src/main/resources/manager/js/modules/rpc/rpc.d.ts index 485de8fe8..3a97e3941 100644 --- a/modules/ui-module/src/main/resources/manager/js/modules/rpc/rpc.d.ts +++ b/modules/ui-module/src/main/resources/manager/js/modules/rpc/rpc.d.ts @@ -1,3 +1,23 @@ +/*- + * #%L + * UI Module + * %% + * Copyright (C) 2023 - 2026 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * #L% + */ interface Options { method: string; parameters?: any; diff --git a/modules/ui-module/src/main/resources/manager/js/modules/sidebar.d.ts b/modules/ui-module/src/main/resources/manager/js/modules/sidebar.d.ts index 268aae235..cdfd08d7f 100644 --- a/modules/ui-module/src/main/resources/manager/js/modules/sidebar.d.ts +++ b/modules/ui-module/src/main/resources/manager/js/modules/sidebar.d.ts @@ -1 +1,21 @@ +/*- + * #%L + * UI Module + * %% + * Copyright (C) 2023 - 2026 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * #L% + */ export function openSidebar(options: any): void; diff --git a/modules/ui-module/src/main/resources/manager/js/modules/state.d.ts b/modules/ui-module/src/main/resources/manager/js/modules/state.d.ts index 7d224a238..80f03d1b9 100644 --- a/modules/ui-module/src/main/resources/manager/js/modules/state.d.ts +++ b/modules/ui-module/src/main/resources/manager/js/modules/state.d.ts @@ -1,3 +1,23 @@ +/*- + * #%L + * UI Module + * %% + * Copyright (C) 2023 - 2026 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * #L% + */ declare class State { constructor(initialState?: {}); state: {}; diff --git a/modules/ui-module/src/main/resources/manager/js/modules/toast.d.ts b/modules/ui-module/src/main/resources/manager/js/modules/toast.d.ts index 0fcd8a129..f5a574c37 100644 --- a/modules/ui-module/src/main/resources/manager/js/modules/toast.d.ts +++ b/modules/ui-module/src/main/resources/manager/js/modules/toast.d.ts @@ -1 +1,21 @@ +/*- + * #%L + * UI Module + * %% + * Copyright (C) 2023 - 2026 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * #L% + */ export function showToast(options: any): void; diff --git a/modules/ui-module/src/main/resources/manager/js/modules/ui-state.d.ts b/modules/ui-module/src/main/resources/manager/js/modules/ui-state.d.ts index d064b5d45..30e36cd4d 100644 --- a/modules/ui-module/src/main/resources/manager/js/modules/ui-state.d.ts +++ b/modules/ui-module/src/main/resources/manager/js/modules/ui-state.d.ts @@ -1,3 +1,23 @@ +/*- + * #%L + * UI Module + * %% + * Copyright (C) 2023 - 2026 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * #L% + */ export namespace UIStateManager { function setTabState(key: any, value: any): void; function getTabState(key: any, defaultValue?: null): any; diff --git a/modules/ui-module/src/main/resources/manager/js/modules/upload.d.ts b/modules/ui-module/src/main/resources/manager/js/modules/upload.d.ts index 760c8f380..757755054 100644 --- a/modules/ui-module/src/main/resources/manager/js/modules/upload.d.ts +++ b/modules/ui-module/src/main/resources/manager/js/modules/upload.d.ts @@ -1,3 +1,23 @@ +/*- + * #%L + * UI Module + * %% + * Copyright (C) 2023 - 2026 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * #L% + */ export function uploadFileWithProgress({ uploadEndpoint, file, uri, onProgress, onSuccess, onError }: { uploadEndpoint: any; file: any; diff --git a/modules/ui-module/src/main/resources/manager/js/modules/utils.d.ts b/modules/ui-module/src/main/resources/manager/js/modules/utils.d.ts index 249b7baa8..2c2466d62 100644 --- a/modules/ui-module/src/main/resources/manager/js/modules/utils.d.ts +++ b/modules/ui-module/src/main/resources/manager/js/modules/utils.d.ts @@ -1,3 +1,23 @@ +/*- + * #%L + * UI Module + * %% + * Copyright (C) 2023 - 2026 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * #L% + */ export declare function getCSRFToken(): string | null; export declare function setCSRFToken(token: string): void; export declare function uuid(): string; diff --git a/modules/ui-module/src/main/resources/manager/js/modules/wizard.d.ts b/modules/ui-module/src/main/resources/manager/js/modules/wizard.d.ts index 13b63e736..bb3a3b4b0 100644 --- a/modules/ui-module/src/main/resources/manager/js/modules/wizard.d.ts +++ b/modules/ui-module/src/main/resources/manager/js/modules/wizard.d.ts @@ -1,3 +1,23 @@ +/*- + * #%L + * UI Module + * %% + * Copyright (C) 2023 - 2026 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * #L% + */ export function openWizard(optionsParam: any): { wizardId: string; modalInstance: any; diff --git a/modules/ui-module/src/main/resources/manager/js/ui-actions.d.ts b/modules/ui-module/src/main/resources/manager/js/ui-actions.d.ts index cb0ff5c3b..4562057ed 100644 --- a/modules/ui-module/src/main/resources/manager/js/ui-actions.d.ts +++ b/modules/ui-module/src/main/resources/manager/js/ui-actions.d.ts @@ -1 +1,21 @@ +/*- + * #%L + * UI Module + * %% + * Copyright (C) 2023 - 2026 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * #L% + */ export {}; diff --git a/modules/ui-module/src/main/resources/manager/public/manager-login.d.ts b/modules/ui-module/src/main/resources/manager/public/manager-login.d.ts index 5b5817d8a..ce810c5cb 100644 --- a/modules/ui-module/src/main/resources/manager/public/manager-login.d.ts +++ b/modules/ui-module/src/main/resources/manager/public/manager-login.d.ts @@ -1,3 +1,23 @@ +/*- + * #%L + * UI Module + * %% + * Copyright (C) 2023 - 2026 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * #L% + */ declare const ui: { state: string; }; diff --git a/modules/ui-module/src/main/ts/package.json b/modules/ui-module/src/main/ts/package.json index 9f8a94e7b..e060cbe03 100644 --- a/modules/ui-module/src/main/ts/package.json +++ b/modules/ui-module/src/main/ts/package.json @@ -3,6 +3,7 @@ "author": "CondationCMS", "version": "0.3.0", "scripts": { + "dist": "tsc", "build": "tsc && node scripts/copy-dist.mjs", "test": "tsc --noEmit && node --test test/*.test.mjs" }, diff --git a/modules/ui-module/src/main/ts/src/js/modules/collection-picker.ts b/modules/ui-module/src/main/ts/src/js/modules/collection-picker.ts index 5ac94275f..81e40bf9b 100644 --- a/modules/ui-module/src/main/ts/src/js/modules/collection-picker.ts +++ b/modules/ui-module/src/main/ts/src/js/modules/collection-picker.ts @@ -36,12 +36,13 @@ export interface CollectionItemPickerOptions { onSelect: (item: CollectionItemSummary) => void | Promise; } -const escapeHtml = (value: unknown): string => String(value ?? '') - .replace(/&/g, '&') - .replace(//g, '>') - .replace(/"/g, '"') - .replace(/'/g, '''); +const escapeHtml = (value: string | number | boolean | null | undefined): string => + String(value ?? '') + .replaceAll('&', '&') + .replaceAll('<', '<') + .replaceAll('>', '>') + .replaceAll('"', '"') + .replaceAll("'", '''); const renderMessage = (message: string): string => `

${escapeHtml(message)}

`; @@ -144,6 +145,7 @@ export const openCollectionItemPicker = (options: CollectionItemPickerOptions): loadPage(); })); } catch (error) { + // exception is ignored, message to user is displayed in the modal if (version !== requestVersion) return; resultsElement.innerHTML = `
diff --git a/modules/ui-module/src/main/ts/src/js/modules/form/field.collection.ts b/modules/ui-module/src/main/ts/src/js/modules/form/field.collection.ts index 5e5656906..00b634e78 100644 --- a/modules/ui-module/src/main/ts/src/js/modules/form/field.collection.ts +++ b/modules/ui-module/src/main/ts/src/js/modules/form/field.collection.ts @@ -30,12 +30,13 @@ export interface CollectionFieldOptions extends FieldOptions { }; } -const escapeHtml = (value: unknown): string => String(value ?? '') - .replace(/&/g, '&') - .replace(//g, '>') - .replace(/"/g, '"') - .replace(/'/g, '''); +const escapeHtml = (value: string | number | boolean | null | undefined): string => + String(value ?? '') + .replaceAll('&', '&') + .replaceAll('<', '<') + .replaceAll('>', '>') + .replaceAll('"', '"') + .replaceAll("'", '''); const createCollectionField = (options: CollectionFieldOptions, value: string = ''): string => { const id = createID(); diff --git a/modules/ui-module/src/main/ts/tsconfig.json b/modules/ui-module/src/main/ts/tsconfig.json index 84a30ee75..b7910b1ea 100644 --- a/modules/ui-module/src/main/ts/tsconfig.json +++ b/modules/ui-module/src/main/ts/tsconfig.json @@ -3,10 +3,10 @@ "outDir": "dist", "rootDir": "src", "module": "esnext", - "target": "es2020", + "target": "es2021", "allowJs": true, "checkJs": false, - "lib": ["dom", "es2020"], + "lib": ["dom", "es2021"], "sourceMap": false, "declaration": true, "paths": { diff --git a/test-server/modules/example-module/libs/example-module-8.2.0.jar b/test-server/modules/example-module/libs/example-module-8.2.0.jar deleted file mode 100644 index a66b86c225c27f0f3a53670647de9b314138e802..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 19411 zcmcJ01zeR$_dkumr5mKXyFS0hG8qj2);DXQJS726@vgYFa@WGXF za6o?KSfO_Ul~qm|9J=oc4fPescf9g9@WOosR-1&`_aPO${bhRxCkuCb;2-;Xg@_pEEGr!i| zsNh5YTo}l32vAUWApd+-ncvZ{ecj~W(EPUe;*MUDPL3d;BS_sH=<22F40JUGIk~c$ z+nc(%rE3|gVvFO5j3weJ;1&gDKMaJ|!>IA2%?pMj0?=x!-9m3p+PF^-JCkhbk@)Uv zB}g=RZvmbCl6HA+TZ0HW)V_l2^nKl$Us{vU>&x3$aAna?Z8|ViDV~Uv#R7o(e9ZcV zM0^@d{f?e^P6~>?91>m@`$ZLZsZ-ILo%K_d7hRvv9mg%Ruq2quu`1FGB1;MzzuT6* zgJe)$5zmp{O3n#Dq7rsZdxOu2_&HR+`AKy38k3Mx1MMpV^*CO>?D#fi@MIH^2Q=sr zEStM8Gln@)V4JQ(l*_o8VnR?LNAvz7s=(R9^DTv9O}4|%ma7hew}n#DgAI;&#>qGL z%m|85-1J(~<7i1rK2FrQVk%5FR zp3n7`>vk;aDBBG#88MMF2}Z-uE_^#k?yMAbKgwbgRC2PLukIu!#1Qa<7Eu(30++`$ zb5>B?&!r1H)6T6z%BMYuuSl(GYc7F6xRv9D322S9vOX@+DH7(KH^seJM zm735pf)0-@ug=K%uv4Np-5TYBN+Cmu$2%1-ftq0-z18SSmJR$|UBAM3=YF(P;lVYIUs_YU(%=~r1! zqxCHDZXco_BaL3Oyq9E?(H7 zQN$eDgE9Aq{@HuyNY8~&KrCPb9tukRA6bC(cQ+*NY$IptXkia@mG*>~gB!$yQvdkX zkN^K}6UACH8rb3(BCr7=?(kC3xVX@4^0krS_}rcpIJbb~jAEoKtts|k3zJG>58BYH zm59Vs#5*l96lU6%2*4c# zKXC%wvso59BQ#`fRa@3msk$Zk=6jWgxYf*`1L@j3MW`Mq?+K|DoXL*Mf-D*k@1>KBXtB;kH2zJJi4GsyJHsAs^-ZS>*>38Ox zgewW+lj4V3Vc^w)M6^d@ofOUAyRx(!5*}#hGX<2& z7f17Daa!!s7}o%j?V{Cr`j%1J4iDPqdUJT~4LV+ia3C(%R;fN(`{b}e1b!p%f$8vJ zmPqn+nRCdZx7)t7wEE)a@!LiVr4Y8ULiMFL8e1GMEh02|lA1~s9Q4vJpiZgOeB@Ku z;NTUeQZ<`>@3O1`YgrGBW-SAl8c{I|tc|Xc*lDCY{ZcMsn$ojY(d>!{x-Q?OO|#m^ zdwjknIE?U0CE0tmw1E9xgilG4)Ie3pjDI~(iKlspDfyHyrY13i@rIhED=E6tvewfuObx!pT@m{9O zXC@Vu2knG7fUv+YovN#tyXGhlfo=yE<(l`N`VqLS`)irE&xv`gG)xZ=5N zj%cCdar7P;dZ91yR2yNcCcDH_g3EGTE_tm)1y6dT!`KHVTs(Sf#(J}x2nRY(L`l2k z@GfFZt`L4UB96}2tCkQW0z!;P;{Rww->oKB%K`Ew!SGAOD=}cQhR2aM!pVg<6<>Nm z`Gi~hRtSb-%EGG;^@%hdK5KJ{>#s-I%&7Mf&z?uRZ6}bk$v=NOJ21GlRXNl+dwKq4 zneHL-U56tIAVs?59Rz|2lKJK6QsOpxW4G>z+FLkJGb6__y(s-Glv?;sYIs8&YdMQR zIw!ulsO>E&qkTnOImn&$A%J!Tr^h~Rx3@#d)#`29B^ZO4+sXMjIU{f~J6Qm_1aB63 zci0C$nZ_x0Yl~BVD(7BxL!=?cQ3Va#27m}g$t32@J+)twMqsxZW~DY-N(km@6`h1? z^ewOgk7_EMOyf;2Y-KX$t?M|mi-PfMV|4_*snnisa;GxWTmp7DA`=@-KBP~B(hj(D z?akqqU5rW9I^2&oU9xChJ%i=V=!j6kF(D8qfqZYfSTmIx>m;|fdkkQArHT1beacA= zyjU>m`sxEopR@7p`juXt2eP&9m<8`t(rnyvD2d+Mt)KW=hqXE?#3|iw@0Td=K4Jqf z2HZ2kH$Bcgnc;bjD|iqU&Xro9ysI(G0^avvafqkeWYo}#L@jN`cIi`<&qo2B zeCWM|75p|b_5PPQTXsI5IC=wWDj(8OyQM=7EHT)Ct&$XZU%#j&0384(S{V=uQM@jM zTt9s#ltDW%e^GmdijLo$_8g5u`app5UK?EE%(^*AFr0|weQ4=6RrBU1r+}pZW-q)A zL)XxKPJvO2NRyD}t&>R{qn1n9pS5`Qt?lG8G!)bxEEJUVuV}HHlarmMmm3J^paHb9 zaRWgjBFNX^hazh@IE}Gmi1Iyn&I0H|ATA~JeJ6-HaH=e+uHBJit+hE`+T+&r=J7lA zEv04SRyu0YJ=in#P`4CEHC{N%*avloB13MX^RIS|n_*g@KI3V%*ojrc^1Q^pRi{Ef zY1>vb9)8fFmZ-q^rbrEe3fw$#{$)L=oKgToGmMvb0R533qvEq>s)VWrxW>WvwMfb^ zOiPh8Hq4)~;V!!CmZF=GSYU{SHpLf)hGMwV&zP0Xdm~_c+ua#IlfQEid(_fDU#NIQ z9TSp814}ZnWtN~DK(4mE8Ae`)BTsX3xFToqXp6o}j&C%r(>zgr?P+U)$PKi zcmq_D&nA$AMD*ToN$OU9g7U$F?eJmb_@#aOpZLaMw*_Uup-agdjZ4e zT#DcNqAb|QDiMlr5l`g?jS^)|9u3wIwXB zA`O9s`p(+7CmD;7Y7tLIeAJ$iWfY3I+TJF;`gRJt4yo_)rmBoVeBoz^FZ^~a{I4PP zkElTb2m*P@fk4i`8dj9S1U`I7$;X5A^q3V(N}lnS7P6uk>B85jg1u}iN(h_vr>G)c zjJar3!J*!24Ro3|)^zx#d(sJt^PaUdED@@d5_eYeV#qcw%Z%Hhf#Oj0R(3_WQS;U< zU>KDVvp`#))>ioNxu^W*D3-@z^d6V2UTt__SK-5tJ_yh0@FmwaDgZ3-S(@E?YRy^iGO;^fX{sAY!~_^&Kd2oTHf%>&%s+Ue(!$}oeuyRM zZ!d&9G$Bqs!~RelR!7O86J!7Ab0CeqY+`)b2@C@Z&1cv}eLIN+VVx&>%8#1IEv(G- z-8C(TR_X0F?%C|{3pno5EvKpj%sX0MhH*q@4ljB+s=iCf@=~eS7~Q0*g+2 zY1k@BR&G{t7o7KWjAf2Wazsn5t}dMEZ0f%t^MOYWKaI_Ps=kc=5<-Myq7ybYoK}b` z$?awlq0ypB=(%TS(4IJXpTOJ9>UGB%ZTo`W)Y}UB#5ve}>abS}^%-a%Gy-a)bq2gH zCb$-nGwq(BBbWF9=kX#q)y~ho&Xo~?VdISXUkYTFvVYNFSU+%q%4EA+xousEEFYkMC2I$OVuGQKyKJS zp1CZNw8$i|QHll-mjol>M1+@%)~H`gl6~Qf=rI!Nvm$G~D=K2{d5=Xzl~+s}JJXqk zV;_3Up;*r?i2?WS$Kq$U3E32w;AF-f%qf`xaJl?I$O%VAupHjG>c~0v&)yh~-+SsI z#D-!~p`he`#T)-eJNgli-?h`k)*bIzWBTL zh5f5yc)SgpJ4kX)(-&XPh0e3KL3_Mo`))5@=Z~G*^J2YQPu;ol z+e3UP{yAO@js-cJ(aajyL!_e1b-Upu%2+wi^DVo2JKN8R3pJ`(#>9ASBoo`&olUJN zM}g+kfmzioblmx}8%k_cX`Vf!iRN|99u&yXwr`iQ*C^AK7OG*t z?cB%^MVz>CZjibvFV!AQ8B?z~IP5@gFFER?O}6_4C*z{qii>ORnQm%lsFSkB z6c;!99Fu=-gH=-YTu)TfI$M0pptCm6j`4kmJ}A4(Wy#Gc(ML8sTC+4 z#pga{K@AT=Ur*%8BIe>Yxb^O#CN)_qO5Wg*b|3a^Ly>Av-4kYhuw&Fl2E)9hsaCv_ z*~-xshsH~A*z#UQmZe@fn7EX-Lc5Qrz2rE_q59}RN?lbXI_)EnJvC}qa9$jr&)iK{ zWnD;Y_V&); zZAC?k_r(eFw#kLfGMj$MSFQR89>7swc(>{aq^gfXIXoY=goDtNM-Q9d_I-SOBp!7? z-wdU4pj&C4TJDAY45ql(u(;1tT%}@H;K;hWw1Z1-m!*55#Nx$-J1XlJ0xCX-HTrxi zgZs(U!n^!!_}m0Cwe$kguSs$TIee+vhGMjpM)a9Pl^3{tOBPUYjhC1hQ@U>_pA>Pm zS4*gktVm>zqb_<(o{(O#lE(uI4OxR^lAEq8cj9 zWNoG~!#)zutQtAH^=&lTY@DVA?!Hu*02#sG8Fu!sV0<8_oiHM=8zy#H{jitK&AVCI ziKsYo;@+fTnem1!+~md-(Jde6pbT4WXhB+QAiEkM>JD{2$oqgIzR;sQ%b7j77V(%O zRwqt%uJZ&QbX!SOCr;Qn4iuZS34H={aQxJJczXqnKaxM%gwC*Xjz9di;U-^T6S>+g z%4qY2Bsja69*O57NNXNaNc@kt^;!ZfhG42>pRJXzD4srvASd1qt(oo4zeQ?FPrMm@ zzr(M659ZBWdv3;sCsp|R5cC}^#aLhH?s8bV^$2dop=Kyqe;$cY$xBWS5{&pxKoJxW z5BP4y3h|^RKr!RQl-RND`QayuthXZKKDe<`@|=nduV7eL_tAB}F^9hL3MR$$Agz z9+l(tEJG6Ubesys+elh&;?S<)K!SEPDCZnIy^C2*3GC1x&&G8?m^QBsM+2W!0tr))CD_tO_X!b}{8i)M8HO(r)dN7?!$F&XbeayMQC+2$m7C3r5zXeSU--0HkS(1Ky_`?pvXpPAzQn*u}0>riYwDbP40Q45R&%2PUPVguWGay0||8GvVw6N zdseMS6e$AbQoU9$0mA1ZW#VIYLIA`4!+A5a)GYs|Eo)tz!Z%N}VpsG*w{;K$8H|l) zSXO5x4?~=TyQ_OH;Q3}NtipgBb|Ux712z3jv#Vvii)s~!pY8ixvU#^-=)13**rYOj z@SiUB=S7hO&7%e`@`{jLrpr#6zu+TjRN1eOK0bQY3HsAifViTO9nHV-WVt^k9tZ=610 zVTf&AAogSriJTPvKiSi7Jf__@ZXY(pum&E4i{q;t0t{2h6NIUGZ7fshX@gv-sZul zX1Q9{C}Va9*b0=fTR-nH**_IT`tpHNhQrddF&L$`hy6ty-cuujj{FtG(WDsP%+S4i zNnhkTd{gbI5=kW&4YR4hRte_;{5Zlm1)apSS(Hp~y)$+1G8>p#f9@9@^%II`a3{~^ zkM}LMGq%dF&$hqy;Ss06h$XK_uM06Y;5jiT;8^L^NChmu4(F#*PNlT2`s92r!!{lR zI){*5uEye{IfbTD;=v7d4q`q+ds0Q^qNff6?2+!DR@J839KcVnByZOjYi}!p_r0I3 z+!dOR>m@G7$6A`GrfbWS3$)J+g0CdY0c^fF0$naK0aO%Id|?Rj7~fV__cbw0gnI{6 z9|jAc&Z!93ky?BiNFQ|NNJRwar`K?bQ>Tg(J;q^+AI;N@TDf;#=%*SU8N~0h@|sT+ zMBm1Xx$Y*Y;xo^n=%ScsscHF2hFhUDKDO7EepGAp{?<^dZ1N>+j~}ebR5y4+sA}1Q za4KsW^WEB;edgkJk}_~ZRbo9_?IVGTBVBPG^t-{9^7t{P+oYyBqPv2|>Vn{gO(W5^ z6?~<^20rhe;@h7<8lBP<5nyHRyz*dp;dQOFEz3csX$!lg7b)-uJ!ZROglg2(&tI>Y zCc}t#bJu#xsj95yN6Hd>g8%tL(bJp)9|$35&LPSz^ec+3Woq?1LH}Ko(_nkh z1sW5xDzK5`2tS*zjblYP)3hMJN?18NI+9I_E=R7~1_rZ6y6Ceg>gsGl1psl-e*}#% z+$P1;(XG&n#StDoWM@AyB4J0p^}s7TJ={WHSGtF)IEq;8R>L9egmJMa+tPEPT%%9= zn6$x+uavM552U5;@g=oW@aMvbE(;{&q zEmxFq0Nn!R`FOS}{5|g*TN|fUHB<`OR$@dbDE(jBS|H>;*c1d*cCrB4f7@gW8^`P2 zmUgsIbFy&+QOiS^x$j$Ft)>6fiCrR0J2Di)!(r~?57*E$Ej0y8m4j*F!^mmRxaZP` zL*~-!g=o$)eIHAiw*`H5V!}Sq;aY)V52lg@eBO98@1}7b9!|bThr%6L9S$-=WgxM! zAVA>##LIVk$AQ2pgyBI11(%&qO^W$C3(n;EW9dwATD0FfU{`5_DDQ~5#D0S?kC>pO zN-z2rpuBx9k1A53u>|?tl?E@DCao_TD+B05JVN_yINXXwaYl1lo5lbf-)-2+t|gRH zneB^AkB+%rDR@9&(!)roPgsLoU82T-FQiE@>VnoTmDL*5B@E3`D|I;+i>wG6<&4h?e09D9x0)2^kq1o}LlC`~?3`fLUN8zt3PF_HF}kjUYqGGQUoRVYHr)PGo~Oej zSIf(&6|GQN$x3RNYI4}zte&#ZuNB3)D*QbVjTG`>J{(#aB$7v53I`<)we&2C=J5y* za|AsCOrlkvv#3s$$Z^v>0X#TPm6y{qM;^v=ikLDvjT~DP@U4UnOG%0%0 zhHC(yZ5S%Ejf_c1@=)9xwKR?)I6aRCFrnbX59_-Tn%~Wev`XG&!6z7u4wI>hD;l0E ze`Zg~sGl<1-ZQ^sE_h|T=57!A zZYfRL-z_Cit)|5c6MENUTx0y$k0~2QyFfdPyi3sxd>@9YLCD=>V%`5h96JgL{SwMQ zet%#mu|W!ZvwdeEP2lOS@7lmXOlJQ)bo~A^6v8T9>sL>t5hZttFQbK6Pw~^}vhxB3 zCA(i9<&v6PGXaL&DIckz81mMeatRkQ4COPtep9X0ta&KWt$Eyon??2Bt#lFjMOtKK z;H5#tqXO3110R;p^+)(i@^X2=*P_KarH?W~*Vz5Q)~caHI=O75((fB@S$=9%jBPF`aPf#n94*o`Z7_0+-H z-FYO#b$?ef3t#&|3gRrb9-QfH{7b<01bQ@m$9~_DE-<&XU}RV@fEv`uqupvHo>Gr;II$Ed}D)yUYa=09Bu z5#;w7+LCzPvYC}Qg&pJ(N8C*iP)-R{K4Ti^ zk}g);K;$7nWw?=HBkvi2&gOk`RtP4Av>UR}1`Eq@O{;T;jJX-^Q~7As^1iNjVLD7v zO>su8i!1|(4`ci-n#nrHedDW8&FhwWI8QF$__2@Z%8NY0OpAu~SO#nps;VGU>wku^ zYhbR(PYsT&r0y@ZsGN`=x?jhgZqeIeolZqL;0gUyqj7UHQ|pTJfsmXx>~?%Ru0gx| zEQgbD0ZWad== zxKFKdnoPvD&24^0Np2g_!+^t{&p}b$MGKFV%kx%svH>@GHsZYl>Lt!qh+nxBmjA;$ zef&N;fKamG_y&v2u|lA7@$e2Rti$Ucj_f0)R7uYN%VkCBz0K$`=6yWP5WFr&=A_-(-* z^Pcm1J8zYom5AIf`A6XfnK~{=%K)22o)Fc1g02|h_HgOD2ZIY0MQgZViTxwre33xA zBsFKlPSjc9);v>qt%Ib~#Kq(NTy-wSD=UF!;^c<}jI}g`D!nY91@xafWSFi4ny{5i zF1lDx39+>z}gZqemi@+ra~zh}ODZN~{Ur0wSzA zi4gmJTp!R}TDc%Sz0+5msi0W&c{r-xB^pqnT)Ia5xsE2H>BvbPkJ_dlhgcYct??o2 zK4trASsmv~_sI`_ZzqBtj_Jxdpni;fJehhx=8iRX)J^`HAj$Wwb)#~MOwX-2w+Cfx zi$}~+`NOrLLfi#UV0V^CDI_s-TbESjs8sHW1dj?x<+(k4Qd;{?b}_OKx@2`gly-9M z&XtOY{3Hgw<~k6kERe7WtH1l`X*pOy&W2Gz=oxjcz$~4H9gaCE`pRZh2zxg85rt?Q z)*{>#ZfyV2xW%ofNH36d9^Z`4J!ycs!VqnCg=q8L|A99Db_AVr923J4i5t{qK(*`F zv$h$9DhXC2)6kYk5KoA$QBY7LqkHpM1sFr+W3aEgrv-;a)*?$$k}ge=A1qeBQkSv3 z={A%mw4C|6`3scEqBJQIHEK?{996r?REFJj#zz+x5#Y3=$w2^jCmoBHn4C$*Esq6f zmJ}mZ{=31aIUn_xgUwYL2Om!*D0~zFTjWmnQuaSag*)BmS|sk_N_#Wu(qyzA`q0*; zfLzIr;7Vgiqp5bq+ea!>7OfXXq1MtQKXmh+C4KrKiX&K0&@v940PDRhcrrl-AY^bn z_4mrKJVpOjpG7B`+oLYuACEwAk%z`1b)C=$)2j`Bt_2bp8XD}mhJhcTS^;T zgzQyB5qKIeWR63kD+v;rD*LT^s)5EXBcpp1y7|V13n2ObGHo~I;)Lw@&465CZkMGQIT}C#A)o^E0r(oM+B{nap$eF!2 zG~M=3gEM2k9WKTvJwDpf!UaA>nZH2Pt3_Z0km0G!VTrG+y$DBCK=Pko6esVQ+B&OM zXL|k-wDc(Xo@BBg!$<;LgdZdK`V9>F<|B!#sq&*qi$E47f!=E@a! zG6JQ^b~`M@^6hqQQ-H)FBVhtZ`b>_Zc&=1c1vOyUs%t`R`tU5#^q2>vqaEbvx`lN zt#i&?lxcet2OZzdSxavC?{0Nvw9U1Ki17*?zHjE^A~ryuW~l}D7uH=xXHKwtnM*TZ zWN2w19nI+IC~GBDuj)Ex3GK8FuCL-D>>Q?3a+)SG$-}LM(yt*X6mt$DKLZwEw!R!E zP3jVQsSVE&_l#q)%eJ|Y8Rk`s1r|MOh=Qk{59M@`MIe!Vw23-Cg4RcWQas{YV>s1; zi19Cz9|Ehfg~0+b>O^odShLZ+907*yYmYVQEFa*&YHE{+96<|o@@%OSpFf%@t&EO* zpf+6zN_HpZemM&5L*0nbx`iZk$wpA#I>@dB{`eYGnM8lZ*<768dl-`&xdUC)hm6V@lI0!k*zTdTl?;CGAbxi$>H~ zz~@z3bLf_uo;Loe7>nnsjBNFKp} z1&@{>id4Hl4sj*du(9?J=Koh4)TmY0kV37nCb#Jo!qXc627gT}rjZw>V}u+AIw6N<)qkqNKQaxp zZ8XhYZJa^k-xOKX$=%f)c!MJ;PMFakmc;;HVLpPVP;phErZAx^hesj`RhFY9@8K-T zX5PXfaIBR>Ees>DMK4I#?ek;0VANtgfbx2&<$YgALJk4+Bw{Xo`^&}V&c?>H|K*oW zC#VL`L|HURG%yN5D}6hd_8HGIM(N0ZSDo$|vzd?xdPY_t`aDn8N`vcMMeR<6n3UV(YyM77%)D6Mn45ZPB5& zXYE2R;#&W>iXr#Bt`=h!Tb#?i07QtUoD*mX%%A5Dh}kO-Qbj()$>dO?g&|Jajlh#+ z{TyW4l=|i4kwJals8jEP!+dzq5Mcq|`2Au-S)W&1SwU=sj6tRv4|g0coLTIL@suaE z$&I@%^COKSG-H@Mz1NX;5xPi=tf{bdTF_H$S6d2Uz;UgEC+6si?{fJ^65__5B#-qtbQJ z6oRMaVy6;}EtCQ4aEs8*vFWf+uanNTd28(`^?f9p0-2zoQbBtOW6#H`FxaKt9ZqQKnCw)6q! z#(hEMRg~pdtm2SY^WWjUnf>;Mgy?^YHe{Ie)2FG{gif0LI`RCClYk(UfxL?U9_jaM*k9Jskl}AY z#XsfJ6`UfxgZevAn(6a)bGieKvzv`Cs&Xpgfn*g7mz8!Hhyfp`=2oC zb{byDL-1)p-mj^D|HAmS{+`nQrUNt);>Zu#0dTv4#=UH=IKpQEqs$uCu@aO=pYWOn z27m=j+I$RCvoCEQ*9((~CrQg9?(d>R(74n-J5);IZg?G9UD3NS=sIuov+JVkj+2mv zp#G)^zkuHTC-0>#|2?Ys<+WuR9Cr(0m*fhHi{M02xE&`R9(1S}5vys)q@Zya9$;X^ zdcz(35$~^P#7(?^6AS%!NBc{tB_EF)@bMpx2SH9AKuFy7wc_e`9r<26_UEs|jRLIS z>bRUCbz`0m_PJWhPBZKP5s#M3Aj~ZyhA;eZ0n4OdN0woG*m~3k#)>8S1RBYTIGpy& zimlP6q(M)lM$w{xLP~@U~{%{C0|11o3IjOQ~@k!In_VCX1ehF=y;%D>y zGdO%j&ehRzDGh{IfJ{O}QAP#Z7avP|WAAXB(4H>5+sAlEC*-d3@nb8kgiyw0A)C-dgw|>q_PC|Qr8B8XUukQH(s;|;@*=bQ5*w8a z^%X=lO-Hr;^XT0tIYn?N6zEie*5^vd*2Xmy(_PleoAEgoQx~0@u4_Tm5f~i;v+l)= z2$;$H=MwWH6^oRzGCV024eaE}spvf}(aYJM#i3+{yTT8$$-WFyW)a|rD3nHB-#PnD+XnzzX`jI>TO#LaFh9reQ0%MJ^l~;I^!vp!g z6D{tVO(TU-Ntnk{JnZ8=p>GE*U$$v@3y>`5U@a^Lt&5P4!;s-D8%JP+Zx?e{S-&_s zOxA$kDeON+cB#2*${oV^^kZ6D;vSiT_kAJD9m!ySYNE3cTB+kiJ}}UwKb)YwFV~aM z=Y4sRqm#k&jn?;NM(e%d_)+&CAU!LN&q~p$9?k1KH?ianap~t9(Lwkm9MRx{7+mKqHzPW`X>4~EyD5 zv#S%NybxpqbW>zz>{U`!V%=9NSN;*=41NoFK73GM>tkbq3a49J`$)64mP?&zvAVWF zvxc`Ru7z{xnvjc!3k&bSauj(4@T`M)5NH@2*#BJO3jqvyFrc=-#zWWGzjwRN@xEE| z`?c}cCntao^7rpk{zC!q9|iwat?sw(-vpOIe*9X7`FB0O*6x0PzCs;^{P=U>=HK=G zTHyNq`P%mq^5f5azgPBNFVju5Uu%26KVSQ^LayZh-2XS9^+y~xYhfvW8|K@rE&q0$ zAI!^7YjSfunxDqY`$yw_s|3ED>dhg4UaAeqkiT#3A1Z_YbN{ck!QY>+OGOCr`oH)8 zty1_ucm7%{eBD{*&z&L5{vRuc|MRe4Ylp9gjrx^gf8)Qt@9vMyxHgKLwZy;4$q877 zNcqo7^4sSAZDsLyO}Z9#vuyKP*arv&{ojTCU_$sB}~0%{rEAk-QN1^N*+aAG7iQvRL*eh?_+K*C6~b|9^n^ z>(#!QlzJ`I0{>T~{z8iC&B@=)<-3Ma1#!Z^wgZ0+^<56w+Z@Fswp)ZR6K#}Lxz z_o(Tgufg9EeK)1wBHm?wyD9l50e3BV>EBEKD?09`;G1;Xwcse8e>LU* zMy}liaFZ*$2EYNiko^}s{$HHTH|Fdnh(A(DUk|LmJvjlpzX0MUq4bvo*x!l%%3l5U zhomUAX^@mgQb}nysdP7jbSfZSf}n6W$Kydc z{^uOM_b#6GJR6vq_g%BTZ|0kC=G`jtuy80)P)JZvDmu6Hp}xD2A)gf`HAIU-(aO0 z+ZgADm**T|R&-7!Et7z>CI`9Izi;&~lleP8V@E3%H!BvPhbz$D1!4y-ESj?Frp{K5uA-)w zcA@Uz=4=X-{Pu78fx$PBTV9r_yKit*et>zrw_lmQUzT}XzC@;vxl8s9WB-^-5Sut> zKwLP-4Y`|fqG16C^5L=S`iydF2Vs7CEPvSf73qD2#)ljbvOK>?`a8yIKnp7uSD>@H zrGvApxPuwcnVhvN@rhx}a9HU_H5=TGEK*I#rtFN4$IxnnI9U1#QBn-}R=Y ztE(f6sEVwNvAvls(D^rbM!_eOhI-QT6-fTsqwlWMY|Cn_xdWjm4!IS6kERTS=I>K9 zD4?cJAf={;|B^f%T?74=~XGCUFWG*~pm0^}+>SkSnxn`-~j{u6lK==9x@YVN!*^PeH_vi1MtB68qN-U&y03q|2u`5s! zGC;ZS8Kt2Nc#1w^?DnWwUW5^D#{UdhS5js#BN!x2Xl z`HIIDWXP4%LLytoq3s55#o4Bqz|Rd0S9DT)F+Wr{{utD(mFD}Z!9qdxLjs@6K(wWZ70@CU~jZ;_oeYY+3{KJ z>x<6porjL!dwPSgO55^fj3lD?4)J-E0Lw9c8f|vIKfieA^TRw+Q%gp`pc_T7(hYr{ zI%7`3BKpAs`d0(h8cpg4Vx8(oUAS44b1p9yQJ$qmRQjLmg$Eb1%kUy_x{ z2fh+6$$b%=5yIxoEq>Q}F$nHC5oK7FYkDETt()wvF=e!Y+K8OX6F<#Ux>RA}`k}$c zqt;T0E9uRQ8oGgHTzdL>49Hp>WC(Vf9i%YjU9IiN&h5K}L1as9$CGcPVKwOYc$QdV z-oIp7N-4fI_zIJ|W!FE;MT+G_PZ~Opkypd^aNe{)#UZ{!4)7%?a!Zvk!_-naWKb)Q zl~i)B0m=MRgM4gLVWE?I6yiv+x^cZhyu{Q&qEiPOSa>^Fiu3-Ccox3qgA~LWY#lh` znaYds*Ga|aEqEn2Ge4rViWqPBa{{|&sw7+TU%rWDW3t1lsi3bj*T5NT)3-uuI&n7` zAWCxex)rLTT%e*0?qd7$qI^WvZmzU(S#Dhgu_4A)EU8r!w0-iCv3)`}qdEHp?Yy5F zzLe8wOH!mRxZlyE8cKAxk7(zdb>pnpLPKubNX; zOTt5(LYjeN%={R&#J?H^n+3g<*{5Mf__XeZTVDER*iJavefpLp9+zw;1r9+wS;S#C zBZM2r1WKPV_3_ryZcZ5_Jf`irtvsj&+TH}5_FYS6nf1oz zKzKBX)fbjA$|p!ua2Ho7Q+5oLQ^5b6Bx?9VCOd4I0A@)WtEDgE0H8F~K);^<6hLb= zmz))ZNiOMvBCyWPJXF){m?33qg8M`^O1ZqJ<87!GV`O8TLGvPWAL66vhh|N;+Q)q2 zt58jAm%2DUo)7r44QtB^1!JZ~!MZO4wg{D#P^ff2!`Rd_RTQKKMpRPuzA&pCmmR!c z%av}{-Da6iNzvy4{Y0%{Vu^TKz+0_tkw%5~kINr7=Jkn8EPmO3q4D zrT^yy^Oq)0a_Fx&*qEG^B^ul!Q9$&^Y1`U>dD9`5_UiY=cWDs0q z!e`UBzMO4rudh!%JpZ!c09EgiD2*nL2D(AeOxFq`KAk}g0I@Luz#BF!I$^!MQV1zb zDO$?ap@BB{r}O8vf zY@L^Zd;*WH!j4q9%-U3TEuF}Ooa-J}(dV7kf-z>WMLFFHT?x??bN$VM1@l~f(YxgV z$|xr|nd}NQFvKZ4;dtUKp972=Q@?yT)T_%Map+#Kn-2>ZBrN0|yI-O&?fr5yD}a@d zA;4Je(YD>0BeU%gp5lZinPJCyL4-lLdNfnJ*BZ*sJ&B$siwd0`$PU@H0jhD9AK#E=mLj?BL8PS^r?|M!1aB=UU`VUc2 ztD+U%qT`sp=ajdMS4Et(#@y^uDC&)al&R)g@qykx&|7lpi|50#k$nGY{M?_MUt9X)o@VMkP)%WM%}e_IyZ0;FAVU+kvVxY^R=$ zn7byR?SQRP2ZMG^Cg*qSeY)K?-Y@Nt3h%!_U3)kUwJ6-NPW!m^X46wjHh}p&-i6+5q?zPIkUBwzsXE^qgq=vRe zccI75_+}gdK5>>!)YIZ>d1X6Tw@)iiYHz&ecuiUNe!WEE9g|fylMRf|Lzw(>|9u0s zi4U`zr>L8hY6&ux=94%Jh{Ov{?vH5i%{ND{RAud9zq=jV<2(G^)hv~#z+M|oR~BYk z_Axxih1|pdva`VRsf6%n>6%-2uH5o+^)10`a^C<33W^D`^uAol{QDdWamzoaTV~@w zR$469E|KaxHq&evUou`TjjP4RbD=SBiVJi!Jg)NNLX8sdmTniGCaN$yq%5HSGViW7 zl3#{u4oGx6&X}7Ol6;~?0uDR~uf2%4rN!oH>_&+}s<{n!IvXC2p{`iusja;hdFCio z*W|}|@ktIdnX@;6>V*DI3D{4nNUf5{Qrl93t+32He-BUR9hoT>*ZY}!{VxY79B(MR zj~P)ZO2aQ%%@U$@88c#p3$NpO8n)~5>YlhB&U4(E9EpB7rrEo@fcJvpRu}~Iy8`(w zH*5R`V4oTEe4vBR?;$xthvIAJ-c%tz4Ng7Ii3)^OnTk71I)b-UiR?U*b-Up|iS4>J zdOi+PujX?k>|%VQ0>pJvEAeK5v&BqnZVB~!_SDQHBM)79181onA!C^;6o37FRIFVd zU6_ENiW3nDtvQ1Nrd+4hzS!Egh|W1?>(QFvkT^^Fo;q?ImNVY{c6(|& zU0N&y#9{@0&tl0;9UMLXFk9oVW^3rQfg~$u9gO!ei^SAGW=bRlp)+eG$0XAZRhuI= zZ7lx$I3qrn-Zp?R065$D%-vmi@tr0j%c70@w(xQ%u7bfm%^QKz3EVKV_AfU~3+=gC z6k%YW= z59~}a!SFPdDpt=mIu6G7h72e<$Bg75X3O!^`Qp-S<5$0#ZEDE^eV=F82pOB&!hMBM zLR+D9Sf%v%H>xNKnl58^;8t)Z%Eax~ZrLkIlOP8_t)g7#+&~$6Y8_1c_yZnXrG=%4 z)x(XJ+P47k;Y?m(L0K$a!I%&}Obt?Ga|5Qhv}pIPpgd{DII=`Is-U#fP3G8s66$F+ zRUCJZr%POXN_`qn%tvad=Gg_6miAypvkfahF-@vqvZYgpcU!}cJ|m#GC*{OJ{9;uV z(CgOVqoWA(2$x@b4!U&o!qRCwSkkU<2x7JcyQuvWkNIOnyYI`mD8=l^5C_Dk9nN#Q z>M77^pI9TiY|~n$y(3E?@h2~tm$%uNW}DeL79~#=S*O53#KJyDYUp1bNHGpR&8-U3 zdgho(XU^0RBdQ7Cm}v!U>G7Tq91rrJ&v&0_#z!~KPoQ6kRG3{5<>lBf^6i(wjD_uk zG7u0^F3eShfz1j#4?2c!T=5VzV0-U}A0UwVB`f!#NPVX><67aNlGeVXhQmpWN)~(Z z31`s2>oM;>E-N|N73yK%Pk~i-TSlF`&qT&8AL83DnsX6-+Tx8p8TUlIGTX}p94rj6 zwG+f_ZQ;M>x&D2i{WJKH1G>6;eqAYEPaFIh`e@i(u3Us*gY;|QU2$=IR3)u$a4akb zh$;fahH%85Tlvp}X3Lo-(IF)2guyuJl&n6?i^$C5Ki?VIIuxwV>6TYtyb z_YBPlHJSuqG$fW4NDkl>+;eRcD)BLfDFNf>>9s*WxfP*7PfkD2Ml&~TM>$$e3NC>K z5`Z!h7}y305g6a(!>49Bo7X5}cKTWKm9bgobQ$d(^CN$GMq<1YXJw$sWhUSU_d79Pws7k(x0d3t##`FwMA^OsUwaxdazyRGR)G)C@k4mL|C zpTlg2n}^mdyw!v!*cLt`6)mm9&zSf$FOg>(Ic#d=-bF3!YwQK3jOuGUR@QAU zN2}`wP4%bE|M~ZC_C2kJEs7xo>lfq(F9D5<3(YDEjtIl&@*u}S0**0=kghbR*n-TA zDv8}`LM~Rq6OR$^ypFEeoNdz(6_ZUEAGbZ;Ok2-zsk3-^etHi3n&N%^FzEgbWJHoR zGmj+%;5LGixZ2SogWH8D1r=OX1fszeS5`HYSLKZbAX;Vr3^bTpp)0~aVgt?P@dORI7AMcs! z?rRn>`n`}XiQ>)TFx#OvtN|q3M5%K3EZ=B3*l(Hb&gHSyYkMBVj<^i2QVw4IWVcQP z8sK}!c>e7JPCi`=m z4!77p`;kAq4n~bHO@1I85r9Th1qr<=Y5hJO>~ylZd*a)Rl>p)7-34^EbDHJZEj1#P z5ZemQZ-?T=7SLZh`;L4(&ShZm&lgEhOiUk05d6{&Jhpy)Gbb3!u*g&QqlgO{zTJ|(gRECw5zn69OvV8~rWABe z8^C8k{2Zd&6cbgw$|#^vPxDewHI9cjJHAB`G|>p;cI|f$l+N3e8pRwhv`*I|%466_ zF(N3Gp`JU7EOd1Dctb88yp|Wu}nyV4XdlVPfE(2|@7<7oFGXaWo{QAI58( z@(SbBL23MU901$ix}^?x*|`&1=3aeF-Xg>no;-tI6d<9KM~=>N?Y3DhMXTO9113r) z!ARK2nNJ(Zot5Iw;4D^t1qYk?>ULs63_f3IA$d_KP^E5!buWyPF4Ks($vM72aV_6j`Rb&DdQcSEDlZB=7tju#xu9%On4%RlX{Jq#!#&K zOv!_237Jen^H1Wj3!)CQ${5Jkar}3jQunJYX^3X0cI;0nRRoq1w79K#w1!WI91^`~ zS1A^his(z--zs_X)eL#-ypO76UdPYV_AQEc>_s~kT+G{zH-0a-mA9K_yzi`4r!+N? zlpT#BzDxDm|14~re1vI=aO=Uro!3oq7_Dq2-9xt{b*s#$(7Kj*whom|s9;#77Cd;4 zwg9d*H;8sbq`R?hWVIuu2zEd@;x!HhDM_u>XAL$ky0XctLcg#?uM4AY${;qbdJu+4 zfbJb_>`zr!z(#0|z8CUQv_fH6z*xOAI;YK(K_HmWt7KuDS{`$77sm7<^p75Jk0(`W z6ygD&A=AiZqWa%G;QRRKXPYYjHkp*c_&<6?!OM;OLI%^g5`b?waY230}k`=D0=k48zF}-ehI#LC}DOP zamE>}TQ*+JIxALz%WTPw!99!+XG{eL9;w{a-)780mi7c^Dr=30Wv##KSSlg}`E3$9 z;>Jg0laL_rE9yz$i?6O%0GQ#k)H`)lnzG(zVz<9kSJe}X_A|hK0PY*oZ$(%v*dI`O z&F;K*fF=IWRseT!T$E~>?U5*~mV#b8#@^v)e`;Il#Q3t0F!apSpJ5kuZNw4;wPJJ> zgPX?8EKGIX)XfLq)7h-wv)aDRXTL+coT>^iZF~JZlszJIXwlPN`E5#;r&68TASYMH zQYwNyn?eQ&v_b6Hx%I<@Bi#x5X8Sevu6yaQZ)6%-Zscu{e8Sm!WmGNc{H#$hVY5q4 zt2i;7Z_!~l4O=P6!o?!)jN_h`q10h%uJ8+si!(=BtGX{JyrAKOPop!Rs?Vdo1QFpF zX@!msr4^xybGev>tG!kx^w_o0YfYTEPvB)@@v7~FrgcGQ@=XO@;w)?dRp`rwx(u{; zYJT7-tv=7Qan41QOq&>Vlu~ctJYG15%IT@+sUjjUbc~^(e1+cbkreCE&I4n%C;I#v zQ;Tx&x7^OUrHli&YmJAH*Vj?D`RmS!x?Y76Ox`ZBl~_YORFuNnX`X973AvFuEOZ3_ zrRsnNAT#tZo~bN?wAjeML4q0(mjol`K!lfv)}RX}$v$&LbRQ1!UXixk5f(D_xW_D{ z%p)R+o$1KTz6ZT&SEA#RM2~y-L&;O?gluw5P%^_d=A={~s9d%$=p%bZpbXxr^6)A4 zkACDq4a^gS_>nWjkM918AN|KolW#HEr2VJ}j!;~`Ha)6Mua2eF2vljH$}Kfbu>{eC z*cv%Gm0Pp}kClMYl-_!K+PfNXShrqFlb5DTk{1Mul&{ogEN{3BrU@)(zH0gcWwaMkgg|C_Q{5E3k5S={w>TGxyExMZCY%}#)F> zGNsYFVdTK(Mg<`o_sr?i4{q3lbokBV&v&lPU$<8Y{7BK}p7BPTzRzVH%tX)6FmuUdgeM z*RMH2$2aEB5Ou%^41imBO0!s^Ybwve5ao~`&M%6Rbxm%bfK?fDKDaIgC*KoK_N5Zjep}%+$xNP1Njn2j^43n9nMi@HkM#B- zR?rJN-wg5s#h7?0s^g`!vp}IFx`OtVB&~8G$=6bF^nwwUq>JqBFpD;>cgpv0HsH); z-y(DmX314*ro6NKqRVBkQap^sLoeLfvQprNjE_^lG zYK^a3dv+4P~H+4MRA>XS^L#}cM30hbG9 zL2u|V4gb&wlgWJE1MW>bX`BZK6LaWLxP9-30t`^;NvzBW5O_ZE@ZQ|EBX9_!e-KX2 zY2#g!V!FnRGjaM@G82>*<+}#hQCKI+KV&MkT_?;ZCMd1ai9!ODx6bBMM#wdkqMSNY zw)4s^_$r=1ad30eNgDoF}EuD_X&);7zlL< zYf!37Rp{{r)CoqM&{`$3ngconq1lVwBgMG;JPbPNSX2bsI`Q0L5T5CNn!zDCdwP+NJzt}hW_zbzA|1g)^*N|2 zu~r#7H&D47bb};?AkyUsUE9t%S&-kin-w$@X8S4M!!Fp_{5*0+BSc!fk_x7p3^p&T ztL*bDc~Q=ahwg|5as@E&4$Sou$->VC0}=;ccP$F%a|;f!2LuBq(5g?Fl_yJOxM*Vl z4~`zNfo4BeOwQHJU`cI)UkinUqbG7_thswT86OHd9;vH0VEL_2D^h>CAM6>7D2f_6>cMC0he&OqU=or%67@oT z5l0`Gp3e;!m-D_2>$4tG(8+@Qo~+S~mp>34CQ}(#I4o86#Fm0VH)W=^YktX8b`n-r zd#1%B9(*AZ9D^bIq)ALbR+8UO)|6tuE>d7vM}*$rT=s}IE`c?Mad zm7$34u}}usV%DPU)3M!zr&;nF)?bVt77Ss+=MC#^iknhv+jp5%AMXac0ZrU(>qdCy zVlAYsI#r(2m*}OF*Vn#b$!aT8+ZY2HO7LdsHAW~_5hdGW^$_ihh0}3vK75A8$fOKx zU_Zpq=51kL5zI6$ET|Gx%#Mm+m7vX)skVZ_tdT7KER6boCZPg=*#B@CjWEnA#n|4Z z$dlO~9zAG#FEKn}TeW%LGdn%ZOjld7i?Sq=SOls50CwE4#DjGyMTKfLs}WWcsy)P$oCue#1Tt3HoF_AV{hOPW zgAFt1r(c$7AEDf}J0u5^r;Fb~AQ&f^Uygb~+(KvQ(isj$!g-PzF^1_$;cKSwn)hQ3 zPmn#Bqu5pJqfZ`c>+6(}o?^~il=iwHK&za?WAB!mTOnjBbylrn3;|57WV{?4;W(M? z%m8hIfkmEew!Tlsaq^v-qEw&Cx!$`VQj_5*yAE0Vxe|=r5}P;m(0ooBj@_)EmD*q~ z#-FcI{4qqWXMqKHSX1F(9B+JPEtN5ES<8`K9EcB&)#CS}RC%(&mC8hY4%lXoNUS${ zmp2d&B`U0g6NIS+DBi@&}MtNIE9<7 zy<+8^hpYewzk3Gw#z&bSr@3F@^6y86ai-QK@2Jf%gZA8+?c!-S7}PW(P)i<+3P+(- z_=JkQBw$B&BJE;Le0m`PX68R}i=J^f-9cukmp84aTz-%FWF~Q(gQ=b(j4E0DA-a$` zo8+<5=R?1CUi5ClirZFFbq~*RHf_8=v3L8`R6e4ma!H5kTcWoDStQBxym|&EaNP%t zH`5~&-S9jUaQ^g}Pzr6|^cna96&=4REeDNUa-WamUJG2}^qMJ2Ae@l+eQ3!RWz(ic z2frmhCQrO|edmxp4!#ky2&15;&5si}2CvUye;lA5*{@TnLag`;#EK1n#ftx9;7O$p zGJA!sn@iyAeTt}G z>EI=HH;_bI6v@4_aTNe^wa$a;r}ymsprl<*i>=m<2~K{s11 z!TlL!wk2;0@0fO-*4cO|<*tP1b;t&X>1Ap;Auj{07P*6z3kW)*1zW=;@9y_6R1~k` zg2eU?eF}v9ZIValV~6@7_VGmO{w+7G(Zf!%R|H8uZ!8-W zQ>40(;#?k-u`V7mMHUQ!Lj<@AV_>(JNXf-9^O~2GWhj;I2?dVuN#wgcig^KkE4>)e z16}&QPnc$6_0ENokn98oo%$LOr_7(Q7^}DQ@JTsHPR5EsPT(n39^VYDnhlOADf-Gr zWDr|6D41Nh1#1y*5;wN@aLf$p3Gy>!t;g51+Gw}ECLcj8ITjTPO6FIr`H$_IpQR(d znr+El8+B|g0$(MWw*+_F@0GkA50g^F(5tex^RGh4vJBvjziV66`(6Z(r+#A_S;k@N z?8~XZY1WqOF3;$m%d=MnqsO*9m@*iay#Viu9Z%Ll&$-^oI~TsYh>t`+$BV!*qhvFf zSOU9bj zb|8X2gfmymjW@f#)JmE9=~L<`9_Q?Sz6?#$Ml(` zzss~e3Vm2%<78J>myuAKf*FT`E9QA~nqbV>UVC7BAwC_MYOiX=7qPX*{Bb80r+}tl zfJLZQ<2^G1r{{6$qa${cHWnP=GijMg??DQ0 zivU}_jSjB~(=>Ne!eFEb_yZnHyK?2^dJE&{N*(2RxBGplS7vs8w?E`YVQ-No%BMl$ z8|-M%4zHIxs#>vs(1zYxde}pgZ1V|D%1OH!7uVDy-PlH7D`k~2E^a0V^Y-dGi@5Zu zjo#dd1@DT!mdKq& z%*myP^!AZD)veSU`Tc{MJ=in##mZf^F-*5X_L1uu^z-7z8u1DyD~A{CYR^HT%exg> z<~rpd;uka(nmydDrAJA2)rb2Ms>(u9X&-=WsgXPU^P>2?rY_o2<02=wq0u{GTQlAS zsF8Rb`RwS!!f~**SZYWV!JsUh;L7vJob7}16vYQvK9R)O+B$-^Y~R1 z&a;M-trzX1d|ZW6#Y>0k@I)K9yc0=z2>@W}#jH8^N}L^MWPOE+wAB=5=m)~-_XdtG zJ?jlt>&I#SJJ03DT@B#x3^_imV0a*-nJ_G?9V&8O{ivJO#j8otfv6;6{N99qnc=!L z+{F4M5t6rKK!&v@G(U|ckWB>;d55aN)oY(TzR0~i%aJV^jCe#Is}-j_+x`*W^`?Tb zR-B+=oNH|E2J}am{i7#dLt86ow8GpZEAq!N;bg>HAvH6d1xTdEbi^A`_uG71cVPx*Tk|r` zJSfA~2BGg@$;bLYcb3D_u7z{S4>mzbKjan*5kKc(C&7qs2NXjA@qlkvED%4w_LI-} zXiRM1l5_BhJnM~+s5fq`ge-@A{Yw~@_j_n1c2wR^&gVIIg@ut4V!+6j#-3%eB(m3$=t?H)2vSBkVl`v$Hr5fUr zVz0sjM-ls=_CRRRMR@+@j$sqbYpBn78n10cs$sdGVoCYaZK6!5s)uXnp93Q+!Z0pHP+Kv5#)doVtX+y~ zL}rE|7T6G77#xh|Og~{#H0=(D@o9Bq_)PZJP9*qs?|hN`Aysrx7BwtM-=;}|wjY_w z)3+(m6YgCG74odeSILg~R( zWa=}m_+xa~TGWl94tn=Y#I}CE?TO(WOXR7kaSO7{-DfbojxX?=pOpoATO>m9F5>Cj z#0E!dquq=*dNF+hhMGM={*W*P)U%5yUZ^u&U`cr+4=!bP7OM9z(sk6lIl)kj3`RU2 z_Evd%E2Bur+4?5w#WnGQ{1|H3BqZ{MgKXaYA;SBc=Ino*Q~!sf9Y5W?sMh$p6D}@> zCk6)x4J~OehsCmiDhWf^Ms|EYx>9@A;Qu_<@mSyVM|1H0N2i;fDJ%8ck!(zjk)OqK2Wrffyr9r z4hwaK8%6=%Vb=WXNCJD4M;>(s%{Yn7@^VRtO-cD!b1}PpZdr1rIWi?3rpne8m3!%R z^p&eF@zquRTran7zC=?G-J(NaH#52o!i+f2YAu@aE8`M3Q{q=+WeZ#iwS#phpcz1i zoHihO1lhl+&2}popY8HpVDIwyV+Vu&j~xuamVDV1dF`luhFlFWk8-@HJ#ClAa{!H| zuNvIgFSP!mh_%(rP%ZOi#f5Q$I@i2G5J~H9JIc_CXEj>1o)|hxS>c$KEsI7lc?w^- zM7PCrfZ(Z6ndqpE06>55VBW+eHS1yHrlmGc(Ljtw?24}IO)W%!dP9S0=Jzw=2SJX3 zoz-1u@Vqk>7NNj(8=?E<{_4KQ+0{~B#b7z&r+eP#tX{1cx^8PmR;i5d9!`}!ip>u!TRk%Ky8!gR`p%y|=o9Emu$w6P zLi~vak`lh$4F5|D_Ep#j*%|yFT1DVj`Nubrp(CWAhn;P@hN8nX8$gKmRSVnw$aoeZ zIKLcHkUjH|Eo>cXJwrt-T{X3MMI27+`Pp-H#``1Yv-T99B!u@=`|QzxF`Hl>v2IVV zpbo1RYW3R38=j|MraOnZ8p&IWN346b)1h=yJnITJtU*q)?s^nDsx^x;>)hw3K))Bm zZ_S;$CYwV-=5~%P?>K>vm-lebYdt&y!>LJ-t*jXcgLS3)B3y*&q->-=yl9V|z(9=i zUhP`McuP-+R-yK;;bifaq8?qV&@Cf|!rJ0RuC8$%?l%vVk{B@6IoumC7*YM@6`K`7 zx*PFQR)i&49cz7vF`IaUdJ5hN~#k2kG=vBoir2FZ)uyvcErJ~#wc~j<= ziXpN(&ilrDNi(N7-4wZw(B9fbVdDJuH!I0_ba=A6VpJo|Kl1?Koj9T4S&VJ5l(9oy zmO5n+SD(X~Ir{^+C~HQic6v_9p6h7pK45Nw?Tsl@KVG7=S4n8vu?oKvE}OfoPFdvjDH{hEUR-OUAREU4Bm zVA3Gm=eo^jxY;6vLQ$Jo3JnN9IM?V_{b<*wap z0Y{0};nT}N<5KA=^?zyFj?NAcRi3LA&_$k!p<6*-fn`sjT=7peYyThW)^~EOJ*>=7 zD734qAE;MX^QaQd-mk7xui|Zp;^4R>q_)L>p|uE(SBd;J6-xe0bfeEzh0NX6_AgA4F-P+L%*;8GeP-P z{Uyl9%X0<)RO3=${`I;Rn-$IG+ zd0ZEGQ+#U%>hc+q{;{1u$IKu4a`%uQz!qSpRxmQY40q_J6(D zZ}Rp3UHMYLepOlK@0I`e6!A~pb}4ATYBu^;n!Psjd_}o(itpvEmjd^1SFYc??@|6T z>A&;;MY;QR0@uarR|HZZOP{~G{$B`uCtv-h2LE5u_3H$#OVO_g2tm&A{|15UE3oT= z@9PAv3(T(wtoL8^1!2h9nt(=kfoSiu^kAbs_i_@@>d+$my~6#kRc`#SV>q4pJYA|$x{jibV<2Kc`uZ`U!d%aX1zu&{p%gX~X{()U0=6Ovp3b>sdQpg&*gKja?1 zrm?Q$UO(r5g_}+A3%Ebov;JwW<2wBH6Vg}ktHl2W{vXdvUkAH>81)Klo%Fweec#7F z+Y9x#o7)&F}A-<{RE{zk4Jf4L%pO!q59 z{&pDVI?(mQ7*{|mkg)SVkC{I)^Pl(d^=^@qQ{>v=`SgY_54 z{L3Ebb=2!w_$yQc?q5K?9ua<@lfMpmJ!N|ZnaleNkpD1<{;P}sEYo#G;}G)2J%2?B tznAyA{>;~N4_9D=kZ)c3zX7|-M5xHaLqa+zC|bymBxG*&5Wf8F{{UgWfouQ( literal 0 HcmV?d00001 diff --git a/test-server/modules/example-module/module.properties b/test-server/modules/example-module/module.properties index 32924449b..b989ecf46 100644 --- a/test-server/modules/example-module/module.properties +++ b/test-server/modules/example-module/module.properties @@ -1,4 +1,4 @@ id=example-module name=example module -version=8.1.0 +version=8.3.0 priority=HIGH \ No newline at end of file From 952b186ae3347081168aef1ad12623a98bc6da30 Mon Sep 17 00:00:00 2001 From: Thorsten Marx Date: Mon, 31 Aug 2026 11:54:50 +0200 Subject: [PATCH 18/28] fix some sonar issues --- .../actions/collection/manage-collection.js | 10 +++++----- .../resources/manager/js/modules/form/forms.js | 10 +++++----- .../ts/src/actions/collection/manage-collection.ts | 13 +++++++------ .../src/main/ts/src/js/modules/form/forms.ts | 14 +++++++------- 4 files changed, 24 insertions(+), 23 deletions(-) diff --git a/modules/ui-module/src/main/resources/manager/actions/collection/manage-collection.js b/modules/ui-module/src/main/resources/manager/actions/collection/manage-collection.js index 53d2d840d..c7f02983f 100644 --- a/modules/ui-module/src/main/resources/manager/actions/collection/manage-collection.js +++ b/modules/ui-module/src/main/resources/manager/actions/collection/manage-collection.js @@ -28,11 +28,11 @@ import { showToast } from '@cms/modules/toast.js'; const PAGE_SIZE = 10; const MIN_SEARCH_LENGTH = 3; const escapeHtml = (value) => String(value ?? '') - .replace(/&/g, '&') - .replace(//g, '>') - .replace(/"/g, '"') - .replace(/'/g, '''); + .replaceAll('&', '&') + .replaceAll('<', '<') + .replaceAll('>', '>') + .replaceAll('"', '"') + .replaceAll("'", '''); const renderItems = (items) => { if (items.length === 0) { return `

${i18n.t('collection.items.empty', 'No collection items found.')}

`; diff --git a/modules/ui-module/src/main/resources/manager/js/modules/form/forms.js b/modules/ui-module/src/main/resources/manager/js/modules/form/forms.js index ba94036ab..3f8408baf 100644 --- a/modules/ui-module/src/main/resources/manager/js/modules/form/forms.js +++ b/modules/ui-module/src/main/resources/manager/js/modules/form/forms.js @@ -48,11 +48,11 @@ const getFormFields = (definition) => { return [...fields, ...tabFields]; }; const escapeHtml = (value) => String(value ?? '') - .replace(/&/g, '&') - .replace(//g, '>') - .replace(/"/g, '"') - .replace(/'/g, '''); + .replaceAll('&', '&') + .replaceAll('<', '<') + .replaceAll('>', '>') + .replaceAll('"', '"') + .replaceAll("'", '''); const createForm = (options) => { const standaloneFields = Array.isArray(options.fields) ? options.fields : []; const tabs = Array.isArray(options.tabs) ? options.tabs : []; diff --git a/modules/ui-module/src/main/ts/src/actions/collection/manage-collection.ts b/modules/ui-module/src/main/ts/src/actions/collection/manage-collection.ts index 6117704d8..50f4db12a 100644 --- a/modules/ui-module/src/main/ts/src/actions/collection/manage-collection.ts +++ b/modules/ui-module/src/main/ts/src/actions/collection/manage-collection.ts @@ -30,12 +30,13 @@ import { showToast } from '@cms/modules/toast.js'; const PAGE_SIZE = 10; const MIN_SEARCH_LENGTH = 3; -const escapeHtml = (value: any): string => String(value ?? '') - .replace(/&/g, '&') - .replace(//g, '>') - .replace(/"/g, '"') - .replace(/'/g, '''); +const escapeHtml = (value: string | number | boolean | null | undefined): string => + String(value ?? '') + .replaceAll('&', '&') + .replaceAll('<', '<') + .replaceAll('>', '>') + .replaceAll('"', '"') + .replaceAll("'", '''); const renderItems = (items: CollectionItemSummary[]): string => { if (items.length === 0) { diff --git a/modules/ui-module/src/main/ts/src/js/modules/form/forms.ts b/modules/ui-module/src/main/ts/src/js/modules/form/forms.ts index 33237642a..0def25aef 100644 --- a/modules/ui-module/src/main/ts/src/js/modules/form/forms.ts +++ b/modules/ui-module/src/main/ts/src/js/modules/form/forms.ts @@ -49,13 +49,13 @@ const getFormFields = (definition: any): any[] => { return [...fields, ...tabFields]; }; -const escapeHtml = (value: any): string => String(value ?? '') - .replace(/&/g, '&') - .replace(//g, '>') - .replace(/"/g, '"') - .replace(/'/g, '''); - +const escapeHtml = (value: string | number | boolean | null | undefined): string => + String(value ?? '') + .replaceAll('&', '&') + .replaceAll('<', '<') + .replaceAll('>', '>') + .replaceAll('"', '"') + .replaceAll("'", '''); const createForm = (options : any) : Form => { const standaloneFields = Array.isArray(options.fields) ? options.fields : []; From 8f99537f21f9d367a5821502c8c1e72ef462b62a Mon Sep 17 00:00:00 2001 From: Thorsten Marx Date: Mon, 31 Aug 2026 14:18:42 +0200 Subject: [PATCH 19/28] fix some sonar issues --- .../java/com/condation/cms/templates/utils/MacroUtils.java | 2 +- .../java/com/condation/cms/templates/utils/TemplateUtils.java | 4 ++-- .../java/com/condation/cms/modules/system/tags/ImageTags.java | 4 ++-- .../condation/cms/modules/ui/http/auth/AjaxLoginHandler.java | 4 +++- 4 files changed, 8 insertions(+), 6 deletions(-) diff --git a/cms-templates/src/main/java/com/condation/cms/templates/utils/MacroUtils.java b/cms-templates/src/main/java/com/condation/cms/templates/utils/MacroUtils.java index 2415d8b2d..f6ec1662d 100644 --- a/cms-templates/src/main/java/com/condation/cms/templates/utils/MacroUtils.java +++ b/cms-templates/src/main/java/com/condation/cms/templates/utils/MacroUtils.java @@ -50,7 +50,7 @@ public static Optional parseMacro(String expression) { List paramList = new ArrayList<>(); if (!params.isEmpty()) { - paramList = Arrays.asList(params.split("\\s*,\\s*")); + paramList = Arrays.asList(params.split("\\s*+,\\s*+")); } return Optional.of(new MacroTag.Macro(methodName, paramList)); diff --git a/cms-templates/src/main/java/com/condation/cms/templates/utils/TemplateUtils.java b/cms-templates/src/main/java/com/condation/cms/templates/utils/TemplateUtils.java index ba682f272..0558881cb 100644 --- a/cms-templates/src/main/java/com/condation/cms/templates/utils/TemplateUtils.java +++ b/cms-templates/src/main/java/com/condation/cms/templates/utils/TemplateUtils.java @@ -44,7 +44,7 @@ public static boolean hasFilters(String expression) { return false; } - String[] parts = expression.split("\\s+\\|\\s+"); // Nur " | " als Trenner verwenden + String[] parts = expression.split("\\s++\\|\\s++"); // Nur " | " als Trenner verwenden return parts.length > 1; } @@ -54,7 +54,7 @@ public static List extractFilters(String expression) { return filters; } - String[] parts = expression.split("\\s+\\|\\s+"); + String[] parts = expression.split("\\s++\\|\\s++"); if (parts.length < 2) { return filters; } diff --git a/modules/system-modules/src/main/java/com/condation/cms/modules/system/tags/ImageTags.java b/modules/system-modules/src/main/java/com/condation/cms/modules/system/tags/ImageTags.java index 9fabd8293..0d8491407 100644 --- a/modules/system-modules/src/main/java/com/condation/cms/modules/system/tags/ImageTags.java +++ b/modules/system-modules/src/main/java/com/condation/cms/modules/system/tags/ImageTags.java @@ -60,8 +60,8 @@ public String getImage (Parameter param) { .formatted( mediaUrl, StringEscapeUtils.ESCAPE_HTML4.translate(altText), - media.meta().getOrDefault("width", -1), - media.meta().getOrDefault("height", -1) + (int) media.meta().getOrDefault("width", -1), + (int) media.meta().getOrDefault("height", -1) ); } diff --git a/modules/ui-module/src/main/java/com/condation/cms/modules/ui/http/auth/AjaxLoginHandler.java b/modules/ui-module/src/main/java/com/condation/cms/modules/ui/http/auth/AjaxLoginHandler.java index a078cfffa..ff7c940fb 100644 --- a/modules/ui-module/src/main/java/com/condation/cms/modules/ui/http/auth/AjaxLoginHandler.java +++ b/modules/ui-module/src/main/java/com/condation/cms/modules/ui/http/auth/AjaxLoginHandler.java @@ -63,6 +63,8 @@ public class AjaxLoginHandler extends JettyHandler { private static final int ATTEMPTS_TO_BLOCK = 3; + private static final SecureRandom SECURE_RANDOM = new SecureRandom(); + public static record Login(User user, String token) { } @@ -210,7 +212,7 @@ private void validate(Request request, Response response, Callback callback, Com } private String generateCode() { - int code = new SecureRandom().nextInt(1_000_000); + int code = SECURE_RANDOM.nextInt(1_000_000); return String.format("%06d", code); } From 9c39de1b234fef742ead9f99d6371c1f4ab98180 Mon Sep 17 00:00:00 2001 From: Thorsten Marx Date: Tue, 1 Sep 2026 13:16:46 +0200 Subject: [PATCH 20/28] fix some review issues --- .../configs/CollectionConfiguration.java | 17 +++-- .../api/db/collection/CollectionItemId.java | 44 +++++++++++ .../cms/content/CollectionRouteResolver.java | 44 +++++++++-- .../template/functions/LinkFunction.java | 14 +++- .../condation/cms/content/utils/SlugUtil.java | 46 ++++++++++++ .../cms/content/CollectionResolverTest.java | 51 +++++++++++-- .../template/functions/LinkFunctionTest.java | 34 +++++++-- .../core/configuration/ConfigManagement.java | 5 +- .../configs/CollectionConfiguration.java | 51 +++++++------ .../CollectionConfigurationTest.java | 52 +++++++++++-- .../cms/filesystem/FileCollections.java | 49 +++++------- .../cms/filesystem/FileCollectionsTest.java | 62 ++++++++++++++- .../filesystem/ReferencedCollectionsTest.java | 4 +- .../RemoteCollectionEndpoints.java | 52 +++++++++++-- .../cms/modules/ui/utils/UIPathUtil.java | 14 +--- .../RemoteCollectionEndpointsTest.java | 75 +++++++++++++++++-- 16 files changed, 494 insertions(+), 120 deletions(-) create mode 100644 cms-api/src/main/java/com/condation/cms/api/db/collection/CollectionItemId.java create mode 100644 cms-content/src/main/java/com/condation/cms/content/utils/SlugUtil.java diff --git a/cms-api/src/main/java/com/condation/cms/api/configuration/configs/CollectionConfiguration.java b/cms-api/src/main/java/com/condation/cms/api/configuration/configs/CollectionConfiguration.java index a3638b71e..a0a841dd1 100644 --- a/cms-api/src/main/java/com/condation/cms/api/configuration/configs/CollectionConfiguration.java +++ b/cms-api/src/main/java/com/condation/cms/api/configuration/configs/CollectionConfiguration.java @@ -23,23 +23,30 @@ import com.condation.cms.api.configuration.Config; import java.util.Map; +import java.util.Objects; import java.util.Optional; -import java.util.concurrent.ConcurrentMap; -import lombok.RequiredArgsConstructor; /** * Reloadable, site-scoped collection definitions. */ -@RequiredArgsConstructor public class CollectionConfiguration implements Config { - private final ConcurrentMap collections; + private volatile Map collections; + + public CollectionConfiguration(Map collections) { + replaceCollections(collections); + } + + /** Atomically replaces the complete set of collection definitions. */ + public void replaceCollections(Map collections) { + this.collections = Map.copyOf(Objects.requireNonNull(collections)); + } public Optional collection(String name) { return Optional.ofNullable(collections.get(name)); } public Map collections() { - return Map.copyOf(collections); + return collections; } } diff --git a/cms-api/src/main/java/com/condation/cms/api/db/collection/CollectionItemId.java b/cms-api/src/main/java/com/condation/cms/api/db/collection/CollectionItemId.java new file mode 100644 index 000000000..42d55d14f --- /dev/null +++ b/cms-api/src/main/java/com/condation/cms/api/db/collection/CollectionItemId.java @@ -0,0 +1,44 @@ +package com.condation.cms.api.db.collection; + +/*- + * #%L + * CMS Api + * %% + * Copyright (C) 2023 - 2026 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * #L% + */ + +import java.util.regex.Pattern; + +/** Canonical validation rules for collection item identifiers. */ +public final class CollectionItemId { + + private static final Pattern VALID_ID = Pattern.compile("[a-zA-Z0-9][a-zA-Z0-9_.-]*"); + + private CollectionItemId() { + } + + public static boolean isValid(String id) { + return id != null && VALID_ID.matcher(id).matches(); + } + + public static String requireValid(String id) { + if (!isValid(id)) { + throw new IllegalArgumentException("invalid collection item id: " + id); + } + return id; + } +} diff --git a/cms-content/src/main/java/com/condation/cms/content/CollectionRouteResolver.java b/cms-content/src/main/java/com/condation/cms/content/CollectionRouteResolver.java index 76f2096f7..c60712f24 100644 --- a/cms-content/src/main/java/com/condation/cms/content/CollectionRouteResolver.java +++ b/cms-content/src/main/java/com/condation/cms/content/CollectionRouteResolver.java @@ -26,7 +26,10 @@ import com.condation.cms.api.configuration.configs.CollectionDetailConfiguration; import com.condation.cms.api.db.DB; import com.condation.cms.api.db.collection.CollectionItem; +import com.condation.cms.api.utils.MapUtil; +import com.condation.cms.content.utils.SlugUtil; import java.util.Comparator; +import java.util.List; import java.util.Optional; import java.util.regex.Pattern; import lombok.RequiredArgsConstructor; @@ -69,16 +72,45 @@ private Optional resolve(CollectionDefinition definition, String if ("id".equals(detail.get().parameter())) { item = findById(collection, routeValue.get()); } else { - item = collection.query() - .where(detail.get().parameter(), routeValue.get()) - .page(1, 1) - .getItems() - .stream() - .findFirst(); + item = findByRouteValue(collection, detail.get().parameter(), routeValue.get()); } return item.map(value -> new ResolvedRoute(definition, detail.get(), value, uri)); } + private static Optional findByRouteValue( + com.condation.cms.api.db.collection.Collection collection, + String parameter, + String routeValue) { + var slug = SlugUtil.slugify(routeValue); + if (slug.isBlank()) { + return Optional.empty(); + } + + var exactMatches = collection.query() + .where(parameter, slug) + .page(1, 2) + .getItems(); + if (exactMatches.size() == 1) { + return Optional.of(exactMatches.getFirst()); + } + if (exactMatches.size() > 1) { + return Optional.empty(); + } + + // Compatibility for collection files whose route value has not yet been + // normalized by the manager (for example "Über uns"). + List slugMatches = collection.query().get().stream() + .filter(item -> { + var value = MapUtil.getValue(item.meta(), parameter); + return value != null && slug.equals(SlugUtil.slugify(value.toString())); + }) + .limit(2) + .toList(); + return slugMatches.size() == 1 + ? Optional.of(slugMatches.getFirst()) + : Optional.empty(); + } + private static Optional findById( com.condation.cms.api.db.collection.Collection collection, String id) { diff --git a/cms-content/src/main/java/com/condation/cms/content/template/functions/LinkFunction.java b/cms-content/src/main/java/com/condation/cms/content/template/functions/LinkFunction.java index 86fe44500..f1364d6b6 100644 --- a/cms-content/src/main/java/com/condation/cms/content/template/functions/LinkFunction.java +++ b/cms-content/src/main/java/com/condation/cms/content/template/functions/LinkFunction.java @@ -28,6 +28,7 @@ import com.condation.cms.api.request.RequestContext; import com.condation.cms.api.utils.HTTPUtil; import com.condation.cms.api.utils.MapUtil; +import com.condation.cms.content.utils.SlugUtil; import java.util.Objects; import lombok.RequiredArgsConstructor; @@ -65,7 +66,8 @@ public String collectionUrl(CollectionItem item) { var detail = definition.detailPage() .orElseThrow(() -> new IllegalArgumentException( "collection has no detail route: " + item.collection())); - var parameterValue = "id".equals(detail.parameter()) + var idParameter = "id".equals(detail.parameter()); + var parameterValue = idParameter ? item.id() : MapUtil.getValue(item.meta(), detail.parameter()); if (parameterValue == null || parameterValue.toString().isBlank()) { @@ -73,9 +75,17 @@ public String collectionUrl(CollectionItem item) { "collection item has no route value for: " + detail.parameter()); } + var routeValue = idParameter + ? parameterValue.toString() + : SlugUtil.slugify(parameterValue.toString()); + if (routeValue.isBlank()) { + throw new IllegalArgumentException( + "collection item route value cannot be converted to a slug: " + detail.parameter()); + } + var route = detail.route().replace( "{" + detail.parameter() + "}", - parameterValue.toString()); + routeValue); return createUrl(route); } } diff --git a/cms-content/src/main/java/com/condation/cms/content/utils/SlugUtil.java b/cms-content/src/main/java/com/condation/cms/content/utils/SlugUtil.java new file mode 100644 index 000000000..3d27264a2 --- /dev/null +++ b/cms-content/src/main/java/com/condation/cms/content/utils/SlugUtil.java @@ -0,0 +1,46 @@ +package com.condation.cms.content.utils; + +/*- + * #%L + * CMS Content + * %% + * Copyright (C) 2023 - 2026 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * #L% + */ + +import com.github.slugify.Slugify; + +/** Shared slug rules for generated paths and collection routes. */ +public final class SlugUtil { + + private static final Slugify SLUGIFIER = Slugify.builder() + .customReplacement("ä", "ae") + .customReplacement("Ä", "ae") + .customReplacement("ü", "ue") + .customReplacement("Ü", "ue") + .customReplacement("ö", "oe") + .customReplacement("Ö", "oe") + .customReplacement("ß", "ss") + .lowerCase(true) + .build(); + + private SlugUtil() { + } + + public static String slugify(String input) { + return SLUGIFIER.slugify(input); + } +} diff --git a/cms-content/src/test/java/com/condation/cms/content/CollectionResolverTest.java b/cms-content/src/test/java/com/condation/cms/content/CollectionResolverTest.java index 4e7a851d7..2b74c38d7 100644 --- a/cms-content/src/test/java/com/condation/cms/content/CollectionResolverTest.java +++ b/cms-content/src/test/java/com/condation/cms/content/CollectionResolverTest.java @@ -68,6 +68,7 @@ class CollectionResolverTest { private final ReadOnlyFile itemFile = mock(ReadOnlyFile.class); private final ConcurrentHashMap definitions = new ConcurrentHashMap<>(); private final Configuration configuration = new Configuration(); + private CollectionConfiguration collectionConfiguration; private final CollectionItem item = new CollectionItem( "first", "blog", @@ -77,7 +78,8 @@ class CollectionResolverTest { @BeforeEach void setUp() throws Exception { - configuration.add(CollectionConfiguration.class, new CollectionConfiguration(definitions)); + collectionConfiguration = new CollectionConfiguration(definitions); + configuration.add(CollectionConfiguration.class, collectionConfiguration); when(db.getCollections()).thenReturn(collections); when(collections.collection("blog")).thenReturn(collection); when(db.getFileSystem()).thenReturn(fileSystem); @@ -99,14 +101,14 @@ void clearServices() { @Test void resolvesAnIdRouteAndUsesReloadedDefinitions() throws Exception { - definitions.put("blog", definition("/old/{id}")); + define(definition("/old/{id}")); when(collection.item("first")).thenReturn(Optional.of(item)); var resolver = new CollectionResolver(renderer, db, configuration); var context = context("/blog/first"); Assertions.assertThat(resolver.getContent(context)).isEmpty(); - definitions.put("blog", definition("/blog/{id}")); + define(definition("/blog/{id}")); var response = resolver.getContent(context); Assertions.assertThat(response) @@ -128,12 +130,12 @@ void resolvesAnIdRouteAndUsesReloadedDefinitions() throws Exception { @Test void resolvesAConfiguredFrontMatterField() throws Exception { - definitions.put("blog", definition("/blog/{slug}")); + define(definition("/blog/{slug}")); @SuppressWarnings("unchecked") var query = (ContentQuery) mock(ContentQuery.class); when(collection.query()).thenReturn(query); when(query.where("slug", "first-post")).thenReturn(query); - when(query.page(1, 1)).thenReturn(new Page<>(1, 1, 1, 1, List.of(item))); + when(query.page(1, 2)).thenReturn(new Page<>(1, 2, 1, 1, List.of(item))); var resolver = new CollectionResolver(renderer, db, configuration); var response = resolver.getContent(context("/blog/first-post/")); @@ -143,9 +145,39 @@ void resolvesAConfiguredFrontMatterField() throws Exception { } @Test - void readsTheItemFileFromTheConfiguredSourceSite() throws Exception { - definitions.put( + void resolvesLegacyFrontMatterByItsSlugifiedValue() throws Exception { + define(definition("/blog/{slug}")); + var legacyItem = new CollectionItem( + "about", "blog", + "blog/about.md", + "# About", + Map.of("title", "About", "slug", "Über uns")); + @SuppressWarnings("unchecked") + var exactQuery = (ContentQuery) mock(ContentQuery.class); + @SuppressWarnings("unchecked") + var fallbackQuery = (ContentQuery) mock(ContentQuery.class); + when(collection.query()).thenReturn(exactQuery, fallbackQuery); + when(exactQuery.where("slug", "ueber-uns")).thenReturn(exactQuery); + when(exactQuery.page(1, 2)).thenReturn(new Page<>(0, 2, 0, 1, List.of())); + when(fallbackQuery.get()).thenReturn(List.of(legacyItem)); + when(collectionsBase.resolve("blog/about.md")).thenReturn(itemFile); + when(renderer.renderCollection( + eq(itemFile), + any(), + eq(legacyItem), + anyString(), + any())).thenReturn("

About

"); + + var response = new CollectionResolver(renderer, db, configuration) + .getContent(context("/blog/ueber-uns")); + + Assertions.assertThat(response).isPresent(); + } + + @Test + void readsTheItemFileFromTheConfiguredSourceSite() throws Exception { + define( new CollectionDefinition( "blog", "content-site", @@ -190,6 +222,11 @@ private static CollectionDefinition definition(String route) { new CollectionDetailConfiguration(route, "collections/detail.html")); } + private void define(CollectionDefinition definition) { + definitions.put("blog", definition); + collectionConfiguration.replaceCollections(definitions); + } + private static RequestContext context(String uri) { var context = new RequestContext(); context.add(RequestFeature.class, new RequestFeature(uri, Map.of())); diff --git a/cms-content/src/test/java/com/condation/cms/content/template/functions/LinkFunctionTest.java b/cms-content/src/test/java/com/condation/cms/content/template/functions/LinkFunctionTest.java index 23c3b6c06..d8ad0bf9b 100644 --- a/cms-content/src/test/java/com/condation/cms/content/template/functions/LinkFunctionTest.java +++ b/cms-content/src/test/java/com/condation/cms/content/template/functions/LinkFunctionTest.java @@ -43,6 +43,7 @@ class LinkFunctionTest { private final ConcurrentHashMap definitions = new ConcurrentHashMap<>(); private final RequestContext context = new RequestContext(); + private CollectionConfiguration collectionConfiguration; private final CollectionItem item = new CollectionItem( "item_1", "blog", @@ -53,7 +54,8 @@ class LinkFunctionTest { @BeforeEach void setUp() { var configuration = new Configuration(); - configuration.add(CollectionConfiguration.class, new CollectionConfiguration(definitions)); + collectionConfiguration = new CollectionConfiguration(definitions); + configuration.add(CollectionConfiguration.class, collectionConfiguration); context.add(ConfigurationFeature.class, new ConfigurationFeature(configuration)); var siteProperties = mock(SiteProperties.class); @@ -63,7 +65,7 @@ void setUp() { @Test void createsContextAwareUrlUsingTheItemId() { - definitions.put("blog", definition("/articles/{id}")); + define("/articles/{id}"); var url = new LinkFunction(context).collectionUrl(item); @@ -72,27 +74,42 @@ void createsContextAwareUrlUsingTheItemId() { @Test void createsContextAwareUrlUsingConfiguredFrontMatter() { - definitions.put("blog", definition("/articles/{slug}")); + define("/articles/{slug}"); var url = new LinkFunction(context).collectionUrl(item); Assertions.assertThat(url).isEqualTo("/docs/articles/first-post"); } + @Test + void slugifiesConfiguredFrontMatterForTheUrl() { + define("/articles/{slug}"); + var itemWithUnnormalizedSlug = new CollectionItem( + "item_2", + "blog", + "blog/item_2.md", + "", + Map.of("slug", "Über uns & das CMS")); + + var url = new LinkFunction(context).collectionUrl(itemWithUnnormalizedSlug); + + Assertions.assertThat(url).isEqualTo("/docs/articles/ueber-uns-das-cms"); + } + @Test void usesReloadedCollectionRoute() { - definitions.put("blog", definition("/old/{id}")); + define("/old/{id}"); var links = new LinkFunction(context); Assertions.assertThat(links.collectionUrl(item)).isEqualTo("/docs/old/item_1"); - definitions.put("blog", definition("/new/{slug}")); + define("/new/{slug}"); Assertions.assertThat(links.collectionUrl(item)).isEqualTo("/docs/new/first-post"); } @Test void rejectsItemsWithoutTheConfiguredRouteValue() { - definitions.put("blog", definition("/articles/{slug}")); + define("/articles/{slug}"); var itemWithoutSlug = new CollectionItem( "item_2", "blog", @@ -110,4 +127,9 @@ private static CollectionDefinition definition(String route) { "blog", new CollectionDetailConfiguration(route, "collections/detail.html")); } + + private void define(String route) { + definitions.put("blog", definition(route)); + collectionConfiguration.replaceCollections(definitions); + } } diff --git a/cms-core/src/main/java/com/condation/cms/core/configuration/ConfigManagement.java b/cms-core/src/main/java/com/condation/cms/core/configuration/ConfigManagement.java index 628b985c3..dc5647654 100644 --- a/cms-core/src/main/java/com/condation/cms/core/configuration/ConfigManagement.java +++ b/cms-core/src/main/java/com/condation/cms/core/configuration/ConfigManagement.java @@ -74,9 +74,8 @@ public void initConfiguration (Configuration configuration) { ); configuration.add( com.condation.cms.api.configuration.configs.CollectionConfiguration.class, - new com.condation.cms.api.configuration.configs.CollectionConfiguration( - ((com.condation.cms.core.configuration.configs.CollectionConfiguration) get("collections") - .orElseThrow()).getCollections()) + ((com.condation.cms.core.configuration.configs.CollectionConfiguration) get("collections") + .orElseThrow()).apiConfiguration() ); var mediaConfig = new com.condation.cms.api.configuration.configs.MediaConfiguration( ((com.condation.cms.core.configuration.configs.MediaConfiguration) get("media") diff --git a/cms-core/src/main/java/com/condation/cms/core/configuration/configs/CollectionConfiguration.java b/cms-core/src/main/java/com/condation/cms/core/configuration/configs/CollectionConfiguration.java index 1d888a885..a7834aadd 100644 --- a/cms-core/src/main/java/com/condation/cms/core/configuration/configs/CollectionConfiguration.java +++ b/cms-core/src/main/java/com/condation/cms/core/configuration/configs/CollectionConfiguration.java @@ -30,11 +30,10 @@ import com.condation.cms.core.configuration.ReloadStrategy; import com.condation.cms.core.configuration.reload.NoReload; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.UUID; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ConcurrentMap; import lombok.extern.slf4j.Slf4j; /** @@ -47,7 +46,8 @@ public class CollectionConfiguration extends AbstractConfiguration implements IC private final ReloadStrategy reloadStrategy; private final EventBus eventBus; private final String id; - private final ConcurrentMap collections = new ConcurrentHashMap<>(); + private final com.condation.cms.api.configuration.configs.CollectionConfiguration apiConfiguration = + new com.condation.cms.api.configuration.configs.CollectionConfiguration(Map.of()); private CollectionConfiguration(Builder builder) { this.sources = builder.sources; @@ -72,33 +72,41 @@ public String id() { return id; } - public ConcurrentMap getCollections() { - return collections; + public Map getCollections() { + return apiConfiguration.collections(); + } + + public com.condation.cms.api.configuration.configs.CollectionConfiguration apiConfiguration() { + return apiConfiguration; } @Override public void reload() { var reloaded = false; - var updatedCollections = new ConcurrentHashMap(); - for (var source : sources) { - reloaded |= source.reload(); - if (!source.exists()) { - continue; - } - for (var entry : source.getMap("collections").entrySet()) { - parse(entry.getKey(), entry.getValue()).ifPresent(definition -> - updatedCollections.put(definition.name(), definition)); + var updatedCollections = new HashMap(); + try { + for (var source : sources) { + reloaded |= source.reload(); + if (!source.exists()) { + continue; + } + for (var entry : source.getMap("collections").entrySet()) { + var definition = parse(entry.getKey(), entry.getValue()); + updatedCollections.put(definition.name(), definition); + } } + } catch (RuntimeException ex) { + log.error("could not reload collection configuration; keeping previous configuration", ex); + return; } - collections.clear(); - collections.putAll(updatedCollections); + apiConfiguration.replaceCollections(updatedCollections); if (reloaded && eventBus != null) { eventBus.publish(new ConfigurationReloadEvent(id)); } } - private java.util.Optional parse(String name, Object value) { + private CollectionDefinition parse(String name, Object value) { try { if (!(value instanceof Map collection)) { throw new IllegalArgumentException("collection definition must be a map"); @@ -107,7 +115,7 @@ private java.util.Optional parse(String name, Object value var site = optionalStringValue(collection.get("site"), "site"); var detailValue = collection.get("detail"); if (detailValue == null) { - return java.util.Optional.of(new CollectionDefinition(name, site, null)); + return new CollectionDefinition(name, site, null); } if (!(detailValue instanceof Map detail)) { throw new IllegalArgumentException("collection detail definition must be a map"); @@ -115,13 +123,12 @@ private java.util.Optional parse(String name, Object value var route = stringValue(detail.get("route"), "collection detail route"); var template = stringValue(detail.get("template"), "collection detail template"); - return java.util.Optional.of(new CollectionDefinition( + return new CollectionDefinition( name, site, - new CollectionDetailConfiguration(route, template))); + new CollectionDetailConfiguration(route, template)); } catch (RuntimeException ex) { - log.error("invalid configuration for collection {}", name, ex); - return java.util.Optional.empty(); + throw new IllegalArgumentException("invalid configuration for collection " + name, ex); } } diff --git a/cms-core/src/test/java/com/condation/cms/core/configuration/CollectionConfigurationTest.java b/cms-core/src/test/java/com/condation/cms/core/configuration/CollectionConfigurationTest.java index 7b4fa3875..438072d06 100644 --- a/cms-core/src/test/java/com/condation/cms/core/configuration/CollectionConfigurationTest.java +++ b/cms-core/src/test/java/com/condation/cms/core/configuration/CollectionConfigurationTest.java @@ -23,6 +23,7 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; import static org.mockito.Mockito.when; import com.condation.cms.api.eventbus.EventBus; @@ -59,20 +60,55 @@ void updatesTheSharedConfigurationOnReload() { .id("collections") .addSource(source) .build(); - var sharedCollections = configuration.getCollections(); + var apiConfiguration = configuration.apiConfiguration(); + var initialSnapshot = configuration.getCollections(); - Assertions.assertThat(sharedCollections).containsOnlyKeys("blog", "listing-only"); - Assertions.assertThat(sharedCollections.get("blog").detailPage().orElseThrow().parameter()) + Assertions.assertThat(initialSnapshot).containsOnlyKeys("blog", "listing-only"); + Assertions.assertThat(initialSnapshot.get("blog").detailPage().orElseThrow().parameter()) .isEqualTo("slug"); - Assertions.assertThat(sharedCollections.get("blog").sourceSite()).contains("content-site"); - Assertions.assertThat(sharedCollections.get("listing-only").sourceSite()).isEmpty(); + Assertions.assertThat(initialSnapshot.get("blog").sourceSite()).contains("content-site"); + Assertions.assertThat(initialSnapshot.get("listing-only").sourceSite()).isEmpty(); configuration.reload(); - Assertions.assertThat(configuration.getCollections()).isSameAs(sharedCollections); - Assertions.assertThat(sharedCollections).containsOnlyKeys("products"); - Assertions.assertThat(sharedCollections.get("products").detailPage().orElseThrow().route()) + var updatedSnapshot = configuration.getCollections(); + Assertions.assertThat(configuration.apiConfiguration()).isSameAs(apiConfiguration); + Assertions.assertThat(apiConfiguration.collections()).isSameAs(updatedSnapshot); + Assertions.assertThat(updatedSnapshot).isNotSameAs(initialSnapshot); + Assertions.assertThat(initialSnapshot).containsOnlyKeys("blog", "listing-only"); + Assertions.assertThat(updatedSnapshot).containsOnlyKeys("products"); + Assertions.assertThat(updatedSnapshot.get("products").detailPage().orElseThrow().route()) .isEqualTo("/products/{id}"); verify(eventBus).publish(new ConfigurationReloadEvent("collections")); } + + @Test + void keepsTheCompletePreviousSnapshotWhenOneDefinitionIsInvalid() { + var eventBus = mock(EventBus.class); + var source = mock(ConfigSource.class); + var initial = Map.of( + "blog", Map.of("detail", Map.of( + "route", "/blog/{id}", + "template", "collections/blog.html"))); + var invalid = Map.of( + "products", Map.of("detail", Map.of( + "route", "/products/{id}", + "template", "collections/product.html")), + "broken", Map.of("detail", Map.of("route", "/broken/{id}"))); + when(source.exists()).thenReturn(true); + when(source.reload()).thenReturn(false, true); + when(source.getMap("collections")).thenReturn(initial, invalid); + + var configuration = CollectionConfiguration.builder(eventBus) + .id("collections") + .addSource(source) + .build(); + var initialSnapshot = configuration.getCollections(); + + configuration.reload(); + + Assertions.assertThat(configuration.getCollections()).isSameAs(initialSnapshot); + Assertions.assertThat(configuration.getCollections()).containsOnlyKeys("blog"); + verifyNoInteractions(eventBus); + } } diff --git a/cms-filesystem/src/main/java/com/condation/cms/filesystem/FileCollections.java b/cms-filesystem/src/main/java/com/condation/cms/filesystem/FileCollections.java index 72ef7c26e..1a5e4e3b2 100644 --- a/cms-filesystem/src/main/java/com/condation/cms/filesystem/FileCollections.java +++ b/cms-filesystem/src/main/java/com/condation/cms/filesystem/FileCollections.java @@ -26,10 +26,13 @@ import com.condation.cms.api.db.ContentQuery; import com.condation.cms.api.db.NodeVisibility; import com.condation.cms.api.db.collection.CollectionItem; +import com.condation.cms.api.db.collection.CollectionItemId; import com.condation.cms.api.db.collection.Collections; import com.condation.cms.api.utils.PathUtil; +import com.condation.cms.core.content.io.ContentFileParser; import com.condation.cms.filesystem.metadata.persistent.CollectionMetaData; import java.io.IOException; +import java.io.UncheckedIOException; import java.nio.file.Files; import java.nio.file.Path; import java.time.Duration; @@ -52,7 +55,6 @@ public class FileCollections implements Collections, AutoCloseable { private static final Pattern COLLECTION_NAME = Pattern.compile("[a-zA-Z0-9][a-zA-Z0-9_-]*"); - private static final Pattern ITEM_ID = Pattern.compile("[a-zA-Z0-9][a-zA-Z0-9_.-]*"); private static final Duration CHANGE_QUIET_PERIOD = Duration.ofMillis(200); private final String siteId; @@ -117,7 +119,7 @@ public Set names() { @Override public void refresh(String collection, String id) { validateCollectionName(collection); - validateItemId(id); + CollectionItemId.requireValid(id); var file = collectionsBase.resolve(collection).resolve(id + ".md"); try { if (Files.isRegularFile(file)) { @@ -171,7 +173,7 @@ private void processPath(Path path) throws IOException { } return; } - if (parts.length != 2 || !isMarkdown(path)) { + if (parts.length != 2 || !isValidItemFile(path)) { return; } if (!isValidCollectionName(parts[0])) { @@ -206,7 +208,7 @@ private void scanCollection(Path collection) throws IOException { collectionNames.add(name); metaData.removeDirectory(name); try (var files = Files.list(collection)) { - for (var file : files.filter(Files::isRegularFile).filter(FileCollections::isMarkdown).toList()) { + for (var file : files.filter(Files::isRegularFile).filter(FileCollections::isValidItemFile).toList()) { index(file); } } @@ -237,28 +239,9 @@ private CollectionItem map(ContentNode node, int ignoredExcerptLength) { private static String readMarkdownBody(Path file) { try { - var lines = Files.readAllLines(file); - var body = new StringBuilder(); - var inFrontMatter = false; - var frontMatterClosed = false; - for (var line : lines) { - if (line.trim().equals("---") && !frontMatterClosed) { - if (!inFrontMatter) { - inFrontMatter = true; - } else { - inFrontMatter = false; - frontMatterClosed = true; - } - continue; - } - if (!inFrontMatter) { - body.append(line).append("\r\n"); - } - } - return body.toString(); + return new ContentFileParser(file.toString()).getContent(); } catch (IOException ex) { - log.error("error reading collection item {}", file, ex); - return ""; + throw new UncheckedIOException("could not read collection item " + file, ex); } } @@ -266,6 +249,14 @@ private static boolean isMarkdown(Path file) { return file.getFileName().toString().endsWith(".md"); } + private static boolean isValidItemFile(Path file) { + if (!isMarkdown(file)) { + return false; + } + var filename = file.getFileName().toString(); + return CollectionItemId.isValid(filename.substring(0, filename.length() - 3)); + } + private static boolean isValidCollectionName(String name) { return name != null && COLLECTION_NAME.matcher(name).matches(); } @@ -276,12 +267,6 @@ private static void validateCollectionName(String name) { } } - private static void validateItemId(String id) { - if (id == null || !ITEM_ID.matcher(id).matches()) { - throw new IllegalArgumentException("invalid collection item id: " + id); - } - } - @Override public void close() throws IOException { if (watcher != null) { @@ -310,7 +295,7 @@ public String name() { @Override public Optional item(String id) { - validateItemId(id); + CollectionItemId.requireValid(id); return metaData.byPath(name + "/" + id + ".md") .filter(NodeVisibility::isVisible) .map(node -> FileCollections.this.map(node, 0)); diff --git a/cms-filesystem/src/test/java/com/condation/cms/filesystem/FileCollectionsTest.java b/cms-filesystem/src/test/java/com/condation/cms/filesystem/FileCollectionsTest.java index bc4fe951b..dec564d38 100644 --- a/cms-filesystem/src/test/java/com/condation/cms/filesystem/FileCollectionsTest.java +++ b/cms-filesystem/src/test/java/com/condation/cms/filesystem/FileCollectionsTest.java @@ -71,7 +71,7 @@ void queriesCollectionsWithFilteringSortingPagingAndRawMarkdown() throws Excepti Assertions.assertThat(item.id()).isEqualTo("third"); Assertions.assertThat(item.collection()).isEqualTo("blog"); Assertions.assertThat(item.path()).isEqualTo("blog/third.md"); - Assertions.assertThat(item.content()).isEqualTo("_Third_\r\n"); + Assertions.assertThat(item.content()).isEqualTo("_Third_"); Assertions.assertThat(item.meta()).containsEntry("title", "Third"); }); @@ -96,7 +96,7 @@ void appliesFlatFileChangesIncrementally() throws Exception { .singleElement() .satisfies(result -> { Assertions.assertThat(result.meta()).containsEntry("title", "After"); - Assertions.assertThat(result.content()).isEqualTo("After\r\n"); + Assertions.assertThat(result.content()).isEqualTo("After"); }); Files.delete(item); @@ -146,13 +146,52 @@ void refreshesOneCollectionItemImmediately() throws Exception { .get() .satisfies(item -> { Assertions.assertThat(item.meta()).containsEntry("title", "After"); - Assertions.assertThat(item.content()).isEqualTo("After\r\n"); + Assertions.assertThat(item.content()).isEqualTo("After"); }); } finally { collections.close(); } } + @Test + void preservesMarkdownHorizontalRulesOutsideFrontMatter() throws Exception { + write( + "blog/item.md", + "title: Horizontal rules", + "Introduction\n\n---\n\nMiddle section\n\n---\n\nConclusion"); + var collections = createCollections(); + try { + Assertions.assertThat(collections.collection("blog").item("item")) + .isPresent() + .get() + .extracting(item -> item.content()) + .isEqualTo("Introduction\n\n---\n\nMiddle section\n\n---\n\nConclusion"); + } finally { + collections.close(); + } + } + + @Test + void preservesHorizontalRulesInMarkdownWithoutFrontMatter() throws Exception { + var file = tempDirectory.resolve("collections/blog/item.md"); + Files.createDirectories(file.getParent()); + Files.writeString(file, "Introduction\n\n---\n\nConclusion\n"); + var collections = createCollections(); + try { + var previewContext = new RequestContext(); + previewContext.add(IsPreviewFeature.class, new IsPreviewFeature(IsPreviewFeature.Mode.PREVIEW)); + var optionalItem = ScopedValue.where(RequestContextScope.REQUEST_CONTEXT, previewContext) + .call(() -> collections.collection("blog").item("item")); + Assertions.assertThat(optionalItem) + .isPresent() + .get() + .extracting(item -> item.content()) + .isEqualTo("Introduction\n\n---\n\nConclusion\n"); + } finally { + collections.close(); + } + } + @Test void removesDeletedCollectionItemImmediatelyOnRefresh() throws Exception { var item = write("blog/item.md", "title: Before", "Before"); @@ -178,6 +217,23 @@ void rejectsUnsafeCollectionNames() throws Exception { } } + @Test + void consistentlyRejectsInvalidItemIdsFromFilesAndDirectLookups() throws Exception { + write("blog/valid-item.md", "title: Valid", "Valid"); + write("blog/invalid item.md", "title: Invalid", "Invalid"); + var collections = createCollections(); + try { + Assertions.assertThat(collections.collection("blog").query().get()) + .extracting(item -> item.id()) + .containsExactly("valid-item"); + Assertions.assertThatIllegalArgumentException() + .isThrownBy(() -> collections.collection("blog").item("invalid item")) + .withMessage("invalid collection item id: invalid item"); + } finally { + collections.close(); + } + } + @Test void appliesDefaultWorkflowSchedulingAndPreview() throws Exception { writeMeta("blog/published.md", "status: published", "Published"); diff --git a/cms-filesystem/src/test/java/com/condation/cms/filesystem/ReferencedCollectionsTest.java b/cms-filesystem/src/test/java/com/condation/cms/filesystem/ReferencedCollectionsTest.java index 03332fb62..c317023db 100644 --- a/cms-filesystem/src/test/java/com/condation/cms/filesystem/ReferencedCollectionsTest.java +++ b/cms-filesystem/src/test/java/com/condation/cms/filesystem/ReferencedCollectionsTest.java @@ -82,12 +82,14 @@ void usesReloadedConfigurationWithoutRecreatingTheCollectionsFacade() { when(local.collection("shared")).thenReturn(localCollection); var definitions = new ConcurrentHashMap(); definitions.put("shared", new CollectionDefinition("shared", "content-site", null)); + var configuration = new CollectionConfiguration(definitions); var collections = new ReferencedCollections( "consumer-site", local, - new CollectionConfiguration(definitions)); + configuration); definitions.put("shared", new CollectionDefinition("shared", null)); + configuration.replaceCollections(definitions); Assertions.assertThat(collections.isLocal("shared")).isTrue(); Assertions.assertThat(collections.collection("shared")).isSameAs(localCollection); diff --git a/modules/ui-module/src/main/java/com/condation/cms/modules/ui/extensionpoints/remotemethods/RemoteCollectionEndpoints.java b/modules/ui-module/src/main/java/com/condation/cms/modules/ui/extensionpoints/remotemethods/RemoteCollectionEndpoints.java index 44e8ac453..eec550f28 100644 --- a/modules/ui-module/src/main/java/com/condation/cms/modules/ui/extensionpoints/remotemethods/RemoteCollectionEndpoints.java +++ b/modules/ui-module/src/main/java/com/condation/cms/modules/ui/extensionpoints/remotemethods/RemoteCollectionEndpoints.java @@ -26,12 +26,15 @@ import com.condation.cms.api.db.DB; import com.condation.cms.api.db.Page; import com.condation.cms.api.db.collection.CollectionItem; +import com.condation.cms.api.db.collection.CollectionItemId; import com.condation.cms.api.eventbus.events.InvalidateContentCacheEvent; import com.condation.cms.api.feature.features.EventBusFeature; import com.condation.cms.api.feature.features.WorkflowFeature; import com.condation.cms.api.ui.annotations.RemoteMethod; import com.condation.cms.api.ui.extensions.UIRemoteMethodExtensionPoint; import com.condation.cms.api.ui.rpc.RPCException; +import com.condation.cms.api.utils.MapUtil; +import com.condation.cms.content.utils.SlugUtil; import com.condation.cms.content.template.functions.LinkFunction; import com.condation.cms.core.content.io.ContentFileParser; import com.condation.cms.core.content.io.YamlHeaderUpdater; @@ -46,7 +49,7 @@ import java.util.Date; import java.util.HashMap; import java.util.Map; -import java.util.regex.Pattern; +import java.util.Objects; import lombok.extern.slf4j.Slf4j; /** Manager endpoints for listing and editing collection items. */ @@ -56,7 +59,6 @@ public class RemoteCollectionEndpoints extends AbstractRemoteMethodeExtension { private static final long DEFAULT_PAGE_SIZE = 10; private static final long MAX_PAGE_SIZE = 100; - private static final Pattern ITEM_ID = Pattern.compile("[a-zA-Z0-9][a-zA-Z0-9_.-]*"); public record ItemDto( String id, @@ -115,7 +117,7 @@ public Object get(Map parameters) throws RPCException { } @RemoteMethod(name = "collections.item.save", permissions = {Permissions.CONTENT_EDIT}) - public Object save(Map parameters) throws RPCException { + public synchronized Object save(Map parameters) throws RPCException { var db = getDB(parameters); var item = item(parameters); ensureLocalCollection(db, item.collection()); @@ -126,6 +128,7 @@ public Object save(Map parameters) throws RPCException { var meta = new HashMap<>(parser.getHeader()); var rawMeta = typedMeta(parameters.get("meta")); YamlHeaderUpdater.mergeFlatMapIntoNestedMap(meta, MetaConverter.convertMeta(rawMeta)); + normalizeAndValidateSlug(db, item.collection(), item.id(), meta); var content = parameters.containsKey(Parameters.CONTENT) ? FormHelper.getContent(parameters.get(Parameters.CONTENT)) : parser.getContent(); @@ -140,7 +143,7 @@ public Object save(Map parameters) throws RPCException { } @RemoteMethod(name = "collections.item.create", permissions = {Permissions.CONTENT_EDIT}) - public Object create(Map parameters) throws RPCException { + public synchronized Object create(Map parameters) throws RPCException { var db = getDB(parameters); var collectionName = requiredString(parameters, Parameters.COLLECTION); var id = requiredItemId(parameters); @@ -155,6 +158,7 @@ public Object create(Map parameters) throws RPCException { YamlHeaderUpdater.mergeFlatMapIntoNestedMap( meta, MetaConverter.convertMeta(typedMeta(parameters.get("meta")))); + normalizeAndValidateSlug(db, collectionName, id, meta); meta.putIfAbsent(Constants.MetaFields.TITLE, id); meta.put("createdAt", Date.from(Instant.now())); meta.put("createdBy", getUserName()); @@ -206,7 +210,7 @@ public Object delete(Map parameters) throws RPCException { private CollectionItem item(Map parameters) throws RPCException { var db = getDB(parameters); var collectionName = requiredString(parameters, Parameters.COLLECTION); - var id = requiredString(parameters, "id"); + var id = requiredItemId(parameters); ensureCollectionExists(db.getCollections().names(), collectionName); try { return db.getCollections().collection(collectionName).item(id) @@ -257,6 +261,37 @@ private static void ensureLocalCollection(DB db, String name) throws RPCExceptio } } + private static void normalizeAndValidateSlug( + DB db, + String collectionName, + String itemId, + Map meta) throws RPCException { + var value = MapUtil.getValue(meta, "slug"); + if (value == null) { + return; + } + if (!(value instanceof CharSequence)) { + throw new RPCException(400, "collection item slug must be text"); + } + + var slug = SlugUtil.slugify(value.toString()); + if (slug.isBlank()) { + throw new RPCException(400, "collection item slug must not be blank"); + } + + var duplicate = db.getCollections().collection(collectionName).query().get().stream() + .filter(item -> !item.id().equals(itemId)) + .map(item -> MapUtil.getValue(item.meta(), "slug")) + .filter(Objects::nonNull) + .map(Object::toString) + .map(SlugUtil::slugify) + .anyMatch(slug::equals); + if (duplicate) { + throw new RPCException(409, "collection item slug already exists: " + slug); + } + meta.put("slug", slug); + } + private static String requiredString(Map parameters, String name) throws RPCException { var value = optionalString(parameters, name); if (value.isBlank()) { @@ -267,10 +302,11 @@ private static String requiredString(Map parameters, String name private static String requiredItemId(Map parameters) throws RPCException { var id = requiredString(parameters, "id"); - if (!ITEM_ID.matcher(id).matches()) { - throw new RPCException(400, "invalid collection item id"); + try { + return CollectionItemId.requireValid(id); + } catch (IllegalArgumentException ex) { + throw new RPCException(400, ex.getMessage()); } - return id; } private static String optionalString(Map parameters, String name) { diff --git a/modules/ui-module/src/main/java/com/condation/cms/modules/ui/utils/UIPathUtil.java b/modules/ui-module/src/main/java/com/condation/cms/modules/ui/utils/UIPathUtil.java index 4d10588da..70095e971 100644 --- a/modules/ui-module/src/main/java/com/condation/cms/modules/ui/utils/UIPathUtil.java +++ b/modules/ui-module/src/main/java/com/condation/cms/modules/ui/utils/UIPathUtil.java @@ -21,7 +21,7 @@ * #L% */ import com.condation.cms.api.utils.FileUtils; -import com.github.slugify.Slugify; +import com.condation.cms.content.utils.SlugUtil; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; @@ -34,16 +34,6 @@ @Slf4j public class UIPathUtil { - private static final Slugify SLUGIFIER = Slugify.builder() - .customReplacement("ä", "ae") - .customReplacement("Ä", "ae") - .customReplacement("ü", "ue") - .customReplacement("Ü", "ue") - .customReplacement("ö", "oe") - .customReplacement("Ö", "oe") - .customReplacement("ß", "ss") - .lowerCase(true).build(); - public static String toUri(final Path contentFile, final Path contentBase) { Path relativize = contentBase.relativize(contentFile); // if (Files.isDirectory(contentFile)) { @@ -79,7 +69,7 @@ public static String getType(Path path) { } public static String slugify (String input) { - return SLUGIFIER.slugify(input); + return SlugUtil.slugify(input); } public static String toValidFilename(String input) { diff --git a/modules/ui-module/src/test/java/com/condation/cms/modules/ui/extensionpoints/remotemethods/RemoteCollectionEndpointsTest.java b/modules/ui-module/src/test/java/com/condation/cms/modules/ui/extensionpoints/remotemethods/RemoteCollectionEndpointsTest.java index 128e484ab..a2a73258e 100644 --- a/modules/ui-module/src/test/java/com/condation/cms/modules/ui/extensionpoints/remotemethods/RemoteCollectionEndpointsTest.java +++ b/modules/ui-module/src/test/java/com/condation/cms/modules/ui/extensionpoints/remotemethods/RemoteCollectionEndpointsTest.java @@ -27,6 +27,10 @@ import com.condation.cms.api.configuration.configs.CollectionDefinition; import com.condation.cms.api.db.DB; import com.condation.cms.api.db.DBFileSystem; +import com.condation.cms.api.db.ContentQuery; +import com.condation.cms.api.db.cms.ReadOnlyFile; +import com.condation.cms.api.db.collection.Collection; +import com.condation.cms.api.db.collection.CollectionItem; import com.condation.cms.api.db.collection.Collections; import com.condation.cms.api.eventbus.EventBus; import com.condation.cms.api.feature.features.AuthFeature; @@ -42,7 +46,9 @@ import com.condation.cms.api.workflow.Workflow; import java.nio.file.Files; import java.nio.file.Path; +import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import org.junit.jupiter.api.BeforeEach; @@ -76,6 +82,12 @@ class RemoteCollectionEndpointsTest { @Mock private Collections collections; + @Mock + private Collection collection; + + @Mock + private ContentQuery collectionQuery; + @Mock private EventBus eventBus; @@ -97,9 +109,12 @@ void setUp() throws Exception { when(moduleContext.get(DBFeature.class)).thenReturn(new DBFeature(db)); lenient().when(db.getFileSystem()).thenReturn(fileSystem); - when(db.getCollections()).thenReturn(collections); - when(collections.names()).thenReturn(Set.of("blog")); - when(collections.isLocal("blog")).thenReturn(true); + lenient().when(db.getCollections()).thenReturn(collections); + lenient().when(collections.names()).thenReturn(Set.of("blog")); + lenient().when(collections.isLocal("blog")).thenReturn(true); + lenient().when(collections.collection("blog")).thenReturn(collection); + lenient().when(collection.query()).thenReturn(collectionQuery); + lenient().when(collectionQuery.get()).thenReturn(List.of()); lenient().when(fileSystem.resolve(Constants.Folders.COLLECTIONS)).thenReturn(collectionsDirectory); } @@ -114,7 +129,9 @@ void createsCollectionItemWithWorkflowMetadataAndRefreshesIndex() throws Excepti "collection", "blog", "id", "first-item", "content", Map.of("type", "markdown", "value", "# Body"), - "meta", Map.of("title", Map.of("type", "text", "value", "First item"))); + "meta", Map.of( + "title", Map.of("type", "text", "value", "First item"), + "slug", Map.of("type", "text", "value", "Über das CMS"))); var result = ScopedValue.where(RequestContextScope.REQUEST_CONTEXT, requestContext) .call(() -> endpoints.create(parameters)); @@ -128,10 +145,46 @@ void createsCollectionItemWithWorkflowMetadataAndRefreshesIndex() throws Excepti .containsExactly("first-item", "First item")); assertThat(collectionsDirectory.resolve("blog/first-item.md")) .content() - .contains("title: First item", "status: draft", "createdBy: editor", "# Body"); + .contains( + "title: First item", + "slug: ueber-das-cms", + "status: draft", + "createdBy: editor", + "# Body"); verify(collections).refresh("blog", "first-item"); } + @Test + void rejectsDuplicateSlugsAfterNormalizationWhenSaving() throws Exception { + var editedItem = new CollectionItem( + "second", + "blog", + "blog/second.md", + "", + Map.of("slug", "second")); + var existingItem = new CollectionItem( + "first", + "blog", + "blog/first.md", + "", + Map.of("slug", "Über uns")); + var collectionsBase = org.mockito.Mockito.mock(ReadOnlyFile.class); + var sourceFile = org.mockito.Mockito.mock(ReadOnlyFile.class); + when(collection.item("second")).thenReturn(Optional.of(editedItem)); + when(fileSystem.collectionsBase()).thenReturn(collectionsBase); + when(collectionsBase.resolve("blog/second.md")).thenReturn(sourceFile); + when(sourceFile.getContent()).thenReturn("---\nslug: second\n---\n\nBody\n"); + when(collectionQuery.get()).thenReturn(List.of(existingItem, editedItem)); + + assertThatThrownBy(() -> endpoints.save(Map.of( + "collection", "blog", + "id", "second", + "meta", Map.of("slug", Map.of("type", "text", "value", "Ueber uns"))))) + .isInstanceOfSatisfying( + RPCException.class, + exception -> assertThat(exception.getCode()).isEqualTo(409)); + } + @Test void rejectsDuplicateAndInvalidCollectionItemIds() throws Exception { Files.writeString(collectionsDirectory.resolve("blog/existing.md"), "existing"); @@ -146,6 +199,18 @@ void rejectsDuplicateAndInvalidCollectionItemIds() throws Exception { exception -> assertThat(exception.getCode()).isEqualTo(400)); } + @Test + void rejectsInvalidItemIdsConsistentlyWhenLoadingItems() { + assertThatThrownBy(() -> endpoints.get(Map.of("collection", "blog", "id", "invalid item"))) + .isInstanceOfSatisfying( + RPCException.class, + exception -> { + assertThat(exception.getCode()).isEqualTo(400); + assertThat(exception.getMessage()) + .isEqualTo("invalid collection item id: invalid item"); + }); + } + @Test void deletesCollectionItemAndRefreshesIndex() throws Exception { when(moduleContext.get(EventBusFeature.class)).thenReturn(new EventBusFeature(eventBus)); From 2d7089450a395d61b1839096f59ea74fc8108cc1 Mon Sep 17 00:00:00 2001 From: Thorsten Marx Date: Tue, 1 Sep 2026 15:25:35 +0200 Subject: [PATCH 21/28] fix some problems with variants together with collections --- .../RemoteContentEndpointsExtension.java | 2 + .../manager/actions/page/create-variant.js | 10 ++-- .../manager/actions/page/variant-selector.js | 4 ++ .../manager/actions/page/variants.js | 4 ++ .../main/resources/manager/css/manager.css | 4 ++ .../src/main/resources/manager/index.html | 1 + .../manager/js/modules/manager-ui.js | 17 ++++++ .../manager/js/modules/preview-context.d.ts | 1 + .../manager/js/modules/variant-support.d.ts | 23 ++++++++ .../manager/js/modules/variant-support.js | 37 ++++++++++++ .../ts/src/actions/page/create-variant.ts | 10 ++-- .../ts/src/actions/page/variant-selector.ts | 4 ++ .../src/main/ts/src/actions/page/variants.ts | 4 ++ .../src/main/ts/src/js/modules/manager-ui.js | 19 ++++++ .../main/ts/src/js/modules/preview-context.ts | 1 + .../main/ts/src/js/modules/variant-support.ts | 45 ++++++++++++++ .../RemoteCollectionEndpointsTest.java | 23 ++++++-- .../RemoteContentEndpointsExtensionTest.java | 58 ++++++++++++++++++- .../hosts/demo/collections/blog/item_1.md | 8 ++- 19 files changed, 257 insertions(+), 18 deletions(-) create mode 100644 modules/ui-module/src/main/resources/manager/js/modules/variant-support.d.ts create mode 100644 modules/ui-module/src/main/resources/manager/js/modules/variant-support.js create mode 100644 modules/ui-module/src/main/ts/src/js/modules/variant-support.ts diff --git a/modules/ui-module/src/main/java/com/condation/cms/modules/ui/extensionpoints/remotemethods/RemoteContentEndpointsExtension.java b/modules/ui-module/src/main/java/com/condation/cms/modules/ui/extensionpoints/remotemethods/RemoteContentEndpointsExtension.java index 16e1ccd39..aab7eac39 100644 --- a/modules/ui-module/src/main/java/com/condation/cms/modules/ui/extensionpoints/remotemethods/RemoteContentEndpointsExtension.java +++ b/modules/ui-module/src/main/java/com/condation/cms/modules/ui/extensionpoints/remotemethods/RemoteContentEndpointsExtension.java @@ -371,12 +371,14 @@ public Object getContentNode (Map parameters) { result.put("canonicalUri", item.path()); result.put("variantId", null); result.put("contentKind", "collection"); + result.put("supportsVariants", false); result.put("collection", item.collection()); result.put("collectionItemId", item.id()); result.put("sections", Map.of()); return result; } result.put("contentKind", "content"); + result.put("supportsVariants", true); var query = com.condation.cms.api.utils.HTTPUtil.queryParameters(requestUri.getQuery()); var variantId = query.getOrDefault( diff --git a/modules/ui-module/src/main/resources/manager/actions/page/create-variant.js b/modules/ui-module/src/main/resources/manager/actions/page/create-variant.js index f1c0ece63..6f5c98595 100644 --- a/modules/ui-module/src/main/resources/manager/actions/page/create-variant.js +++ b/modules/ui-module/src/main/resources/manager/actions/page/create-variant.js @@ -25,6 +25,7 @@ import { getContentNode } from '@cms/modules/rpc/rpc-content.js'; import { getPageTemplates } from '@cms/modules/rpc/rpc-manager.js'; import { createVariant, getVariants } from '@cms/modules/rpc/rpc-variant.js'; import { showToast } from '@cms/modules/toast.js'; +import { ensureVariantsSupported } from '@cms/modules/variant-support.js'; const value = (id) => document.getElementById(id)?.value.trim() ?? ''; const validate = () => { const copyContent = document.querySelector('input[name="cms-variant-content"]:checked')?.value === 'copy'; @@ -46,10 +47,11 @@ const validate = () => { }; export const runAction = async () => { try { - const [activeContentNode, templatesResponse] = await Promise.all([ - getContentNode({ url: getPreviewUrl() }), - getPageTemplates({}) - ]); + const activeContentNode = await getContentNode({ url: getPreviewUrl() }); + if (!ensureVariantsSupported(activeContentNode.result)) { + return; + } + const templatesResponse = await getPageTemplates({}); const variantContext = await getVariants({ uri: activeContentNode.result.uri }); const templates = Array.from(templatesResponse.result ?? []) .sort((left, right) => String(left.name).localeCompare(String(right.name))); diff --git a/modules/ui-module/src/main/resources/manager/actions/page/variant-selector.js b/modules/ui-module/src/main/resources/manager/actions/page/variant-selector.js index 8205c730d..63eed7ee0 100644 --- a/modules/ui-module/src/main/resources/manager/actions/page/variant-selector.js +++ b/modules/ui-module/src/main/resources/manager/actions/page/variant-selector.js @@ -24,6 +24,7 @@ import { getPreviewUrl } from '@cms/modules/preview.utils.js'; import { getContentNode } from '@cms/modules/rpc/rpc-content.js'; import { getVariantSelectors, setVariantSelector } from '@cms/modules/rpc/rpc-variant.js'; import { showToast } from '@cms/modules/toast.js'; +import { ensureVariantsSupported } from '@cms/modules/variant-support.js'; const SELECT_ID = 'cms-variant-selector'; const escapeHtml = (input) => { const element = document.createElement('div'); @@ -41,6 +42,9 @@ const showError = (error) => showToast({ export const runAction = async () => { try { const contentNode = await getContentNode({ url: getPreviewUrl() }); + if (!ensureVariantsSupported(contentNode.result)) { + return; + } const result = await getVariantSelectors(contentNode.result.uri); openModal({ title: i18n.t('manager.actions.page.variant-selector.title', 'Configure variant selection'), diff --git a/modules/ui-module/src/main/resources/manager/actions/page/variants.js b/modules/ui-module/src/main/resources/manager/actions/page/variants.js index e67bc84f6..c95ca7235 100644 --- a/modules/ui-module/src/main/resources/manager/actions/page/variants.js +++ b/modules/ui-module/src/main/resources/manager/actions/page/variants.js @@ -25,6 +25,7 @@ import { getPreviewUrl, loadPreview } from '@cms/modules/preview.utils.js'; import { getContentNode } from '@cms/modules/rpc/rpc-content.js'; import { deleteVariant, getVariants } from '@cms/modules/rpc/rpc-variant.js'; import { showToast } from '@cms/modules/toast.js'; +import { ensureVariantsSupported } from '@cms/modules/variant-support.js'; const VARIANT_LIST_ID = 'cms-page-variants'; const variantTitle = (variant) => { const title = variant.meta?.title; @@ -137,6 +138,9 @@ export const runAction = async () => { const contentNode = await getContentNode({ url: getPreviewUrl() }); + if (!ensureVariantsSupported(contentNode.result)) { + return; + } const result = await getVariants({ uri: contentNode.result.uri }); diff --git a/modules/ui-module/src/main/resources/manager/css/manager.css b/modules/ui-module/src/main/resources/manager/css/manager.css index a8e8b0e1d..32928ce66 100644 --- a/modules/ui-module/src/main/resources/manager/css/manager.css +++ b/modules/ui-module/src/main/resources/manager/css/manager.css @@ -126,6 +126,10 @@ i[data-cms-section-handle] { white-space: nowrap; } +.cms-current-variant[hidden] { + display: none; +} + .cms-current-variant:disabled { opacity: 0.65; } diff --git a/modules/ui-module/src/main/resources/manager/index.html b/modules/ui-module/src/main/resources/manager/index.html index 6a1475af2..7ed77cbf1 100644 --- a/modules/ui-module/src/main/resources/manager/index.html +++ b/modules/ui-module/src/main/resources/manager/index.html @@ -214,6 +214,7 @@ class="badge rounded-pill text-bg-secondary cms-current-variant" type="button" title="Current page variant" + hidden disabled> Loading… diff --git a/modules/ui-module/src/main/resources/manager/js/modules/manager-ui.js b/modules/ui-module/src/main/resources/manager/js/modules/manager-ui.js index 66066392b..c17852563 100644 --- a/modules/ui-module/src/main/resources/manager/js/modules/manager-ui.js +++ b/modules/ui-module/src/main/resources/manager/js/modules/manager-ui.js @@ -28,6 +28,23 @@ const updateVariantBadge = (content) => { if (!badge || !label) { return; } + const hasContent = Boolean(content?.uri); + const supportsVariants = hasContent + && content?.supportsVariants !== false + && content?.contentKind !== 'collection'; + badge.hidden = !hasContent; + if (!hasContent) { + badge.disabled = true; + return; + } + if (!supportsVariants) { + label.textContent = 'Unsupported'; + badge.disabled = false; + badge.classList.remove('text-bg-warning'); + badge.classList.add('text-bg-secondary'); + badge.setAttribute('title', 'Collections do not support variants'); + return; + } const variantId = content?.variantId; label.textContent = content ? (variantId || 'Original') : 'Loading…'; badge.disabled = !content?.uri; diff --git a/modules/ui-module/src/main/resources/manager/js/modules/preview-context.d.ts b/modules/ui-module/src/main/resources/manager/js/modules/preview-context.d.ts index 3422613f0..e9d912a35 100644 --- a/modules/ui-module/src/main/resources/manager/js/modules/preview-context.d.ts +++ b/modules/ui-module/src/main/resources/manager/js/modules/preview-context.d.ts @@ -24,6 +24,7 @@ export interface ActivePreviewContent { canonicalUri?: string; variantId?: string | null; contentKind?: 'content' | 'collection'; + supportsVariants?: boolean; collection?: string; collectionItemId?: string; } diff --git a/modules/ui-module/src/main/resources/manager/js/modules/variant-support.d.ts b/modules/ui-module/src/main/resources/manager/js/modules/variant-support.d.ts new file mode 100644 index 000000000..4837ac7da --- /dev/null +++ b/modules/ui-module/src/main/resources/manager/js/modules/variant-support.d.ts @@ -0,0 +1,23 @@ +/*- + * #%L + * UI Module + * %% + * Copyright (C) 2023 - 2026 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * #L% + */ +import { ActivePreviewContent } from '@cms/modules/preview-context.js'; +export declare const variantsSupported: (content?: ActivePreviewContent | null) => boolean; +export declare const ensureVariantsSupported: (content?: ActivePreviewContent | null) => boolean; diff --git a/modules/ui-module/src/main/resources/manager/js/modules/variant-support.js b/modules/ui-module/src/main/resources/manager/js/modules/variant-support.js new file mode 100644 index 000000000..4db0ce03b --- /dev/null +++ b/modules/ui-module/src/main/resources/manager/js/modules/variant-support.js @@ -0,0 +1,37 @@ +/*- + * #%L + * UI Module + * %% + * Copyright (C) 2023 - 2026 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * #L% + */ +import { i18n } from '@cms/modules/localization.js'; +import { showToast } from '@cms/modules/toast.js'; +export const variantsSupported = (content) => Boolean(content?.uri) + && content?.supportsVariants !== false + && content?.contentKind !== 'collection'; +export const ensureVariantsSupported = (content) => { + if (variantsSupported(content)) { + return true; + } + showToast({ + title: i18n.t('manager.actions.page.variants.unsupported.title', 'Variants not supported'), + message: i18n.t('manager.actions.page.variants.unsupported.message', 'Collections do not support variants.'), + type: 'info', + timeout: 3000 + }); + return false; +}; diff --git a/modules/ui-module/src/main/ts/src/actions/page/create-variant.ts b/modules/ui-module/src/main/ts/src/actions/page/create-variant.ts index eb26bb54e..0b4f2a95c 100644 --- a/modules/ui-module/src/main/ts/src/actions/page/create-variant.ts +++ b/modules/ui-module/src/main/ts/src/actions/page/create-variant.ts @@ -26,6 +26,7 @@ import { getContentNode } from '@cms/modules/rpc/rpc-content.js' import { getPageTemplates } from '@cms/modules/rpc/rpc-manager.js' import { createVariant, getVariants } from '@cms/modules/rpc/rpc-variant.js' import { showToast } from '@cms/modules/toast.js' +import { ensureVariantsSupported } from '@cms/modules/variant-support.js' const value = (id: string): string => (document.getElementById(id) as HTMLInputElement | HTMLSelectElement)?.value.trim() ?? ''; @@ -58,10 +59,11 @@ const validate = (): boolean => { export const runAction = async () => { try { - const [activeContentNode, templatesResponse] = await Promise.all([ - getContentNode({ url: getPreviewUrl() }), - getPageTemplates({}) - ]); + const activeContentNode = await getContentNode({ url: getPreviewUrl() }); + if (!ensureVariantsSupported(activeContentNode.result)) { + return; + } + const templatesResponse = await getPageTemplates({}); const variantContext = await getVariants({ uri: activeContentNode.result.uri }); const templates = Array.from(templatesResponse.result ?? []) .sort((left: any, right: any) => String(left.name).localeCompare(String(right.name))); diff --git a/modules/ui-module/src/main/ts/src/actions/page/variant-selector.ts b/modules/ui-module/src/main/ts/src/actions/page/variant-selector.ts index b51570359..e0cc02e4f 100644 --- a/modules/ui-module/src/main/ts/src/actions/page/variant-selector.ts +++ b/modules/ui-module/src/main/ts/src/actions/page/variant-selector.ts @@ -29,6 +29,7 @@ import { VariantSelectorDto } from '@cms/modules/rpc/rpc-variant.js' import { showToast } from '@cms/modules/toast.js' +import { ensureVariantsSupported } from '@cms/modules/variant-support.js' const SELECT_ID = 'cms-variant-selector'; @@ -55,6 +56,9 @@ const showError = (error: unknown) => showToast({ export const runAction = async () => { try { const contentNode = await getContentNode({ url: getPreviewUrl() }); + if (!ensureVariantsSupported(contentNode.result)) { + return; + } const result = await getVariantSelectors(contentNode.result.uri); openModal({ diff --git a/modules/ui-module/src/main/ts/src/actions/page/variants.ts b/modules/ui-module/src/main/ts/src/actions/page/variants.ts index f9b2ad2d7..e1370c9a8 100644 --- a/modules/ui-module/src/main/ts/src/actions/page/variants.ts +++ b/modules/ui-module/src/main/ts/src/actions/page/variants.ts @@ -31,6 +31,7 @@ import { VariantDto } from '@cms/modules/rpc/rpc-variant.js' import { showToast } from '@cms/modules/toast.js' +import { ensureVariantsSupported } from '@cms/modules/variant-support.js' const VARIANT_LIST_ID = 'cms-page-variants'; @@ -201,6 +202,9 @@ export const runAction = async () => { const contentNode = await getContentNode({ url: getPreviewUrl() }); + if (!ensureVariantsSupported(contentNode.result)) { + return; + } const result = await getVariants({ uri: contentNode.result.uri }); diff --git a/modules/ui-module/src/main/ts/src/js/modules/manager-ui.js b/modules/ui-module/src/main/ts/src/js/modules/manager-ui.js index 92cb6103e..8b96c4482 100644 --- a/modules/ui-module/src/main/ts/src/js/modules/manager-ui.js +++ b/modules/ui-module/src/main/ts/src/js/modules/manager-ui.js @@ -31,6 +31,25 @@ const updateVariantBadge = (content) => { return; } + const hasContent = Boolean(content?.uri); + const supportsVariants = hasContent + && content?.supportsVariants !== false + && content?.contentKind !== 'collection'; + badge.hidden = !hasContent; + if (!hasContent) { + badge.disabled = true; + return; + } + + if (!supportsVariants) { + label.textContent = 'Unsupported'; + badge.disabled = false; + badge.classList.remove('text-bg-warning'); + badge.classList.add('text-bg-secondary'); + badge.setAttribute('title', 'Collections do not support variants'); + return; + } + const variantId = content?.variantId; label.textContent = content ? (variantId || 'Original') : 'Loading…'; badge.disabled = !content?.uri; diff --git a/modules/ui-module/src/main/ts/src/js/modules/preview-context.ts b/modules/ui-module/src/main/ts/src/js/modules/preview-context.ts index 0d0a4297a..59160adda 100644 --- a/modules/ui-module/src/main/ts/src/js/modules/preview-context.ts +++ b/modules/ui-module/src/main/ts/src/js/modules/preview-context.ts @@ -25,6 +25,7 @@ export interface ActivePreviewContent { canonicalUri?: string; variantId?: string | null; contentKind?: 'content' | 'collection'; + supportsVariants?: boolean; collection?: string; collectionItemId?: string; } diff --git a/modules/ui-module/src/main/ts/src/js/modules/variant-support.ts b/modules/ui-module/src/main/ts/src/js/modules/variant-support.ts new file mode 100644 index 000000000..3c70961bb --- /dev/null +++ b/modules/ui-module/src/main/ts/src/js/modules/variant-support.ts @@ -0,0 +1,45 @@ +/*- + * #%L + * UI Module + * %% + * Copyright (C) 2023 - 2026 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * #L% + */ + +import { i18n } from '@cms/modules/localization.js'; +import { ActivePreviewContent } from '@cms/modules/preview-context.js'; +import { showToast } from '@cms/modules/toast.js'; + +export const variantsSupported = (content?: ActivePreviewContent | null): boolean => + Boolean(content?.uri) + && content?.supportsVariants !== false + && content?.contentKind !== 'collection'; + +export const ensureVariantsSupported = (content?: ActivePreviewContent | null): boolean => { + if (variantsSupported(content)) { + return true; + } + showToast({ + title: i18n.t('manager.actions.page.variants.unsupported.title', 'Variants not supported'), + message: i18n.t( + 'manager.actions.page.variants.unsupported.message', + 'Collections do not support variants.' + ), + type: 'info', + timeout: 3000 + }); + return false; +}; diff --git a/modules/ui-module/src/test/java/com/condation/cms/modules/ui/extensionpoints/remotemethods/RemoteCollectionEndpointsTest.java b/modules/ui-module/src/test/java/com/condation/cms/modules/ui/extensionpoints/remotemethods/RemoteCollectionEndpointsTest.java index a2a73258e..e5430f484 100644 --- a/modules/ui-module/src/test/java/com/condation/cms/modules/ui/extensionpoints/remotemethods/RemoteCollectionEndpointsTest.java +++ b/modules/ui-module/src/test/java/com/condation/cms/modules/ui/extensionpoints/remotemethods/RemoteCollectionEndpointsTest.java @@ -22,9 +22,11 @@ */ import com.condation.cms.api.Constants; +import com.condation.cms.api.SiteProperties; import com.condation.cms.api.configuration.Configuration; import com.condation.cms.api.configuration.configs.CollectionConfiguration; import com.condation.cms.api.configuration.configs.CollectionDefinition; +import com.condation.cms.api.configuration.configs.CollectionDetailConfiguration; import com.condation.cms.api.db.DB; import com.condation.cms.api.db.DBFileSystem; import com.condation.cms.api.db.ContentQuery; @@ -37,6 +39,7 @@ import com.condation.cms.api.feature.features.ConfigurationFeature; import com.condation.cms.api.feature.features.DBFeature; import com.condation.cms.api.feature.features.EventBusFeature; +import com.condation.cms.api.feature.features.SitePropertiesFeature; import com.condation.cms.api.feature.features.WorkflowFeature; import com.condation.cms.api.module.SiteModuleContext; import com.condation.cms.api.request.RequestContext; @@ -138,11 +141,14 @@ void createsCollectionItemWithWorkflowMetadataAndRefreshesIndex() throws Excepti assertThat(result).isInstanceOfSatisfying( RemoteCollectionEndpoints.ItemDto.class, - item -> assertThat(item) - .extracting( - RemoteCollectionEndpoints.ItemDto::id, - RemoteCollectionEndpoints.ItemDto::title) - .containsExactly("first-item", "First item")); + item -> { + assertThat(item) + .extracting( + RemoteCollectionEndpoints.ItemDto::id, + RemoteCollectionEndpoints.ItemDto::title) + .containsExactly("first-item", "First item"); + assertThat(item.detailUrl()).isEqualTo("/articles/ueber-das-cms"); + }); assertThat(collectionsDirectory.resolve("blog/first-item.md")) .content() .contains( @@ -236,11 +242,16 @@ void rejectsWritesToReferencedCollections() { private RequestContext requestContext() { var configuration = new Configuration(); var definitions = new ConcurrentHashMap(); - definitions.put("blog", new CollectionDefinition("blog", null)); + definitions.put("blog", new CollectionDefinition( + "blog", + new CollectionDetailConfiguration("/articles/{slug}", "article.html"))); configuration.add(CollectionConfiguration.class, new CollectionConfiguration(definitions)); + var siteProperties = org.mockito.Mockito.mock(SiteProperties.class); + when(siteProperties.contextPath()).thenReturn("/"); var requestContext = new RequestContext(); requestContext.add(AuthFeature.class, new AuthFeature("editor")); requestContext.add(ConfigurationFeature.class, new ConfigurationFeature(configuration)); + requestContext.add(SitePropertiesFeature.class, new SitePropertiesFeature(siteProperties)); return requestContext; } } diff --git a/modules/ui-module/src/test/java/com/condation/cms/modules/ui/extensionpoints/remotemethods/RemoteContentEndpointsExtensionTest.java b/modules/ui-module/src/test/java/com/condation/cms/modules/ui/extensionpoints/remotemethods/RemoteContentEndpointsExtensionTest.java index de63bf01d..95e7cb7eb 100644 --- a/modules/ui-module/src/test/java/com/condation/cms/modules/ui/extensionpoints/remotemethods/RemoteContentEndpointsExtensionTest.java +++ b/modules/ui-module/src/test/java/com/condation/cms/modules/ui/extensionpoints/remotemethods/RemoteContentEndpointsExtensionTest.java @@ -27,6 +27,14 @@ import com.condation.cms.api.db.cms.ReadOnlyFile; import com.condation.cms.api.db.collection.CollectionItem; import com.condation.cms.api.db.collection.Collections; +import com.condation.cms.api.configuration.Configuration; +import com.condation.cms.api.configuration.configs.CollectionConfiguration; +import com.condation.cms.api.configuration.configs.CollectionDefinition; +import com.condation.cms.api.configuration.configs.CollectionDetailConfiguration; +import com.condation.cms.api.db.ContentQuery; +import com.condation.cms.api.db.Page; +import com.condation.cms.api.db.collection.Collection; +import com.condation.cms.api.feature.features.ConfigurationFeature; import com.condation.cms.api.feature.features.DBFeature; import com.condation.cms.api.feature.features.CurrentCollectionItemFeature; import com.condation.cms.api.feature.features.CurrentNodeFeature; @@ -40,6 +48,7 @@ import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -180,7 +189,54 @@ void getContentNodeResolvesUriFromCustomPageUrl() throws Exception { assertThat(result) .containsEntry("uri", "pages/other.md") - .containsEntry("canonicalUri", "pages/other.md"); + .containsEntry("canonicalUri", "pages/other.md") + .containsEntry("contentKind", "content") + .containsEntry("supportsVariants", true); verify(content).byUrl("/total-other-page"); } + + @Test + void getContentNodeKeepsPublicCollectionRouteAndDisablesVariants() throws Exception { + var nonExistingPath = org.mockito.Mockito.mock(ReadOnlyFile.class); + var authorCollection = org.mockito.Mockito.mock(Collection.class); + @SuppressWarnings("unchecked") + var query = (ContentQuery) org.mockito.Mockito.mock(ContentQuery.class); + var item = new CollectionItem( + "author-1", "authors", "authors/author-1.md", "", Map.of("slug", "jane-doe")); + + when(db.getContent()).thenReturn(content); + when(content.byUrl("/people/jane-doe")).thenReturn(Optional.empty()); + when(contentBase.resolve("people/jane-doe")).thenReturn(nonExistingPath); + when(contentBase.resolve("people/jane-doe.md")).thenReturn(nonExistingPath); + when(collections.collection("authors")).thenReturn(authorCollection); + when(authorCollection.query()).thenReturn(query); + when(query.where("slug", "jane-doe")).thenReturn(query); + when(query.page(1, 2)).thenReturn(new Page<>(1, 2, 1, 1, List.of(item))); + + var configuration = new Configuration(); + configuration.add(CollectionConfiguration.class, new CollectionConfiguration( + new ConcurrentHashMap<>(Map.of( + "authors", + new CollectionDefinition( + "authors", + new CollectionDetailConfiguration("/people/{slug}", "author.html")))))); + when(moduleContext.get(ConfigurationFeature.class)) + .thenReturn(new ConfigurationFeature(configuration)); + + var requestContext = new RequestContext(); + requestContext.add(RequestFeature.class, new RequestFeature("/", "/people/jane-doe", Map.of(), null)); + + @SuppressWarnings("unchecked") + var result = (Map) ScopedValue.where( + RequestContextScope.REQUEST_CONTEXT, + requestContext + ).call(() -> endpoints.getContentNode(Map.of( + "url", "https://example.test/people/jane-doe?preview=manager"))); + + assertThat(result) + .containsEntry("url", "https://example.test/people/jane-doe?preview=manager") + .containsEntry("uri", "authors/author-1.md") + .containsEntry("contentKind", "collection") + .containsEntry("supportsVariants", false); + } } diff --git a/test-server/hosts/demo/collections/blog/item_1.md b/test-server/hosts/demo/collections/blog/item_1.md index d6c134067..38b593449 100644 --- a/test-server/hosts/demo/collections/blog/item_1.md +++ b/test-server/hosts/demo/collections/blog/item_1.md @@ -1,6 +1,8 @@ --- -title: Blog item 1 -status: published description: This is the first item +title: Blog item 1 publish_date: 2026-04-07T00:00:00Z ---- \ No newline at end of file +status: draft +--- + + From 508bff939fce6b8d7c5dde3f4d2aa9266f961e4c Mon Sep 17 00:00:00 2001 From: Thorsten Marx Date: Tue, 1 Sep 2026 15:33:02 +0200 Subject: [PATCH 22/28] use static import --- .../remotemethods/RemoteCollectionEndpointsTest.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/modules/ui-module/src/test/java/com/condation/cms/modules/ui/extensionpoints/remotemethods/RemoteCollectionEndpointsTest.java b/modules/ui-module/src/test/java/com/condation/cms/modules/ui/extensionpoints/remotemethods/RemoteCollectionEndpointsTest.java index e5430f484..27d5d98e6 100644 --- a/modules/ui-module/src/test/java/com/condation/cms/modules/ui/extensionpoints/remotemethods/RemoteCollectionEndpointsTest.java +++ b/modules/ui-module/src/test/java/com/condation/cms/modules/ui/extensionpoints/remotemethods/RemoteCollectionEndpointsTest.java @@ -66,6 +66,7 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import static org.mockito.Mockito.lenient; +import static org.mockito.Mockito.mock; @ExtendWith(MockitoExtension.class) class RemoteCollectionEndpointsTest { @@ -174,8 +175,8 @@ void rejectsDuplicateSlugsAfterNormalizationWhenSaving() throws Exception { "blog/first.md", "", Map.of("slug", "Über uns")); - var collectionsBase = org.mockito.Mockito.mock(ReadOnlyFile.class); - var sourceFile = org.mockito.Mockito.mock(ReadOnlyFile.class); + var collectionsBase = mock(ReadOnlyFile.class); + var sourceFile = mock(ReadOnlyFile.class); when(collection.item("second")).thenReturn(Optional.of(editedItem)); when(fileSystem.collectionsBase()).thenReturn(collectionsBase); when(collectionsBase.resolve("blog/second.md")).thenReturn(sourceFile); @@ -246,7 +247,7 @@ private RequestContext requestContext() { "blog", new CollectionDetailConfiguration("/articles/{slug}", "article.html"))); configuration.add(CollectionConfiguration.class, new CollectionConfiguration(definitions)); - var siteProperties = org.mockito.Mockito.mock(SiteProperties.class); + var siteProperties = mock(SiteProperties.class); when(siteProperties.contextPath()).thenReturn("/"); var requestContext = new RequestContext(); requestContext.add(AuthFeature.class, new AuthFeature("editor")); From 58f04e4044e9124e352cd3286df8343066bdba7b Mon Sep 17 00:00:00 2001 From: Thorsten Marx Date: Tue, 1 Sep 2026 15:33:52 +0200 Subject: [PATCH 23/28] use static import --- .../remotemethods/RemoteContentEndpointsExtensionTest.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/modules/ui-module/src/test/java/com/condation/cms/modules/ui/extensionpoints/remotemethods/RemoteContentEndpointsExtensionTest.java b/modules/ui-module/src/test/java/com/condation/cms/modules/ui/extensionpoints/remotemethods/RemoteContentEndpointsExtensionTest.java index 95e7cb7eb..750983d71 100644 --- a/modules/ui-module/src/test/java/com/condation/cms/modules/ui/extensionpoints/remotemethods/RemoteContentEndpointsExtensionTest.java +++ b/modules/ui-module/src/test/java/com/condation/cms/modules/ui/extensionpoints/remotemethods/RemoteContentEndpointsExtensionTest.java @@ -60,6 +60,7 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import static org.mockito.Mockito.lenient; +import static org.mockito.Mockito.mock; @ExtendWith(MockitoExtension.class) class RemoteContentEndpointsExtensionTest { @@ -197,10 +198,10 @@ void getContentNodeResolvesUriFromCustomPageUrl() throws Exception { @Test void getContentNodeKeepsPublicCollectionRouteAndDisablesVariants() throws Exception { - var nonExistingPath = org.mockito.Mockito.mock(ReadOnlyFile.class); - var authorCollection = org.mockito.Mockito.mock(Collection.class); + var nonExistingPath = mock(ReadOnlyFile.class); + var authorCollection = mock(Collection.class); @SuppressWarnings("unchecked") - var query = (ContentQuery) org.mockito.Mockito.mock(ContentQuery.class); + var query = (ContentQuery) mock(ContentQuery.class); var item = new CollectionItem( "author-1", "authors", "authors/author-1.md", "", Map.of("slug", "jane-doe")); From 1ca98793a341e6d80c65b21d53e004196b217039 Mon Sep 17 00:00:00 2001 From: Thorsten Marx Date: Wed, 2 Sep 2026 11:40:13 +0200 Subject: [PATCH 24/28] tiny refactoring --- .../com/condation/cms/hooksystem/executor/ActionExecutor.java | 4 +++- .../com/condation/cms/hooksystem/registry/ActionRegistry.java | 4 ++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/cms-hooksystem/src/main/java/com/condation/cms/hooksystem/executor/ActionExecutor.java b/cms-hooksystem/src/main/java/com/condation/cms/hooksystem/executor/ActionExecutor.java index 98f165d3b..3fb96e7ec 100644 --- a/cms-hooksystem/src/main/java/com/condation/cms/hooksystem/executor/ActionExecutor.java +++ b/cms-hooksystem/src/main/java/com/condation/cms/hooksystem/executor/ActionExecutor.java @@ -22,6 +22,7 @@ */ import com.condation.cms.api.hooks.ActionContext; +import com.condation.cms.hooksystem.ActionHook; import com.condation.cms.hooksystem.registry.ActionRegistry; import java.util.ArrayList; import java.util.HashMap; @@ -47,7 +48,8 @@ public List execute(String name, Map arguments) { registry.get(name).forEach(hook -> { try { - T result = (T) hook.function().apply(context); + var typedHook = (ActionHook) hook; + T result = (T) typedHook.function().apply(context); if (result != null) { context.results().add(result); } diff --git a/cms-hooksystem/src/main/java/com/condation/cms/hooksystem/registry/ActionRegistry.java b/cms-hooksystem/src/main/java/com/condation/cms/hooksystem/registry/ActionRegistry.java index 5ad3fcad8..074dde7d3 100644 --- a/cms-hooksystem/src/main/java/com/condation/cms/hooksystem/registry/ActionRegistry.java +++ b/cms-hooksystem/src/main/java/com/condation/cms/hooksystem/registry/ActionRegistry.java @@ -35,13 +35,13 @@ */ public class ActionRegistry { - private final Multimap hooks = ArrayListMultimap.create(); + private final Multimap> hooks = ArrayListMultimap.create(); public void register(String name, ActionFunction function, int priority) { hooks.put(name, new ActionHook<>(name, priority, function)); } - public List get(String name) { + public List> get(String name) { return hooks.get(name).stream() .sorted(Comparator.comparingInt(ActionHook::priority)) .toList(); From f87c49301186d98d9f64cb0ff1bf869642f06c27 Mon Sep 17 00:00:00 2001 From: Thorsten Marx Date: Wed, 2 Sep 2026 14:43:40 +0200 Subject: [PATCH 25/28] some collection optimizations --- .../com/condation/cms/api/db/CursorPage.java | 39 +++ .../cms/api/db/collection/Collection.java | 3 + .../collection/CollectionCursorSupport.java | 40 +++ .../db/collection/CollectionItemMetadata.java | 32 +++ .../cms/content/CollectionRouteResolver.java | 23 +- .../cms/content/CollectionResolverTest.java | 34 +-- .../cms/filesystem/FileCollections.java | 72 ++++- .../cms/filesystem/ReferencedCollections.java | 49 +++- .../persistent/CollectionMetaData.java | 95 ++++++- .../metadata/persistent/LuceneIndex.java | 256 +++++++++++++++++- .../metadata/persistent/LuceneQuery.java | 62 +++-- .../cms/filesystem/FileCollectionsTest.java | 107 ++++++++ .../RemoteCollectionEndpoints.java | 78 ++++-- .../actions/collection/manage-collection.js | 35 ++- .../js/modules/rpc/rpc-collection.d.ts | 11 + .../manager/js/modules/rpc/rpc-collection.js | 6 + .../actions/collection/manage-collection.ts | 40 ++- .../ts/src/js/modules/rpc/rpc-collection.ts | 21 ++ .../RemoteCollectionEndpointsTest.java | 19 +- .../RemoteContentEndpointsExtensionTest.java | 10 +- .../hosts/demo/collections/blog/item_1.md | 2 +- 21 files changed, 888 insertions(+), 146 deletions(-) create mode 100644 cms-api/src/main/java/com/condation/cms/api/db/CursorPage.java create mode 100644 cms-api/src/main/java/com/condation/cms/api/db/collection/CollectionCursorSupport.java create mode 100644 cms-api/src/main/java/com/condation/cms/api/db/collection/CollectionItemMetadata.java diff --git a/cms-api/src/main/java/com/condation/cms/api/db/CursorPage.java b/cms-api/src/main/java/com/condation/cms/api/db/CursorPage.java new file mode 100644 index 000000000..a11fd0acb --- /dev/null +++ b/cms-api/src/main/java/com/condation/cms/api/db/CursorPage.java @@ -0,0 +1,39 @@ +package com.condation.cms.api.db; + +/*- + * #%L + * CMS Api + * %% + * Copyright (C) 2023 - 2026 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * #L% + */ + +import java.util.List; + +/** + * One forward-only cursor page. The cursor is opaque and expires when the + * underlying index changes. + */ +public record CursorPage(List items, String nextCursor) { + + public CursorPage { + items = List.copyOf(items); + } + + public boolean hasNext() { + return nextCursor != null && !nextCursor.isBlank(); + } +} diff --git a/cms-api/src/main/java/com/condation/cms/api/db/collection/Collection.java b/cms-api/src/main/java/com/condation/cms/api/db/collection/Collection.java index 327a61d82..2f3337290 100644 --- a/cms-api/src/main/java/com/condation/cms/api/db/collection/Collection.java +++ b/cms-api/src/main/java/com/condation/cms/api/db/collection/Collection.java @@ -34,4 +34,7 @@ public interface Collection { Optional item(String id); ContentQuery query(); + + /** Queries collection metadata without opening the Markdown body files. */ + ContentQuery metadataQuery(); } diff --git a/cms-api/src/main/java/com/condation/cms/api/db/collection/CollectionCursorSupport.java b/cms-api/src/main/java/com/condation/cms/api/db/collection/CollectionCursorSupport.java new file mode 100644 index 000000000..454add884 --- /dev/null +++ b/cms-api/src/main/java/com/condation/cms/api/db/collection/CollectionCursorSupport.java @@ -0,0 +1,40 @@ +package com.condation.cms.api.db.collection; + +/*- + * #%L + * CMS Api + * %% + * Copyright (C) 2023 - 2026 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * #L% + */ + +import com.condation.cms.api.db.ContentQuery; +import com.condation.cms.api.db.CursorPage; +import java.util.function.Consumer; + +/** + * Internal capability for infrastructure that needs cursor-based collection + * traversal. It is deliberately separate from {@link Collection} and + * {@link ContentQuery}, which are exposed to templates. + */ +public interface CollectionCursorSupport { + + CursorPage metadataCursorPage( + String collection, + String cursor, + long size, + Consumer> queryConfigurer); +} diff --git a/cms-api/src/main/java/com/condation/cms/api/db/collection/CollectionItemMetadata.java b/cms-api/src/main/java/com/condation/cms/api/db/collection/CollectionItemMetadata.java new file mode 100644 index 000000000..9ee625926 --- /dev/null +++ b/cms-api/src/main/java/com/condation/cms/api/db/collection/CollectionItemMetadata.java @@ -0,0 +1,32 @@ +package com.condation.cms.api.db.collection; + +/*- + * #%L + * CMS Api + * %% + * Copyright (C) 2023 - 2026 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * #L% + */ + +import java.util.Map; + +/** A collection entry without loading its Markdown body. */ +public record CollectionItemMetadata( + String id, + String collection, + String path, + Map meta) { +} diff --git a/cms-content/src/main/java/com/condation/cms/content/CollectionRouteResolver.java b/cms-content/src/main/java/com/condation/cms/content/CollectionRouteResolver.java index c60712f24..f1f6dc000 100644 --- a/cms-content/src/main/java/com/condation/cms/content/CollectionRouteResolver.java +++ b/cms-content/src/main/java/com/condation/cms/content/CollectionRouteResolver.java @@ -26,10 +26,8 @@ import com.condation.cms.api.configuration.configs.CollectionDetailConfiguration; import com.condation.cms.api.db.DB; import com.condation.cms.api.db.collection.CollectionItem; -import com.condation.cms.api.utils.MapUtil; import com.condation.cms.content.utils.SlugUtil; import java.util.Comparator; -import java.util.List; import java.util.Optional; import java.util.regex.Pattern; import lombok.RequiredArgsConstructor; @@ -86,29 +84,14 @@ private static Optional findByRouteValue( return Optional.empty(); } - var exactMatches = collection.query() + var exactMatches = collection.metadataQuery() .where(parameter, slug) .page(1, 2) .getItems(); if (exactMatches.size() == 1) { - return Optional.of(exactMatches.getFirst()); + return collection.item(exactMatches.getFirst().id()); } - if (exactMatches.size() > 1) { - return Optional.empty(); - } - - // Compatibility for collection files whose route value has not yet been - // normalized by the manager (for example "Über uns"). - List slugMatches = collection.query().get().stream() - .filter(item -> { - var value = MapUtil.getValue(item.meta(), parameter); - return value != null && slug.equals(SlugUtil.slugify(value.toString())); - }) - .limit(2) - .toList(); - return slugMatches.size() == 1 - ? Optional.of(slugMatches.getFirst()) - : Optional.empty(); + return Optional.empty(); } private static Optional findById( diff --git a/cms-content/src/test/java/com/condation/cms/content/CollectionResolverTest.java b/cms-content/src/test/java/com/condation/cms/content/CollectionResolverTest.java index 2b74c38d7..94452b451 100644 --- a/cms-content/src/test/java/com/condation/cms/content/CollectionResolverTest.java +++ b/cms-content/src/test/java/com/condation/cms/content/CollectionResolverTest.java @@ -40,6 +40,7 @@ import com.condation.cms.api.db.cms.ReadOnlyFile; import com.condation.cms.api.db.collection.Collection; import com.condation.cms.api.db.collection.CollectionItem; +import com.condation.cms.api.db.collection.CollectionItemMetadata; import com.condation.cms.api.feature.features.CurrentCollectionItemFeature; import com.condation.cms.api.feature.features.CurrentNodeFeature; import com.condation.cms.api.feature.features.RequestFeature; @@ -132,10 +133,12 @@ void resolvesAnIdRouteAndUsesReloadedDefinitions() throws Exception { void resolvesAConfiguredFrontMatterField() throws Exception { define(definition("/blog/{slug}")); @SuppressWarnings("unchecked") - var query = (ContentQuery) mock(ContentQuery.class); - when(collection.query()).thenReturn(query); + var query = (ContentQuery) mock(ContentQuery.class); + when(collection.metadataQuery()).thenReturn(query); when(query.where("slug", "first-post")).thenReturn(query); - when(query.page(1, 2)).thenReturn(new Page<>(1, 2, 1, 1, List.of(item))); + when(query.page(1, 2)).thenReturn(new Page<>(1, 2, 1, 1, List.of( + new CollectionItemMetadata("first", "blog", "blog/first.md", item.meta())))); + when(collection.item("first")).thenReturn(Optional.of(item)); var resolver = new CollectionResolver(renderer, db, configuration); var response = resolver.getContent(context("/blog/first-post/")); @@ -145,34 +148,19 @@ void resolvesAConfiguredFrontMatterField() throws Exception { } @Test - void resolvesLegacyFrontMatterByItsSlugifiedValue() throws Exception { + void doesNotScanTheCollectionForLegacyUnnormalizedRouteValues() throws Exception { define(definition("/blog/{slug}")); - var legacyItem = new CollectionItem( - "about", - "blog", - "blog/about.md", - "# About", - Map.of("title", "About", "slug", "Über uns")); @SuppressWarnings("unchecked") - var exactQuery = (ContentQuery) mock(ContentQuery.class); - @SuppressWarnings("unchecked") - var fallbackQuery = (ContentQuery) mock(ContentQuery.class); - when(collection.query()).thenReturn(exactQuery, fallbackQuery); + var exactQuery = (ContentQuery) mock(ContentQuery.class); + when(collection.metadataQuery()).thenReturn(exactQuery); when(exactQuery.where("slug", "ueber-uns")).thenReturn(exactQuery); when(exactQuery.page(1, 2)).thenReturn(new Page<>(0, 2, 0, 1, List.of())); - when(fallbackQuery.get()).thenReturn(List.of(legacyItem)); - when(collectionsBase.resolve("blog/about.md")).thenReturn(itemFile); - when(renderer.renderCollection( - eq(itemFile), - any(), - eq(legacyItem), - anyString(), - any())).thenReturn("

About

"); var response = new CollectionResolver(renderer, db, configuration) .getContent(context("/blog/ueber-uns")); - Assertions.assertThat(response).isPresent(); + Assertions.assertThat(response).isEmpty(); + verify(collection).metadataQuery(); } @Test diff --git a/cms-filesystem/src/main/java/com/condation/cms/filesystem/FileCollections.java b/cms-filesystem/src/main/java/com/condation/cms/filesystem/FileCollections.java index 1a5e4e3b2..b158e7353 100644 --- a/cms-filesystem/src/main/java/com/condation/cms/filesystem/FileCollections.java +++ b/cms-filesystem/src/main/java/com/condation/cms/filesystem/FileCollections.java @@ -24,9 +24,12 @@ import com.condation.cms.api.Constants; import com.condation.cms.api.db.ContentNode; import com.condation.cms.api.db.ContentQuery; +import com.condation.cms.api.db.CursorPage; import com.condation.cms.api.db.NodeVisibility; +import com.condation.cms.api.db.collection.CollectionCursorSupport; import com.condation.cms.api.db.collection.CollectionItem; import com.condation.cms.api.db.collection.CollectionItemId; +import com.condation.cms.api.db.collection.CollectionItemMetadata; import com.condation.cms.api.db.collection.Collections; import com.condation.cms.api.utils.PathUtil; import com.condation.cms.core.content.io.ContentFileParser; @@ -35,6 +38,7 @@ import java.io.UncheckedIOException; import java.nio.file.Files; import java.nio.file.Path; +import java.nio.file.attribute.BasicFileAttributes; import java.time.Duration; import java.time.LocalDate; import java.time.ZoneId; @@ -44,6 +48,7 @@ import java.util.Optional; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Consumer; import java.util.function.Function; import java.util.regex.Pattern; import lombok.extern.slf4j.Slf4j; @@ -52,7 +57,7 @@ * Site-scoped, file-backed collections implementation. */ @Slf4j -public class FileCollections implements Collections, AutoCloseable { +public class FileCollections implements Collections, CollectionCursorSupport, AutoCloseable { private static final Pattern COLLECTION_NAME = Pattern.compile("[a-zA-Z0-9][a-zA-Z0-9_-]*"); private static final Duration CHANGE_QUIET_PERIOD = Duration.ofMillis(200); @@ -116,6 +121,21 @@ public Set names() { return Set.copyOf(collectionNames); } + @Override + public CursorPage metadataCursorPage( + String collection, + String cursor, + long size, + Consumer> queryConfigurer) { + validateCollectionName(collection); + return metaData.cursorPage( + collection, + this::mapMetadata, + cursor, + size, + queryConfigurer); + } + @Override public void refresh(String collection, String id) { validateCollectionName(collection); @@ -187,13 +207,16 @@ private void processPath(Path path) throws IOException { } private void rebuild() throws IOException { - metaData.clear(); + var staleCollections = metaData.collectionNames(); collectionNames.clear(); metaData.startBatch(); try (var collections = Files.list(collectionsBase)) { - for (var collection : collections.filter(Files::isDirectory).toList()) { + for (var iterator = collections.filter(Files::isDirectory).iterator(); iterator.hasNext();) { + var collection = iterator.next(); + staleCollections.remove(collection.getFileName().toString()); scanCollection(collection); } + staleCollections.forEach(metaData::removeDirectory); } finally { metaData.stopBatch(); } @@ -206,21 +229,40 @@ private void scanCollection(Path collection) throws IOException { return; } collectionNames.add(name); - metaData.removeDirectory(name); + var stalePaths = metaData.paths(name); try (var files = Files.list(collection)) { - for (var file : files.filter(Files::isRegularFile).filter(FileCollections::isValidItemFile).toList()) { - index(file); + for (var iterator = files.filter(Files::isRegularFile) + .filter(FileCollections::isValidItemFile).iterator(); iterator.hasNext();) { + var file = iterator.next(); + var path = PathUtil.toRelativeEntry(file, collectionsBase); + stalePaths.remove(path); + var stamp = fileStamp(file); + if (metaData.byPath(path).isEmpty() + || !metaData.fileStamp(path).filter(stamp::equals).isPresent()) { + index(file, path, stamp); + } } } + stalePaths.forEach(metaData::removeFile); } private void index(Path file) throws IOException { var path = PathUtil.toRelativeEntry(file, collectionsBase); + index(file, path, fileStamp(file)); + } + + private void index(Path file, String path, String stamp) throws IOException { collectionNames.add(path.substring(0, path.indexOf('/'))); + var attributes = Files.readAttributes(file, BasicFileAttributes.class); var modified = LocalDate.ofInstant( - Files.getLastModifiedTime(file).toInstant(), + attributes.lastModifiedTime().toInstant(), ZoneId.systemDefault()); - metaData.addFile(path, metaParser.apply(file), modified); + metaData.addFile(path, metaParser.apply(file), modified, stamp); + } + + private static String fileStamp(Path file) throws IOException { + var attributes = Files.readAttributes(file, BasicFileAttributes.class); + return attributes.lastModifiedTime().toMillis() + ":" + attributes.size(); } private CollectionItem map(ContentNode node, int ignoredExcerptLength) { @@ -237,6 +279,15 @@ private CollectionItem map(ContentNode node, int ignoredExcerptLength) { node.data()); } + private CollectionItemMetadata mapMetadata(ContentNode node, int ignoredExcerptLength) { + var path = node.path(); + var separator = path.indexOf('/'); + var collection = path.substring(0, separator); + var filename = path.substring(separator + 1); + var id = filename.substring(0, filename.length() - 3); + return new CollectionItemMetadata(id, collection, path, node.data()); + } + private static String readMarkdownBody(Path file) { try { return new ContentFileParser(file.toString()).getContent(); @@ -305,5 +356,10 @@ public Optional item(String id) { public ContentQuery query() { return metaData.query(name, FileCollections.this::map); } + + @Override + public ContentQuery metadataQuery() { + return metaData.query(name, FileCollections.this::mapMetadata); + } } } diff --git a/cms-filesystem/src/main/java/com/condation/cms/filesystem/ReferencedCollections.java b/cms-filesystem/src/main/java/com/condation/cms/filesystem/ReferencedCollections.java index a3ab2127f..fc9aed81d 100644 --- a/cms-filesystem/src/main/java/com/condation/cms/filesystem/ReferencedCollections.java +++ b/cms-filesystem/src/main/java/com/condation/cms/filesystem/ReferencedCollections.java @@ -22,19 +22,24 @@ */ import com.condation.cms.api.configuration.configs.CollectionConfiguration; +import com.condation.cms.api.db.ContentQuery; +import com.condation.cms.api.db.CursorPage; import com.condation.cms.api.db.collection.Collection; +import com.condation.cms.api.db.collection.CollectionCursorSupport; +import com.condation.cms.api.db.collection.CollectionItemMetadata; import com.condation.cms.api.db.collection.Collections; import com.condation.cms.core.serivce.ServiceRegistry; import com.condation.cms.core.serivce.impl.SiteDBService; import java.util.HashSet; import java.util.Set; +import java.util.function.Consumer; /** * Adds lazy, read-only collection references to the collections stored by one * site. Source sites are looked up for every access so configuration and site * reloads do not leave cached cross-site references behind. */ -final class ReferencedCollections implements Collections { +final class ReferencedCollections implements Collections, CollectionCursorSupport { private final String siteId; private final Collections localCollections; @@ -51,19 +56,20 @@ final class ReferencedCollections implements Collections { @Override public Collection collection(String name) { - var sourceSite = sourceSite(name); - if (sourceSite == null) { - return localCollections.collection(name); - } - var source = ServiceRegistry.getInstance().get(sourceSite, SiteDBService.class) - .orElseThrow(() -> new IllegalStateException( - "collection source site is not available: " + sourceSite)); - var sourceCollections = source.db().getCollections(); - if (!sourceCollections.isLocal(name)) { - throw new IllegalStateException( - "referenced collections must point to a local collection: " + sourceSite + "/" + name); + return targetCollections(name).collection(name); + } + + @Override + public CursorPage metadataCursorPage( + String collection, + String cursor, + long size, + Consumer> queryConfigurer) { + var target = targetCollections(collection); + if (!(target instanceof CollectionCursorSupport cursorSupport)) { + throw new UnsupportedOperationException("collection storage does not support cursor paging"); } - return sourceCollections.collection(name); + return cursorSupport.metadataCursorPage(collection, cursor, size, queryConfigurer); } @Override @@ -98,4 +104,21 @@ private String sourceSite(String collection) { .filter(source -> !siteId.equals(source)) .orElse(null); } + + private Collections targetCollections(String collection) { + var sourceSite = sourceSite(collection); + if (sourceSite == null) { + return localCollections; + } + var source = ServiceRegistry.getInstance().get(sourceSite, SiteDBService.class) + .orElseThrow(() -> new IllegalStateException( + "collection source site is not available: " + sourceSite)); + var sourceCollections = source.db().getCollections(); + if (!sourceCollections.isLocal(collection)) { + throw new IllegalStateException( + "referenced collections must point to a local collection: " + + sourceSite + "/" + collection); + } + return sourceCollections; + } } diff --git a/cms-filesystem/src/main/java/com/condation/cms/filesystem/metadata/persistent/CollectionMetaData.java b/cms-filesystem/src/main/java/com/condation/cms/filesystem/metadata/persistent/CollectionMetaData.java index b0f80fb12..8fd3b882f 100644 --- a/cms-filesystem/src/main/java/com/condation/cms/filesystem/metadata/persistent/CollectionMetaData.java +++ b/cms-filesystem/src/main/java/com/condation/cms/filesystem/metadata/persistent/CollectionMetaData.java @@ -23,6 +23,7 @@ import com.condation.cms.api.db.ContentNode; import com.condation.cms.api.db.ContentQuery; +import com.condation.cms.api.db.CursorPage; import com.condation.cms.filesystem.MetaData; import com.condation.cms.filesystem.metadata.query.ExcerptMapperFunction; import java.io.IOException; @@ -30,11 +31,14 @@ import java.nio.file.Path; import java.time.LocalDate; import java.util.ArrayList; +import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.function.BiFunction; +import java.util.function.Consumer; import lombok.extern.slf4j.Slf4j; import org.apache.lucene.document.Document; import org.apache.lucene.document.Field; @@ -51,6 +55,7 @@ @Slf4j public class CollectionMetaData implements MetaData { + private static final String INDEX_SCHEMA_VERSION = "1"; public static final String FIELD_COLLECTION = "_collection"; public static final String FIELD_ID = "_id"; @@ -58,6 +63,9 @@ public class CollectionMetaData implements MetaData { private LuceneIndex index; private MVStore store; private MVMap nodes; + private MVMap fileStamps; + private MVMap settings; + private boolean batchMode; public CollectionMetaData(Path hostPath) { this.hostPath = hostPath; @@ -69,11 +77,21 @@ public void open() throws IOException { Files.createDirectories(dataPath.resolve("store")); Files.createDirectories(dataPath.resolve("index")); - index = new LuceneIndex(); - index.open(dataPath.resolve("index")); store = MVStore.open(dataPath.resolve("store/data.db").toString()); nodes = store.openMap("nodes"); - nodes.clear(); + fileStamps = store.openMap("file-stamps"); + settings = store.openMap("settings"); + var indexPath = dataPath.resolve("index"); + var recreate = !INDEX_SCHEMA_VERSION.equals(settings.get("index-schema")) + || !LuceneIndex.exists(indexPath); + index = new LuceneIndex(); + index.open(indexPath, recreate); + if (recreate) { + nodes.clear(); + fileStamps.clear(); + settings.put("index-schema", INDEX_SCHEMA_VERSION); + store.commit(); + } } @Override @@ -91,6 +109,7 @@ public void close() throws IOException { } public void startBatch() { + batchMode = true; index.setBatchMode(true); } @@ -98,13 +117,24 @@ public void stopBatch() { try { index.setBatchMode(false); index.commit(); + store.commit(); } catch (IOException ex) { log.error("error committing collection index", ex); + } finally { + batchMode = false; } } @Override public synchronized void addFile(String path, Map data, LocalDate lastModified) { + addFile(path, data, lastModified, null); + } + + public synchronized void addFile( + String path, + Map data, + LocalDate lastModified, + String fileStamp) { var normalizedPath = normalize(path); var separator = normalizedPath.indexOf('/'); if (separator <= 0 || separator == normalizedPath.length() - 1) { @@ -133,6 +163,10 @@ public synchronized void addFile(String path, Map data, LocalDat DocumentHelper.addAvailableFields(document); try { index.update(new Term("_uri", normalizedPath), document); + if (fileStamp != null) { + fileStamps.put(normalizedPath, fileStamp); + } + commitStoreIfNecessary(); } catch (IOException ex) { log.error("error indexing collection item {}", normalizedPath, ex); } @@ -141,9 +175,11 @@ public synchronized void addFile(String path, Map data, LocalDat @Override public synchronized void removeFile(String path) { var normalizedPath = normalize(path); - nodes.remove(normalizedPath); try { index.delete(new TermQuery(new Term("_uri", normalizedPath))); + nodes.remove(normalizedPath); + fileStamps.remove(normalizedPath); + commitStoreIfNecessary(); } catch (IOException ex) { log.error("error deleting collection item {}", normalizedPath, ex); } @@ -165,6 +201,8 @@ public synchronized void removeDirectory(String path) { affectedPaths.forEach(nodes::remove); try { index.delete(new TermQuery(new Term(FIELD_COLLECTION, collection))); + affectedPaths.forEach(fileStamps::remove); + commitStoreIfNecessary(); } catch (IOException ex) { log.error("error deleting collection {}", collection, ex); } @@ -221,6 +259,7 @@ public List listSectionEntries(String pagePath) { @Override public synchronized void clear() { nodes.clear(); + fileStamps.clear(); try { index.delete(MatchAllDocsQuery.INSTANCE); } catch (IOException ex) { @@ -228,6 +267,41 @@ public synchronized void clear() { } } + public Optional fileStamp(String path) { + return Optional.ofNullable(fileStamps.get(normalize(path))); + } + + public Set paths(String collection) { + var result = new HashSet(); + var prefix = normalize(collection) + "/"; + var cursor = nodes.cursor(prefix); + while (cursor.hasNext()) { + var path = cursor.next(); + if (!path.startsWith(prefix)) { + break; + } + result.add(path); + } + return result; + } + + public Set collectionNames() { + var result = new HashSet(); + for (var path : nodes.keySet()) { + var separator = path.indexOf('/'); + if (separator > 0) { + result.add(path.substring(0, separator)); + } + } + return result; + } + + private void commitStoreIfNecessary() { + if (!batchMode) { + store.commit(); + } + } + @Override public Map getNodes() { return new ConcurrentHashMap<>(nodes); @@ -248,7 +322,18 @@ public ContentQuery query(String collection, BiFunction ContentQuery collectionQuery( + public CursorPage cursorPage( + String collection, + BiFunction nodeMapper, + String cursor, + long size, + Consumer> queryConfigurer) { + var query = collectionQuery(collection, nodeMapper); + queryConfigurer.accept(query); + return query.cursorPage(cursor, size); + } + + private LuceneQuery collectionQuery( String collection, BiFunction nodeMapper) { var scope = collection == null diff --git a/cms-filesystem/src/main/java/com/condation/cms/filesystem/metadata/persistent/LuceneIndex.java b/cms-filesystem/src/main/java/com/condation/cms/filesystem/metadata/persistent/LuceneIndex.java index 1a4fcca54..8f93a31c4 100644 --- a/cms-filesystem/src/main/java/com/condation/cms/filesystem/metadata/persistent/LuceneIndex.java +++ b/cms-filesystem/src/main/java/com/condation/cms/filesystem/metadata/persistent/LuceneIndex.java @@ -24,10 +24,17 @@ import com.condation.cms.api.utils.FileUtils; import com.condation.cms.filesystem.metadata.persistent.lucene.TitleAnalyzer; import com.condation.cms.filesystem.metadata.persistent.lucene.TitlePrefixAnalyzer; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.DataInputStream; +import java.io.DataOutputStream; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Base64; import java.util.EnumSet; +import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; @@ -37,12 +44,15 @@ import org.apache.lucene.analysis.miscellaneous.PerFieldAnalyzerWrapper; import org.apache.lucene.document.Document; import org.apache.lucene.index.DocValuesType; +import org.apache.lucene.index.DirectoryReader; import org.apache.lucene.index.FieldInfos; import org.apache.lucene.index.IndexWriter; import org.apache.lucene.index.IndexWriterConfig; import org.apache.lucene.index.Term; import org.apache.lucene.search.IndexSearcher; +import org.apache.lucene.search.FieldDoc; import org.apache.lucene.search.Query; +import org.apache.lucene.search.ScoreDoc; import org.apache.lucene.search.SearcherFactory; import org.apache.lucene.search.SearcherManager; import org.apache.lucene.search.Sort; @@ -54,6 +64,7 @@ import org.apache.lucene.store.Directory; import org.apache.lucene.store.FSDirectory; import org.apache.lucene.store.NRTCachingDirectory; +import org.apache.lucene.util.BytesRef; /** * @@ -62,11 +73,19 @@ @Slf4j public class LuceneIndex implements AutoCloseable { + private static final int CURSOR_VERSION = 1; + @FunctionalInterface interface UriVisitor { boolean visit(String uri) throws IOException; } + record CursorResult(List uris, String nextCursor) { + } + + record SeekPageResult(long totalItems, List uris) { + } + public static final Analyzer INDEX_ANALYZER = new TitlePrefixAnalyzer(); public static final Analyzer SEARCH_ANALYZER = new TitleAnalyzer(); @@ -121,15 +140,6 @@ void delete(Query query) throws IOException { } } - int count(Query query) throws IOException { - IndexSearcher searcher = nrt_manager.acquire(); - try { - return searcher.count(query); - } finally { - nrt_manager.release(searcher); - } - } - void scanUris(Query query, Sort sort, int batchSize, UriVisitor visitor) throws IOException { if (batchSize < 1) { throw new IllegalArgumentException("batchSize must be greater than zero"); @@ -161,6 +171,215 @@ void scanUris(Query query, Sort sort, int batchSize, UriVisitor visitor) throws } } + CursorResult cursorPage( + Query query, + Sort sort, + int pageSize, + String cursor, + String cursorKey) throws IOException { + if (pageSize < 1 || pageSize > 10_000) { + throw new IllegalArgumentException("cursor page size must be between 1 and 10000"); + } + + IndexSearcher searcher = nrt_manager.acquire(); + try { + long generation = ((DirectoryReader) searcher.getIndexReader()).getVersion(); + var queryKey = cursorKey; + var sortKey = sort == null ? "" : sort.toString(); + ScoreDoc after = decodeCursor(cursor, generation, queryKey, sortKey, sort); + var hits = sort == null + ? searcher.searchAfter(after, query, pageSize + 1) + : searcher.searchAfter(after, query, pageSize + 1, sort); + var storedFields = searcher.storedFields(); + var uris = new ArrayList(Math.min(pageSize, hits.scoreDocs.length)); + int returned = Math.min(pageSize, hits.scoreDocs.length); + for (int index = 0; index < returned; index++) { + uris.add(storedFields.document(hits.scoreDocs[index].doc, Set.of("_uri")).get("_uri")); + } + var nextCursor = hits.scoreDocs.length > pageSize + ? encodeCursor(hits.scoreDocs[returned - 1], generation, queryKey, sortKey) + : null; + return new CursorResult(List.copyOf(uris), nextCursor); + } finally { + nrt_manager.release(searcher); + } + } + + SeekPageResult seekPage( + Query query, + Sort sort, + long offset, + int pageSize) throws IOException { + if (offset < 0) { + throw new IllegalArgumentException("offset must not be negative"); + } + if (pageSize < 1) { + throw new IllegalArgumentException("pageSize must be greater than zero"); + } + + IndexSearcher searcher = nrt_manager.acquire(); + try { + long totalItems = searcher.count(query); + if (offset >= totalItems) { + return new SeekPageResult(totalItems, List.of()); + } + + ScoreDoc after = seekToOffset(searcher, query, sort, offset); + var hits = sort == null + ? searcher.searchAfter(after, query, pageSize) + : searcher.searchAfter(after, query, pageSize, sort); + var storedFields = searcher.storedFields(); + var uris = new ArrayList(hits.scoreDocs.length); + for (var hit : hits.scoreDocs) { + uris.add(storedFields.document(hit.doc, Set.of("_uri")).get("_uri")); + } + return new SeekPageResult(totalItems, List.copyOf(uris)); + } finally { + nrt_manager.release(searcher); + } + } + + private ScoreDoc seekToOffset( + IndexSearcher searcher, + Query query, + Sort sort, + long offset) throws IOException { + ScoreDoc after = null; + long remaining = offset; + while (remaining > 0) { + int stepSize = (int) Math.min(remaining, 1024); + var hits = sort == null + ? searcher.searchAfter(after, query, stepSize) + : searcher.searchAfter(after, query, stepSize, sort); + if (hits.scoreDocs.length == 0) { + return after; + } + after = hits.scoreDocs[hits.scoreDocs.length - 1]; + remaining -= hits.scoreDocs.length; + } + return after; + } + + private String encodeCursor(ScoreDoc scoreDoc, long generation, String queryKey, String sortKey) { + try { + var bytes = new ByteArrayOutputStream(); + try (var output = new DataOutputStream(bytes)) { + output.writeInt(CURSOR_VERSION); + output.writeLong(generation); + output.writeUTF(queryKey); + output.writeUTF(sortKey); + output.writeInt(scoreDoc.doc); + output.writeFloat(scoreDoc.score); + if (scoreDoc instanceof FieldDoc fieldDoc) { + output.writeInt(fieldDoc.fields.length); + for (var field : fieldDoc.fields) { + writeCursorValue(output, field); + } + } else { + output.writeInt(-1); + } + } + return Base64.getUrlEncoder().withoutPadding().encodeToString(bytes.toByteArray()); + } catch (IOException ex) { + throw new IllegalStateException("could not encode cursor", ex); + } + } + + private ScoreDoc decodeCursor( + String cursor, + long generation, + String queryKey, + String sortKey, + Sort sort) { + if (cursor == null || cursor.isBlank()) { + return null; + } + try { + var bytes = Base64.getUrlDecoder().decode(cursor); + try (var input = new DataInputStream(new ByteArrayInputStream(bytes))) { + if (input.readInt() != CURSOR_VERSION + || input.readLong() != generation + || !input.readUTF().equals(queryKey) + || !input.readUTF().equals(sortKey)) { + throw new IllegalArgumentException("cursor has expired or belongs to another query"); + } + int doc = input.readInt(); + float score = input.readFloat(); + int fieldCount = input.readInt(); + if (fieldCount < 0) { + if (sort != null) { + throw new IllegalArgumentException("cursor sort does not match query sort"); + } + return new ScoreDoc(doc, score); + } + if (sort == null || fieldCount != sort.getSort().length) { + throw new IllegalArgumentException("cursor sort does not match query sort"); + } + var fields = new Object[fieldCount]; + for (int index = 0; index < fieldCount; index++) { + fields[index] = readCursorValue(input); + } + return new FieldDoc(doc, score, fields); + } + } catch (IllegalArgumentException ex) { + throw ex; + } catch (IOException ex) { + throw new IllegalArgumentException("invalid cursor", ex); + } + } + + private void writeCursorValue(DataOutputStream output, Object value) throws IOException { + switch (value) { + case null -> output.writeByte(0); + case BytesRef bytes -> { + output.writeByte(1); + output.writeInt(bytes.length); + output.write(bytes.bytes, bytes.offset, bytes.length); + } + case Long longValue -> { + output.writeByte(2); + output.writeLong(longValue); + } + case Integer intValue -> { + output.writeByte(3); + output.writeInt(intValue); + } + case Double doubleValue -> { + output.writeByte(4); + output.writeDouble(doubleValue); + } + case Float floatValue -> { + output.writeByte(5); + output.writeFloat(floatValue); + } + case String stringValue -> { + output.writeByte(6); + output.writeUTF(stringValue); + } + default -> throw new IllegalArgumentException( + "unsupported cursor sort value: " + value.getClass().getName()); + } + } + + private Object readCursorValue(DataInputStream input) throws IOException { + return switch (input.readByte()) { + case 0 -> null; + case 1 -> { + int length = input.readInt(); + if (length < 0 || length > 1_000_000) { + throw new IllegalArgumentException("invalid cursor value length"); + } + yield new BytesRef(input.readNBytes(length)); + } + case 2 -> input.readLong(); + case 3 -> input.readInt(); + case 4 -> input.readDouble(); + case 5 -> input.readFloat(); + case 6 -> input.readUTF(); + default -> throw new IllegalArgumentException("invalid cursor value type"); + }; + } + Optional resolveSort(String field, boolean reverse) throws IOException { IndexSearcher searcher = nrt_manager.acquire(); try { @@ -231,7 +450,20 @@ private SortField numericSortField( } public void open(Path path) throws IOException { - if (Files.exists(path)) { + open(path, true); + } + + public static boolean exists(Path path) throws IOException { + if (!Files.isDirectory(path)) { + return false; + } + try (var directory = FSDirectory.open(path)) { + return DirectoryReader.indexExists(directory); + } + } + + public void open(Path path, boolean recreate) throws IOException { + if (recreate && Files.exists(path)) { FileUtils.deleteFolder(path); } Files.createDirectories(path); @@ -247,7 +479,9 @@ public void open(Path path) throws IOException { ); IndexWriterConfig indexWriterConfig = new IndexWriterConfig(analyzer); - indexWriterConfig.setOpenMode(IndexWriterConfig.OpenMode.CREATE); + indexWriterConfig.setOpenMode(recreate + ? IndexWriterConfig.OpenMode.CREATE + : IndexWriterConfig.OpenMode.CREATE_OR_APPEND); indexWriterConfig.setCommitOnClose(true); nrt_index = new NRTCachingDirectory(directory, 5.0, 60.0); writer = new IndexWriter(nrt_index, indexWriterConfig); diff --git a/cms-filesystem/src/main/java/com/condation/cms/filesystem/metadata/persistent/LuceneQuery.java b/cms-filesystem/src/main/java/com/condation/cms/filesystem/metadata/persistent/LuceneQuery.java index 112d77da1..467c79bb6 100644 --- a/cms-filesystem/src/main/java/com/condation/cms/filesystem/metadata/persistent/LuceneQuery.java +++ b/cms-filesystem/src/main/java/com/condation/cms/filesystem/metadata/persistent/LuceneQuery.java @@ -23,6 +23,7 @@ import com.condation.cms.api.Constants; import com.condation.cms.api.db.ContentNode; import com.condation.cms.api.db.ContentQuery; +import com.condation.cms.api.db.CursorPage; import com.condation.cms.api.db.DistanceUnit; import com.condation.cms.api.db.NodeVisibility; import com.condation.cms.api.db.Page; @@ -194,6 +195,43 @@ public List get() { return result.nodes; } + CursorPage cursorPage(String cursor, long size) { + if (size < 1 || size > 10_000) { + throw new IllegalArgumentException("cursor page size must be between 1 and 10000"); + } + if (!extensionOperations.isEmpty()) { + throw new UnsupportedOperationException( + "cursor paging is not available with in-memory query extensions"); + } + try { + var structuralQuery = structuralVisibilityQuery(buildBaseQuery()); + var completeQuery = completeVisibilityQuery(structuralQuery) + .orElseThrow(() -> new UnsupportedOperationException( + "cursor paging requires an indexable workflow visibility filter")); + org.apache.lucene.search.Sort sort = null; + if (orderByField.isPresent()) { + sort = index.resolveSort(orderByField.get(), Order.DESC.equals(sortOrder)) + .orElseThrow(() -> new UnsupportedOperationException( + "cursor paging requires an indexed sort field: " + orderByField.get())); + } + // Visibility schedule bounds contain the current millisecond and therefore + // are intentionally excluded from the stable cursor identity. + var cursorKey = structuralQuery.toString() + + "|preview=" + hasPreview() + + "|workflow=" + statusProvider().getClass().getName(); + var result = index.cursorPage( + completeQuery, sort, Math.toIntExact(size), cursor, cursorKey); + var nodes = result.uris().stream() + .map(metaData::byPath) + .flatMap(Optional::stream) + .toList(); + return new CursorPage<>(mapContentNodes(nodes).nodes, result.nextCursor()); + } catch (IOException ex) { + log.error("error cursor-paging lucene query", ex); + return new CursorPage<>(List.of(), null); + } + } + private List queryContentNodes() { try { var contentNodes = new ArrayList(); @@ -217,26 +255,16 @@ private Page fastPage( long page, long size, long offset) throws IOException { - long totalItems = index.count(query); - if (offset >= totalItems) { - return new Page<>(totalItems, size, totalPages(totalItems, size), (int) page, List.of()); - } - - var nodes = new ArrayList((int) Math.min(size, Integer.MAX_VALUE)); - var hitIndex = new long[]{0}; - index.scanUris(query, sort, batchSize(size), uri -> { - long currentIndex = hitIndex[0]++; - if (currentIndex < offset) { - return true; - } - metaData.byPath(uri).ifPresent(nodes::add); - return nodes.size() < size; - }); + var result = index.seekPage(query, sort, offset, Math.toIntExact(size)); + var nodes = result.uris().stream() + .map(metaData::byPath) + .flatMap(Optional::stream) + .toList(); return new Page<>( - totalItems, + result.totalItems(), size, - totalPages(totalItems, size), + totalPages(result.totalItems(), size), (int) page, mapContentNodes(nodes).nodes); } diff --git a/cms-filesystem/src/test/java/com/condation/cms/filesystem/FileCollectionsTest.java b/cms-filesystem/src/test/java/com/condation/cms/filesystem/FileCollectionsTest.java index dec564d38..6a41188c4 100644 --- a/cms-filesystem/src/test/java/com/condation/cms/filesystem/FileCollectionsTest.java +++ b/cms-filesystem/src/test/java/com/condation/cms/filesystem/FileCollectionsTest.java @@ -23,6 +23,7 @@ import com.condation.cms.api.db.ContentNode; import com.condation.cms.api.db.ContentQuery; +import com.condation.cms.api.db.collection.CollectionCursorSupport; import com.condation.cms.api.feature.features.IsPreviewFeature; import com.condation.cms.api.feature.features.WorkflowFeature; import com.condation.cms.api.request.RequestContext; @@ -34,6 +35,7 @@ import java.nio.file.Path; import java.util.Map; import java.util.Optional; +import java.util.concurrent.atomic.AtomicInteger; import org.assertj.core.api.Assertions; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; @@ -133,6 +135,111 @@ void searchesCollectionTitlesWithPaging() throws Exception { } } + @Test + void queriesMetadataWithoutReturningMarkdownBodies() throws Exception { + write("blog/first.md", "title: First", "Large body"); + var collections = createCollections(); + try { + var result = collections.collection("blog").metadataQuery().page(1, 10); + + Assertions.assertThat(result.getItems()) + .singleElement() + .satisfies(item -> { + Assertions.assertThat(item.id()).isEqualTo("first"); + Assertions.assertThat(item.meta()).containsEntry("title", "First"); + }); + } finally { + collections.close(); + } + } + + @Test + void reusesPersistedCollectionIndexWhenFilesAreUnchanged() throws Exception { + write("blog/first.md", "title: First", "First"); + write("blog/second.md", "title: Second", "Second"); + var parseCount = new AtomicInteger(); + var parser = (java.util.function.Function>) path -> { + parseCount.incrementAndGet(); + return parseMeta(path); + }; + + var first = new FileCollections("test-site", tempDirectory, parser); + first.init(); + first.close(); + Assertions.assertThat(parseCount).hasValue(2); + + var second = new FileCollections("test-site", tempDirectory, parser); + try { + second.init(); + Assertions.assertThat(parseCount).hasValue(2); + Assertions.assertThat(second.collection("blog").metadataQuery().page(1, 10).getTotalItems()) + .isEqualTo(2); + } finally { + second.close(); + } + } + + @Test + void pagesCollectionsWithAnOpaqueCursorAndExpiresItAfterIndexChanges() throws Exception { + write("blog/first.md", "title: A", "First"); + write("blog/second.md", "title: B", "Second"); + write("blog/third.md", "title: C", "Third"); + var collections = createCollections(); + try { + var cursorSupport = (CollectionCursorSupport) collections; + var firstPage = cursorSupport.metadataCursorPage( + "blog", null, 2, query -> query.orderby("title").asc()); + var secondPage = cursorSupport.metadataCursorPage( + "blog", firstPage.nextCursor(), 2, query -> query.orderby("title").asc()); + + Assertions.assertThat(firstPage.items()).extracting(item -> item.id()) + .containsExactly("first", "second"); + Assertions.assertThat(firstPage.hasNext()).isTrue(); + Assertions.assertThat(secondPage.items()).extracting(item -> item.id()) + .containsExactly("third"); + Assertions.assertThat(secondPage.hasNext()).isFalse(); + + write("blog/third.md", "title: D", "Changed"); + collections.refresh("blog", "third"); + Assertions.assertThatIllegalArgumentException().isThrownBy(() -> + cursorSupport.metadataCursorPage( + "blog", + firstPage.nextCursor(), + 2, + query -> query.orderby("title").asc())) + .withMessageContaining("cursor has expired"); + } finally { + collections.close(); + } + } + + @Test + void regularPageUsesServerSideSeekingWithoutAnExternalCursor() throws Exception { + write("blog/first.md", "title: A", "First"); + write("blog/second.md", "title: B", "Second"); + write("blog/third.md", "title: C", "Third"); + write("blog/fourth.md", "title: D", "Fourth"); + write("blog/fifth.md", "title: E", "Fifth"); + var collections = createCollections(); + try { + var query = collections.collection("blog").metadataQuery(); + var page = query + .orderby("title").asc() + .page(2, 2); + + Assertions.assertThat(page.getTotalItems()).isEqualTo(5); + Assertions.assertThat(page.getTotalPages()).isEqualTo(3); + Assertions.assertThat(page.getPage()).isEqualTo(2); + Assertions.assertThat(page.getItems()).extracting(item -> item.id()) + .containsExactly("third", "fourth"); + Assertions.assertThat(query.getClass().getMethods()) + .noneMatch(method -> method.getName().equals("cursorPage") + || method.getName().equals("seekPage")); + } finally { + collections.close(); + } + } + @Test void refreshesOneCollectionItemImmediately() throws Exception { write("blog/item.md", "title: Before", "Before"); diff --git a/modules/ui-module/src/main/java/com/condation/cms/modules/ui/extensionpoints/remotemethods/RemoteCollectionEndpoints.java b/modules/ui-module/src/main/java/com/condation/cms/modules/ui/extensionpoints/remotemethods/RemoteCollectionEndpoints.java index eec550f28..31458ff0b 100644 --- a/modules/ui-module/src/main/java/com/condation/cms/modules/ui/extensionpoints/remotemethods/RemoteCollectionEndpoints.java +++ b/modules/ui-module/src/main/java/com/condation/cms/modules/ui/extensionpoints/remotemethods/RemoteCollectionEndpoints.java @@ -24,9 +24,12 @@ import com.condation.cms.api.Constants; import com.condation.cms.api.auth.Permissions; import com.condation.cms.api.db.DB; +import com.condation.cms.api.db.CursorPage; import com.condation.cms.api.db.Page; +import com.condation.cms.api.db.collection.CollectionCursorSupport; import com.condation.cms.api.db.collection.CollectionItem; import com.condation.cms.api.db.collection.CollectionItemId; +import com.condation.cms.api.db.collection.CollectionItemMetadata; import com.condation.cms.api.eventbus.events.InvalidateContentCacheEvent; import com.condation.cms.api.feature.features.EventBusFeature; import com.condation.cms.api.feature.features.WorkflowFeature; @@ -48,8 +51,8 @@ import java.time.Instant; import java.util.Date; import java.util.HashMap; +import java.util.List; import java.util.Map; -import java.util.Objects; import lombok.extern.slf4j.Slf4j; /** Manager endpoints for listing and editing collection items. */ @@ -77,6 +80,9 @@ public record EditableItemDto( Map meta) { } + public record CursorItemsDto(List items, String nextCursor) { + } + @RemoteMethod(name = "collections.items", permissions = {Permissions.CONTENT_EDIT}) public Object items(Map parameters) throws RPCException { var db = getDB(parameters); @@ -90,12 +96,12 @@ public Object items(Map parameters) throws RPCException { MAX_PAGE_SIZE); var title = optionalString(parameters, "query"); - var query = db.getCollections().collection(collectionName).query(); + var query = db.getCollections().collection(collectionName).metadataQuery(); if (!title.isBlank()) { query.searchByTitle(title); } query.orderby(Constants.MetaFields.TITLE).asc(); - Page result = query.page(page, size); + Page result = query.page(page, size); return new Page<>( result.getTotalItems(), result.getPageSize(), @@ -104,6 +110,34 @@ public Object items(Map parameters) throws RPCException { result.getItems().stream().map(this::itemDto).toList()); } + @RemoteMethod(name = "collections.items.cursor", permissions = {Permissions.CONTENT_EDIT}) + public Object cursorItems(Map parameters) throws RPCException { + var db = getDB(parameters); + var collectionName = requiredString(parameters, Parameters.COLLECTION); + ensureCollectionExists(db.getCollections().names(), collectionName); + long size = Math.clamp( + NumberUtils.toLong(parameters.getOrDefault("size", DEFAULT_PAGE_SIZE)), + 1, + MAX_PAGE_SIZE); + if (!(db.getCollections() instanceof CollectionCursorSupport cursorSupport)) { + throw new RPCException(501, "collection storage does not support cursor paging"); + } + var title = optionalString(parameters, "query"); + CursorPage result = cursorSupport.metadataCursorPage( + collectionName, + optionalString(parameters, "cursor"), + size, + query -> { + if (!title.isBlank()) { + query.searchByTitle(title); + } + query.orderby(Constants.MetaFields.TITLE).asc(); + }); + return new CursorItemsDto( + result.items().stream().map(this::itemDto).toList(), + result.nextCursor()); + } + @RemoteMethod(name = "collections.item.get", permissions = {Permissions.CONTENT_EDIT}) public Object get(Map parameters) throws RPCException { @@ -221,14 +255,26 @@ private CollectionItem item(Map parameters) throws RPCException } private ItemDto itemDto(CollectionItem item) { - var title = item.meta().get(Constants.MetaFields.TITLE); + return itemDto(item.id(), item.collection(), item.path(), item.meta()); + } + + private ItemDto itemDto(CollectionItemMetadata item) { + return itemDto(item.id(), item.collection(), item.path(), item.meta()); + } + + private ItemDto itemDto( + String id, + String collection, + String path, + Map meta) { + var title = meta.get(Constants.MetaFields.TITLE); return new ItemDto( - item.id(), - item.collection(), - item.path(), - title == null || title.toString().isBlank() ? item.id() : title.toString(), - detailUrl(item), - item.meta()); + id, + collection, + path, + title == null || title.toString().isBlank() ? id : title.toString(), + detailUrl(new CollectionItem(id, collection, path, "", meta)), + meta); } private String detailUrl(CollectionItem item) { @@ -279,13 +325,13 @@ private static void normalizeAndValidateSlug( throw new RPCException(400, "collection item slug must not be blank"); } - var duplicate = db.getCollections().collection(collectionName).query().get().stream() + var duplicate = db.getCollections().collection(collectionName).metadataQuery() + .where("slug", slug) + .page(1, 2) + .getItems().stream() .filter(item -> !item.id().equals(itemId)) - .map(item -> MapUtil.getValue(item.meta(), "slug")) - .filter(Objects::nonNull) - .map(Object::toString) - .map(SlugUtil::slugify) - .anyMatch(slug::equals); + .findAny() + .isPresent(); if (duplicate) { throw new RPCException(409, "collection item slug already exists: " + slug); } diff --git a/modules/ui-module/src/main/resources/manager/actions/collection/manage-collection.js b/modules/ui-module/src/main/resources/manager/actions/collection/manage-collection.js index c7f02983f..4788a72c3 100644 --- a/modules/ui-module/src/main/resources/manager/actions/collection/manage-collection.js +++ b/modules/ui-module/src/main/resources/manager/actions/collection/manage-collection.js @@ -23,7 +23,7 @@ import { createCollectionItemEditor } from './edit-collection-item.js'; import { i18n } from '@cms/modules/localization.js'; import { openModal } from '@cms/modules/modal.js'; import { loadPreview } from '@cms/modules/preview.utils.js'; -import { deleteCollectionItem, listCollectionItems } from '@cms/modules/rpc/rpc-collection.js'; +import { deleteCollectionItem, listCollectionItemsCursor } from '@cms/modules/rpc/rpc-collection.js'; import { showToast } from '@cms/modules/toast.js'; const PAGE_SIZE = 10; const MIN_SEARCH_LENGTH = 3; @@ -60,6 +60,7 @@ const renderItems = (items) => { }; export const runAction = async (options) => { let currentPage = 1; + let cursorHistory = ['']; let currentQuery = ''; let requestVersion = 0; let editorRequestVersion = 0; @@ -142,6 +143,8 @@ export const runAction = async (options) => { collection: options.collection, id, onSaved: async () => { + currentPage = 1; + cursorHistory = ['']; await update(); closeEditor(); } @@ -183,10 +186,10 @@ export const runAction = async (options) => { return; root.innerHTML = `
${i18n.t('collection.items.loading', 'Loading collection items...')}
`; try { - const page = await listCollectionItems({ + const page = await listCollectionItemsCursor({ collection: options.collection, query: currentQuery, - page: currentPage, + cursor: cursorHistory[currentPage - 1], size: PAGE_SIZE }); if (version !== requestVersion) @@ -197,17 +200,17 @@ export const runAction = async (options) => { return; } root.innerHTML = renderItems(page.items); - pagination.innerHTML = page.totalPages > 1 ? ` + pagination.innerHTML = currentPage > 1 || page.nextCursor ? `