From 3ac768735d077da88409ba7dc46279b09b49cbe9 Mon Sep 17 00:00:00 2001
From: aboeckle <alexander.boeckle@frentix.com>
Date: Wed, 2 Sep 2020 13:42:56 +0200
Subject: [PATCH] OO-4854: Alphabetical sorting for catalog

---
 .../form/flexible/elements/FormToggle.java    |   6 +
 .../org/olat/repository/CatalogEntry.java     |  37 ++++-
 .../org/olat/repository/RepositoryModule.java |  66 ++++++--
 .../repository/manager/CatalogManager.java    | 157 ++++++++++++------
 .../repository/model/CatalogEntryImpl.java    |  28 +++-
 .../ui/admin/CatalogAdminController.java      |  63 +++++--
 .../ui/admin/_i18n/LocalStrings_de.properties |  10 +-
 .../ui/admin/_i18n/LocalStrings_en.properties |  12 +-
 .../catalog/CatalogEntryEditController.java   |  41 ++++-
 .../ui/catalog/CatalogNodeController.java     |  55 +++---
 .../catalog/CatalogNodeManagerController.java | 154 +++++++++++------
 .../repository/ui/catalog/_content/node.html  |   3 +
 .../catalog/_i18n/LocalStrings_de.properties  |  11 +-
 .../catalog/_i18n/LocalStrings_en.properties  |   9 +-
 .../org/olat/upgrade/OLATUpgrade_15_2_3.java  | 136 +++++++++++++++
 .../_spring/databaseUpgradeContext.xml        |  10 +-
 .../olat/upgrade/_spring/upgradeContext.xml   |   3 +-
 .../database/mysql/alter_15_2_x_to_15_2_3.sql |   2 +
 .../oracle/alter_15_2_x_to_15_2_3.sql         |   2 +
 .../postgresql/alter_15_2_x_to_15_2_3.sql     |   2 +
 20 files changed, 629 insertions(+), 178 deletions(-)
 create mode 100644 src/main/java/org/olat/upgrade/OLATUpgrade_15_2_3.java
 create mode 100644 src/main/resources/database/mysql/alter_15_2_x_to_15_2_3.sql
 create mode 100644 src/main/resources/database/oracle/alter_15_2_x_to_15_2_3.sql
 create mode 100644 src/main/resources/database/postgresql/alter_15_2_x_to_15_2_3.sql

diff --git a/src/main/java/org/olat/core/gui/components/form/flexible/elements/FormToggle.java b/src/main/java/org/olat/core/gui/components/form/flexible/elements/FormToggle.java
index 7f7fd4a7db4..0e18f0efa4a 100644
--- a/src/main/java/org/olat/core/gui/components/form/flexible/elements/FormToggle.java
+++ b/src/main/java/org/olat/core/gui/components/form/flexible/elements/FormToggle.java
@@ -61,4 +61,10 @@ public void setToggledOnCSS(String toggledOnCSS);
  */
 public void setToggledOffCSS(String toggledOffCSS);
 
+/**
+ * set the i18n key
+ * @param i18n
+ */
+public void setI18nKey(String i18n);
+
 }
\ No newline at end of file
diff --git a/src/main/java/org/olat/repository/CatalogEntry.java b/src/main/java/org/olat/repository/CatalogEntry.java
index 0760c0c9e4a..04fbb1abf41 100644
--- a/src/main/java/org/olat/repository/CatalogEntry.java
+++ b/src/main/java/org/olat/repository/CatalogEntry.java
@@ -186,6 +186,42 @@ public interface CatalogEntry extends CatalogEntryRef, CreateInfo, Persistable,
 	 * @param shortTitle
 	 */
 	public void setShortTitle(String shortTitle);
+
+	/**
+	 * Returns whether new entries should be added on top, bottom or alphabetically
+	 * 0 - Alphabetically
+	 * 1 - On top
+	 * 2 - On bottom
+	 *
+	 * @return
+	 */
+	public Integer getEntryAddPosition();
+
+	/**
+	 * Set how new entries should be added
+	 * 0 - Alphabetically
+	 * 1 - On top
+	 * 2 - On bottom
+	 */
+	public void setEntryAddPosition(Integer addEntryPosition);
+
+	/**
+	 * Returns whether new categories should be added on top, bottom or alphabetically
+	 * 0 - Alphabetically
+	 * 1 - On top
+	 * 2 - On bottom
+	 *
+	 * @return
+	 */
+	public Integer getCategoryAddPosition();
+
+	/**
+	 * Set how new entries should be added
+	 * 0 - Alphabetically
+	 * 1 - On top
+	 * 2 - On bottom
+	 */
+	public void setCategoryAddPosition(Integer addCategoryPosition);
 	
 	
 	public enum OrderBy {
@@ -197,6 +233,5 @@ public interface CatalogEntry extends CatalogEntryRef, CreateInfo, Persistable,
 		tiles,
 		list,
 		compact,
-		
 	}
 }
\ No newline at end of file
diff --git a/src/main/java/org/olat/repository/RepositoryModule.java b/src/main/java/org/olat/repository/RepositoryModule.java
index f40ed446bdd..763e833e1b9 100644
--- a/src/main/java/org/olat/repository/RepositoryModule.java
+++ b/src/main/java/org/olat/repository/RepositoryModule.java
@@ -55,7 +55,9 @@ public class RepositoryModule extends AbstractSpringModule {
 	private static final String CATALOG_SITE_ENABLED = "site.catalog.enable";
 	private static final String CATALOG_ENABLED = "catalog.enable";
 	private static final String CATALOG_BROWSING_ENABLED = "catalog.brwosing.enable";
-	private static final String CATALOG_ADD_AT_LAST = "catalog.add.last";
+	private static final String CATALOG_MULTI_SELECT_ENABLED = "catalog.multi.select.enable";
+	private static final String CATALOG_ADD_ENTRY_POSITION = "catalog.add.entry.position";
+	private static final String CATALOG_ADD_CATEGORY_POSITION = "catalog.add.catalog.position";
 	private static final String MYCOURSES_SEARCH_ENABLED = "mycourses.search.enabled";
 	private static final String MYCOURSES_ALL_RESOURCES_ENABLED = "mycourses.all.resources.enabled";
 	
@@ -76,8 +78,12 @@ public class RepositoryModule extends AbstractSpringModule {
 	private boolean catalogEnabled;
 	@Value("${repo.catalog.browsing.enable}")
 	private boolean catalogBrowsingEnabled;
-	@Value("${catalog.add.last:true}")
-	private boolean catalogAddLast;
+	@Value("${catalog.multi.select.enable:false}")
+	private boolean catalogMultiSelectEnabled;
+	@Value("${catalog.add.entry.position:0}")
+	private int catalogAddEntryPosition;
+	@Value("${catalog.add.category.position:0}")
+	private int catalogAddCategoryPosition;
 	
 	@Value("${repo.managed}")
 	private boolean managedRepositoryEntries;
@@ -161,11 +167,16 @@ public class RepositoryModule extends AbstractSpringModule {
 		if(StringHelper.containsNonWhitespace(catalogRepo)) {
 			catalogEnabled = "true".equals(catalogRepo);
 		}
-		
+
 		String myCourses = getStringPropertyValue(CATALOG_BROWSING_ENABLED, true);
 		if(StringHelper.containsNonWhitespace(myCourses)) {
 			catalogBrowsingEnabled = "true".equals(myCourses);
 		}
+
+		String catalogMultiSelect = getStringPropertyValue(CATALOG_MULTI_SELECT_ENABLED, true);
+		if(StringHelper.containsNonWhitespace(catalogMultiSelect)) {
+			catalogMultiSelectEnabled = "true".equals(catalogMultiSelect);
+		}
 		
 		String myCoursesSearch = getStringPropertyValue(MYCOURSES_SEARCH_ENABLED, true);
 		if(StringHelper.containsNonWhitespace(myCoursesSearch)) {
@@ -211,11 +222,12 @@ public class RepositoryModule extends AbstractSpringModule {
 		if(StringHelper.containsNonWhitespace(taxonomyTreeKeyObj)) {
 			taxonomyTreeKey = taxonomyTreeKeyObj;
 		}
-		
-		String catalogAddLastObj = getStringPropertyValue(CATALOG_ADD_AT_LAST, true);
-		if(StringHelper.containsNonWhitespace(taxonomyTreeKeyObj)) {
-			catalogAddLast = "true".equals(catalogAddLastObj);
-		}
+
+		// 0 -> Alphabetical
+		// 1 -> Add on top
+		// 2 -> Add on bottom
+		catalogAddEntryPosition = getIntPropertyValue(CATALOG_ADD_ENTRY_POSITION, catalogAddEntryPosition);
+		catalogAddCategoryPosition = getIntPropertyValue(CATALOG_ADD_CATEGORY_POSITION, catalogAddCategoryPosition);
 	}
 
 	/**
@@ -278,14 +290,32 @@ public class RepositoryModule extends AbstractSpringModule {
 		catalogBrowsingEnabled = enabled;
 		setStringProperty(CATALOG_BROWSING_ENABLED, Boolean.toString(enabled), true);
 	}
-	
-	public boolean isCatalogAddAtLast() {
-		return catalogAddLast;
+
+	public boolean isCatalogMultiSelectEnabled() {
+		return catalogMultiSelectEnabled;
 	}
-	
-	public void setCatalogAddAtLast(boolean addAtLast) {
-		catalogAddLast = addAtLast;
-		setStringProperty(CATALOG_ADD_AT_LAST, Boolean.toString(addAtLast), true);
+
+	public void setCatalogMultiSelectEnabled(boolean enabled) {
+		this.catalogMultiSelectEnabled = enabled;
+		setStringProperty(CATALOG_MULTI_SELECT_ENABLED, Boolean.toString(enabled), true);
+	}
+
+	public int getCatalogAddEntryPosition() {
+		return catalogAddEntryPosition;
+	}
+
+	public void setCatalogAddEntryPosition(int position) {
+		catalogAddEntryPosition = position;
+		setIntProperty(CATALOG_ADD_ENTRY_POSITION, position, true);
+	}
+
+	public int getCatalogAddCategoryPosition() {
+		return catalogAddCategoryPosition;
+	}
+
+	public void setCatalogAddCategoryPosition(int position) {
+		catalogAddCategoryPosition = position;
+		setIntProperty(CATALOG_ADD_CATEGORY_POSITION, position, true);
 	}
 
 	public boolean isMyCoursesSearchEnabled() {
@@ -362,7 +392,7 @@ public class RepositoryModule extends AbstractSpringModule {
 		this.lifecycleAutoDelete = lifecycleAutoDelete;
 		setStringProperty(LIFECYCLE_AUTO_DELETE, lifecycleAutoDelete, true);
 	}
-	
+
 	public boolean isLifecycleNotificationByCloseDeleteEnabled() {
 		return "enabled".equals(lifecycleNotificationByCloseDelete);
 	}
@@ -380,4 +410,4 @@ public class RepositoryModule extends AbstractSpringModule {
 		this.taxonomyTreeKey = taxonomyTreeKey;
 		setStringProperty(TAXONOMY_TREE_KEY, taxonomyTreeKey, true);
 	}
-}
\ No newline at end of file
+}
diff --git a/src/main/java/org/olat/repository/manager/CatalogManager.java b/src/main/java/org/olat/repository/manager/CatalogManager.java
index 8182259f533..6d91434c944 100644
--- a/src/main/java/org/olat/repository/manager/CatalogManager.java
+++ b/src/main/java/org/olat/repository/manager/CatalogManager.java
@@ -26,11 +26,13 @@
 package org.olat.repository.manager;
 
 import java.io.File;
+import java.text.Collator;
 import java.util.ArrayList;
 import java.util.Collections;
+import java.util.Comparator;
 import java.util.Iterator;
 import java.util.List;
-
+import java.util.stream.Collectors;
 import javax.persistence.FlushModeType;
 import javax.persistence.TypedQuery;
 
@@ -155,7 +157,37 @@ public class CatalogManager implements UserDataDeletable, InitializingBean {
 	 * @return List of catalog entries that are childern entries of given entry
 	 */
 	public List<CatalogEntry> getChildrenOf(CatalogEntry ce) {
-		return getChildrenOf(ce, 0, -1, CatalogEntry.OrderBy.position, true);
+		List<CatalogEntry> children = getChildrenOf(ce, 0, -1, CatalogEntry.OrderBy.position, true);
+
+		if (isCategorySortingManually(ce) || isEntrySortingManually(ce)) {
+			// Create 3 lists: Categories, entries, closed entries
+			String closed = RepositoryEntryStatusEnum.closed.name();
+			List<CatalogEntry> categories = children.stream().filter(catalogEntry -> catalogEntry.getType() == CatalogEntry.TYPE_NODE).collect(Collectors.toList());
+			children.removeAll(categories);
+			List<CatalogEntry> entries = children.stream().filter(catalogEntry -> catalogEntry.getType() == CatalogEntry.TYPE_LEAF && !catalogEntry.getRepositoryEntry().getStatus().equals(closed)).collect(Collectors.toList());
+			children.removeAll(entries);
+			// To be sure only correct entries are in the final list, the last step is also filtered
+			List<CatalogEntry> closedEntries = children.stream().filter(catalogEntry -> catalogEntry.getType() == CatalogEntry.TYPE_LEAF && catalogEntry.getRepositoryEntry().getStatus().equals(closed)).collect(Collectors.toList());
+			// Now remove all remaining entries
+			children.removeAll(children);
+
+			Collator collator = Collator.getInstance();
+			collator.setStrength(Collator.IDENTICAL);
+
+			if (isCategorySortingManually(ce)) {
+				categories.sort(Comparator.comparing(CatalogEntry::getName, collator));
+			}
+			if (isEntrySortingManually(ce)) {
+				entries.sort(Comparator.comparing(CatalogEntry::getName, collator));
+				closedEntries.sort(Comparator.comparing(CatalogEntry::getName, collator));
+			}
+
+			children.addAll(categories);
+			children.addAll(entries);
+			children.addAll(closedEntries);
+		}
+
+		return children;
 	}
 
 	/**
@@ -597,8 +629,6 @@ public class CatalogManager implements UserDataDeletable, InitializingBean {
 		parentEntry = loadCatalogEntry(parentEntry);
 		newEntry = loadCatalogEntry(newEntry);
 		List<CatalogEntry> catEntries = parentEntry.getChildren();
-		int index = 0;
-		boolean added = false;
 		String closed = RepositoryEntryStatusEnum.closed.name();
 		RepositoryEntry repoEntry = newEntry.getRepositoryEntry();
 		
@@ -608,60 +638,59 @@ public class CatalogManager implements UserDataDeletable, InitializingBean {
 		}
 
 		cleanNullEntries(catEntries);
-		
-		for (CatalogEntry catalogEntry : catEntries) {
-			// Add entries
-			if (catalogEntry.getType() == CatalogEntry.TYPE_LEAF && newEntry.getType() == CatalogEntry.TYPE_LEAF) {
-				if (repositoryModule.isCatalogAddAtLast()) {
-					// Closed entry to the end
-					if (repoEntry.getStatus().equals(closed)) {
-						catEntries.add(newEntry);
-						added = true;
-						break;
-					} 
-					// Not closed entry to the end of not closed entries
-					else if (catalogEntry.getRepositoryEntry().getStatus().equals(closed)) {
-						catEntries.add(index, newEntry);
-						added = true;
-						break;
-					}
-				} else {
-					// Closed entry to the beginning of closed
-					if (repoEntry.getStatus().equals(closed) && catalogEntry.getRepositoryEntry().getStatus().equals(closed)) {
-						catEntries.add(index, newEntry);
-						added = true;
-						break;
-					} 
-					// Not closed entry to the beginning of not closed entries
-					else if (!repoEntry.getStatus().equals(closed) && !catalogEntry.getRepositoryEntry().getStatus().equals(closed)) {
-						catEntries.add(index, newEntry);
-						added = true;
-						break;
-					}
-				}
-			} 
-			// Add categories
-			else if (newEntry.getType() == CatalogEntry.TYPE_NODE) {
-				if (repositoryModule.isCatalogAddAtLast()) {
-					if (catalogEntry.getType() == CatalogEntry.TYPE_LEAF) {
-						catEntries.add(index, newEntry);
-						added = true; 
-						break;
-					}
-				} else {
-					catEntries.add(0, newEntry);
-					added = true;
-					break;
-				}
+
+		// Create 3 lists: Categories, entries, closed entries
+		List<CatalogEntry> categories = catEntries.stream().filter(catalogEntry -> catalogEntry.getType() == CatalogEntry.TYPE_NODE).collect(Collectors.toList());
+		catEntries.removeAll(categories);
+		List<CatalogEntry> entries = catEntries.stream().filter(catalogEntry -> catalogEntry.getType() == CatalogEntry.TYPE_LEAF && !catalogEntry.getRepositoryEntry().getStatus().equals(closed)).collect(Collectors.toList());
+		catEntries.removeAll(entries);
+		// To be sure only correct entries are in the final list, the last step is also filtered
+		List<CatalogEntry> closedEntries = catEntries.stream().filter(catalogEntry -> catalogEntry.getType() == CatalogEntry.TYPE_LEAF && catalogEntry.getRepositoryEntry().getStatus().equals(closed)).collect(Collectors.toList());
+		// Now remove all remaining entries
+		catEntries.removeAll(catEntries);
+
+		// Add to categories
+		if (newEntry.getType() == CatalogEntry.TYPE_NODE) {
+			// If added on top or alphabetically
+			if ((parentEntry.getCategoryAddPosition() == null && repositoryModule.getCatalogAddCategoryPosition() == 1)
+				|| (parentEntry.getCategoryAddPosition() != null && parentEntry.getCategoryAddPosition() == 1)) {
+				categories.add(0, newEntry);
+			}
+			// If added in the end
+			else {
+				categories.add(newEntry);
+			}
+
+		}
+		// Add to entries
+		else if (newEntry.getType() == CatalogEntry.TYPE_LEAF && !newEntry.getRepositoryEntry().getStatus().equals(closed)) {
+			// If added on top or alphabetically
+			if ((parentEntry.getEntryAddPosition() == null && repositoryModule.getCatalogAddEntryPosition() == 1)
+				|| (parentEntry.getEntryAddPosition() != null && parentEntry.getEntryAddPosition() == 1)) {
+				entries.add(0, newEntry);
+			}
+			// If added in the end
+			else {
+				entries.add(newEntry);
 			}
-			index++;
 		}
-		
-		// If not added already, add it to the bottom of the list
-		if (!added) {
-			catEntries.add(newEntry);
+		// Add to closed entries
+		else {
+			// If added on top or alphabetically
+			if ((parentEntry.getEntryAddPosition() == null && repositoryModule.getCatalogAddEntryPosition() == 1)
+					|| (parentEntry.getEntryAddPosition() != null && parentEntry.getEntryAddPosition() == 1)) {
+				closedEntries.add(0, newEntry);
+			}
+			// If added in the end
+			else {
+				closedEntries.add(newEntry);
+			}
 		}
 
+		catEntries.addAll(categories);
+		catEntries.addAll(entries);
+		catEntries.addAll(closedEntries);
+
 		updateCatalogEntry(parentEntry);
 	}
 
@@ -985,4 +1014,24 @@ public class CatalogManager implements UserDataDeletable, InitializingBean {
 			return -1;
 		}
 	}
+
+	public void setCategoryAddPosition(CatalogEntry catEntry, Integer position) {
+		catEntry = loadCatalogEntry(catEntry);
+		catEntry.setCategoryAddPosition(position);
+		updateCatalogEntry(catEntry);
+	}
+
+	public void setEntryAddPosition(CatalogEntry catEntry, Integer position) {
+		catEntry = loadCatalogEntry(catEntry);
+		catEntry.setEntryAddPosition(position);
+		updateCatalogEntry(catEntry);
+	}
+
+	public boolean isEntrySortingManually(CatalogEntry ce) {
+		return !((ce.getEntryAddPosition() != null && ce.getEntryAddPosition() == 0) || (ce.getEntryAddPosition() == null && repositoryModule.getCatalogAddEntryPosition() == 0));
+	}
+
+	public boolean isCategorySortingManually(CatalogEntry ce) {
+		return !((ce.getCategoryAddPosition() != null && ce.getCategoryAddPosition() == 0) || (ce.getCategoryAddPosition() == null && repositoryModule.getCatalogAddCategoryPosition() == 0));
+	}
 }
diff --git a/src/main/java/org/olat/repository/model/CatalogEntryImpl.java b/src/main/java/org/olat/repository/model/CatalogEntryImpl.java
index a3f07ebcfa5..20111f2381a 100644
--- a/src/main/java/org/olat/repository/model/CatalogEntryImpl.java
+++ b/src/main/java/org/olat/repository/model/CatalogEntryImpl.java
@@ -123,7 +123,13 @@ public class CatalogEntryImpl implements CatalogEntry {
 	@GeneratedValue
 	@Column(name = "order_index", updatable = false, insertable = false)
 	private Integer position;
-	
+
+	@Column(name = "add_entry_position", unique = false, nullable = true)
+	private Integer addEntryPosition;
+
+	@Column(name = "add_category_position", unique = false, nullable = true)
+	private Integer addCategoryPosition;
+
 	
 	public CatalogEntryImpl() {
 	// for hibernate
@@ -161,6 +167,26 @@ public class CatalogEntryImpl implements CatalogEntry {
 		this.shortTitle = shortTitle;
 	}
 
+	@Override
+	public Integer getEntryAddPosition() {
+		return addEntryPosition;
+	}
+
+	@Override
+	public void setEntryAddPosition(Integer addEntryPosition) {
+		this.addEntryPosition = addEntryPosition;
+	}
+
+	@Override
+	public Integer getCategoryAddPosition() {
+		return addCategoryPosition;
+	}
+
+	@Override
+	public void setCategoryAddPosition(Integer addCategoryPosition) {
+		this.addCategoryPosition = addCategoryPosition;
+	}
+
 	public String getStyleString() {
 		return styleString;
 	}
diff --git a/src/main/java/org/olat/repository/ui/admin/CatalogAdminController.java b/src/main/java/org/olat/repository/ui/admin/CatalogAdminController.java
index 5b422a05806..e4874d5c362 100644
--- a/src/main/java/org/olat/repository/ui/admin/CatalogAdminController.java
+++ b/src/main/java/org/olat/repository/ui/admin/CatalogAdminController.java
@@ -44,12 +44,13 @@ public class CatalogAdminController extends FormBasicController {
 	private MultipleSelectionElement enableEl;
 	private MultipleSelectionElement enableBrowsingEl;
 	private MultipleSelectionElement siteEl;
+	private MultipleSelectionElement addMultipleEntriesEl;
 	private SingleSelection addEntryPosEl;
-	
-	
+	private SingleSelection addCategoryPosEl;
+
 	@Autowired
 	private RepositoryModule repositoryModule;
-	
+
 	/**
 	 * @param ureq
 	 * @param wControl
@@ -68,7 +69,7 @@ public class CatalogAdminController extends FormBasicController {
 		enableEl = uifactory.addCheckboxesHorizontal("catalog.enable", "catalog.enable", formLayout, new String[]{"xx"}, new String[]{""});
 		enableEl.select("xx", enabled);
 		enableEl.addActionListener(FormEvent.ONCLICK);
-		
+
 		enableBrowsingEl = uifactory.addCheckboxesHorizontal("catalog.browsing", "catalog.browsing", formLayout, new String[]{"xx"}, new String[]{""});
 		enableBrowsingEl.select("xx", repositoryModule.isCatalogBrowsingEnabled());
 		enableBrowsingEl.setEnabled(enabled);
@@ -78,18 +79,37 @@ public class CatalogAdminController extends FormBasicController {
 		siteEl.select("xx", repositoryModule.isCatalogSiteEnabled());
 		siteEl.setEnabled(enabled);
 		siteEl.addActionListener(FormEvent.ONCLICK);
-		
-		String[] addEntryKeys = {AddEntryPosition.top.name(), AddEntryPosition.bottom.name()};
-		String[] addEntryValues = {translate("catalog.addposition." + AddEntryPosition.top.name()), translate("catalog.addposition." + AddEntryPosition.bottom.name())};
-		
-		addEntryPosEl = uifactory.addDropdownSingleselect("catalog.addposition", "catalog.addposition", formLayout, addEntryKeys, addEntryValues);
-		if (repositoryModule.isCatalogAddAtLast()) {
+
+		addMultipleEntriesEl = uifactory.addCheckboxesHorizontal("catalog.add.multiple.entries", "catalog.add.multiple.entries", formLayout, new String[]{"xx"}, new String[]{""});
+		addMultipleEntriesEl.select("xx", repositoryModule.isCatalogMultiSelectEnabled());
+		addMultipleEntriesEl.setEnabled(enabled);
+		addMultipleEntriesEl.addActionListener(FormEvent.ONCLICK);
+
+		String[] addEntryKeys = {AddEntryPosition.alphabetical.name(), AddEntryPosition.top.name(), AddEntryPosition.bottom.name()};
+		String[] addEntryValues = {translate("catalog.add.position." + AddEntryPosition.alphabetical.name()), translate("catalog.add.position." + AddEntryPosition.top.name()), translate("catalog.add.position." + AddEntryPosition.bottom.name())};
+
+		addEntryPosEl = uifactory.addDropdownSingleselect("catalog.add.entry.position", "catalog.add.entry.position", formLayout, addEntryKeys, addEntryValues);
+		if (repositoryModule.getCatalogAddEntryPosition() == 2) {
 			addEntryPosEl.select(AddEntryPosition.bottom.name(), true);
-		} else {
+		} else if (repositoryModule.getCatalogAddEntryPosition() == 1) {
 			addEntryPosEl.select(AddEntryPosition.top.name(), true);
+		} else {
+			addEntryPosEl.select(AddEntryPosition.alphabetical.name(), true);
 		}
 		addEntryPosEl.setEnabled(enabled);
 		addEntryPosEl.addActionListener(FormEvent.ONCHANGE);
+
+		addCategoryPosEl = uifactory.addDropdownSingleselect("catalog.add.category.position", "catalog.add.category.position", formLayout, addEntryKeys, addEntryValues);
+		if (repositoryModule.getCatalogAddCategoryPosition() == 2) {
+			addCategoryPosEl.select(AddEntryPosition.bottom.name(), true);
+		} else if (repositoryModule.getCatalogAddCategoryPosition() == 1) {
+			addCategoryPosEl.select(AddEntryPosition.top.name(), true);
+		} else {
+			addCategoryPosEl.select(AddEntryPosition.alphabetical.name(), true);
+		}
+
+		addCategoryPosEl.setEnabled(enabled);
+		addCategoryPosEl.addActionListener(FormEvent.ONCHANGE);
 	}
 
 	@Override
@@ -111,10 +131,22 @@ public class CatalogAdminController extends FormBasicController {
 			repositoryModule.setCatalogBrowsingEnabled(enableBrowsingEl.isSelected(0));
 		} else if (source == addEntryPosEl) {
 			if (addEntryPosEl.getSelectedKey().equals(AddEntryPosition.bottom.name())) {
-				repositoryModule.setCatalogAddAtLast(true);
+				repositoryModule.setCatalogAddEntryPosition(2);
+			} else if (addEntryPosEl.getSelectedKey().equals(AddEntryPosition.top.name())) {
+				repositoryModule.setCatalogAddEntryPosition(1);
 			} else {
-				repositoryModule.setCatalogAddAtLast(false);
+				repositoryModule.setCatalogAddEntryPosition(0);
 			}
+		} else if (source == addCategoryPosEl) {
+			if (addCategoryPosEl.getSelectedKey().equals(AddEntryPosition.bottom.name())) {
+				repositoryModule.setCatalogAddCategoryPosition(2);
+			} else if (addCategoryPosEl.getSelectedKey().equals(AddEntryPosition.top.name())) {
+				repositoryModule.setCatalogAddCategoryPosition(1);
+			} else {
+				repositoryModule.setCatalogAddCategoryPosition(0);
+			}
+		} else if (source == addMultipleEntriesEl) {
+			repositoryModule.setCatalogMultiSelectEnabled(addMultipleEntriesEl.isSelected(0));
 		}
 		super.formInnerEvent(ureq, source, event);
 	}
@@ -123,9 +155,10 @@ public class CatalogAdminController extends FormBasicController {
 	protected void formOK(UserRequest ureq) {
 		//
 	}
-	
+
 	private enum AddEntryPosition {
+		alphabetical,
 		top,
 		bottom;
 	}
-}
\ No newline at end of file
+}
diff --git a/src/main/java/org/olat/repository/ui/admin/_i18n/LocalStrings_de.properties b/src/main/java/org/olat/repository/ui/admin/_i18n/LocalStrings_de.properties
index 306dd7abc5f..1f57933508b 100644
--- a/src/main/java/org/olat/repository/ui/admin/_i18n/LocalStrings_de.properties
+++ b/src/main/java/org/olat/repository/ui/admin/_i18n/LocalStrings_de.properties
@@ -2,9 +2,13 @@
 admin.catalog.settings=Katalog settings
 admin.menu.title=Katalog
 admin.menu.title.alt=Katalog
-catalog.addposition=Neue Eintr\u00e4ge hinzuf\u00fcgen
-catalog.addposition.bottom=Am Ende
-catalog.addposition.top=Am Anfang
+catalog.add.entry.position=Neue Eintr\u00e4ge hinzuf\u00fcgen
+catalog.add.category.position=Neue Kategorien hinzuf\u00fcgen
+catalog.add.multiple.entries=Mehrere Eintr\u00e4ge zusammen hinzuf\u00fcgen
+catalog.add.position.alphabetical=Alphabtisch
+catalog.add.position.bottom=Am Ende
+catalog.add.position.top=Am Anfang
 catalog.browsing=Katalog in "Kurse"
 catalog.enable=Katalog einschalten
 catalog.site=Katalog in eigener Site
+catalog.sort.completely=Katalog komplett alphabetisch sortieren
diff --git a/src/main/java/org/olat/repository/ui/admin/_i18n/LocalStrings_en.properties b/src/main/java/org/olat/repository/ui/admin/_i18n/LocalStrings_en.properties
index e05ce147533..f64f464baaa 100644
--- a/src/main/java/org/olat/repository/ui/admin/_i18n/LocalStrings_en.properties
+++ b/src/main/java/org/olat/repository/ui/admin/_i18n/LocalStrings_en.properties
@@ -2,9 +2,13 @@
 admin.catalog.settings=Catalog settings
 admin.menu.title=Catalog
 admin.menu.title.alt=Catalog
-catalog.addposition=Add new entries
-catalog.addposition.bottom=At the end
-catalog.addposition.top=At the beginning
+catalog.add.entry.position=Add new entries
+catalog.add.category.position=Add new categories
+catalog.add.multiple.entries=Add multiple entries at once
+catalog.add.position.alphabetical=Alphabetical
+catalog.add.position.bottom=At the end
+catalog.add.position.top=At the beginning
 catalog.enable=Enable catalog
 catalog.browsing=Catalog in "Courses"
-catalog.site=Catalog in its own site
\ No newline at end of file
+catalog.site=Catalog in its own site
+catalog.sort.completely=Sort complete catalog alphabetically
diff --git a/src/main/java/org/olat/repository/ui/catalog/CatalogEntryEditController.java b/src/main/java/org/olat/repository/ui/catalog/CatalogEntryEditController.java
index 2147c3ef5ba..3eb7cc7edae 100644
--- a/src/main/java/org/olat/repository/ui/catalog/CatalogEntryEditController.java
+++ b/src/main/java/org/olat/repository/ui/catalog/CatalogEntryEditController.java
@@ -26,6 +26,7 @@
 package org.olat.repository.ui.catalog;
 
 import java.io.File;
+import java.util.Arrays;
 import java.util.HashSet;
 import java.util.Set;
 import java.util.UUID;
@@ -44,6 +45,7 @@ import org.olat.core.gui.components.form.flexible.impl.elements.FileElementEvent
 import org.olat.core.gui.control.Controller;
 import org.olat.core.gui.control.Event;
 import org.olat.core.gui.control.WindowControl;
+import org.olat.core.gui.translator.TranslatorHelper;
 import org.olat.core.util.StringHelper;
 import org.olat.core.util.Util;
 import org.olat.core.util.WebappHelper;
@@ -54,6 +56,7 @@ import org.olat.core.util.vfs.VFSLeaf;
 import org.olat.repository.CatalogEntry;
 import org.olat.repository.CatalogEntry.Style;
 import org.olat.repository.RepositoryManager;
+import org.olat.repository.RepositoryModule;
 import org.olat.repository.manager.CatalogManager;
 import org.springframework.beans.factory.annotation.Autowired;
 
@@ -72,7 +75,6 @@ import org.springframework.beans.factory.annotation.Autowired;
 public class CatalogEntryEditController extends FormBasicController {
 	
 	private static final int picUploadlimitKB = 5024;
-	
 	private static final Set<String> mimeTypes = new HashSet<>();
 	static {
 		mimeTypes.add("image/gif");
@@ -84,6 +86,9 @@ public class CatalogEntryEditController extends FormBasicController {
 	private static final String[] styleKeys = new String[]{
 		Style.tiles.name(), Style.list.name(), Style.compact.name()
 	};
+	private static final String[] sortSelectKeys = new String[]{
+			"add.default", "add.alphabetically", "add.top", "add.bottom"
+	};
 
 	private TextElement nameEl;
 	private TextElement shortTitleEl;
@@ -91,11 +96,16 @@ public class CatalogEntryEditController extends FormBasicController {
 	private RichTextElement descriptionEl;
 	private FileElement fileUpload;
 
+	private SingleSelection nodesSortSelect;
+	private SingleSelection entriesSortSelect;
+
 	private CatalogEntry parentEntry;
 	private CatalogEntry catalogEntry;
 	
 	@Autowired
 	private CatalogManager catalogManager;
+	@Autowired
+	private RepositoryModule repositoryModule;
 	
 	public CatalogEntryEditController(UserRequest ureq, WindowControl wControl, CatalogEntry entry) {
 		this(ureq, wControl, entry, null);
@@ -142,7 +152,16 @@ public class CatalogEntryEditController extends FormBasicController {
 		if(!styleEl.isOneSelected()) {
 			styleEl.select(styleKeys[0], true);
 		}
-		
+
+		String[] translatedNodeKeys = TranslatorHelper.translateAll(getTranslator(), sortSelectKeys);
+		String[] translatedEntryKeys = TranslatorHelper.translateAll(getTranslator(), sortSelectKeys);
+
+		translatedNodeKeys[0] += " (" + translate(sortSelectKeys[repositoryModule.getCatalogAddCategoryPosition() + 1]) + ")";
+		translatedEntryKeys[0] += " (" + translate(sortSelectKeys[repositoryModule.getCatalogAddEntryPosition() + 1]) + ")";
+
+		nodesSortSelect = uifactory.addDropdownSingleselect("sort.nodes", formLayout, sortSelectKeys, translatedNodeKeys);
+		entriesSortSelect = uifactory.addDropdownSingleselect("sort.entries", formLayout, sortSelectKeys, translatedEntryKeys);
+
 		VFSLeaf img = catalogEntry == null || catalogEntry.getKey() == null ? null : catalogManager.getImage(catalogEntry);
 		fileUpload = uifactory.addFileElement(getWindowControl(), "entry.pic", "entry.pic", formLayout);
 		fileUpload.setMaxUploadSizeKB(picUploadlimitKB, null, null);
@@ -160,6 +179,13 @@ public class CatalogEntryEditController extends FormBasicController {
 		formLayout.add(buttonLayout);
 		uifactory.addFormSubmitButton("submit", buttonLayout);
 		uifactory.addFormCancelButton("cancel", buttonLayout, ureq, getWindowControl());
+
+		if (catalogEntry.getCategoryAddPosition() != null) {
+			nodesSortSelect.select(sortSelectKeys[catalogEntry.getCategoryAddPosition() + 1], true);
+		}
+		if (catalogEntry.getEntryAddPosition() != null) {
+			entriesSortSelect.select(sortSelectKeys[catalogEntry.getEntryAddPosition() + 1], true);
+		}
 	}
 	
 	public CatalogEntry getEditedCatalogEntry() {
@@ -243,6 +269,17 @@ public class CatalogEntryEditController extends FormBasicController {
 		}
 		catalogEntry.setDescription(descriptionEl.getValue());
 		catalogEntry.setShortTitle(shortTitleEl.getValue());
+
+		if (!nodesSortSelect.getSelectedKey().equals(sortSelectKeys[0])) {
+			catalogManager.setCategoryAddPosition(catalogEntry, Arrays.asList(sortSelectKeys).indexOf(nodesSortSelect.getSelectedKey()) - 1);
+		} else {
+			catalogManager.setCategoryAddPosition(catalogEntry, null);
+		}
+		if (!entriesSortSelect.getSelectedKey().equals(sortSelectKeys[0])) {
+			catalogManager.setEntryAddPosition(catalogEntry, Arrays.asList(sortSelectKeys).indexOf(entriesSortSelect.getSelectedKey()) - 1);
+		} else {
+			catalogManager.setEntryAddPosition(catalogEntry,null);
+		}
 		
 		if(catalogEntry.getKey() == null) {
 			//a new one
diff --git a/src/main/java/org/olat/repository/ui/catalog/CatalogNodeController.java b/src/main/java/org/olat/repository/ui/catalog/CatalogNodeController.java
index 066a804a229..5a598d3af30 100644
--- a/src/main/java/org/olat/repository/ui/catalog/CatalogNodeController.java
+++ b/src/main/java/org/olat/repository/ui/catalog/CatalogNodeController.java
@@ -19,9 +19,12 @@
  */
 package org.olat.repository.ui.catalog;
 
+import java.text.Collator;
 import java.util.ArrayList;
 import java.util.Collections;
+import java.util.Comparator;
 import java.util.List;
+import java.util.stream.Collectors;
 
 import org.olat.core.dispatcher.mapper.MapperService;
 import org.olat.core.dispatcher.mapper.manager.MapperKey;
@@ -117,31 +120,43 @@ public class CatalogNodeController extends BasicController implements Activateab
 		}
 		
 		List<CatalogEntry> childCe = catalogManager.getChildrenOf(catalogEntry);
+		List<CatalogEntry> nodeEntries = childCe.stream().filter(entry -> entry != null && entry.getType() == CatalogEntry.TYPE_NODE).collect(Collectors.toList());
 		List<String> subCategories = new ArrayList<>();
 		int count = 0;
 		boolean tiles = catalogEntry.getStyle() == Style.tiles;
+
+		// Sort nodeEntries
+		if (catalogManager.isCategorySortingManually(catalogEntry)) {
+			Comparator<CatalogEntry> comparator = Comparator.comparingInt(CatalogEntry::getPosition);
+			nodeEntries.sort(comparator);
+		} else {
+			Collator collator = Collator.getInstance(getLocale());
+			collator.setStrength(Collator.IDENTICAL);
+
+			// Sort depending on view type
+			nodeEntries.sort(Comparator.comparing(tiles ? CatalogEntry::getShortTitle : CatalogEntry::getName, collator));
+		}
 		
-		for (CatalogEntry entry : childCe) {
-			if(entry != null && entry.getType() == CatalogEntry.TYPE_NODE) {
-				String cmpId = "cat_" + (++count);
-				
-				VFSLeaf img = catalogManager.getImage(entry);
-				if(img != null) {
-					String imgId = "image_" + count;
-					mainVC.contextPut(imgId, img.getName());
-				}
-				mainVC.contextPut("k" + cmpId, entry.getKey());
-				
-				String title = StringHelper.escapeHtml(tiles ? entry.getShortTitle() : entry.getName());
-				Link link = LinkFactory.createCustomLink(cmpId, "select_node", cmpId, Link.LINK + Link.NONTRANSLATED, mainVC, this);
-				link.setCustomDisplayText(title);
-				link.setIconLeftCSS("o_icon o_icon_catalog_sub");
-				link.setUserObject(entry.getKey());
-				subCategories.add(Integer.toString(count));
-				String titleId = "title_" + count;
-				mainVC.contextPut(titleId, title);
+		for (CatalogEntry entry : nodeEntries) {
+			String cmpId = "cat_" + (++count);
+
+			VFSLeaf img = catalogManager.getImage(entry);
+			if(img != null) {
+				String imgId = "image_" + count;
+				mainVC.contextPut(imgId, img.getName());
 			}
+			mainVC.contextPut("k" + cmpId, entry.getKey());
+
+			String title = StringHelper.escapeHtml(tiles ? entry.getShortTitle() : entry.getName());
+			Link link = LinkFactory.createCustomLink(cmpId, "select_node", cmpId, Link.LINK + Link.NONTRANSLATED, mainVC, this);
+			link.setCustomDisplayText(title);
+			link.setIconLeftCSS("o_icon o_icon_catalog_sub");
+			link.setUserObject(entry.getKey());
+			subCategories.add(Integer.toString(count));
+			String titleId = "title_" + count;
+			mainVC.contextPut(titleId, title);
 		}
+
 		mainVC.contextPut("subCategories", subCategories);
 
 		//catalog resources
@@ -268,4 +283,4 @@ public class CatalogNodeController extends BasicController implements Activateab
 		Collections.reverse(parentLine);
 		activate(ureq, parentLine, null);
 	}
-}
\ No newline at end of file
+}
diff --git a/src/main/java/org/olat/repository/ui/catalog/CatalogNodeManagerController.java b/src/main/java/org/olat/repository/ui/catalog/CatalogNodeManagerController.java
index c4175e3a55e..002c5bb1acd 100644
--- a/src/main/java/org/olat/repository/ui/catalog/CatalogNodeManagerController.java
+++ b/src/main/java/org/olat/repository/ui/catalog/CatalogNodeManagerController.java
@@ -19,6 +19,7 @@
  */
 package org.olat.repository.ui.catalog;
 
+import java.text.Collator;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.Comparator;
@@ -156,8 +157,11 @@ public class CatalogNodeManagerController extends FormBasicController implements
 	private Link contactLink;
 	private Link addCategoryLink;
 	private Link addResourceLink;
-	private Link orderLink;
-	
+	private Link orderManuallyLink;
+
+	private boolean showCategoryUpDownColumn;
+	private boolean showEntryUpDownColumn;
+
 	private DefaultFlexiColumnModel leafUpColumnModel;
 	private DefaultFlexiColumnModel leafDownColumnModel;
 	private DefaultFlexiColumnModel leafPositionColumnModel;
@@ -257,8 +261,20 @@ public class CatalogNodeManagerController extends FormBasicController implements
 			isLocalTreeAdmin = localTreeAdmin || catalogManager.isOwner(catalogEntry, getIdentity());
 		}
 
+		if(catalogEntry.getEntryAddPosition() == null) {
+			showEntryUpDownColumn = repositoryModule.getCatalogAddEntryPosition() != 0;
+		} else {
+			showEntryUpDownColumn = catalogEntry.getEntryAddPosition() != 0;
+		}
+
+		if(catalogEntry.getCategoryAddPosition() == null) {
+			showCategoryUpDownColumn = repositoryModule.getCatalogAddCategoryPosition() != 0;
+		} else {
+			showCategoryUpDownColumn = catalogEntry.getCategoryAddPosition() != 0;
+		}
+
 		initForm(ureq);
-		
+
 		loadEntryInfos();
 		loadNodesChildren();
 		loadResources(ureq);
@@ -289,25 +305,25 @@ public class CatalogNodeManagerController extends FormBasicController implements
 		
 		leafColumns = new ArrayList<>();
 		
-		FlexiTableColumnModel entriesColumnsModel = getCatalogFlexiTableColumnModel("opened-", !isOrdering);
+		FlexiTableColumnModel entriesColumnsModel = getCatalogFlexiTableColumnModel("opened-", !isOrdering, showEntryUpDownColumn);
 		entriesModel = new CatalogEntryRowModel(entriesColumnsModel);
-		entriesEl = uifactory.addTableElement(getWindowControl(), "entries", entriesModel, getTranslator(), formLayout);
+		entriesEl = uifactory.addTableElement(getWindowControl(), "entries", entriesModel, 20, false, getTranslator(), formLayout);
 		
-		FlexiTableColumnModel closedEntriesColumnsModel = getCatalogFlexiTableColumnModel("closed-", !isOrdering);
+		FlexiTableColumnModel closedEntriesColumnsModel = getCatalogFlexiTableColumnModel("closed-", !isOrdering, showEntryUpDownColumn);
 		closedEntriesModel = new CatalogEntryRowModel(closedEntriesColumnsModel);
-		closedEntriesEl = uifactory.addTableElement(getWindowControl(), "closedEntries", closedEntriesModel, getTranslator(), formLayout);
+		closedEntriesEl = uifactory.addTableElement(getWindowControl(), "closedEntries", closedEntriesModel, 20, false, getTranslator(), formLayout);
 		
-		FlexiTableColumnModel nodeEntriesColumnsModel = getNodeFlexiTableColumnModel("nodes-");
+		FlexiTableColumnModel nodeEntriesColumnsModel = getNodeFlexiTableColumnModel("nodes-", showCategoryUpDownColumn);
 		nodeEntriesModel = new NodeEntryRowModel(nodeEntriesColumnsModel);
 		nodeEntriesEl = uifactory.addTableElement(getWindowControl(), "nodeEntries", nodeEntriesModel, getTranslator(), formLayout);
 	}
 	
-	private FlexiTableColumnModel getCatalogFlexiTableColumnModel(String cmdPrefix, boolean sortEnabled) {
+	private FlexiTableColumnModel getCatalogFlexiTableColumnModel(String cmdPrefix, boolean sortEnabled, boolean showUpDownColumn) {
 		//add the table
 		FlexiTableColumnModel columnsModel = FlexiTableDataModelFactory.createFlexiTableColumnModel();
 		DefaultFlexiColumnModel columnModel;
 		
-		if (!sortEnabled) {
+		if (!sortEnabled && showUpDownColumn) {
 			leafUpColumnModel = new DefaultFlexiColumnModel(true, Cols.up.i18nKey(), Cols.up.ordinal(), CMD_UP, false, null);
 			leafUpColumnModel.setCellRenderer(new BooleanCellRenderer(
 					new StaticFlexiCellRenderer("", CMD_UP, "o_icon o_icon-lg o_icon_move_up"),
@@ -408,32 +424,34 @@ public class CatalogNodeManagerController extends FormBasicController implements
 		return columnsModel;
 	}
 	
-	private FlexiTableColumnModel getNodeFlexiTableColumnModel(String cmdPrefix) {
+	private FlexiTableColumnModel getNodeFlexiTableColumnModel(String cmdPrefix, boolean showUpDownColumn) {
 		//add the table
 		FlexiTableColumnModel columnsModel = FlexiTableDataModelFactory.createFlexiTableColumnModel();
-		
-		nodeUpColumnModel = new DefaultFlexiColumnModel(true, NodeCols.up.i18nKey(), NodeCols.up.ordinal(), CMD_UP, false, null);
-		nodeUpColumnModel.setCellRenderer(new BooleanCellRenderer(
-				new StaticFlexiCellRenderer("", CMD_UP, "o_icon o_icon_fw o_icon-lg o_icon_move_up"),
-				null));
-		nodeUpColumnModel.setIconHeader("o_icon o_icon_fw o_icon-lg o_icon_move_up");
-		nodeUpColumnModel.setAlignment(FlexiColumnModel.ALIGNMENT_ICON);
-		nodeUpColumnModel.setAlwaysVisible(true);
 
-		
-		nodeDownColumnModel = new DefaultFlexiColumnModel(true, NodeCols.down.i18nKey(), NodeCols.down.ordinal(), CMD_DOWN, false, null);
-		nodeDownColumnModel.setCellRenderer(new BooleanCellRenderer(
-				new StaticFlexiCellRenderer("", CMD_DOWN, "o_icon o_icon_fw o_icon-lg o_icon_move_down"),
-				null));
-		nodeDownColumnModel.setIconHeader("o_icon o_icon_fw o_icon-lg o_icon_move_down");
-		nodeDownColumnModel.setAlignment(FlexiColumnModel.ALIGNMENT_ICON);
-		nodeDownColumnModel.setAlwaysVisible(true);
-		
-		nodePositionColumnModel = new DefaultFlexiColumnModel(true, NodeCols.position.i18nKey(), NodeCols.position.ordinal(), false, null);
-		
-		columnsModel.addFlexiColumnModel(nodeUpColumnModel);
-		columnsModel.addFlexiColumnModel(nodeDownColumnModel);
-		columnsModel.addFlexiColumnModel(nodePositionColumnModel);
+		if (showUpDownColumn) {
+			nodeUpColumnModel = new DefaultFlexiColumnModel(true, NodeCols.up.i18nKey(), NodeCols.up.ordinal(), CMD_UP, false, null);
+			nodeUpColumnModel.setCellRenderer(new BooleanCellRenderer(
+					new StaticFlexiCellRenderer("", CMD_UP, "o_icon o_icon_fw o_icon-lg o_icon_move_up"),
+					null));
+			nodeUpColumnModel.setIconHeader("o_icon o_icon_fw o_icon-lg o_icon_move_up");
+			nodeUpColumnModel.setAlignment(FlexiColumnModel.ALIGNMENT_ICON);
+			nodeUpColumnModel.setAlwaysVisible(true);
+
+
+			nodeDownColumnModel = new DefaultFlexiColumnModel(true, NodeCols.down.i18nKey(), NodeCols.down.ordinal(), CMD_DOWN, false, null);
+			nodeDownColumnModel.setCellRenderer(new BooleanCellRenderer(
+					new StaticFlexiCellRenderer("", CMD_DOWN, "o_icon o_icon_fw o_icon-lg o_icon_move_down"),
+					null));
+			nodeDownColumnModel.setIconHeader("o_icon o_icon_fw o_icon-lg o_icon_move_down");
+			nodeDownColumnModel.setAlignment(FlexiColumnModel.ALIGNMENT_ICON);
+			nodeDownColumnModel.setAlwaysVisible(true);
+
+			nodePositionColumnModel = new DefaultFlexiColumnModel(true, NodeCols.position.i18nKey(), NodeCols.position.ordinal(), false, null);
+
+			columnsModel.addFlexiColumnModel(nodeUpColumnModel);
+			columnsModel.addFlexiColumnModel(nodeDownColumnModel);
+			columnsModel.addFlexiColumnModel(nodePositionColumnModel);
+		}
 		columnsModel.addFlexiColumnModel(new DefaultFlexiColumnModel(false, NodeCols.key.i18nKey(), NodeCols.key.ordinal(), false, null));
 		columnsModel.addFlexiColumnModel(new DefaultFlexiColumnModel(true, NodeCols.displayName.i18nKey(), NodeCols.displayName.ordinal(), false, null));
 		columnsModel.addFlexiColumnModel(new DefaultFlexiColumnModel(false, NodeCols.creationDate.i18nKey(), NodeCols.creationDate.ordinal(), false, null));
@@ -523,13 +541,19 @@ public class CatalogNodeManagerController extends FormBasicController implements
 				items.add(row);
 			}
 		}
-		
-		Comparator<CatalogEntryRow> comparator = (row1, row2) -> {
-			return row1.getPosition().compareTo(row2.getPosition());
-		};
 
-		Collections.sort(items, comparator);
-		Collections.sort(closedItems, comparator);
+		if (catalogManager.isEntrySortingManually(catalogEntry)) {
+			Comparator<CatalogEntryRow> comparator = Comparator.comparing(CatalogEntryRow::getPosition);
+
+			items.sort(comparator);
+			closedItems.sort(comparator);
+		} else {
+			Collator collator = Collator.getInstance(getLocale());
+			collator.setStrength(Collator.IDENTICAL);
+
+			items.sort(Comparator.comparing(CatalogEntryRow::getDisplayname, collator));
+			closedItems.sort(Comparator.comparing(CatalogEntryRow::getDisplayname, collator));
+		}
 		
 		entriesModel.setObjects(items);
 		entriesEl.reset(true, true, true);
@@ -549,6 +573,20 @@ public class CatalogNodeManagerController extends FormBasicController implements
 		List<NodeEntryRow> nodeEntries = new ArrayList<>();
 		int count = 0;
 		boolean tiles = catalogEntry.getStyle() == Style.tiles;
+
+		if (catalogManager.isCategorySortingManually(catalogEntry)) {
+			Comparator<CatalogEntry> comparator = Comparator.comparingInt(CatalogEntry::getPosition);
+			catalogChildren.sort(comparator);
+		} else {
+			Collator collator = Collator.getInstance(getLocale());
+			collator.setStrength(Collator.IDENTICAL);
+
+			if (catalogEntry.getStyle().equals(Style.tiles)) {
+				catalogChildren.sort(Comparator.comparing(entry -> entry.getShortTitle() != null ? entry.getShortTitle() : entry.getName(), collator));
+			} else {
+				catalogChildren.sort(Comparator.comparing(CatalogEntry::getName, collator));
+			}
+		}
 		
 		for (CatalogEntry entry : catalogChildren) {
 			if(entry != null && entry.getType() == CatalogEntry.TYPE_NODE) {
@@ -581,12 +619,16 @@ public class CatalogNodeManagerController extends FormBasicController implements
 		}
 		flc.contextPut("subCategories", subCategories);
 
-		Comparator<NodeEntryRow> comparator = (row1, row2) -> {
-			return ((Integer)row1.getPosition()).compareTo(row2.getPosition());
-		};
+		if (catalogManager.isCategorySortingManually(catalogEntry)) {
+			Comparator<NodeEntryRow> comparator = Comparator.comparingInt(NodeEntryRow::getPosition);
+			nodeEntries.sort(comparator);
+		} else {
+			Collator collator = Collator.getInstance(getLocale());
+			collator.setStrength(Collator.IDENTICAL);
+
+			nodeEntries.sort(Comparator.comparing(NodeEntryRow::getDisplayname, collator));
+		}
 
-		Collections.sort(nodeEntries, comparator);
-	
 		nodeEntriesModel.setObjects(nodeEntries);
 		nodeEntriesEl.reset(true, true, true);
 		nodeEntriesEl.setVisible(nodeEntriesModel.getRowCount() > 0);
@@ -601,12 +643,12 @@ public class CatalogNodeManagerController extends FormBasicController implements
 	
 		if (canAdministrateCategory || canAddLinks) {
 			if (canAdministrateCategory) {
-				if (orderLink == null) {
-					orderLink = LinkFactory.createToolLink("order", translate("tools.order.catalog"), this, "o_icon_order");
-					orderLink.setElementCssClass("o_sel_catalog_order_category");
-					toolbarPanel.addTool(orderLink, Align.right);
+				if (orderManuallyLink == null) {
+					orderManuallyLink = LinkFactory.createToolLink("order", translate("tools.order.catalog"), this, "o_icon_order");
+					orderManuallyLink.setElementCssClass("o_sel_catalog_order_category");
+					toolbarPanel.addTool(orderManuallyLink, Align.right);
 				} else {
-					orderLink.setVisible(true);
+					orderManuallyLink.setVisible(true);
 				}
 			}
 			if (canAdministrateCategory) {
@@ -809,9 +851,8 @@ public class CatalogNodeManagerController extends FormBasicController implements
 				
 				doOpenPositionDialog(ureq, link, smallest, biggest);
 			}
-
 		}
-		
+
 		super.formInnerEvent(ureq, source, event);
 	}
 
@@ -827,7 +868,7 @@ public class CatalogNodeManagerController extends FormBasicController implements
 			doConfirmDelete(ureq);
 		} else if(moveLink == source) {
 			doMoveCategory(ureq);
-		} else if (orderLink == source) {
+		} else if (orderManuallyLink == source) {
 			doActivateOrdering(ureq);
 		} else if(addCategoryLink == source) {
 			doAddCategory(ureq);
@@ -857,7 +898,6 @@ public class CatalogNodeManagerController extends FormBasicController implements
 				loadResources(ureq);
 				loadEntryInfos();
 			}
-			
 		}
 		super.event(ureq, source, event);
 	}
@@ -929,6 +969,10 @@ public class CatalogNodeManagerController extends FormBasicController implements
 				RepositoryEntry selectedEntry = entrySearchCtrl.getSelectedEntry();
 				doAddResource(ureq, selectedEntry);
 				fireEvent(ureq, Event.CHANGED_EVENT);
+			} else if(event.getCommand().equals(RepositoryTableModel.TABLE_ACTION_SELECT_ENTRIES)) {
+				List<RepositoryEntry> selectedEntries = entrySearchCtrl.getSelectedEntries();
+				selectedEntries.forEach(entry -> doAddResource(ureq, entry));
+				fireEvent(ureq, Event.CHANGED_EVENT);
 			}
 			cmc.deactivate();
 			cleanUp();
@@ -1100,7 +1144,7 @@ public class CatalogNodeManagerController extends FormBasicController implements
 
 		catModificationLock = CoordinatorManager.getInstance().getCoordinator().getLocker().acquireLock(lockRes, getIdentity(), LOCK_TOKEN, getWindow());
 		if (catModificationLock.isSuccess()) {
-			entrySearchCtrl = new RepositorySearchController(translate("choose"), ureq, getWindowControl(), true, false, new String[0], false, null);
+			entrySearchCtrl = new RepositorySearchController(translate("choose"), ureq, getWindowControl(), true, repositoryModule.isCatalogMultiSelectEnabled(), new String[0], false, null);
 			listenTo(entrySearchCtrl);
 			// OLAT-Admin has search form
 			if (isAdministrator) {
@@ -1290,4 +1334,4 @@ public class CatalogNodeManagerController extends FormBasicController implements
 		String businessPath = "[CatalogAdmin:0][CatalogEntry:" + ref.getKey() + "]";
 		NewControllerFactory.getInstance().launch(businessPath, ureq, getWindowControl());
 	}
-}
\ No newline at end of file
+}
diff --git a/src/main/java/org/olat/repository/ui/catalog/_content/node.html b/src/main/java/org/olat/repository/ui/catalog/_content/node.html
index 1d504e1ffdd..4b2db8e2f0a 100644
--- a/src/main/java/org/olat/repository/ui/catalog/_content/node.html
+++ b/src/main/java/org/olat/repository/ui/catalog/_content/node.html
@@ -75,6 +75,9 @@
 	## Render node list component
 	#if($isOrdering)
 		$r.render("nodeEntries")
+		#if($r.available("entries"))
+			<hr>
+		#end
 	#end
 	
 	## Render course list component
diff --git a/src/main/java/org/olat/repository/ui/catalog/_i18n/LocalStrings_de.properties b/src/main/java/org/olat/repository/ui/catalog/_i18n/LocalStrings_de.properties
index 8c6e939492e..77ed6a2275d 100644
--- a/src/main/java/org/olat/repository/ui/catalog/_i18n/LocalStrings_de.properties
+++ b/src/main/java/org/olat/repository/ui/catalog/_i18n/LocalStrings_de.properties
@@ -1,4 +1,9 @@
 #Mon Mar 02 09:54:04 CET 2009
+add.default=Standard
+add.alphabetically=Automatisch - Alphabetische Sortierung
+add.bottom=Manuell - Neue Eintr\u00E4ge am Ende
+add.new.entries=Neue Eintr\u00E4ge hinzuf\u00FCgen: 
+add.top=Manuell - Neue Eintr\u00E4ge am Anfang
 admin.menu.title=Katalog
 admin.menu.title.alt=Katalog
 admin.catalog.settings=Katalog settings
@@ -16,7 +21,7 @@ catalog.popup.position.biggest=\u226B
 catalog.popup.position.save=Speichern
 catalog.popup.position.smaller=\u003C
 catalog.popup.position.smallest=\u226A
-catalog.position.deactivated=Bitte f\u00FCgen Sie weiter Elemente hinzu um die Sortierung zu verändern!
+catalog.position.deactivated=Bitte f\u00FCgen Sie weiter Elemente hinzu um die Sortierung zu ver�ndern!
 catalog.tree.add.already.exists=Die Lernressource {0} ist bereits in dieser Katalogkategorie vorhanden.
 catalog.tree.add.intro=W\u00E4hlen Sie eine Kategorie aus, in die Sie die Lernressource {0} verschieben m\u00F6chten.
 catalog.tree.add.title=Lernressource "{0}" in Katalog hinzuf\u00FCgen
@@ -47,6 +52,8 @@ filtered.second=).
 move=Verschieben
 no.leaves=Dieser Kategorie sind keine Lernressourcen zugeteilt.
 repo.nocategories=Diese Lernressource ist noch nicht im Katalog eingebunden.
+sort.nodes=Sortierung der Kategorien
+sort.entries=Sortierung der Eintr\u00E4ge
 title.categories=Unterkategorien
 title.leaves=Lernressourcen
 tocontent=zum Inhalt
@@ -63,7 +70,7 @@ tools.move.catalog.entry=Verschieben
 tools.move.catalog.entry.failed=Es ist ein Fehler aufgetreten, das Element konnte nicht verschoben werden.
 tools.move.catalog.entry.success=Der Katalogeintrag "{0}" wurde erfolgreich verschoben.
 tools.new.catalog.categoryrequest=Verwalter kontaktieren
-tools.order.catalog=Manuell ordnen
+tools.order.catalog=Ordnen
 tools.pastestructure=Struktur einf\u00FCgen
 tools.set.catalog.position=Position anpassen
 entry.pic=Bild
diff --git a/src/main/java/org/olat/repository/ui/catalog/_i18n/LocalStrings_en.properties b/src/main/java/org/olat/repository/ui/catalog/_i18n/LocalStrings_en.properties
index 7e90e35117d..cbb11acee4f 100644
--- a/src/main/java/org/olat/repository/ui/catalog/_i18n/LocalStrings_en.properties
+++ b/src/main/java/org/olat/repository/ui/catalog/_i18n/LocalStrings_en.properties
@@ -1,4 +1,8 @@
 #Tue Dec 16 09:07:21 CET 2014
+add.default=Default
+add.alphabetically=Automatic - Alphabetical order
+add.bottom=Manual - New items at last
+add.top=Manual - New items on top
 admin.catalog.settings=Catalog settings
 admin.menu.title=Catalog
 admin.menu.title.alt=Catalog
@@ -46,6 +50,9 @@ list.compact=Compact list
 move=Move
 no.leaves=No learning resources assigned to this category
 repo.nocategories=This learning resource is not yet added to the catalog
+sort.alphabetically=Sort alphabetically
+sort.nodes=Sorting of categories
+sort.entries=Sorting of entries
 style=Style
 tiles=Tiles
 title.categories=Sub-categories
@@ -63,7 +70,7 @@ tools.edit.header=Category
 tools.move.catalog.entry=Move
 tools.move.catalog.entry.failed=An error occurred. This element could not be moved.
 tools.move.catalog.entry.success=Catalog entry "{0}" successfully moved
-tools.order.catalog=Manual ordering
+tools.order.catalog=Ordering
 tools.new.catalog.categoryrequest=Contact administrator
 tools.pastestructure=Insert structure
 tools.set.catalog.position=Modifiy position
diff --git a/src/main/java/org/olat/upgrade/OLATUpgrade_15_2_3.java b/src/main/java/org/olat/upgrade/OLATUpgrade_15_2_3.java
new file mode 100644
index 00000000000..8733c5ff6b4
--- /dev/null
+++ b/src/main/java/org/olat/upgrade/OLATUpgrade_15_2_3.java
@@ -0,0 +1,136 @@
+/**
+ * <a href="http://www.openolat.org">
+ * OpenOLAT - Online Learning and Training</a><br>
+ * <p>
+ * Licensed under the Apache License, Version 2.0 (the "License"); <br>
+ * you may not use this file except in compliance with the License.<br>
+ * You may obtain a copy of the License at the
+ * <a href="http://www.apache.org/licenses/LICENSE-2.0">Apache homepage</a>
+ * <p>
+ * Unless required by applicable law or agreed to in writing,<br>
+ * software distributed under the License is distributed on an "AS IS" BASIS, <br>
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. <br>
+ * See the License for the specific language governing permissions and <br>
+ * limitations under the License.
+ * <p>
+ * Initial code contributed and copyrighted by<br>
+ * frentix GmbH, http://www.frentix.com
+ * <p>
+ */
+package org.olat.upgrade;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.file.Paths;
+import java.util.Properties;
+
+import org.apache.logging.log4j.Logger;
+import org.olat.core.logging.Tracing;
+import org.olat.core.util.WebappHelper;
+import org.olat.repository.RepositoryModule;
+import org.olat.repository.manager.CatalogManager;
+import org.springframework.beans.factory.annotation.Autowired;
+
+/**
+ * 
+ * Initial date: 2 juin 2020<br>
+ * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com
+ *
+ */
+public class OLATUpgrade_15_2_3 extends OLATUpgrade {
+
+	private static final Logger log = Tracing.createLoggerFor(OLATUpgrade_15_2_3.class);
+
+	private static final String VERSION = "OLAT_15.2.3";
+	private static final String MIGRATE_CATALOG_SORTING = "MIGRATE CATALOG SORTING";
+
+	private static final String CATALOG_ADD_LAST = "catalog.add.last";
+
+	@Autowired
+	RepositoryModule repositoryModule;
+	@Autowired
+	CatalogManager catalogManager;
+
+	public OLATUpgrade_15_2_3() {
+		super();
+	}
+	
+	@Override
+	public String getVersion() {
+		return VERSION;
+	}
+
+	@Override
+	public boolean doPostSystemInitUpgrade(UpgradeManager upgradeManager) {
+		UpgradeHistoryData uhd = upgradeManager.getUpgradesHistory(VERSION);
+		if (uhd == null) {
+			// has never been called, initialize
+			uhd = new UpgradeHistoryData();
+		} else if (uhd.isInstallationComplete()) {
+			return false;
+		}
+		
+		boolean allOk = true;
+		allOk &= migrateCatalogSorting(upgradeManager, uhd);
+
+		uhd.setInstallationComplete(allOk);
+		upgradeManager.setUpgradesHistory(uhd, VERSION);
+		if(allOk) {
+			log.info(Tracing.M_AUDIT, "Finished OLATUpgrade_15_2_2 successfully!");
+		} else {
+			log.info(Tracing.M_AUDIT, "OLATUpgrade_15_2_2 not finished, try to restart OpenOlat!");
+		}
+		return allOk;
+	}
+
+	private boolean migrateCatalogSorting(UpgradeManager upgradeManager, UpgradeHistoryData uhd) {
+		boolean allOk = true;
+		if (!uhd.getBooleanDataValue(MIGRATE_CATALOG_SORTING)) {
+			String userDataDirectory = WebappHelper.getUserDataRoot();
+			File configurationPropertiesFile = Paths.get(userDataDirectory, "system", "configuration", "org.olat.repository.RepositoryModule.properties").toFile();
+			if (configurationPropertiesFile.exists()) {
+				InputStream is = null;
+				OutputStream fileStream = null;
+				try {
+					is = new FileInputStream(configurationPropertiesFile);
+					Properties configuredProperties = new Properties();
+					configuredProperties.load(is);
+					is.close();
+
+					String addAtLast = configuredProperties.getProperty(CATALOG_ADD_LAST);
+					if (addAtLast != null) {
+						if (addAtLast.equals("true")) {
+							// Add at last
+							repositoryModule.setCatalogAddCategoryPosition(2);
+							repositoryModule.setCatalogAddEntryPosition(2);
+						} else if (addAtLast.equals("false")) {
+							// Add at first
+							repositoryModule.setCatalogAddCategoryPosition(1);
+							repositoryModule.setCatalogAddEntryPosition(1);
+						}
+					} else {
+						// Add alphabetically
+						repositoryModule.setCatalogAddCategoryPosition(0);
+						repositoryModule.setCatalogAddEntryPosition(0);
+					}
+				} catch (Exception e) {
+					log.error("Error when reading / writing user properties config file from path::" + configurationPropertiesFile.getAbsolutePath(), e);
+					allOk &= false;
+				} finally {
+					try {
+						if (is != null ) is.close();
+					} catch (Exception e) {
+						log.error("Could not close stream from " + configurationPropertiesFile.getAbsolutePath(), e);
+						allOk &= false;
+					}
+				}
+			}
+
+			uhd.setBooleanDataValue(MIGRATE_CATALOG_SORTING, allOk);
+			upgradeManager.setUpgradesHistory(uhd, VERSION);
+		}
+		return allOk;
+	}
+}
diff --git a/src/main/java/org/olat/upgrade/_spring/databaseUpgradeContext.xml b/src/main/java/org/olat/upgrade/_spring/databaseUpgradeContext.xml
index 0eaf7eae050..2b176470c00 100644
--- a/src/main/java/org/olat/upgrade/_spring/databaseUpgradeContext.xml
+++ b/src/main/java/org/olat/upgrade/_spring/databaseUpgradeContext.xml
@@ -284,8 +284,16 @@
 					<constructor-arg index="0" value="OLAT_15.2.1" />
 					<property name="alterDbStatements" value="alter_15_2_x_to_15_2_1.sql" />
 				</bean>
+				<bean id="database_upgrade_15_2_3" class="org.olat.upgrade.DatabaseUpgrade">
+					<constructor-arg index="0" value="OLAT_15.2.3" />
+					<property name="alterDbStatements" value="alter_15_2_x_to_15_2_3.sql" />
+				</bean>
+				<bean id="database_upgrade_15_3_0" class="org.olat.upgrade.DatabaseUpgrade">
+					<constructor-arg index="0" value="OLAT_15.3.0" />
+					<property name="alterDbStatements" value="alter_15_2_x_to_15_3_0.sql" />
+				</bean>
 			</list>
 		</property>
 	</bean>
 
-</beans>
\ No newline at end of file
+</beans>
diff --git a/src/main/java/org/olat/upgrade/_spring/upgradeContext.xml b/src/main/java/org/olat/upgrade/_spring/upgradeContext.xml
index e8ff361b0ec..93e543551db 100644
--- a/src/main/java/org/olat/upgrade/_spring/upgradeContext.xml
+++ b/src/main/java/org/olat/upgrade/_spring/upgradeContext.xml
@@ -64,8 +64,9 @@
 				<bean id="upgrade_15_pre_6_ae" class="org.olat.upgrade.OLATUpgrade_15_pre_6_ae"/>
 				<bean id="upgrade_15_1_0" class="org.olat.upgrade.OLATUpgrade_15_1_0"/>
 				<bean id="upgrade_15_2_0" class="org.olat.upgrade.OLATUpgrade_15_2_0"/>
+				<bean id="upgrade_15_2_3" class="org.olat.upgrade.OLATUpgrade_15_2_3"/>
 			</list>
 		</property>
 	</bean>
 	
-</beans>
\ No newline at end of file
+</beans>
diff --git a/src/main/resources/database/mysql/alter_15_2_x_to_15_2_3.sql b/src/main/resources/database/mysql/alter_15_2_x_to_15_2_3.sql
new file mode 100644
index 00000000000..c5c25c2ee6f
--- /dev/null
+++ b/src/main/resources/database/mysql/alter_15_2_x_to_15_2_3.sql
@@ -0,0 +1,2 @@
+alter table o_catentry add column add_entry_position int;
+alter table o_catentry add column add_category_position int;
\ No newline at end of file
diff --git a/src/main/resources/database/oracle/alter_15_2_x_to_15_2_3.sql b/src/main/resources/database/oracle/alter_15_2_x_to_15_2_3.sql
new file mode 100644
index 00000000000..3cddf35305f
--- /dev/null
+++ b/src/main/resources/database/oracle/alter_15_2_x_to_15_2_3.sql
@@ -0,0 +1,2 @@
+alter table o_catentry add add_entry_position number default null;
+alter table o_catentry add add_category_position number default null;
\ No newline at end of file
diff --git a/src/main/resources/database/postgresql/alter_15_2_x_to_15_2_3.sql b/src/main/resources/database/postgresql/alter_15_2_x_to_15_2_3.sql
new file mode 100644
index 00000000000..c5c25c2ee6f
--- /dev/null
+++ b/src/main/resources/database/postgresql/alter_15_2_x_to_15_2_3.sql
@@ -0,0 +1,2 @@
+alter table o_catentry add column add_entry_position int;
+alter table o_catentry add column add_category_position int;
\ No newline at end of file
-- 
GitLab