Skip to content

Commit 6478e3e

Browse files
committed
[ADD] dms_file_description: add descriptions to DMS files
1 parent b5c4821 commit 6478e3e

22 files changed

Lines changed: 493 additions & 0 deletions
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# DMS Field File Description Preview
2+
3+
Shows DMS file descriptions in the embedded DMS preview pane provided by
4+
`dms_field`.
5+
6+
## Usage
7+
8+
Install this module together with `dms_field` and
9+
`dms_file_description`. When a file is selected in an embedded DMS tree,
10+
its description is shown in the preview pane.
11+
12+
## Contributors
13+
14+
- Keith Brandenburg
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
{
2+
"name": "DMS Field File Description Preview",
3+
"summary": "Shows DMS file descriptions in embedded DMS preview panes",
4+
"version": "18.0.1.0.0",
5+
"license": "AGPL-3",
6+
"category": "Document Management",
7+
"author": "Keith Brandenburg",
8+
"website": "https://github.com/OCA/dms",
9+
"depends": ["dms_field", "dms_file_description"],
10+
"assets": {
11+
"web.assets_backend": [
12+
"dms_field_file_description_preview/static/src/js/"
13+
"dms_document_preview_description.esm.js",
14+
"dms_field_file_description_preview/static/src/scss/"
15+
"dms_field_file_description_preview.scss",
16+
],
17+
},
18+
"auto_install": True,
19+
"installable": True,
20+
"application": False,
21+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
[project]
2+
name = "odoo-addon-dms-field-file-description-preview"
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
- Keith Brandenburg
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Shows DMS file descriptions in the embedded DMS preview pane provided by
2+
`dms_field`.
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Install this module together with `dms_field` and `dms_file_description`.
2+
When a file is selected in an embedded DMS tree, its description is shown in
3+
the preview pane.
Lines changed: 222 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,222 @@
1+
/** @odoo-module **/
2+
3+
import {registry} from "@web/core/registry";
4+
5+
const DESCRIPTION_BLOCK_CLASS = "o_dms_document_preview_description";
6+
const DESCRIPTION_TEXT_CLASS = "o_dms_document_preview_description_text";
7+
8+
function parseFileIdFromPreview(previewEl) {
9+
const contentLink = previewEl.querySelector(
10+
'a[href*="/web/content"][href*="model=dms.file"], a[href*="/web/content?id="]'
11+
);
12+
const href = contentLink?.getAttribute("href") || "";
13+
if (href) {
14+
try {
15+
const url = new URL(href, window.location.origin);
16+
const id = url.searchParams.get("id");
17+
if (id && /^\d+$/.test(id)) {
18+
return Number.parseInt(id, 10);
19+
}
20+
} catch {
21+
const match = href.match(/[?&]id=(\d+)/);
22+
if (match) {
23+
return Number.parseInt(match[1], 10);
24+
}
25+
}
26+
}
27+
28+
const image = previewEl.querySelector('img[src*="/web/image/dms.file/"]');
29+
const src = image?.getAttribute("src") || "";
30+
const imageMatch = src.match(/\/web\/image\/dms\.file\/(\d+)\//);
31+
if (imageMatch) {
32+
return Number.parseInt(imageMatch[1], 10);
33+
}
34+
35+
return null;
36+
}
37+
38+
function descriptionBlock(previewEl) {
39+
return previewEl.querySelector(`:scope > .${DESCRIPTION_BLOCK_CLASS}`);
40+
}
41+
42+
function removeDescription(previewEl) {
43+
const existing = descriptionBlock(previewEl);
44+
if (existing) {
45+
existing.remove();
46+
}
47+
}
48+
49+
function renderDescription(previewEl, description) {
50+
const normalized = (description || "").trim();
51+
if (!normalized) {
52+
removeDescription(previewEl);
53+
return;
54+
}
55+
56+
let block = descriptionBlock(previewEl);
57+
if (!block) {
58+
block = document.createElement("div");
59+
block.className = DESCRIPTION_BLOCK_CLASS;
60+
61+
const title = document.createElement("div");
62+
title.className = "o_dms_document_preview_description_title";
63+
title.textContent = "Description";
64+
65+
const text = document.createElement("div");
66+
text.className = DESCRIPTION_TEXT_CLASS;
67+
68+
block.append(title, text);
69+
70+
const previewDirectory = previewEl.querySelector(":scope > .o_preview_directory");
71+
if (previewDirectory) {
72+
previewDirectory.insertAdjacentElement("afterend", block);
73+
} else {
74+
previewEl.append(block);
75+
}
76+
}
77+
78+
const textEl = block.querySelector(`.${DESCRIPTION_TEXT_CLASS}`);
79+
if (textEl) {
80+
textEl.textContent = normalized;
81+
}
82+
}
83+
84+
const dmsFileDescriptionPreviewService = {
85+
dependencies: ["orm"],
86+
start(env, {orm}) {
87+
const pending = new Map();
88+
const minPassiveRefreshMs = 10000;
89+
let scheduled = null;
90+
91+
async function readDescription(fileId) {
92+
// Do not cache completed descriptions here. DMS preview panes can be reused
93+
// after editing a file in a dialog, and stale cached descriptions are very
94+
// confusing. We only deduplicate concurrent reads for the same file.
95+
if (pending.has(fileId)) {
96+
return pending.get(fileId);
97+
}
98+
99+
const promise = orm
100+
.read("dms.file", [fileId], ["description"])
101+
.then((records) => {
102+
pending.delete(fileId);
103+
return records?.[0]?.description || "";
104+
})
105+
.catch((error) => {
106+
pending.delete(fileId);
107+
// Keep this non-fatal. The preview pane should still work even if
108+
// permissions or transient DMS state prevent reading the description.
109+
console.warn(
110+
"Unable to load DMS file description for preview panel",
111+
fileId,
112+
error
113+
);
114+
return "";
115+
});
116+
117+
pending.set(fileId, promise);
118+
return promise;
119+
}
120+
121+
async function updatePreview(previewEl, {force = false} = {}) {
122+
const fileId = parseFileIdFromPreview(previewEl);
123+
if (!fileId) {
124+
removeDescription(previewEl);
125+
delete previewEl.dataset.dmsFileDescriptionId;
126+
delete previewEl.dataset.dmsFileDescriptionCheckedAt;
127+
return;
128+
}
129+
130+
const now = Date.now();
131+
const idKey = String(fileId);
132+
const previousId = previewEl.dataset.dmsFileDescriptionId;
133+
const lastChecked = Number.parseInt(
134+
previewEl.dataset.dmsFileDescriptionCheckedAt || "0",
135+
10
136+
);
137+
138+
// MutationObserver fires again when this service inserts/updates the block.
139+
// Avoid repeatedly reading the same preview just because our own DOM changed.
140+
if (
141+
!force &&
142+
previousId === idKey &&
143+
descriptionBlock(previewEl) &&
144+
now - lastChecked < minPassiveRefreshMs
145+
) {
146+
return;
147+
}
148+
149+
previewEl.dataset.dmsFileDescriptionId = idKey;
150+
previewEl.dataset.dmsFileDescriptionCheckedAt = String(now);
151+
152+
const description = await readDescription(fileId);
153+
// The preview may have changed while the RPC was pending.
154+
if (parseFileIdFromPreview(previewEl) === fileId) {
155+
renderDescription(previewEl, description);
156+
}
157+
}
158+
159+
function updateAllPreviews(options = {}) {
160+
for (const previewEl of document.querySelectorAll(".dms_document_preview")) {
161+
updatePreview(previewEl, options);
162+
}
163+
}
164+
165+
function schedulePassiveUpdate() {
166+
if (scheduled) {
167+
window.clearTimeout(scheduled);
168+
}
169+
scheduled = window.setTimeout(() => {
170+
scheduled = null;
171+
updateAllPreviews({force: false});
172+
}, 80);
173+
}
174+
175+
function scheduleForcedRefreshes() {
176+
// A form save may still be in-flight when the click happens. Refresh a few
177+
// times after likely save/close events so the preview catches the new
178+
// description without requiring a full browser refresh.
179+
for (const delay of [250, 900, 1800]) {
180+
window.setTimeout(() => updateAllPreviews({force: true}), delay);
181+
}
182+
}
183+
184+
const observer = new MutationObserver(schedulePassiveUpdate);
185+
observer.observe(document.body, {
186+
childList: true,
187+
subtree: true,
188+
attributes: true,
189+
attributeFilter: ["src", "href", "class"],
190+
});
191+
192+
document.body.addEventListener(
193+
"click",
194+
(event) => {
195+
schedulePassiveUpdate();
196+
// Refresh after likely file-detail saves or dialog confirm actions.
197+
if (
198+
event.target.closest(
199+
".o_form_button_save, .modal-footer .btn-primary, .o_dialog .btn-primary"
200+
)
201+
) {
202+
scheduleForcedRefreshes();
203+
}
204+
},
205+
true
206+
);
207+
window.addEventListener("focus", scheduleForcedRefreshes);
208+
209+
schedulePassiveUpdate();
210+
211+
return {
212+
update() {
213+
updateAllPreviews({force: true});
214+
},
215+
};
216+
},
217+
};
218+
219+
registry.category("services").add(
220+
"dms_field_file_description_preview.preview_panel",
221+
dmsFileDescriptionPreviewService
222+
);
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
.dms_document_preview > .o_dms_document_preview_description {
2+
margin-top: 0.75rem;
3+
padding: 0.75rem;
4+
border-top: 1px solid var(--border-color, #dee2e6);
5+
white-space: normal;
6+
overflow-wrap: anywhere;
7+
}
8+
9+
.o_dms_document_preview_description_title {
10+
font-weight: 600;
11+
font-size: 0.875rem;
12+
color: var(--text-muted, #6c757d);
13+
margin-bottom: 0.25rem;
14+
}
15+
16+
.o_dms_document_preview_description_text {
17+
white-space: pre-wrap;
18+
line-height: 1.35;
19+
}

dms_file_description/README.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# DMS File Description
2+
3+
Adds an editable description field to DMS files.
4+
5+
The description is shown in the standard DMS file form, list, search, and kanban
6+
views.
7+
8+
## Usage
9+
10+
To use this module:
11+
12+
1. Go to **Documents**.
13+
2. Open or create a DMS file.
14+
3. Fill in the **Description** field.
15+
16+
The description can be searched from the DMS file search view and is displayed
17+
as a short preview on DMS kanban cards.
18+
19+
## Contributors
20+
21+
- Keith Brandenburg

0 commit comments

Comments
 (0)