diff --git a/components/template/template-application-ui-harmonia-java/src/main/resources/META-INF/dirigible/template-application-ui-harmonia-java/ui/my/my-document-page.js.template b/components/template/template-application-ui-harmonia-java/src/main/resources/META-INF/dirigible/template-application-ui-harmonia-java/ui/my/my-document-page.js.template index 554c27f7b76..e4bfb3925fc 100644 --- a/components/template/template-application-ui-harmonia-java/src/main/resources/META-INF/dirigible/template-application-ui-harmonia-java/ui/my/my-document-page.js.template +++ b/components/template/template-application-ui-harmonia-java/src/main/resources/META-INF/dirigible/template-application-ui-harmonia-java/ui/my/my-document-page.js.template @@ -48,6 +48,15 @@ document.addEventListener('alpine:init', () => { itemMode: 'create', draft: {}, itemOptions: {}, +#if($documentItemsLayout == "chat") + // Chat composer state (documentItemsLayout: chat) - the personal document renders the same + // conversation thread the power document does, through the personal items controller. + chatDraft: '', + chatSending: false, +#if($chatInternalProperty) + chatInternal: false, +#end +#end // The item column metadata, reused from detail-register (App.detailsFor). Resolved lazily: the // detail-register script may run after this component's init, so re-resolve until it is present. @@ -252,6 +261,35 @@ document.addEventListener('alpine:init', () => { this.itemsError = (e && e.message) || 'Could not delete the line item.'; } }, +#if($documentItemsLayout == "chat") + + // Append a chat message through the PERSONAL items controller - ownership of the parent + // document is enforced server-side (requireMyParent), so a foreign document cannot be + // written to even by hand-crafting the request. + async sendMessage(value) { + const body = (value || '').trim(); + if (!body || this.chatSending || !this.def() || this.id == null) return; + this.chatSending = true; + this.itemsError = null; + try { + const payload = { '${chatBodyProperty}': body }; + payload[this.def().masterEntityId] = this.id; +#if($chatInternalProperty) + payload['${chatInternalProperty}'] = !!this.chatInternal; +#end + await App.services.api.post(this.itemsApiPath, payload); + this.chatDraft = ''; +#if($chatInternalProperty) + this.chatInternal = false; +#end + await this.loadItems(); + } catch (e) { + this.itemsError = (e && e.message) || 'Could not send the message.'; + } finally { + this.chatSending = false; + } + }, +#end /** After an item change the server recomputes the header aggregates - re-read them. */ async reloadHeaderTotals() { diff --git a/components/template/template-application-ui-harmonia-java/src/main/resources/META-INF/dirigible/template-application-ui-harmonia-java/ui/my/my-document-view.html.template b/components/template/template-application-ui-harmonia-java/src/main/resources/META-INF/dirigible/template-application-ui-harmonia-java/ui/my/my-document-view.html.template index 92db5ad36ff..f6c65dcecf7 100644 --- a/components/template/template-application-ui-harmonia-java/src/main/resources/META-INF/dirigible/template-application-ui-harmonia-java/ui/my/my-document-view.html.template +++ b/components/template/template-application-ui-harmonia-java/src/main/resources/META-INF/dirigible/template-application-ui-harmonia-java/ui/my/my-document-view.html.template @@ -75,6 +75,50 @@ #end +#if($documentItemsLayout == "chat") + +
+
+ +
+
+
+ +
+
+
+#if($chatInternalProperty) + +#end +
+ + +
+
+
+#else
@@ -114,18 +158,21 @@
+#end - + #set($hasAgg = false) #foreach($property in $properties) -#if($property.aggregate == "true")#set($hasAgg = true)#end +#if($property.aggregate == "true" && !$property.sensitiveProperty)#set($hasAgg = true)#end #end #if($hasAgg)
#foreach($property in $properties) -#if($property.aggregate == "true") +#if($property.aggregate == "true" && !$property.sensitiveProperty)
diff --git a/tests/tests-integrations/src/main/java/org/eclipse/dirigible/integration/tests/api/IntentEmissionCoverageIT.java b/tests/tests-integrations/src/main/java/org/eclipse/dirigible/integration/tests/api/IntentEmissionCoverageIT.java index db28ddee293..19e12bbbeef 100644 --- a/tests/tests-integrations/src/main/java/org/eclipse/dirigible/integration/tests/api/IntentEmissionCoverageIT.java +++ b/tests/tests-integrations/src/main/java/org/eclipse/dirigible/integration/tests/api/IntentEmissionCoverageIT.java @@ -190,12 +190,17 @@ class IntentEmissionCoverageIT extends IntegrationTest { # documentItemsLayout: chat - the document master's line-items child renders as a # conversation thread (x-h-chat bubbles + a composer) instead of the editable table; # the body maps to the messageBody field, author/timestamp to the child's audit columns. + # The personal owner makes it a personal root too: the PERSONAL document must render + # the SAME chat thread (never the generic items table), through the personal items + # controller. - name: Ticket function: Document documentItemsLayout: chat fields: - { name: id, type: integer, primaryKey: true, generated: true } - { name: subject, type: string, length: 200 } + relations: + - { name: Agent, kind: manyToOne, to: Person, personal: true } - name: TicketMessage function: DocumentItem audit: true @@ -585,6 +590,16 @@ private void assertEmission() { "documentItemsLayout: chat must emit the message composer into the document view"); String ticketPage = contentOf("gen/emission/js/components/pages/Ticket/TicketDocumentPage.js"); assertTrue(ticketPage.contains("sendMessage"), "the chat document page must emit the append-message composer handler"); + // The PERSONAL document of a chat entity renders the SAME thread + composer (never the + // generic line-items table), writing through the personal items controller so ownership is + // enforced server-side (the my-document chat parity class). + String myTicketDoc = contentOf("gen/emission/views/my/Ticket-document.html"); + assertTrue(myTicketDoc.contains("role=\"log\""), + "the personal document of a chat entity must render the conversation thread, not the items table"); + assertTrue(myTicketDoc.contains("x-model=\"chatDraft\""), "the personal document must carry the message composer"); + String myTicketPage = contentOf("gen/emission/js/components/pages/my/TicketMyDocumentPage.js"); + assertTrue(myTicketPage.contains("sendMessage") && myTicketPage.contains("TicketMessageMyController"), + "the personal chat composer must append through the personal items controller"); // transitions: the server half is a controller that guards the source status + the when // guard (409) and flips ONLY the status column via the targeted updateProperty; the client