You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
* feat(intent): function: Attachment — file-attachment composition child (end-to-end)
A `function: Attachment` entity is a first-class composition child of its master (native
1:n, master-detail wiring unchanged), turning file attachments into a modeled, type-safe
detail rather than a bespoke subsystem. Builds on the merged CMS Attachments SDK (#6370).
Intent/EDM (engine-intent):
- `attachment` added to IntentParser ENTITY_FUNCTIONS; EntityIntent.isAttachment().
- EdmIntentGenerator injects the standard file-metadata columns (FileName [major],
ContentType, FileSize [BIGINT], StoragePath, Uuid — all read-only, upload-set) + implicit
audit, and marks the entity attachmentEntity="true". The author declares no primary key on
an attachment child, so a generated integer Id is synthesized (otherwise the entity/
controller would have no PK).
Controller (template-application-rest-java):
- Gated on attachmentEntity, the generated controller gains POST /upload (multipart →
stores each file in the tenant CMS at /Attachments/<Master>/<yyyy>/<MM>/<uuid>/<file>,
persists one row per file with the reference + metadata), GET /{id}/download (permission-
scoped stream, never a raw documents?path=), and deleteById also removes the CMS file.
SDK (api-modules-java):
- Attachments.storeUploads(master) reads the servlet parts Spring's multipart resolver has
already parsed (the controller is served by a Spring @PostMapping, which consumes the body
before commons-fileupload could), so the generated controller only ever sees Attachment.
Test: EdmIntentGeneratorTest asserts the injected metadata + Id PK; RecordAttachmentIT drives
the full HTTP round-trip (multipart upload → verbatim download → delete → gone) through the
real multipart servlet path and tenant CMS.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(harmonia): Attachments / Snapshot files panel for function:Attachment children
Renders a composition child declared `function: Attachment` (or, forthcoming, `function:
Snapshot`) as a purpose-built Files panel instead of a generic row table — the UI half of
the record-attachments feature (backend in #6371, storage SDK in #6370).
Shared runtime (application-core):
- detailPanel gains a `files` mode (parallel to the existing `calendar` mode): lists the
master-filtered files with per-file Download (a plain browser GET to the controller's
`/{id}/download`), and — unless read-only — an Upload control (multipart POST to `/upload`)
and per-file Remove (reuses the detail delete, which also drops the CMS file). Like a
calendar, a files panel always renders (its empty state is meaningful).
- api client: request() now sends a FormData body as-is (browser sets the multipart boundary)
instead of JSON-encoding it, so the upload goes through the same client + auth/error path.
Harmonia templates:
- detail-register emits `files: { readOnly: <attachmentReadOnly> }` for an `attachmentEntity`
child (read-only flips it to download-only — the generated Snapshot case).
- form-view (editable), document-view (editable), master-view (read-only browse) each render
the files branch: an x-h-info-page empty state + file cards (name/size/Download[/Remove]),
gated on `def.files`; the row table + Add button are suppressed for a files detail. $refs is
${dollar}-escaped so Velocity leaves the Alpine ref alone.
Editable path verified end-to-end on a generated CompanyAttachment (upload → master-filtered
list → verbatim download → remove). Read-only (Snapshot) branch is in place, activated once the
`attachmentReadOnly` marker lands with `function: Snapshot`.
Depends on #6371 (function: Attachment backend + controller verbs).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Copy file name to clipboardExpand all lines: components/resources/application-core/src/main/resources/META-INF/dirigible/application-core/shell/js/components/detailPanel.js
+59Lines changed: 59 additions & 0 deletions
Original file line number
Diff line number
Diff line change
@@ -27,6 +27,14 @@
27
27
* table: the same master-filtered rows become events; event-click edits the child, date-click
28
28
* creates one with the master FK AND the clicked date preset. An empty month is meaningful, so a
29
29
* calendar panel shows the calendar even with zero rows.
30
+
*
31
+
* A def carrying `files: { readOnly }` (a composition child declared `function: Attachment` or
32
+
* `function: Snapshot`) renders as a Files panel instead of the table: the master-filtered rows are
33
+
* uploaded files, each downloadable via the controller's `/{id}/download` route. Editable (Attachment):
34
+
* an Upload control adds files (multipart POST to `/upload`) and each row can be removed. Read-only
35
+
* (Snapshot): download only — no upload, no delete (copies are generated server-side, e.g. on issue).
36
+
* Like a calendar, a files panel always renders (its empty state is meaningful), never the shared
37
+
* "no records" line.
30
38
*/
31
39
functiondetailPanel(def,masterId){
32
40
return{
@@ -39,6 +47,8 @@ function detailPanel(def, masterId) {
39
47
deleteOpen: false,
40
48
deleteTarget: null,
41
49
deleteBusy: false,
50
+
uploading: false,// files defs only
51
+
fileError: null,
42
52
// Reactive config for the embedded x-h-calendar (calendar defs only); rebuilt on every load.
Copy file name to clipboardExpand all lines: components/resources/application-core/src/main/resources/META-INF/dirigible/application-core/shell/js/services/api.js
+7-2Lines changed: 7 additions & 2 deletions
Original file line number
Diff line number
Diff line change
@@ -89,10 +89,15 @@ App.services.api = {
89
89
asyncrequest(method,url,body,opts={}){
90
90
constbaseUrl=this.resolveBaseUrl(opts);
91
91
92
+
// A FormData body is a multipart upload: send it as-is and let the browser set the
93
+
// Content-Type (with its boundary) — never JSON-encode it or override the header.
Copy file name to clipboardExpand all lines: components/template/template-application-ui-harmonia-java/src/main/resources/META-INF/dirigible/template-application-ui-harmonia-java/ui/perspective/document/document-view.html.template
<div x-h-info-page-description x-text="filesReadOnly ? T('$projectName:${tprefix}.messages.noCopiesHint', 'A copy is stored automatically when the document is issued.') : T('$projectName:${tprefix}.messages.noFilesHint', 'Upload files to attach them to this document.')"></div>
332
+
</div>
333
+
</div>
334
+
</template>
335
+
<template x-if="rows.length > 0">
336
+
<div class="vbox gap-2">
337
+
<template x-for="row in rows" :key="row[def.primaryKey]">
Copy file name to clipboardExpand all lines: 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
<div x-h-info-page-description x-text="filesReadOnly ? T('$projectName:${tprefix}.messages.noCopiesHint', 'A copy is stored automatically when the document is issued.') : T('$projectName:${tprefix}.messages.noFilesHint', 'Upload files to attach them to this record.')"></div>
260
+
</div>
261
+
</div>
262
+
</template>
263
+
<template x-if="rows.length > 0">
264
+
<div class="vbox gap-2">
265
+
<template x-for="row in rows" :key="row[def.primaryKey]">
Copy file name to clipboardExpand all lines: components/template/template-application-ui-harmonia-java/src/main/resources/META-INF/dirigible/template-application-ui-harmonia-java/ui/perspective/master/detail-register.js.template
Copy file name to clipboardExpand all lines: components/template/template-application-ui-harmonia-java/src/main/resources/META-INF/dirigible/template-application-ui-harmonia-java/ui/perspective/master/master-view.html.template
<div x-h-info-page-description x-text="filesReadOnly ? T('$projectName:${tprefix}.messages.noCopiesHint', 'A copy is stored automatically when the document is issued.') : T('$projectName:${tprefix}.messages.noFilesHint', 'Files are managed from the record’s Edit form.')"></div>
199
+
</div>
200
+
</div>
201
+
</template>
202
+
<template x-if="rows.length > 0">
203
+
<div class="vbox gap-2">
204
+
<template x-for="row in rows" :key="row[def.primaryKey]">
0 commit comments