Skip to content

Commit 78ed1e0

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

22 files changed

Lines changed: 495 additions & 0 deletions
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# DMS Field File Description Preview
2+
3+
Shows DMS file descriptions in the embedded DMS preview pane provided by `dms_field`.
4+
5+
## Usage
6+
7+
Install this module together with `dms_field` and `dms_file_description`. When a file is
8+
selected in an embedded DMS tree, its description is shown in the preview pane.
9+
10+
## Contributors
11+
12+
- Keith Brandenburg

dms_field_file_description_preview/__init__.py

Whitespace-only changes.
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, Odoo Community Association (OCA)",
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: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
[project]
2+
name = "odoo-addon-dms-field-file-description-preview"
3+
4+
[build-system]
5+
requires = ["whool"]
6+
build-backend = "whool.buildapi"
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: 228 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,228 @@
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(
71+
":scope > .o_preview_directory"
72+
);
73+
if (previewDirectory) {
74+
previewDirectory.insertAdjacentElement("afterend", block);
75+
} else {
76+
previewEl.append(block);
77+
}
78+
}
79+
80+
const textEl = block.querySelector(`.${DESCRIPTION_TEXT_CLASS}`);
81+
if (textEl) {
82+
textEl.textContent = normalized;
83+
}
84+
}
85+
86+
const dmsFileDescriptionPreviewService = {
87+
dependencies: ["orm"],
88+
start(env, {orm}) {
89+
const pending = new Map();
90+
const minPassiveRefreshMs = 10000;
91+
let scheduled = null;
92+
93+
async function readDescription(fileId) {
94+
// Do not cache completed descriptions here. DMS preview panes can be reused
95+
// after editing a file in a dialog, and stale cached descriptions are very
96+
// confusing. We only deduplicate concurrent reads for the same file.
97+
if (pending.has(fileId)) {
98+
return pending.get(fileId);
99+
}
100+
101+
const promise = orm
102+
.read("dms.file", [fileId], ["description"])
103+
.then((records) => {
104+
pending.delete(fileId);
105+
return records?.[0]?.description || "";
106+
})
107+
.catch((error) => {
108+
pending.delete(fileId);
109+
// Keep this non-fatal. The preview pane should still work even if
110+
// permissions or transient DMS state prevent reading the description.
111+
console.warn(
112+
"Unable to load DMS file description for preview panel",
113+
fileId,
114+
error
115+
);
116+
return "";
117+
});
118+
119+
pending.set(fileId, promise);
120+
return promise;
121+
}
122+
123+
async function updatePreview(previewEl, {force = false} = {}) {
124+
const fileId = parseFileIdFromPreview(previewEl);
125+
if (!fileId) {
126+
removeDescription(previewEl);
127+
delete previewEl.dataset.dmsFileDescriptionId;
128+
delete previewEl.dataset.dmsFileDescriptionCheckedAt;
129+
return;
130+
}
131+
132+
const now = Date.now();
133+
const idKey = String(fileId);
134+
const previousId = previewEl.dataset.dmsFileDescriptionId;
135+
const lastChecked = Number.parseInt(
136+
previewEl.dataset.dmsFileDescriptionCheckedAt || "0",
137+
10
138+
);
139+
140+
// MutationObserver fires again when this service inserts/updates the block.
141+
// Avoid repeatedly reading the same preview just because our own DOM changed.
142+
if (
143+
!force &&
144+
previousId === idKey &&
145+
descriptionBlock(previewEl) &&
146+
now - lastChecked < minPassiveRefreshMs
147+
) {
148+
return;
149+
}
150+
151+
previewEl.dataset.dmsFileDescriptionId = idKey;
152+
previewEl.dataset.dmsFileDescriptionCheckedAt = String(now);
153+
154+
const description = await readDescription(fileId);
155+
// The preview may have changed while the RPC was pending.
156+
if (parseFileIdFromPreview(previewEl) === fileId) {
157+
renderDescription(previewEl, description);
158+
}
159+
}
160+
161+
function updateAllPreviews(options = {}) {
162+
for (const previewEl of document.querySelectorAll(
163+
".dms_document_preview"
164+
)) {
165+
updatePreview(previewEl, options);
166+
}
167+
}
168+
169+
function schedulePassiveUpdate() {
170+
if (scheduled) {
171+
window.clearTimeout(scheduled);
172+
}
173+
scheduled = window.setTimeout(() => {
174+
scheduled = null;
175+
updateAllPreviews({force: false});
176+
}, 80);
177+
}
178+
179+
function scheduleForcedRefreshes() {
180+
// A form save may still be in-flight when the click happens. Refresh a few
181+
// times after likely save/close events so the preview catches the new
182+
// description without requiring a full browser refresh.
183+
for (const delay of [250, 900, 1800]) {
184+
window.setTimeout(() => updateAllPreviews({force: true}), delay);
185+
}
186+
}
187+
188+
const observer = new MutationObserver(schedulePassiveUpdate);
189+
observer.observe(document.body, {
190+
childList: true,
191+
subtree: true,
192+
attributes: true,
193+
attributeFilter: ["src", "href", "class"],
194+
});
195+
196+
document.body.addEventListener(
197+
"click",
198+
(event) => {
199+
schedulePassiveUpdate();
200+
// Refresh after likely file-detail saves or dialog confirm actions.
201+
if (
202+
event.target.closest(
203+
".o_form_button_save, .modal-footer .btn-primary, .o_dialog .btn-primary"
204+
)
205+
) {
206+
scheduleForcedRefreshes();
207+
}
208+
},
209+
true
210+
);
211+
window.addEventListener("focus", scheduleForcedRefreshes);
212+
213+
schedulePassiveUpdate();
214+
215+
return {
216+
update() {
217+
updateAllPreviews({force: true});
218+
},
219+
};
220+
},
221+
};
222+
223+
registry
224+
.category("services")
225+
.add(
226+
"dms_field_file_description_preview.preview_panel",
227+
dmsFileDescriptionPreviewService
228+
);
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: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
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 views.
6+
7+
## Usage
8+
9+
To use this module:
10+
11+
1. Go to **Documents**.
12+
2. Open or create a DMS file.
13+
3. Fill in the **Description** field.
14+
15+
The description can be searched from the DMS file search view and is displayed as a
16+
short preview on DMS kanban cards.
17+
18+
## Contributors
19+
20+
- Keith Brandenburg

0 commit comments

Comments
 (0)