diff --git a/src/main/java/com/netgrif/application/engine/menu/domain/MenuItemBody.java b/src/main/java/com/netgrif/application/engine/menu/domain/MenuItemBody.java index ea8ef6dd76..3d42b8da67 100644 --- a/src/main/java/com/netgrif/application/engine/menu/domain/MenuItemBody.java +++ b/src/main/java/com/netgrif/application/engine/menu/domain/MenuItemBody.java @@ -10,9 +10,7 @@ import lombok.Data; import lombok.NoArgsConstructor; -import java.util.HashMap; -import java.util.List; -import java.util.Map; +import java.util.*; /** * Class, that holds configurable attributes of menu item. @@ -26,6 +24,7 @@ public class MenuItemBody { private String menuIcon = "filter_none"; private I18nString menuName; + private List allowedAuthorities = List.of("ROLE_USER"); private Map allowedRoles = new HashMap<>(); private Map bannedRoles = new HashMap<>(); private boolean useCustomView = false; @@ -198,6 +197,7 @@ public ToDataSetOutcome toDataSet(String parentId, String nodePath, Case viewCas outcome.putDataSetEntry(MenuItemConstants.FIELD_CUSTOM_VIEW_SELECTOR, FieldType.TEXT, this.customViewSelector); outcome.putDataSetEntry(MenuItemConstants.FIELD_IS_AUTO_SELECT, FieldType.BOOLEAN, this.isAutoSelect); + outcome.putDataSetEntry(MenuItemConstants.FIELD_ALLOWED_AUTHORITIES, FieldType.MULTICHOICE_MAP, this.allowedAuthorities); outcome.putDataSetEntryOptions(MenuItemConstants.FIELD_ALLOWED_ROLES, FieldType.MULTICHOICE_MAP, this.allowedRoles); outcome.putDataSetEntryOptions(MenuItemConstants.FIELD_BANNED_ROLES, FieldType.MULTICHOICE_MAP, this.bannedRoles); outcome.putDataSetEntry(MenuItemConstants.FIELD_CONFIGURATION_TEMPLATES, FieldType.ENUMERATION_MAP, this.configurationTemplateIdentifier); diff --git a/src/main/java/com/netgrif/application/engine/menu/domain/MenuItemConstants.java b/src/main/java/com/netgrif/application/engine/menu/domain/MenuItemConstants.java index c4ab1f141c..895f7fc55c 100644 --- a/src/main/java/com/netgrif/application/engine/menu/domain/MenuItemConstants.java +++ b/src/main/java/com/netgrif/application/engine/menu/domain/MenuItemConstants.java @@ -9,6 +9,7 @@ public class MenuItemConstants { public static final String FIELD_IS_FOLDER = "is_folder"; public static final String FIELD_IDENTIFIER = "menu_item_identifier"; public static final String FIELD_APPEND_MENU_ITEM = "append_menu_item_stringId"; + public static final String FIELD_ALLOWED_AUTHORITIES = "allowed_authorities"; public static final String FIELD_ALLOWED_ROLES = "allowed_roles"; public static final String FIELD_BANNED_ROLES = "banned_roles"; public static final String FIELD_MENU_NAME = "menu_name"; diff --git a/src/main/java/com/netgrif/application/engine/menu/service/MenuItemTemplateHolder.java b/src/main/java/com/netgrif/application/engine/menu/service/MenuItemTemplateHolder.java index c3573264c2..8037e245da 100644 --- a/src/main/java/com/netgrif/application/engine/menu/service/MenuItemTemplateHolder.java +++ b/src/main/java/com/netgrif/application/engine/menu/service/MenuItemTemplateHolder.java @@ -5,6 +5,7 @@ import com.netgrif.application.engine.menu.utils.MenuItemUtils; import com.netgrif.application.engine.petrinet.domain.I18nString; +import java.util.LinkedHashMap; import java.util.Map; import java.util.Optional; import java.util.stream.Collectors; @@ -26,16 +27,19 @@ public class MenuItemTemplateHolder { * The key is the template identifier (String), and the value is the Template instance. *

*/ - private final static Map templates = Map.of( - TabbedCaseViewTemplate.IDENTIFIER, new TabbedCaseViewTemplate(), - TabbedTaskViewTemplate.IDENTIFIER, new TabbedTaskViewTemplate(), - SimpleCaseViewTemplate.IDENTIFIER, new SimpleCaseViewTemplate(), - SimpleTaskViewTemplate.IDENTIFIER, new SimpleTaskViewTemplate(), - SingleTaskViewTemplate.IDENTIFIER, new SingleTaskViewTemplate(), - TabbedTicketViewTemplate.IDENTIFIER, new TabbedTicketViewTemplate(), - CustomViewTemplate.IDENTIFIER, new CustomViewTemplate(), - FolderTemplate.IDENTIFIER, new FolderTemplate() - ); + private final static Map templates; + + static { + templates = new LinkedHashMap<>(); + templates.put(FolderTemplate.IDENTIFIER, new FolderTemplate()); + templates.put(TabbedCaseViewTemplate.IDENTIFIER, new TabbedCaseViewTemplate()); + templates.put(TabbedTaskViewTemplate.IDENTIFIER, new TabbedTaskViewTemplate()); + templates.put(SimpleCaseViewTemplate.IDENTIFIER, new SimpleCaseViewTemplate()); + templates.put(SimpleTaskViewTemplate.IDENTIFIER, new SimpleTaskViewTemplate()); + templates.put(SingleTaskViewTemplate.IDENTIFIER, new SingleTaskViewTemplate()); + templates.put(TabbedTicketViewTemplate.IDENTIFIER, new TabbedTicketViewTemplate()); + templates.put(CustomViewTemplate.IDENTIFIER, new CustomViewTemplate()); + } /** @@ -116,7 +120,9 @@ public static Map transformToOptions() { return templates.entrySet().stream() .collect(Collectors.toMap( Map.Entry::getKey, - entry -> entry.getValue().getName() + entry -> entry.getValue().getName(), + (a, b) -> a, + LinkedHashMap::new )); } } diff --git a/src/main/java/com/netgrif/application/engine/menu/utils/MenuItemUtils.java b/src/main/java/com/netgrif/application/engine/menu/utils/MenuItemUtils.java index b7b3107a8b..a899f69815 100644 --- a/src/main/java/com/netgrif/application/engine/menu/utils/MenuItemUtils.java +++ b/src/main/java/com/netgrif/application/engine/menu/utils/MenuItemUtils.java @@ -92,7 +92,10 @@ public static String findTaskIdInCase(Case useCase, String transId) { * */ public static boolean isCyclicNodePath(Case folderItem, String destUri) { String oldNodePath = (String) folderItem.getFieldValue(MenuItemConstants.FIELD_NODE_PATH); - return oldNodePath != null && destUri.contains(oldNodePath); + if (oldNodePath == null || destUri == null) { + return false; + } + return destUri.contains(oldNodePath + "/") || destUri.endsWith(oldNodePath); } /** diff --git a/src/main/resources/petriNets/engine-processes/menu/case_view_configuration.xml b/src/main/resources/petriNets/engine-processes/menu/case_view_configuration.xml index 77bf89b0f7..2329ace50e 100644 --- a/src/main/resources/petriNets/engine-processes/menu/case_view_configuration.xml +++ b/src/main/resources/petriNets/engine-processes/menu/case_view_configuration.xml @@ -155,6 +155,24 @@ show_create_case_button Show create case button? true + + 0 + + + trans: t.create_case_btn_settings, + show_create_case_button: f.show_create_case_button, + require_title_in_creation: f.require_title_in_creation, + create_case_button_title: f.create_case_button_title, + create_case_button_icon: f.create_case_button_icon, + create_case_button_icon_preview: f.create_case_button_icon_preview, + banned_nets_in_creation: f.banned_nets_in_creation; + + make [require_title_in_creation, create_case_button_title, create_case_button_icon, create_case_button_icon_preview, banned_nets_in_creation], hidden on trans when { !show_create_case_button.value } + make [require_title_in_creation, create_case_button_title, create_case_button_icon, banned_nets_in_creation], editable on trans when { show_create_case_button.value } + make create_case_button_icon_preview, visible on trans when { show_create_case_button.value } + + + require_title_in_creation @@ -351,6 +369,20 @@ case_all_allowed_nets All allowed nets? true + + 0 + + + trans: t.settings, + case_all_allowed_nets: f.case_all_allowed_nets, + case_allowed_nets: f.case_allowed_nets, + case_inherit_allowed_nets: f.case_inherit_allowed_nets; + + make [case_allowed_nets, case_inherit_allowed_nets], hidden on trans when { case_all_allowed_nets.value } + make [case_allowed_nets, case_inherit_allowed_nets], editable on trans when { !case_all_allowed_nets.value } + + + case_inherit_allowed_nets @@ -534,7 +566,7 @@ case_show_more_menu editable - + 1 0 @@ -562,7 +594,7 @@ case_empty_content_text editable - + 0 1 @@ -576,7 +608,7 @@ case_empty_content_icon editable - + 1 1 @@ -590,7 +622,7 @@ case_empty_content_icon_preview visible - + 2 1 @@ -604,7 +636,7 @@ case_all_allowed_nets editable - + 0 2 @@ -617,7 +649,7 @@ case_allowed_nets - editable + hidden 1 @@ -631,8 +663,8 @@ case_inherit_allowed_nets - editable - + hidden + 2 2 @@ -1063,7 +1095,6 @@ case_headers_sort_mode_active editable - required 0 diff --git a/src/main/resources/petriNets/engine-processes/menu/menu_item.xml b/src/main/resources/petriNets/engine-processes/menu/menu_item.xml index 01b2dc56b0..3102eb6243 100644 --- a/src/main/resources/petriNets/engine-processes/menu/menu_item.xml +++ b/src/main/resources/petriNets/engine-processes/menu/menu_item.xml @@ -256,6 +256,20 @@ childItemForms </data> + <data type="i18n"> + <id>empty_child_items_info</id> + <title/> + <init dynamic="true"> + new com.netgrif.application.engine.petrinet.domain.I18nString("This menu item has no child items to order.", + [sk: "Táto menu položka neobsahuje žiadnu položku na zoradenie", de: "Zu diesem Menüpunkt gibt es keine Untergerichte."]) + </init> + <component> + <name>text</name> + <property key="plainText">true</property> + <property key="fontSize">20</property> + <property key="boldText">true</property> + </component> + </data> <data type="boolean" immediate="true"> <id>is_folder</id> <title/> @@ -299,7 +313,7 @@ <data type="text" immediate="true"> <id>menu_icon</id> <title name="icon_identifier">Menu icon identifier - Material icon identifier. List of icons with identifiers is available online. + Material icon identifier icon: f.this, iconPreview: f.menu_icon_preview; @@ -326,7 +340,6 @@ menu_name_as_visible Name of the item - Will be shown in the menu autocomplete @@ -334,7 +347,6 @@ menu_name Name of the item - Will be shown in the menu menu_name_as_visible: f.menu_name_as_visible, name: f.menu_name; @@ -457,6 +469,16 @@ [:] + + allowed_authorities + Allowed authorities + + + + + + ROLE_USER + use_custom_view Use custom view? @@ -502,7 +524,7 @@ tab_icon Tab icon identifier - Material icon identifier. List of icons with identifiers is available online. + Material icon identifier icon: f.this, iconPreview: f.tab_icon_preview; @@ -665,13 +687,16 @@ com.netgrif.application.engine.menu.service.MenuItemTemplateHolder.transformToOptions() + + autocomplete + Náhľad ikony Identifikátor ikony - Identifikátor Material ikony. Zoznam ikon s identifikátormi je dostupný online. + Identifikátor Material ikony. Pridaj k povoleným roliam Odstráň z povolených rolí Pridaj k zakázaným roliam @@ -684,9 +709,8 @@ Identifikátor duplikovanej položky Musí byť jedinečný Názov položky - Bude zobrazený v menu Identifikátor ikony v karte - Identifikátor Material ikony. Zoznam ikon s identifikátormi je dostupný online. + Identifikátor Material ikony. Náhľad ikony v karte Zobraziť ikonu v karte? Názov položky @@ -719,14 +743,18 @@ Zoradiť pod-položky Vytvoriť menu položku Konfiguračná šablóna - Rola - Zobrazenie + Povolenia + Zobrazenie v karte Menu položka + Povolené autority + Používateľ + Admin + Anonymný používateľ Ikonevorschau Ikone ID - Material Ikone ID. Liste den Ikonen mit IDs ist online verfügbar. + Material Ikone ID Zu zulässigen Rollen hinzufügen Aus zulässigen Rollen entfernen Zu verbotenen Rollen hinzufügen @@ -734,7 +762,7 @@ Ihre Prozesse Wählen Sie einen Prozess mit Rollen aus, die Sie zu Listen mit zulässigen oder verbotenen Rollen hinzufügen möchten. Verfügbare Rollen - Material Ikone ID. Liste den Ikonen mit IDs ist online verfügbar. + Material Ikone ID Beispiel: "demo-tabbed-views" Rollen Zulässige Rollen @@ -747,7 +775,6 @@ Identifikator der kopierten Ansicht Muss einzigartig sein Titel des Eintrages - Wird im Menü angezeigt Ikonen Identifikator der Registerkarte Vorschau der Registerkarte Ikone Zeige die Registerkarte Ikone an? @@ -774,9 +801,13 @@ Untereintrage sortieren Erstellen Konfigurationsvorlage - Rolle - Ansicht + Berechtigungen + Ansicht der Registerkarte Menüeintrag + Zulässige Behörden + Benutzer + Admin + Anonymer Nutzer @@ -874,61 +905,46 @@ - move_dest_uri + configuration_templates editable required 0 - 1 - 1 - 2 - - outline - - - - move_dest_uri_new_node - - editable - - - 2 - 1 + 2 1 - 1 + 4 outline - move_add_node + menu_icon editable - 3 - 1 + 0 + 3 1 - 1 + 2 outline - configuration_templates + menu_icon_preview - editable - required + visible - 0 - 2 + 2 + 3 1 - 4 + 2 - outline + standard @@ -1294,6 +1310,28 @@ outline + + 0 + + + duplicate_view_identifier: f.duplicate_view_identifier, + duplicate_new_title: f.duplicate_new_title; + + if (duplicate_view_identifier.value != null && !duplicate_view_identifier.value.isEmpty() + || duplicate_new_title.value == null) { + return + } + + String defaultName = duplicate_new_title.value.defaultValue + if (defaultName == null || defaultName.isEmpty()) { + defaultName = duplicate_new_title.value.translations?.values()?.find() + } + + String identifier = com.netgrif.application.engine.menu.utils.MenuItemUtils.sanitize(defaultName) + change duplicate_view_identifier value { identifier } + + + duplicate_view_identifier @@ -1309,6 +1347,27 @@ outline + + 0 + + + duplicate_view_identifier: f.duplicate_view_identifier; + + if (menuItemService.existsMenuItem(duplicate_view_identifier.value)) { + change duplicate_view_identifier validations { validation('''regex $.^''', + new com.netgrif.application.engine.petrinet.domain.I18nString("Such menu item already exists", + [sk: "Taká položka menu už existuje", de: "Ein solcher Menüeintrag existiert bereits"])) + } + } else { + change duplicate_view_identifier validations { null } + String sanitizedIdentifier = com.netgrif.application.engine.menu.utils.MenuItemUtils.sanitize(duplicate_view_identifier.value) + if (duplicate_view_identifier.value != sanitizedIdentifier) { + change duplicate_view_identifier value { sanitizedIdentifier } + } + } + + + @@ -1319,6 +1378,7 @@ title: f.duplicate_new_title; duplicateMenuItem(useCase, title.value, identifier.value) + Frontend.reloadMenu() Duplicate @@ -1343,30 +1403,53 @@ children_order_0 - 4 + 3 grid + + empty_child_items_info + + hidden + + + 1 + 0 + 1 + 1 + + outline + + childItemForms editable - - forms: f.childItemForms, - ids: f.childItemIds; - - def orderedTaskIds = ids.value?.collect { id -> workflowService.findOne(id).tasks.find { it.transition == "row_for_ordering" }.task } - change forms value { orderedTaskIds } - 0 - 0 + 1 1 - 4 + 3 outline + + assign + + + trans: t.this, + empty_child_items_info: f.empty_child_items_info, + forms: f.childItemForms, + ids: f.childItemIds; + + def orderedTaskIds = ids.value?.collect { id -> workflowService.findOne(id).tasks.find { it.transition == "row_for_ordering" }.task } ?: [] + make empty_child_items_info, visible on trans when { orderedTaskIds.isEmpty() } + make empty_child_items_info, hidden on trans when { !orderedTaskIds.isEmpty() } + change forms value { orderedTaskIds } + + + row_for_ordering @@ -1495,7 +1578,7 @@ role_settings 400 48 - + person auto @@ -1515,13 +1598,28 @@ 3 grid - processes_available + allowed_authorities editable 0 0 + 1 + 3 + 0 + + outline + + + + processes_available + + editable + + + 0 + 1 2 1 0 @@ -1536,7 +1634,7 @@ 1 - 0 + 1 2 1 0 @@ -1551,7 +1649,7 @@ 2 - 0 + 1 1 1 0 @@ -1565,7 +1663,7 @@ 2 - 1 + 2 1 1 0 @@ -1579,7 +1677,7 @@ 0 - 2 + 3 1 3 0 @@ -1593,7 +1691,7 @@ 0 - 3 + 4 1 2 0 @@ -1608,7 +1706,7 @@ 2 - 3 + 4 1 1 0 @@ -1622,7 +1720,7 @@ 0 - 4 + 5 1 2 0 @@ -1637,7 +1735,7 @@ 2 - 4 + 5 1 1 0 @@ -1666,7 +1764,7 @@ view_settings 528 48 - + image auto diff --git a/src/main/resources/petriNets/engine-processes/menu/task_view_configuration.xml b/src/main/resources/petriNets/engine-processes/menu/task_view_configuration.xml index 0b1ea4c7be..37b6906162 100644 --- a/src/main/resources/petriNets/engine-processes/menu/task_view_configuration.xml +++ b/src/main/resources/petriNets/engine-processes/menu/task_view_configuration.xml @@ -706,7 +706,6 @@ task_headers_sort_mode_active editable - required 0 diff --git a/src/test/java/com/netgrif/application/engine/menu/MenuItemServiceTest.java b/src/test/java/com/netgrif/application/engine/menu/MenuItemServiceTest.java index 1f18dc3f42..d7ba08735d 100644 --- a/src/test/java/com/netgrif/application/engine/menu/MenuItemServiceTest.java +++ b/src/test/java/com/netgrif/application/engine/menu/MenuItemServiceTest.java @@ -135,6 +135,7 @@ private void createByTemplateAndAssert(MenuItemBody menuItemBody) throws Transit assertEquals(menuItemBody.isUseTabbedView(), menuItemCase.getFieldValue(MenuItemConstants.FIELD_USE_TABBED_VIEW)); assertEquals(menuItemBody.isUseCustomView(), menuItemCase.getFieldValue(MenuItemConstants.FIELD_USE_CUSTOM_VIEW)); assertEquals(menuItemBody.getConfigurationTemplateIdentifier(), menuItemCase.getFieldValue(MenuItemConstants.FIELD_CONFIGURATION_TEMPLATES)); + assertEquals(List.of("ROLE_USER"), menuItemCase.getFieldValue(MenuItemConstants.FIELD_ALLOWED_AUTHORITIES)); assertEquals((menuItemBody.getUri() + uriService.getUriSeparator() + menuItemBody.getIdentifier()).replaceAll("//", uriService.getUriSeparator()), menuItemCase.getFieldValue(MenuItemConstants.FIELD_NODE_PATH)); diff --git a/src/test/java/com/netgrif/application/engine/menu/MenuItemUtilsTest.java b/src/test/java/com/netgrif/application/engine/menu/MenuItemUtilsTest.java new file mode 100644 index 0000000000..8f6cbd3899 --- /dev/null +++ b/src/test/java/com/netgrif/application/engine/menu/MenuItemUtilsTest.java @@ -0,0 +1,56 @@ +package com.netgrif.application.engine.menu; + +import com.netgrif.application.engine.menu.domain.MenuItemConstants; +import com.netgrif.application.engine.menu.utils.MenuItemUtils; +import com.netgrif.application.engine.workflow.domain.Case; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +@SpringBootTest +@ActiveProfiles({"test"}) +@ExtendWith(SpringExtension.class) +public class MenuItemUtilsTest { + + @Test + public void testIsCyclicNodePath_withCyclicPath_returnsTrue() { + Case folderItem = mock(Case.class); + + when(folderItem.getFieldValue(MenuItemConstants.FIELD_NODE_PATH)).thenReturn("/node1/node2"); + boolean result = MenuItemUtils.isCyclicNodePath(folderItem, "/node1/node2/node3"); + assertTrue(result); + + when(folderItem.getFieldValue(MenuItemConstants.FIELD_NODE_PATH)).thenReturn("/node1/node2"); + result = MenuItemUtils.isCyclicNodePath(folderItem, "/node4/node1/node2/node3"); + assertTrue(result); + } + + @Test + public void testIsCyclicNodePath_withNonCyclicPath_returnsFalse() { + Case folderItem = mock(Case.class); + + when(folderItem.getFieldValue(MenuItemConstants.FIELD_NODE_PATH)).thenReturn("/node1/node2"); + boolean result = MenuItemUtils.isCyclicNodePath(folderItem, "/node3/node4"); + assertFalse(result); + + when(folderItem.getFieldValue(MenuItemConstants.FIELD_NODE_PATH)).thenReturn("/node1/node2"); + result = MenuItemUtils.isCyclicNodePath(folderItem, null); + assertFalse(result); + + when(folderItem.getFieldValue(MenuItemConstants.FIELD_NODE_PATH)).thenReturn("/node1/node2"); + result = MenuItemUtils.isCyclicNodePath(folderItem, "/node1/node2_2/node3"); + assertFalse(result); + + + when(folderItem.getFieldValue(MenuItemConstants.FIELD_NODE_PATH)).thenReturn(null); + result = MenuItemUtils.isCyclicNodePath(folderItem, "/node1/node2"); + assertFalse(result); + } + +}