diff --git a/components/resources/application-core/src/main/resources/META-INF/dirigible/application-core/shell/js/components/detailPanel.js b/components/resources/application-core/src/main/resources/META-INF/dirigible/application-core/shell/js/components/detailPanel.js
index 6ad1b571b09..f179841251b 100644
--- a/components/resources/application-core/src/main/resources/META-INF/dirigible/application-core/shell/js/components/detailPanel.js
+++ b/components/resources/application-core/src/main/resources/META-INF/dirigible/application-core/shell/js/components/detailPanel.js
@@ -266,6 +266,22 @@ function detailPanel(def, masterId, master) {
this.openForm("/" + this.def.entity + "/" + encodeURIComponent(row[this.def.primaryKey]) + "/preview?" + this.masterQuery() + "&embedded=1&dialog=1", window.T ? T(this.def.tkey, this.def.label) : this.def.label);
},
+ // A row's record's own contributed actions (#8289), for the row menu: the child entity's actions its
+ // status guards and roles allow - the rule of the list's row menu (#8116). A composition child has no
+ // list, and the record dialog Preview opens shows no actions, so the row menu is the one place they
+ // can be offered. A composition child belongs to this application, so every panel asks; relatedPanel
+ // keeps its own rule (#8118: a cross-project register asks nothing). An action is the child's own
+ // declared operation, guarded by its own endpoint - not an edit through the master - so the row menu
+ // offers it on the master's preview too, and the panel re-reads after it (onActionDone in init).
+ rowActions(row) {
+ const store = Alpine.store("customActions");
+ return row && store ? store.getActions(this.def.entity, "entity", row) : [];
+ },
+
+ runRowAction(action, row) {
+ Alpine.store("customActions").trigger(action, row[this.def.primaryKey], row);
+ },
+
// --- embedded calendar (calendar defs only) -------------------------------------------------
// Row -> event mapping, the same conventions as the standalone calendar page: Jackson java.time
// arrays / epoch seconds / ISO strings normalize via toISO; rows with no start are skipped.
diff --git a/components/template/template-application-ui-harmonia-java/src/main/resources/META-INF/dirigible/template-application-ui-harmonia-java/ui/perspective/manage/form-view.html.template b/components/template/template-application-ui-harmonia-java/src/main/resources/META-INF/dirigible/template-application-ui-harmonia-java/ui/perspective/manage/form-view.html.template
index 9c756ba1542..7dfdf4d58ea 100644
--- a/components/template/template-application-ui-harmonia-java/src/main/resources/META-INF/dirigible/template-application-ui-harmonia-java/ui/perspective/manage/form-view.html.template
+++ b/components/template/template-application-ui-harmonia-java/src/main/resources/META-INF/dirigible/template-application-ui-harmonia-java/ui/perspective/manage/form-view.html.template
@@ -580,7 +580,7 @@
-
+
@@ -594,7 +594,7 @@
-
+
+
+
+
+
+
+
+
+
-
-
-
+
+
+
diff --git a/tests/tests-integrations/src/main/java/org/eclipse/dirigible/integration/tests/api/HarmoniaRecordActionsIT.java b/tests/tests-integrations/src/main/java/org/eclipse/dirigible/integration/tests/api/HarmoniaRecordActionsIT.java
index 8e45fa8fe97..b969dbe6422 100644
--- a/tests/tests-integrations/src/main/java/org/eclipse/dirigible/integration/tests/api/HarmoniaRecordActionsIT.java
+++ b/tests/tests-integrations/src/main/java/org/eclipse/dirigible/integration/tests/api/HarmoniaRecordActionsIT.java
@@ -38,6 +38,8 @@ class HarmoniaRecordActionsIT {
private static final String RELATED = "/META-INF/dirigible/application-core/shell/js/components/relatedPanel.js";
+ private static final String DETAIL = "/META-INF/dirigible/application-core/shell/js/components/detailPanel.js";
+
/**
* The proforma's three actions: Print-like (unguarded), a create-from guarded by {@code allowed},
* and a transition guarded by {@code from}.
@@ -152,6 +154,28 @@ void aRegisterRowOffersItsSourcesActionsByStatus() throws Exception {
}
}
+ /**
+ * A composition child's detail panel row offers its record's actions by the row's status (#8289) -
+ * every panel, since a composition child belongs to this application - and the panel's row menu
+ * lists them, on the master's preview too, where Edit and Delete stay withheld.
+ */
+ @Test
+ void aDetailPanelRowOffersItsChildsActionsByStatus() throws Exception {
+ try (Context context = load()) {
+ context.eval("js", "var panel = { def: { entity: 'ProformaInvoice', primaryKey: 'Id' }, "
+ + method(DETAIL, " rowActions(row) {") + " };");
+ assertEquals("notes,invoice,void", ids(context, "panel.rowActions({ Id: 1, Status: 2 })"), "an ISSUED row offers all three");
+ assertEquals("notes", ids(context, "panel.rowActions({ Id: 2, Status: 3 })"), "an INVOICED row only the unguarded one");
+ }
+ String view = read(MANAGE + "form-view.html.template");
+ assertTrue(view.contains("x-show=\"!isPreview || def.locksWithMaster === false || rowActions(row).length\""),
+ "the panel's row menu must show on the master's preview for a row with actions");
+ assertTrue(view.contains("x-show=\"(!isPreview || def.locksWithMaster === false) && !isForbidden('update')\""),
+ "Edit must stay withheld on the master's preview");
+ assertTrue(view.contains("x-show=\"(!isPreview || def.locksWithMaster === false) && !isForbidden('delete')\""),
+ "Delete must stay withheld on the master's preview");
+ }
+
/**
* An action declared primary is the record page's filled button when it applies (#8119), after a
* pending process task, and leaves the overflow menu.
diff --git a/tests/tests-integrations/src/main/java/org/eclipse/dirigible/integration/tests/ui/tests/IntentCallActionSampleIT.java b/tests/tests-integrations/src/main/java/org/eclipse/dirigible/integration/tests/ui/tests/IntentCallActionSampleIT.java
index 94bf9d5dd30..b5239af96eb 100644
--- a/tests/tests-integrations/src/main/java/org/eclipse/dirigible/integration/tests/ui/tests/IntentCallActionSampleIT.java
+++ b/tests/tests-integrations/src/main/java/org/eclipse/dirigible/integration/tests/ui/tests/IntentCallActionSampleIT.java
@@ -72,12 +72,18 @@ class IntentCallActionSampleIT extends UserInterfaceIntegrationTest {
fields:
- { name: id, type: integer, primaryKey: true, generated: true }
- { name: name, type: string }
+ - name: WatcherStatus
+ function: Setting
+ fields:
+ - { name: id, type: integer, primaryKey: true, generated: true }
+ - { name: name, type: string }
- name: Ticket
fields:
- { name: id, type: integer, primaryKey: true, generated: true }
- { name: subject, type: string, required: true }
relations:
- { name: Status, kind: manyToOne, to: TicketStatus, function: EntityStatus, init: 1 }
+ - { name: watchers, kind: oneToMany, to: Watcher }
related:
- { entity: Escalation, show: [id, reason, Status] }
- name: Escalation
@@ -87,6 +93,13 @@ class IntentCallActionSampleIT extends UserInterfaceIntegrationTest {
relations:
- { name: Ticket, kind: manyToOne, to: Ticket }
- { name: Status, kind: manyToOne, to: EscalationStatus, function: EntityStatus, init: 1 }
+ - name: Watcher
+ fields:
+ - { name: id, type: integer, primaryKey: true, generated: true }
+ - { name: email, type: string }
+ relations:
+ - { name: Ticket, kind: manyToOne, to: Ticket, composition: true }
+ - { name: Status, kind: manyToOne, to: WatcherStatus, function: EntityStatus, init: 1 }
actions:
- name: EscalateTicket
forEntity: Ticket
@@ -112,11 +125,19 @@ class IntentCallActionSampleIT extends UserInterfaceIntegrationTest {
call:
method: POST
path: /services/java/helpdesk/custom/api/TicketApi/escalations/{id}/retry
+ - name: MuteWatcher
+ forEntity: Watcher
+ label: Mute
+ fromStatus: [ACTIVE]
+ can: Watcher:mute
+ call:
+ method: POST
+ path: /services/java/helpdesk/custom/api/TicketApi/watchers/{id}/mute
permissions:
- role: SupportLead
- can: [Ticket:read, Ticket:escalate, Escalation:read, Escalation:retry]
+ can: [Ticket:read, Ticket:escalate, Escalation:read, Escalation:retry, Watcher:read, Watcher:mute]
- role: Agent
- can: [Ticket:read, Escalation:read]
+ can: [Ticket:read, Escalation:read, Watcher:read]
seeds:
- name: ticket-statuses
entity: TicketStatus
@@ -139,6 +160,16 @@ class IntentCallActionSampleIT extends UserInterfaceIntegrationTest {
entity: Escalation
rows:
- { id: 1, reason: Pager down, Ticket: 1, Status: 3 }
+ - name: watcher-statuses
+ entity: WatcherStatus
+ rows:
+ - { id: 1, name: ACTIVE }
+ - { id: 2, name: MUTED }
+ - name: watchers
+ entity: Watcher
+ rows:
+ - { id: 1, email: ann@example.com, Ticket: 1, Status: 1 }
+ - { id: 2, email: bob@example.com, Ticket: 1, Status: 2 }
""";
/**
@@ -196,6 +227,12 @@ public void retry(@PathParam("id") Integer id) {
Response.getNative().setStatus(202);
}
+ @Post("/watchers/{id}/mute")
+ public void mute(@PathParam("id") Integer id) {
+ received.add(Map.of("call", "mute " + id));
+ Response.getNative().setStatus(202);
+ }
+
@Get("/received")
public List