saltcorn-samba 0.4.14 → 0.4.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +147 -0
- package/README.md +75 -5
- package/filemanager-view.js +93 -17
- package/i18n/de.json +93 -0
- package/i18n/en.json +93 -0
- package/i18n.js +155 -0
- package/index.js +45 -15
- package/package.json +4 -2
- package/pdf-view.js +8 -5
- package/public/samba-common.js +175 -0
- package/public/samba-filemanager.js +373 -381
- package/public/samba-tree.js +126 -137
- package/tree-view.js +88 -20
package/public/samba-tree.js
CHANGED
|
@@ -1,88 +1,69 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* saltcorn-samba – Client-Controller für die `SambaTree`-View.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Rendert einen Lazy-loading-Verzeichnisbaum. Kinder werden per Klick über
|
|
5
|
+
* /sambadir?path=... nachgeladen. Klick auf eine Datei öffnet sie entweder
|
|
6
|
+
* inline (PDF/Bilder) im eingebauten Viewer <div> oder springt via /sambalink
|
|
7
|
+
* (smb://) in den nativen Datei-Manager.
|
|
8
|
+
*
|
|
9
|
+
* Utilities (iconFor, joinPath, fmtSize, isViewable, i18n-Übersetzungen)
|
|
10
|
+
* kommen aus dem gemeinsamen Modul `SambaCommon` (public/samba-common.js),
|
|
11
|
+
* das der Server per <script>-Tag vor dieser Datei einbindet.
|
|
7
12
|
*/
|
|
8
13
|
(function () {
|
|
9
14
|
"use strict";
|
|
10
15
|
|
|
11
|
-
|
|
16
|
+
var C = window.SambaCommon || {};
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Kleines DOM-Konstruktions-Helferlein. `attrs` unterstützt `class`,
|
|
20
|
+
* `text` (setzt textContent) und beliebige `onXxx`-Handler; alle anderen
|
|
21
|
+
* Attribute landen als HTML-Attribut.
|
|
22
|
+
*/
|
|
23
|
+
function element(tag, attrs, children) {
|
|
12
24
|
var el = document.createElement(tag);
|
|
13
|
-
if (attrs)
|
|
14
|
-
Object.keys(attrs).forEach(function (
|
|
15
|
-
if (
|
|
16
|
-
else if (
|
|
17
|
-
else if (
|
|
18
|
-
el.addEventListener(
|
|
19
|
-
else
|
|
25
|
+
if (attrs) {
|
|
26
|
+
Object.keys(attrs).forEach(function (key) {
|
|
27
|
+
if (key === "class") el.className = attrs[key];
|
|
28
|
+
else if (key === "text") el.textContent = attrs[key];
|
|
29
|
+
else if (key.slice(0, 2) === "on") {
|
|
30
|
+
el.addEventListener(key.slice(2).toLowerCase(), attrs[key]);
|
|
31
|
+
} else {
|
|
32
|
+
el.setAttribute(key, attrs[key]);
|
|
33
|
+
}
|
|
20
34
|
});
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
35
|
+
}
|
|
36
|
+
(children || []).forEach(function (child) {
|
|
37
|
+
if (child == null) return;
|
|
38
|
+
el.appendChild(typeof child === "string" ? document.createTextNode(child) : child);
|
|
24
39
|
});
|
|
25
40
|
return el;
|
|
26
41
|
}
|
|
27
42
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
var n = (item.name || "").toLowerCase();
|
|
31
|
-
if (n.endsWith(".pdf")) return "📄";
|
|
32
|
-
if (/\.(png|jpe?g|gif|webp|svg|bmp)$/.test(n)) return "🖼️";
|
|
33
|
-
if (/\.(docx?|odt|rtf|txt|md)$/.test(n)) return "📝";
|
|
34
|
-
if (/\.(xlsx?|ods|csv)$/.test(n)) return "📊";
|
|
35
|
-
if (/\.(zip|tar|gz|7z|rar)$/.test(n)) return "🗜️";
|
|
36
|
-
return "📎";
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function isViewable(name) {
|
|
40
|
-
var n = (name || "").toLowerCase();
|
|
41
|
-
return (
|
|
42
|
-
n.endsWith(".pdf") ||
|
|
43
|
-
/\.(png|jpe?g|gif|webp|svg|bmp)$/.test(n) ||
|
|
44
|
-
/\.(txt|md|json|xml|csv|html?)$/.test(n)
|
|
45
|
-
);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function joinPath(a, b) {
|
|
49
|
-
if (!a) return b;
|
|
50
|
-
if (!b) return a;
|
|
51
|
-
return (a.replace(/\/+$/, "") + "/" + b.replace(/^\/+/, "")).replace(
|
|
52
|
-
/\/+/g,
|
|
53
|
-
"/"
|
|
54
|
-
);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
function fmtSize(n) {
|
|
58
|
-
if (!n) return "";
|
|
59
|
-
var u = ["B", "KB", "MB", "GB", "TB"];
|
|
60
|
-
var i = 0;
|
|
61
|
-
while (n >= 1024 && i < u.length - 1) {
|
|
62
|
-
n /= 1024;
|
|
63
|
-
i++;
|
|
64
|
-
}
|
|
65
|
-
return n.toFixed(n >= 10 || i === 0 ? 0 : 1) + " " + u[i];
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
async function fetchDir(path, showHidden) {
|
|
43
|
+
/** GET /sambadir für ein Verzeichnis. Wirft bei nicht-OK-Antworten. */
|
|
44
|
+
async function fetchDirectory(path, showHidden) {
|
|
69
45
|
var url =
|
|
70
|
-
"/sambadir?path=" +
|
|
71
|
-
encodeURIComponent(path || "") +
|
|
46
|
+
"/sambadir?path=" + encodeURIComponent(path || "") +
|
|
72
47
|
(showHidden ? "&show_hidden=1" : "");
|
|
73
|
-
var
|
|
74
|
-
if (!
|
|
75
|
-
return
|
|
48
|
+
var response = await fetch(url, { credentials: "same-origin" });
|
|
49
|
+
if (!response.ok) throw new Error("HTTP " + response.status);
|
|
50
|
+
return response.json();
|
|
76
51
|
}
|
|
77
52
|
|
|
78
|
-
|
|
53
|
+
/**
|
|
54
|
+
* Rendert eine Ebene des Baums in `container`. Für jedes Item wird eine
|
|
55
|
+
* Zeile mit Toggle-, Label- und Meta-Span erzeugt; Ordner haben zusätzlich
|
|
56
|
+
* einen leeren Kindercontainer, der bei Bedarf lazy befüllt wird.
|
|
57
|
+
*/
|
|
58
|
+
function renderLevel(container, path, items, opts, viewerElement) {
|
|
79
59
|
container.innerHTML = "";
|
|
80
|
-
var
|
|
60
|
+
var list = element("ul", { class: "samba-tree-list list-unstyled mb-0" }, []);
|
|
81
61
|
|
|
82
62
|
items.forEach(function (item) {
|
|
83
|
-
var
|
|
84
|
-
var childrenBox =
|
|
85
|
-
|
|
63
|
+
var fullPath = C.joinPath(path, item.name);
|
|
64
|
+
var childrenBox = element("div", { class: "samba-tree-children ms-3" }, []);
|
|
65
|
+
|
|
66
|
+
var toggle = element(
|
|
86
67
|
"span",
|
|
87
68
|
{
|
|
88
69
|
class: "samba-tree-toggle me-1",
|
|
@@ -91,120 +72,126 @@
|
|
|
91
72
|
},
|
|
92
73
|
[]
|
|
93
74
|
);
|
|
94
|
-
var label =
|
|
75
|
+
var label = element(
|
|
95
76
|
"span",
|
|
96
77
|
{
|
|
97
78
|
class: "samba-tree-label",
|
|
98
|
-
text: iconFor(item) + " " + item.name,
|
|
79
|
+
text: C.iconFor(item) + " " + item.name,
|
|
99
80
|
style: "cursor:pointer;",
|
|
100
|
-
title:
|
|
81
|
+
title: fullPath,
|
|
101
82
|
},
|
|
102
83
|
[]
|
|
103
84
|
);
|
|
104
|
-
var meta =
|
|
85
|
+
var meta = element(
|
|
105
86
|
"span",
|
|
106
87
|
{
|
|
107
88
|
class: "samba-tree-meta text-muted small ms-2",
|
|
108
|
-
text: item.isDir ? "" : fmtSize(item.size),
|
|
89
|
+
text: item.isDir ? "" : C.fmtSize(item.size),
|
|
109
90
|
},
|
|
110
91
|
[]
|
|
111
92
|
);
|
|
112
93
|
|
|
113
|
-
var
|
|
94
|
+
var externalButton = null;
|
|
114
95
|
if (opts.exposeSmbLink) {
|
|
115
|
-
|
|
96
|
+
externalButton = element(
|
|
116
97
|
"a",
|
|
117
98
|
{
|
|
118
99
|
class: "samba-tree-external btn btn-sm btn-link p-0 ms-2",
|
|
119
|
-
href: "/sambalink?path=" + encodeURIComponent(
|
|
100
|
+
href: "/sambalink?path=" + encodeURIComponent(fullPath),
|
|
120
101
|
target: "_blank",
|
|
121
|
-
title: "
|
|
102
|
+
title: C.t("tree.open_in_fm_title"),
|
|
122
103
|
text: "↗",
|
|
123
104
|
},
|
|
124
105
|
[]
|
|
125
106
|
);
|
|
126
107
|
}
|
|
127
108
|
|
|
128
|
-
|
|
109
|
+
/**
|
|
110
|
+
* Toggle-Handler: erster Klick lädt die Kinder nach, weitere Klicks
|
|
111
|
+
* schalten nur zwischen ausgeklappt/eingeklappt um.
|
|
112
|
+
*/
|
|
113
|
+
var toggleDirectory = function () {
|
|
129
114
|
if (childrenBox.dataset.loaded === "1") {
|
|
130
|
-
var
|
|
131
|
-
childrenBox.style.display =
|
|
132
|
-
toggle.textContent =
|
|
115
|
+
var visible = childrenBox.style.display !== "none";
|
|
116
|
+
childrenBox.style.display = visible ? "none" : "block";
|
|
117
|
+
toggle.textContent = visible ? "▸" : "▾";
|
|
133
118
|
return;
|
|
134
119
|
}
|
|
135
120
|
toggle.textContent = "…";
|
|
136
|
-
|
|
121
|
+
fetchDirectory(fullPath, opts.showHidden)
|
|
137
122
|
.then(function (data) {
|
|
138
|
-
|
|
123
|
+
renderLevel(childrenBox, fullPath, data.items || [], opts, viewerElement);
|
|
139
124
|
childrenBox.dataset.loaded = "1";
|
|
140
125
|
childrenBox.style.display = "block";
|
|
141
126
|
toggle.textContent = "▾";
|
|
142
127
|
})
|
|
143
|
-
.catch(function (
|
|
128
|
+
.catch(function (err) {
|
|
144
129
|
childrenBox.innerHTML =
|
|
145
|
-
'<div class="text-danger small">
|
|
130
|
+
'<div class="text-danger small">' + C.t("tree.error_prefix") + err.message + "</div>";
|
|
146
131
|
childrenBox.dataset.loaded = "1";
|
|
147
132
|
toggle.textContent = "▸";
|
|
148
133
|
});
|
|
149
134
|
};
|
|
150
135
|
|
|
136
|
+
/**
|
|
137
|
+
* Datei-Klick-Handler: inline für ansehbare Formate, sonst Sprung ins
|
|
138
|
+
* OS-Datei-Manager-Fenster.
|
|
139
|
+
*/
|
|
151
140
|
var openFile = function () {
|
|
152
|
-
if (!
|
|
153
|
-
if (opts.pdfInline && isViewable(item.name)) {
|
|
154
|
-
var
|
|
155
|
-
"/sambafile?path=" +
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
var
|
|
159
|
-
viewerEl.innerHTML = "";
|
|
160
|
-
var header = h(
|
|
141
|
+
if (!viewerElement) return;
|
|
142
|
+
if (opts.pdfInline && C.isViewable(item.name)) {
|
|
143
|
+
var streamUrl =
|
|
144
|
+
"/sambafile?path=" + encodeURIComponent(fullPath) + "&disposition=inline";
|
|
145
|
+
var isImage = /\.(png|jpe?g|gif|webp|svg|bmp)$/i.test(item.name);
|
|
146
|
+
viewerElement.innerHTML = "";
|
|
147
|
+
var header = element(
|
|
161
148
|
"div",
|
|
162
149
|
{ class: "samba-viewer-header d-flex align-items-center mb-2" },
|
|
163
150
|
[
|
|
164
|
-
|
|
165
|
-
|
|
151
|
+
element("strong", { text: item.name }, []),
|
|
152
|
+
element(
|
|
166
153
|
"a",
|
|
167
154
|
{
|
|
168
|
-
href:
|
|
155
|
+
href: streamUrl,
|
|
169
156
|
target: "_blank",
|
|
170
157
|
class: "btn btn-sm btn-outline-secondary ms-auto me-2",
|
|
171
|
-
text: "
|
|
158
|
+
text: C.t("tree.open_new_tab"),
|
|
172
159
|
},
|
|
173
160
|
[]
|
|
174
161
|
),
|
|
175
|
-
|
|
162
|
+
element(
|
|
176
163
|
"a",
|
|
177
164
|
{
|
|
178
165
|
href:
|
|
179
166
|
"/sambafile?path=" +
|
|
180
|
-
encodeURIComponent(
|
|
167
|
+
encodeURIComponent(fullPath) +
|
|
181
168
|
"&disposition=attachment",
|
|
182
169
|
class: "btn btn-sm btn-outline-secondary me-2",
|
|
183
|
-
text: "
|
|
170
|
+
text: C.t("fm.download"),
|
|
184
171
|
},
|
|
185
172
|
[]
|
|
186
173
|
),
|
|
187
174
|
opts.exposeSmbLink
|
|
188
|
-
?
|
|
175
|
+
? element(
|
|
189
176
|
"a",
|
|
190
177
|
{
|
|
191
|
-
href: "/sambalink?path=" + encodeURIComponent(
|
|
178
|
+
href: "/sambalink?path=" + encodeURIComponent(fullPath),
|
|
192
179
|
target: "_blank",
|
|
193
180
|
class: "btn btn-sm btn-outline-primary",
|
|
194
|
-
text: "
|
|
181
|
+
text: C.t("tree.open_in_fm"),
|
|
195
182
|
},
|
|
196
183
|
[]
|
|
197
184
|
)
|
|
198
185
|
: null,
|
|
199
186
|
]
|
|
200
187
|
);
|
|
201
|
-
|
|
202
|
-
if (
|
|
203
|
-
|
|
204
|
-
|
|
188
|
+
viewerElement.appendChild(header);
|
|
189
|
+
if (isImage) {
|
|
190
|
+
viewerElement.appendChild(
|
|
191
|
+
element(
|
|
205
192
|
"img",
|
|
206
193
|
{
|
|
207
|
-
src:
|
|
194
|
+
src: streamUrl,
|
|
208
195
|
style:
|
|
209
196
|
"max-width:100%;max-height:70vh;border:1px solid #dee2e6;border-radius:4px;",
|
|
210
197
|
},
|
|
@@ -212,11 +199,11 @@
|
|
|
212
199
|
)
|
|
213
200
|
);
|
|
214
201
|
} else {
|
|
215
|
-
|
|
216
|
-
|
|
202
|
+
viewerElement.appendChild(
|
|
203
|
+
element(
|
|
217
204
|
"iframe",
|
|
218
205
|
{
|
|
219
|
-
src:
|
|
206
|
+
src: streamUrl,
|
|
220
207
|
style:
|
|
221
208
|
"width:100%;height:70vh;border:1px solid #dee2e6;border-radius:4px;",
|
|
222
209
|
},
|
|
@@ -225,66 +212,68 @@
|
|
|
225
212
|
);
|
|
226
213
|
}
|
|
227
214
|
} else {
|
|
228
|
-
//
|
|
229
|
-
window.open(
|
|
230
|
-
"/sambalink?path=" + encodeURIComponent(full),
|
|
231
|
-
"_blank"
|
|
232
|
-
);
|
|
215
|
+
// Nicht inline-fähig: Klassischer Sprung in die smb://-Zwischenseite.
|
|
216
|
+
window.open("/sambalink?path=" + encodeURIComponent(fullPath), "_blank");
|
|
233
217
|
}
|
|
234
218
|
};
|
|
235
219
|
|
|
236
220
|
label.addEventListener("click", function () {
|
|
237
|
-
if (item.isDir)
|
|
221
|
+
if (item.isDir) toggleDirectory();
|
|
238
222
|
else openFile();
|
|
239
223
|
});
|
|
240
224
|
toggle.addEventListener("click", function () {
|
|
241
|
-
if (item.isDir)
|
|
225
|
+
if (item.isDir) toggleDirectory();
|
|
242
226
|
});
|
|
243
227
|
|
|
244
228
|
var lineChildren = [toggle, label, meta];
|
|
245
|
-
if (
|
|
229
|
+
if (externalButton) lineChildren.push(externalButton);
|
|
246
230
|
|
|
247
|
-
var
|
|
248
|
-
|
|
231
|
+
var lineItem = element("li", { class: "samba-tree-item" }, [
|
|
232
|
+
element("div", { class: "samba-tree-line d-flex align-items-center" }, lineChildren),
|
|
249
233
|
childrenBox,
|
|
250
234
|
]);
|
|
251
235
|
childrenBox.style.display = "none";
|
|
252
|
-
|
|
236
|
+
list.appendChild(lineItem);
|
|
253
237
|
});
|
|
254
238
|
|
|
255
239
|
if (!items.length) {
|
|
256
240
|
container.appendChild(
|
|
257
|
-
|
|
241
|
+
element(
|
|
258
242
|
"div",
|
|
259
|
-
{ class: "text-muted small fst-italic p-2", text: "
|
|
243
|
+
{ class: "text-muted small fst-italic p-2", text: C.t("ui.empty_short") },
|
|
260
244
|
[]
|
|
261
245
|
)
|
|
262
246
|
);
|
|
263
247
|
} else {
|
|
264
|
-
container.appendChild(
|
|
248
|
+
container.appendChild(list);
|
|
265
249
|
}
|
|
266
250
|
}
|
|
267
251
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
252
|
+
/**
|
|
253
|
+
* Einstiegspunkt – vom View-Shell aufgerufen.
|
|
254
|
+
* `elementId` bezeichnet das Baum-Root-`<div>`; ein optionales
|
|
255
|
+
* `elementId + "-viewer"` wird als Inline-Anzeige verwendet.
|
|
256
|
+
*/
|
|
257
|
+
function mount(elementId) {
|
|
258
|
+
var root = document.getElementById(elementId);
|
|
259
|
+
if (!root || root.dataset.mounted === "1") return;
|
|
260
|
+
root.dataset.mounted = "1";
|
|
261
|
+
var viewer = document.getElementById(elementId + "-viewer");
|
|
273
262
|
var opts = {};
|
|
274
263
|
try {
|
|
275
|
-
opts = JSON.parse(
|
|
264
|
+
opts = JSON.parse(root.getAttribute("data-opts") || "{}");
|
|
276
265
|
} catch (_) {
|
|
277
266
|
opts = {};
|
|
278
267
|
}
|
|
279
|
-
|
|
280
|
-
'<div class="text-muted small p-2">
|
|
281
|
-
|
|
268
|
+
root.innerHTML =
|
|
269
|
+
'<div class="text-muted small p-2">' + C.t("ui.loading") + "</div>";
|
|
270
|
+
fetchDirectory(opts.startPath || "", opts.showHidden)
|
|
282
271
|
.then(function (data) {
|
|
283
|
-
|
|
272
|
+
renderLevel(root, opts.startPath || "", data.items || [], opts, viewer);
|
|
284
273
|
})
|
|
285
|
-
.catch(function (
|
|
286
|
-
|
|
287
|
-
'<div class="alert alert-danger">
|
|
274
|
+
.catch(function (err) {
|
|
275
|
+
root.innerHTML =
|
|
276
|
+
'<div class="alert alert-danger">' + C.t("tree.samba_prefix") + err.message + "</div>";
|
|
288
277
|
});
|
|
289
278
|
}
|
|
290
279
|
|
package/tree-view.js
CHANGED
|
@@ -19,6 +19,7 @@ const Field = require("@saltcorn/data/models/field");
|
|
|
19
19
|
const Table = require("@saltcorn/data/models/table");
|
|
20
20
|
|
|
21
21
|
const { withClient, toSmbUrl, sanitizeRelativePath } = require("./smb-client");
|
|
22
|
+
const { catalogFor, resolveLocaleFromReq, tFor } = require("./i18n");
|
|
22
23
|
|
|
23
24
|
// ---------------------------------------------------------------------------
|
|
24
25
|
// Configuration workflow
|
|
@@ -41,30 +42,42 @@ const configuration_workflow = () =>
|
|
|
41
42
|
.map((f) => f.name);
|
|
42
43
|
return new Form({
|
|
43
44
|
fields: [
|
|
45
|
+
new Field({
|
|
46
|
+
name: "view_base_path",
|
|
47
|
+
label: "View-Basispfad (relativ zum Plugin-Basispfad)",
|
|
48
|
+
type: "String",
|
|
49
|
+
sublabel:
|
|
50
|
+
"Optional. Statischer Pfad relativ zum Plugin-Basispfad, der f\u00fcr diese View gilt. " +
|
|
51
|
+
"Beispiel: Plugin-Basispfad = 'static', View-Basispfad = 'projekte/2026' \u2192 View listet 'static/projekte/2026'. " +
|
|
52
|
+
"Keine f\u00fchrenden/abschlie\u00dfenden Slashes n\u00f6tig. Traversal (\u201e..\u201c) wird abgelehnt.",
|
|
53
|
+
}),
|
|
44
54
|
new Field({
|
|
45
55
|
name: "mode",
|
|
46
|
-
label: "
|
|
56
|
+
label: "Row-Modus",
|
|
47
57
|
type: "String",
|
|
48
58
|
required: true,
|
|
49
59
|
attributes: {
|
|
50
60
|
options: ["static", "from_field"],
|
|
51
61
|
},
|
|
52
62
|
sublabel:
|
|
53
|
-
"static =
|
|
63
|
+
"static = immer nur der View-Basispfad. from_field = h\u00e4nge einen Feldwert der aktuellen Zeile an " +
|
|
64
|
+
"(nur sinnvoll, wenn die View in einer Show-View eingebettet ist).",
|
|
65
|
+
default: "static",
|
|
54
66
|
}),
|
|
55
67
|
new Field({
|
|
56
68
|
name: "path_field",
|
|
57
|
-
label: "
|
|
69
|
+
label: "Feld mit Unterpfad",
|
|
58
70
|
type: "String",
|
|
59
71
|
attributes: { options: stringFieldOptions },
|
|
60
72
|
showIf: { mode: "from_field" },
|
|
61
73
|
}),
|
|
62
74
|
new Field({
|
|
63
75
|
name: "extra_subpath",
|
|
64
|
-
label: "
|
|
76
|
+
label: "Zus\u00e4tzlicher Suffix (nach dem Feldwert)",
|
|
65
77
|
type: "String",
|
|
66
78
|
sublabel:
|
|
67
|
-
"Optional.
|
|
79
|
+
"Optional. Statischer Suffix, der nach dem Feldwert angeh\u00e4ngt wird. Beispiel: 'invoices'.",
|
|
80
|
+
showIf: { mode: "from_field" },
|
|
68
81
|
}),
|
|
69
82
|
new Field({
|
|
70
83
|
name: "show_hidden",
|
|
@@ -104,17 +117,35 @@ async function get_state_fields(table_id, viewname, { mode, path_field }) {
|
|
|
104
117
|
}
|
|
105
118
|
|
|
106
119
|
/**
|
|
107
|
-
*
|
|
108
|
-
*
|
|
120
|
+
* Compose the start path (relative to the plugin base_path) from three
|
|
121
|
+
* optional parts:
|
|
122
|
+
* 1. `view_base_path` — static, per-view prefix
|
|
123
|
+
* 2. row field value — only in "from_field" mode
|
|
124
|
+
* 3. `extra_subpath` — static suffix, only in "from_field" mode
|
|
125
|
+
*
|
|
126
|
+
* Empty parts are dropped and slash-trimmed. The final joined path is
|
|
127
|
+
* passed through sanitizeRelativePath so traversal / absolute paths /
|
|
128
|
+
* backslashes are rejected consistently.
|
|
109
129
|
*/
|
|
110
130
|
function computeStartPath(configuration, row) {
|
|
111
131
|
const parts = [];
|
|
132
|
+
if (configuration.view_base_path) {
|
|
133
|
+
parts.push(String(configuration.view_base_path));
|
|
134
|
+
}
|
|
112
135
|
if (configuration.mode === "from_field" && configuration.path_field && row) {
|
|
113
136
|
const v = row[configuration.path_field];
|
|
114
137
|
if (v) parts.push(String(v));
|
|
115
138
|
}
|
|
116
|
-
if (
|
|
117
|
-
|
|
139
|
+
if (
|
|
140
|
+
configuration.mode === "from_field" &&
|
|
141
|
+
configuration.extra_subpath
|
|
142
|
+
) {
|
|
143
|
+
parts.push(String(configuration.extra_subpath));
|
|
144
|
+
}
|
|
145
|
+
const joined = parts
|
|
146
|
+
.map((p) => p.replace(/^\/+|\/+$/g, ""))
|
|
147
|
+
.filter(Boolean)
|
|
148
|
+
.join("/");
|
|
118
149
|
// sanitizeRelativePath throws on traversal – we let the caller handle it.
|
|
119
150
|
return sanitizeRelativePath(joined);
|
|
120
151
|
}
|
|
@@ -123,37 +154,59 @@ function computeStartPath(configuration, row) {
|
|
|
123
154
|
// Render – client-side JS is served from /public/samba-tree.js
|
|
124
155
|
// ---------------------------------------------------------------------------
|
|
125
156
|
|
|
126
|
-
|
|
157
|
+
/**
|
|
158
|
+
* Escaped-Serialisierung des i18n-Katalogs, damit ein enthaltenes `</script>`
|
|
159
|
+
* im \u00dcbersetzungstext den HTML-Parser nicht vorzeitig beendet.
|
|
160
|
+
*/
|
|
161
|
+
function serialiseCatalog(catalog) {
|
|
162
|
+
return JSON.stringify(catalog || {}).replace(/</g, "\\u003c");
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function renderShell(viewname, startPath, configuration, locale, catalog) {
|
|
127
166
|
const treeId = `samba-tree-${Math.random().toString(36).slice(2, 10)}`;
|
|
128
167
|
const pluginVersion = configuration.__pluginVersion || "0.2.0";
|
|
168
|
+
const pathLabel = (catalog && catalog["ui.path"]) || "Path:";
|
|
129
169
|
const opts = {
|
|
130
170
|
viewname,
|
|
131
171
|
startPath,
|
|
132
172
|
pdfInline: !!configuration.pdf_inline,
|
|
133
173
|
exposeSmbLink: configuration.expose_smb_link !== false,
|
|
134
174
|
showHidden: !!configuration.show_hidden,
|
|
175
|
+
locale: locale || "en",
|
|
135
176
|
};
|
|
177
|
+
const catalogJson = serialiseCatalog(catalog);
|
|
136
178
|
return (
|
|
137
179
|
div(
|
|
138
180
|
{ class: "samba-tree-container card p-2" },
|
|
139
181
|
div(
|
|
140
182
|
{ class: "samba-tree-toolbar d-flex align-items-center mb-2" },
|
|
141
|
-
span({ class: "text-muted small me-2" },
|
|
183
|
+
span({ class: "text-muted small me-2" }, pathLabel),
|
|
142
184
|
span({ class: "samba-tree-breadcrumb small fw-bold" }, startPath || "/")
|
|
143
185
|
),
|
|
144
186
|
div({ id: treeId, class: "samba-tree", "data-opts": JSON.stringify(opts) }),
|
|
145
187
|
div({ id: treeId + "-viewer", class: "samba-viewer mt-3" })
|
|
146
188
|
) +
|
|
147
189
|
script(
|
|
190
|
+
// Erst samba-common.js (i18n + Utilities) nachladen, Katalog setzen,
|
|
191
|
+
// danach die eigentliche samba-tree.js booten.
|
|
148
192
|
`(function(){
|
|
149
|
-
|
|
193
|
+
var CATALOG=${catalogJson};
|
|
194
|
+
function applyCatalog(){ if(window.SambaCommon && window.SambaCommon.setCatalog) window.SambaCommon.setCatalog(CATALOG); }
|
|
195
|
+
function mount(){window.saltcornSambaMount && window.saltcornSambaMount(${JSON.stringify(
|
|
150
196
|
treeId
|
|
151
197
|
)});}
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
198
|
+
function loadTree(){
|
|
199
|
+
if(window.saltcornSambaMount){ mount(); return; }
|
|
200
|
+
var s=document.createElement('script');
|
|
201
|
+
s.src='/plugins/public/saltcorn-samba@${pluginVersion}/samba-tree.js';
|
|
202
|
+
s.onload=mount;
|
|
203
|
+
document.head.appendChild(s);
|
|
204
|
+
}
|
|
205
|
+
if(window.SambaCommon){ applyCatalog(); loadTree(); return; }
|
|
206
|
+
var c=document.createElement('script');
|
|
207
|
+
c.src='/plugins/public/saltcorn-samba@${pluginVersion}/samba-common.js';
|
|
208
|
+
c.onload=function(){ applyCatalog(); loadTree(); };
|
|
209
|
+
document.head.appendChild(c);
|
|
157
210
|
})();`
|
|
158
211
|
)
|
|
159
212
|
);
|
|
@@ -163,6 +216,16 @@ function renderShell(viewname, startPath, configuration) {
|
|
|
163
216
|
// View: run (single row) and runMany
|
|
164
217
|
// ---------------------------------------------------------------------------
|
|
165
218
|
|
|
219
|
+
/**
|
|
220
|
+
* Bestimmt Locale und Katalog aus dem Saltcorn-Request. `resolveLocaleFromReq`
|
|
221
|
+
* f\u00e4llt bei fehlendem `req` auf "en" zur\u00fcck – kein Crash, kein leeres UI.
|
|
222
|
+
*/
|
|
223
|
+
function resolveLocaleAndCatalog(extra) {
|
|
224
|
+
const req = extra && extra.req;
|
|
225
|
+
const locale = resolveLocaleFromReq(req);
|
|
226
|
+
return { locale, catalog: catalogFor(locale) };
|
|
227
|
+
}
|
|
228
|
+
|
|
166
229
|
async function run(table_id, viewname, configuration, state, extra) {
|
|
167
230
|
// "state" typically contains the primary key of the row when the view is
|
|
168
231
|
// opened directly. If we are embedded in a Show view, `extra.row` is set.
|
|
@@ -177,13 +240,16 @@ async function run(table_id, viewname, configuration, state, extra) {
|
|
|
177
240
|
try {
|
|
178
241
|
startPath = computeStartPath(configuration, row || {});
|
|
179
242
|
} catch (e) {
|
|
180
|
-
|
|
243
|
+
const _t = tFor(resolveLocaleFromReq(extra && extra.req));
|
|
244
|
+
return div({ class: "alert alert-danger" }, _t("tree.samba_prefix") + e.message);
|
|
181
245
|
}
|
|
182
|
-
|
|
246
|
+
const { locale, catalog } = resolveLocaleAndCatalog(extra);
|
|
247
|
+
return renderShell(viewname, startPath, configuration, locale, catalog);
|
|
183
248
|
}
|
|
184
249
|
|
|
185
250
|
async function runMany(table_id, viewname, configuration, state, extra) {
|
|
186
251
|
const rows = extra && extra.rows ? extra.rows : [];
|
|
252
|
+
const { locale, catalog } = resolveLocaleAndCatalog(extra);
|
|
187
253
|
return rows.map((row) => ({
|
|
188
254
|
html: renderShell(
|
|
189
255
|
viewname,
|
|
@@ -194,7 +260,9 @@ async function runMany(table_id, viewname, configuration, state, extra) {
|
|
|
194
260
|
return "";
|
|
195
261
|
}
|
|
196
262
|
})(),
|
|
197
|
-
configuration
|
|
263
|
+
configuration,
|
|
264
|
+
locale,
|
|
265
|
+
catalog
|
|
198
266
|
),
|
|
199
267
|
row,
|
|
200
268
|
}));
|