Skip to content

Commit 93c7dbd

Browse files
authored
fix(harmonia): the personal (my) document honors documentItemsLayout: chat + omits sensitive aggregates from its totals footer (#6348)
The personal document of a chat entity rendered the generic line-items table - the conversation UX (bubbles + composer) existed only on the power surface. Port the chat branch to the my document templates: the same thread + composer, appending through the PERSONAL items controller so parent ownership is enforced server-side (requireMyParent); own/other alignment keys on the audit author vs the logged-in user; the internal-memo toggle and tint carry over. Also: a SENSITIVE aggregate is now omitted from the personal totals footer entirely - the personal controller nulls it on the wire, so the row only ever rendered a permanently empty value the owner is not meant to see. IntentEmissionCoverageIT: the chat fixture gains a personal owner and asserts the my document view renders the thread + composer (role=log, chatDraft) and the my page appends through the personal items controller. Ran green locally (1/1).
1 parent 9825410 commit 93c7dbd

3 files changed

Lines changed: 103 additions & 3 deletions

File tree

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

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,15 @@ document.addEventListener('alpine:init', () => {
4848
itemMode: 'create',
4949
draft: {},
5050
itemOptions: {},
51+
#if($documentItemsLayout == "chat")
52+
// Chat composer state (documentItemsLayout: chat) - the personal document renders the same
53+
// conversation thread the power document does, through the personal items controller.
54+
chatDraft: '',
55+
chatSending: false,
56+
#if($chatInternalProperty)
57+
chatInternal: false,
58+
#end
59+
#end
5160

5261
// The item column metadata, reused from detail-register (App.detailsFor). Resolved lazily: the
5362
// 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', () => {
252261
this.itemsError = (e && e.message) || 'Could not delete the line item.';
253262
}
254263
},
264+
#if($documentItemsLayout == "chat")
265+
266+
// Append a chat message through the PERSONAL items controller - ownership of the parent
267+
// document is enforced server-side (requireMyParent), so a foreign document cannot be
268+
// written to even by hand-crafting the request.
269+
async sendMessage(value) {
270+
const body = (value || '').trim();
271+
if (!body || this.chatSending || !this.def() || this.id == null) return;
272+
this.chatSending = true;
273+
this.itemsError = null;
274+
try {
275+
const payload = { '${chatBodyProperty}': body };
276+
payload[this.def().masterEntityId] = this.id;
277+
#if($chatInternalProperty)
278+
payload['${chatInternalProperty}'] = !!this.chatInternal;
279+
#end
280+
await App.services.api.post(this.itemsApiPath, payload);
281+
this.chatDraft = '';
282+
#if($chatInternalProperty)
283+
this.chatInternal = false;
284+
#end
285+
await this.loadItems();
286+
} catch (e) {
287+
this.itemsError = (e && e.message) || 'Could not send the message.';
288+
} finally {
289+
this.chatSending = false;
290+
}
291+
},
292+
#end
255293

256294
/** After an item change the server recomputes the header aggregates - re-read them. */
257295
async reloadHeaderTotals() {

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

Lines changed: 50 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,50 @@
7575
#end
7676
</div>
7777

78+
#if($documentItemsLayout == "chat")
79+
<!-- Conversation thread (chat items) - the personal document renders the SAME chat the power
80+
document does (bubbles + composer through the personal items controller), never the generic
81+
line-items table. Own vs other alignment keys on the audit CreatedBy vs the logged-in user;
82+
an internal memo is tinted distinctly. Composed from shipped Harmonia primitives (inline
83+
styles + theme vars - see the power document view for why); the x-h-chat component family
84+
will own this styling once it ships. -->
85+
<div class="vbox gap-2" style="max-width: 900px" x-show="itemsEnabled">
86+
<div x-h-toolbar data-variant="transparent" data-size="sm">
87+
<span x-h-toolbar-title class="shrink-0" x-text="itemsDef ? T(itemsDef.tkey, '${documentItemsLabel}') : '${documentItemsLabel}'"></span>
88+
</div>
89+
<div x-show="itemsError" x-h-alert data-variant="negative" role="alert"><div x-h-alert-description x-text="itemsError"></div></div>
90+
<div class="vbox gap-3 w-full rounded-control border border-input bg-input-inner p-4" style="max-height: 28rem; overflow-y: auto" role="log" aria-live="polite">
91+
<template x-for="(row, idx) in items" :key="row[itemsDef.primaryKey] != null ? row[itemsDef.primaryKey] : idx">
92+
<div class="vbox gap-1" style="max-width: 85%"
93+
:style="row.CreatedBy === $store.currentUser.name ? 'align-self:flex-end;align-items:flex-end' : 'align-self:flex-start;align-items:flex-start'">
94+
<div class="hbox items-baseline gap-2">
95+
<span class="text-foreground text-xs font-medium" x-text="row.CreatedBy || T('$projectName:${tprefix}.defaults.system', 'System')"></span>
96+
<span class="text-muted-foreground text-xs" x-text="window.HarmoniaFormat.value(row.CreatedAt, true)"></span>
97+
#if($chatInternalProperty)
98+
<span x-show="row.${chatInternalProperty}" x-h-badge data-variant="warning" x-text="T('$projectName:${tprefix}.defaults.internal', 'Internal')"></span>
99+
#end
100+
</div>
101+
<div class="text-sm" style="border-radius: 0.5rem; padding: 0.5rem 0.75rem; white-space: pre-wrap; overflow-wrap: anywhere"
102+
:style="#if($chatInternalProperty)row.${chatInternalProperty} ? 'background:var(--muted);color:var(--foreground)' : (#end(row.CreatedBy === $store.currentUser.name ? 'background:var(--primary);color:var(--primary-foreground)' : 'background:var(--secondary);color:var(--secondary-foreground)')#if($chatInternalProperty))#end"
103+
x-text="row.${chatBodyProperty}"></div>
104+
</div>
105+
</template>
106+
<div x-show="items.length === 0" x-h-text.muted class="p-2" x-text="T('$projectName:${tprefix}.messages.noData', 'No messages yet.')"></div>
107+
</div>
108+
<div class="vbox gap-2">
109+
#if($chatInternalProperty)
110+
<label class="hbox items-center gap-2 text-sm">
111+
<span x-h-switch data-size="sm"><input type="checkbox" x-model="chatInternal" /></span>
112+
<span x-text="T('$projectName:${tprefix}.defaults.internalNote', 'Internal note (not visible to the requester)')"></span>
113+
</label>
114+
#end
115+
<div class="hbox items-end gap-2 w-full">
116+
<textarea x-h-textarea rows="1" class="grow" x-model="chatDraft" @keydown.enter.prevent="sendMessage(chatDraft)" :disabled="chatSending" :aria-label="T('$projectName:${tprefix}.defaults.writeMessage', 'Write a message')" :placeholder="T('$projectName:${tprefix}.defaults.writeMessage', 'Write a message...')"></textarea>
117+
<button type="button" x-h-button data-variant="primary" @click="sendMessage(chatDraft)" :disabled="chatSending || !chatDraft.trim()" :aria-label="T('$projectName:${tprefix}.defaults.send', 'Send')"><i role="img" x-h-lucide data-lucide="send"></i></button>
118+
</div>
119+
</div>
120+
</div>
121+
#else
78122
<!-- Line items (only once the document exists). Add/edit via a dialog; delete inline. -->
79123
<div class="vbox gap-2" style="max-width: 900px" x-show="itemsEnabled">
80124
<div x-h-toolbar data-variant="transparent" data-size="sm">
@@ -114,18 +158,21 @@
114158
</table>
115159
</div>
116160
</div>
161+
#end
117162

118-
<!-- Totals footer (aggregate fields, read-only). -->
163+
<!-- Totals footer (aggregate fields, read-only). A SENSITIVE aggregate is omitted entirely: the
164+
personal controller nulls it on the wire, so rendering the row would only show a permanently
165+
empty value the owner is not meant to see. -->
119166
#set($hasAgg = false)
120167
#foreach($property in $properties)
121-
#if($property.aggregate == "true")#set($hasAgg = true)#end
168+
#if($property.aggregate == "true" && !$property.sensitiveProperty)#set($hasAgg = true)#end
122169
#end
123170
#if($hasAgg)
124171
<div class="hbox justify-end" style="max-width: 900px" x-show="itemsEnabled">
125172
<div x-h-card class="min-w-3xs">
126173
<div x-h-card-content class="vbox gap-1">
127174
#foreach($property in $properties)
128-
#if($property.aggregate == "true")
175+
#if($property.aggregate == "true" && !$property.sensitiveProperty)
129176
<div class="hbox items-baseline justify-between gap-4">
130177
<span class="text-sm text-secondary-foreground" x-text="T('$projectName:${tprefix}.t.${property.dataName}', '#if($property.widgetLabel)${property.widgetLabel}#else${property.name}#end')"></span>
131178
<span class="text-sm tabular-nums" x-text="window.HarmoniaFormat ? window.HarmoniaFormat.number(form.${property.name}) : form.${property.name}"></span>

tests/tests-integrations/src/main/java/org/eclipse/dirigible/integration/tests/api/IntentEmissionCoverageIT.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,12 +193,17 @@ class IntentEmissionCoverageIT extends IntegrationTest {
193193
# documentItemsLayout: chat - the document master's line-items child renders as a
194194
# conversation thread (x-h-chat bubbles + a composer) instead of the editable table;
195195
# the body maps to the messageBody field, author/timestamp to the child's audit columns.
196+
# The personal owner makes it a personal root too: the PERSONAL document must render
197+
# the SAME chat thread (never the generic items table), through the personal items
198+
# controller.
196199
- name: Ticket
197200
function: Document
198201
documentItemsLayout: chat
199202
fields:
200203
- { name: id, type: integer, primaryKey: true, generated: true }
201204
- { name: subject, type: string, length: 200 }
205+
relations:
206+
- { name: Agent, kind: manyToOne, to: Person, personal: true }
202207
- name: TicketMessage
203208
function: DocumentItem
204209
audit: true
@@ -593,6 +598,16 @@ private void assertEmission() {
593598
"documentItemsLayout: chat must emit the message composer into the document view");
594599
String ticketPage = contentOf("gen/emission/js/components/pages/Ticket/TicketDocumentPage.js");
595600
assertTrue(ticketPage.contains("sendMessage"), "the chat document page must emit the append-message composer handler");
601+
// The PERSONAL document of a chat entity renders the SAME thread + composer (never the
602+
// generic line-items table), writing through the personal items controller so ownership is
603+
// enforced server-side (the my-document chat parity class).
604+
String myTicketDoc = contentOf("gen/emission/views/my/Ticket-document.html");
605+
assertTrue(myTicketDoc.contains("role=\"log\""),
606+
"the personal document of a chat entity must render the conversation thread, not the items table");
607+
assertTrue(myTicketDoc.contains("x-model=\"chatDraft\""), "the personal document must carry the message composer");
608+
String myTicketPage = contentOf("gen/emission/js/components/pages/my/TicketMyDocumentPage.js");
609+
assertTrue(myTicketPage.contains("sendMessage") && myTicketPage.contains("TicketMessageMyController"),
610+
"the personal chat composer must append through the personal items controller");
596611

597612
// transitions: the server half is a controller that guards the source status + the when
598613
// guard (409) and flips ONLY the status column via the targeted updateProperty; the client

0 commit comments

Comments
 (0)