@inteli.city/node-red-plugin-project-files 1.0.0

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.
@@ -0,0 +1,11 @@
1
+ // Word-wrap preference stored in localStorage.
2
+ const DSFF_WRAP_KEY = "projectFiles.wordwrap";
3
+
4
+ function dsffWrapEnabled() {
5
+ return localStorage.getItem(DSFF_WRAP_KEY) === "1";
6
+ }
7
+
8
+ function dsffSetWrap(on) {
9
+ if (on) localStorage.setItem(DSFF_WRAP_KEY, "1");
10
+ else localStorage.removeItem(DSFF_WRAP_KEY);
11
+ }
@@ -0,0 +1,47 @@
1
+ // rules.js — Pure policy/decision functions for the Project Files plugin.
2
+ // No DOM, no state, no AJAX, no side effects.
3
+ // Loaded before plugin.js; all four exports are plain globals on window.
4
+ (function () {
5
+ var PROTECTED_NAMES = {
6
+ "flows.json": 1, "flows_cred.json": 1, "package.json": 1,
7
+ ".flows.json.backup": 1, ".flows_cred.json.backup": 1,
8
+ };
9
+ var PROTECTED_DIRS = { ".git": 1 };
10
+ var READONLY_NAMES = {
11
+ "flows.json": 1, "flows_cred.json": 1,
12
+ ".flows.json.backup": 1, ".flows_cred.json.backup": 1,
13
+ };
14
+ var EXT_LANG = {
15
+ js:"javascript", ts:"typescript", py:"python", json:"json",
16
+ md:"markdown", html:"html", css:"css", yml:"yaml", yaml:"yaml",
17
+ sh:"shell", bat:"bat", c:"c", cpp:"cpp",
18
+ h:"cpp", hpp:"cpp", java:"java", cs:"csharp",
19
+ rs:"rust", go:"go", sql:"sql", xml:"xml",
20
+ ini:"ini", toml:"toml", txt:"plaintext",
21
+ };
22
+
23
+ // item: { type: "file"|"dir", name: basename }
24
+ window.isProtected = function (item) {
25
+ if (item.type === "file") return !!PROTECTED_NAMES[item.name];
26
+ if (item.type === "dir") return !!PROTECTED_DIRS[item.name];
27
+ return false;
28
+ };
29
+ // Returns true for dirs the user cannot navigate into.
30
+ window.isProtectedDir = function (item) {
31
+ return item.type === "dir" && !!PROTECTED_DIRS[item.name];
32
+ };
33
+ // path is a relative path; only the basename is matched.
34
+ window.isReadOnly = function (path) {
35
+ var name = (path || "").split("/").pop();
36
+ return !!READONLY_NAMES[name];
37
+ };
38
+ // Monaco language identifier from a filename.
39
+ window.langFromName = function (name) {
40
+ var ext = (name.split(".").pop() || "").toLowerCase();
41
+ return EXT_LANG[ext] || "plaintext";
42
+ };
43
+ // item: { type: "file"|"dir", name: basename }
44
+ window.canMove = function (item) {
45
+ return !window.isProtected(item);
46
+ };
47
+ })();
@@ -0,0 +1,492 @@
1
+ (function () {
2
+ if (document.getElementById("dsff-style")) return;
3
+ const css = `
4
+
5
+ /* ── Tree rows ───────────────────────────────────────────────────────────── */
6
+ .dsff-row {
7
+ display: flex;
8
+ align-items: center;
9
+ padding: 4px 6px;
10
+ cursor: pointer;
11
+ user-select: none;
12
+ border-radius: 3px;
13
+ min-width: 0;
14
+ }
15
+ .dsff-row:hover { background: var(--red-ui-secondary-background-alt, #f0f0f0); }
16
+ .dsff-row-selected { background: var(--red-ui-secondary-background-alt, #e5e5e5); }
17
+ .dsff-row-hidden { opacity: 0.58; font-style: italic; }
18
+ .dsff-row-protected-dir { cursor: not-allowed; }
19
+ .dsff-row-name {
20
+ flex: 1 1 auto;
21
+ overflow: hidden;
22
+ text-overflow: ellipsis;
23
+ white-space: nowrap;
24
+ min-width: 0;
25
+ }
26
+ .dsff-mtime {
27
+ flex: 0 0 auto;
28
+ font-size: 0.78em;
29
+ opacity: 0.55;
30
+ white-space: nowrap;
31
+ margin-left: 8px;
32
+ }
33
+
34
+ /* ── Tree sort header ────────────────────────────────────────────────────── */
35
+ .dsff-tree-header {
36
+ display: flex;
37
+ align-items: center;
38
+ padding: 3px 6px;
39
+ font-size: 0.72em;
40
+ text-transform: uppercase;
41
+ letter-spacing: 0.05em;
42
+ opacity: 0.6;
43
+ border-bottom: 1px solid var(--red-ui-secondary-background);
44
+ user-select: none;
45
+ }
46
+ .dsff-tree-header span { cursor: pointer; white-space: nowrap; }
47
+ .dsff-tree-header span:hover { opacity: 1; }
48
+ .dsff-tree-header-name { flex: 1 1 auto; }
49
+ .dsff-tree-header-mod { flex: 0 0 auto; text-align: right; padding-left: 8px; }
50
+
51
+ /* ── Expand/collapse-sidebar buttons (mutually exclusive via .dsff-sidebar-
52
+ expanded). Both degrade to a muted, non-interactive look when the host
53
+ adapter reports the sidebar can't be resized. ─────────────────────── */
54
+ .dsff-btn-collapse { display: none !important; }
55
+ .dsff-sidebar-expanded .dsff-btn-collapse { display: inline-block !important; }
56
+ .dsff-btn-expand { display: inline-block !important; }
57
+ .dsff-sidebar-expanded .dsff-btn-expand { display: none !important; }
58
+ .dsff-btn-expand:disabled,
59
+ .dsff-btn-collapse:disabled { opacity: 0.45; cursor: default; }
60
+
61
+ /* ── Compact-only elements (hidden by default, shown by mode classes) ────── */
62
+ .dsff-compact-folder,
63
+ .dsff-btn-new-compact,
64
+ .dsff-btn-back,
65
+ .dsff-compact-filename,
66
+ .dsff-editor-empty { display: none; }
67
+
68
+ /* ── Compact folder label ────────────────────────────────────────────────── */
69
+ .dsff-compact-folder {
70
+ flex: 1 1 auto;
71
+ font-size: 0.88em;
72
+ font-weight: 600;
73
+ overflow: hidden;
74
+ text-overflow: ellipsis;
75
+ white-space: nowrap;
76
+ opacity: 0.85;
77
+ }
78
+
79
+ /* ── Compact filename in editor toolbar ──────────────────────────────────── */
80
+ .dsff-compact-filename {
81
+ flex: 1 1 auto;
82
+ font-size: 0.85em;
83
+ font-weight: 500;
84
+ overflow: hidden;
85
+ text-overflow: ellipsis;
86
+ white-space: nowrap;
87
+ min-width: 0;
88
+ opacity: 0.85;
89
+ }
90
+
91
+ /* ── Empty states ────────────────────────────────────────────────────────── */
92
+ .dsff-tree-empty {
93
+ padding: 24px 16px;
94
+ text-align: center;
95
+ font-size: 0.82em;
96
+ opacity: 0.42;
97
+ font-style: italic;
98
+ user-select: none;
99
+ }
100
+ .dsff-editor-empty {
101
+ position: absolute;
102
+ inset: 0;
103
+ display: flex;
104
+ align-items: center;
105
+ justify-content: center;
106
+ flex-direction: column;
107
+ gap: 12px;
108
+ font-size: 0.88em;
109
+ opacity: 0.38;
110
+ pointer-events: none;
111
+ user-select: none;
112
+ text-align: center;
113
+ padding: 24px;
114
+ }
115
+ .dsff-editor-empty i { font-size: 2.2em; }
116
+
117
+ /* ── Compact "+" dropdown ────────────────────────────────────────────────── */
118
+ .dsff-compact-menu {
119
+ position: fixed;
120
+ z-index: 10000;
121
+ background: var(--red-ui-secondary-background, #fff);
122
+ border: 1px solid var(--red-ui-secondary-border-color, #ccc);
123
+ border-radius: 5px;
124
+ min-width: 148px;
125
+ box-shadow: 0 4px 18px rgba(0,0,0,.18);
126
+ padding: 4px 0;
127
+ font-size: 13px;
128
+ }
129
+ .dsff-compact-menu-item {
130
+ padding: 7px 12px;
131
+ cursor: pointer;
132
+ display: flex;
133
+ align-items: center;
134
+ gap: 9px;
135
+ white-space: nowrap;
136
+ }
137
+ .dsff-compact-menu-item:hover {
138
+ background: var(--red-ui-primary-background, #e8f0fe);
139
+ }
140
+ .dsff-compact-menu-item .fa { width: 14px; text-align: center; opacity: 0.75; }
141
+
142
+ /* ── Context menu (right-click on file/folder) ──────────────────────────── */
143
+ .dsff-ctx-menu {
144
+ position: fixed;
145
+ z-index: 10000;
146
+ background: var(--red-ui-secondary-background, #fff);
147
+ border: 1px solid var(--red-ui-secondary-border-color, #ccc);
148
+ border-radius: 5px;
149
+ min-width: 160px;
150
+ box-shadow: 0 4px 18px rgba(0,0,0,.18);
151
+ padding: 4px 0;
152
+ font-size: 13px;
153
+ }
154
+ .dsff-ctx-menu-label {
155
+ padding: 5px 12px 4px;
156
+ font-size: 0.8em;
157
+ opacity: 0.5;
158
+ white-space: nowrap;
159
+ overflow: hidden;
160
+ text-overflow: ellipsis;
161
+ max-width: 200px;
162
+ border-bottom: 1px solid var(--red-ui-secondary-border-color, #ddd);
163
+ margin-bottom: 3px;
164
+ cursor: default;
165
+ }
166
+ .dsff-ctx-menu-item {
167
+ padding: 7px 12px;
168
+ cursor: pointer;
169
+ display: flex;
170
+ align-items: center;
171
+ gap: 9px;
172
+ white-space: nowrap;
173
+ font-weight: normal;
174
+ }
175
+ .dsff-ctx-menu-item:hover { background: var(--red-ui-secondary-background-alt, #e8f0fe); }
176
+ .dsff-ctx-menu-sep {
177
+ height: 1px;
178
+ background: var(--red-ui-secondary-border-color, #e0e0e0);
179
+ margin: 3px 0;
180
+ }
181
+ .dsff-ctx-menu-item.dsff-ctx-danger { color: #c0392b; }
182
+ .dsff-ctx-menu-item.dsff-ctx-danger:hover { background: #fdf2f2; }
183
+ .dsff-ctx-menu-item.dsff-ctx-disabled { opacity: 0.38; cursor: not-allowed; }
184
+ .dsff-ctx-menu-item.dsff-ctx-disabled:hover { background: none; }
185
+ .dsff-ctx-menu-item .fa { width: 14px; text-align: center; opacity: 0.75; }
186
+
187
+ /* ── Read-only file banner ───────────────────────────────────────────────── */
188
+ .dsff-readonly-banner {
189
+ display: none;
190
+ align-items: center;
191
+ gap: 7px;
192
+ padding: 5px 10px;
193
+ background: #fff8e1;
194
+ border-bottom: 1px solid #ffe082;
195
+ font-size: 0.82em;
196
+ color: #6d4c00;
197
+ flex-shrink: 0;
198
+ user-select: none;
199
+ }
200
+ .dsff-readonly-banner .fa { opacity: 0.7; }
201
+ .dsff-readonly-open .dsff-readonly-banner { display: flex; }
202
+
203
+ /* ── Protected file indicator ────────────────────────────────────────────── */
204
+ .dsff-protected-icon {
205
+ font-size: 0.68em;
206
+ opacity: 0.38;
207
+ margin-left: 5px;
208
+ flex-shrink: 0;
209
+ }
210
+
211
+ /* ── Drag-and-drop file move ─────────────────────────────────────────────── */
212
+ .dsff-row[draggable="true"] { cursor: grab; }
213
+ .dsff-row[draggable="true"]:active { cursor: grabbing; }
214
+ .dsff-row-dragging { opacity: 0.4; }
215
+ .dsff-row-drag-over {
216
+ background: var(--red-ui-secondary-background-alt, #e8f0fe) !important;
217
+ outline: 2px solid var(--red-ui-primary-background, #4285f4);
218
+ outline-offset: -2px;
219
+ }
220
+
221
+ /* ── Drag-and-drop upload ────────────────────────────────────────────────── */
222
+ .dsff-drop-overlay {
223
+ position: absolute;
224
+ inset: 0;
225
+ display: none;
226
+ align-items: center;
227
+ justify-content: center;
228
+ flex-direction: column;
229
+ gap: 10px;
230
+ background: rgba(66,133,244,0.10);
231
+ border: 2px dashed var(--red-ui-primary-background, #4285f4);
232
+ border-radius: 4px;
233
+ z-index: 100;
234
+ pointer-events: none;
235
+ font-size: 0.88em;
236
+ font-weight: 600;
237
+ color: var(--red-ui-primary-background, #4285f4);
238
+ text-align: center;
239
+ padding: 16px;
240
+ }
241
+ .dsff-drop-overlay i { font-size: 2em; margin-bottom: 4px; }
242
+ .dsff-drop-active .dsff-drop-overlay { display: flex; }
243
+
244
+ /* ── Header path label ───────────────────────────────────────────────────────
245
+ The scope/current path is shown here in BOTH modes (and in Project Mode it
246
+ is where the active project name lives — no banner needed). Ellipsize from
247
+ the LEFT so the meaningful tail (…/project/current-folder) stays visible in
248
+ a narrow sidebar instead of being cut off. */
249
+ .dsff-base-label {
250
+ direction: rtl;
251
+ text-align: left;
252
+ }
253
+ .dsff-base-label > * { direction: ltr; } /* keep any child content LTR */
254
+
255
+ /* ── Operating-mode warning bar ──────────────────────────────────────────────
256
+ Shown ONLY in the fallback (User Directory) Mode. Project Mode is the
257
+ normal state and renders no bar (no vertical file-list space consumed).
258
+ Amber so it reads clearly as "not in a project", and wraps so the full
259
+ message stays legible in a narrow sidebar. */
260
+ .dsff-mode-bar {
261
+ display: flex;
262
+ align-items: flex-start;
263
+ gap: 6px;
264
+ padding: 5px 8px;
265
+ font-size: 0.74em;
266
+ line-height: 1.35;
267
+ border-bottom: 1px solid #ffe082;
268
+ background: #fff8e1;
269
+ color: #6d4c00;
270
+ user-select: none;
271
+ }
272
+ .dsff-mode-bar .fa {
273
+ flex: 0 0 auto;
274
+ margin-top: 1px;
275
+ color: #b8860b;
276
+ }
277
+ .dsff-mode-text {
278
+ flex: 1 1 auto;
279
+ min-width: 0;
280
+ overflow-wrap: anywhere;
281
+ }
282
+
283
+ /* ── Python venv bar ─────────────────────────────────────────────────────── */
284
+ .dsff-venv-bar {
285
+ display: flex;
286
+ align-items: center;
287
+ gap: 6px;
288
+ padding: 3px 8px;
289
+ font-size: 0.76em;
290
+ opacity: 0.8;
291
+ border-bottom: 1px solid var(--red-ui-secondary-background);
292
+ user-select: none;
293
+ white-space: nowrap;
294
+ overflow: hidden;
295
+ text-overflow: ellipsis;
296
+ }
297
+ .dsff-venv-bar .fa-cube { opacity: 0.55; font-size: 0.95em; }
298
+ .dsff-venv-bar.dsff-venv-ready .fa-cube { color: var(--red-ui-primary-background, #6e4db2); opacity: 0.8; }
299
+ .dsff-venv-status {
300
+ flex: 1 1 auto;
301
+ overflow: hidden;
302
+ text-overflow: ellipsis;
303
+ }
304
+ .dsff-venv-action {
305
+ color: var(--red-ui-primary-background, #6e4db2);
306
+ cursor: pointer;
307
+ text-decoration: underline;
308
+ flex: 0 0 auto;
309
+ }
310
+ .dsff-venv-action:hover { opacity: 0.85; }
311
+
312
+ /* ── Split drag handle ───────────────────────────────────────────────────── */
313
+ .dsff-split-handle {
314
+ width: 8px;
315
+ flex-shrink: 0;
316
+ cursor: col-resize;
317
+ background: var(--red-ui-secondary-background);
318
+ display: flex;
319
+ align-items: center;
320
+ justify-content: center;
321
+ }
322
+ .dsff-split-handle::before {
323
+ content: "";
324
+ width: 2px;
325
+ height: 32px;
326
+ border-radius: 999px;
327
+ background: rgba(0,0,0,0.18);
328
+ }
329
+ .dsff-split-handle:hover::before { background: rgba(0,0,0,0.38); }
330
+ .dsff-root-splitting { cursor: col-resize; }
331
+ .dsff-root-splitting .dsff-split-handle { background: var(--red-ui-secondary-background-alt, #dcdcdc); }
332
+
333
+ /* ── Toggle-active button (e.g. show hidden files) ──────────────────────── */
334
+ .red-ui-button.dsff-btn-active {
335
+ background-color: var(--red-ui-secondary-background-alt, #e0e0e0) !important;
336
+ box-shadow: inset 0 1px 3px rgba(0,0,0,0.15);
337
+ }
338
+
339
+ /* ── Dirty save button ───────────────────────────────────────────────────── */
340
+ .red-ui-button.dsff-dirty:not(.disabled) {
341
+ background-color: #d9534f;
342
+ color: #fff !important;
343
+ border-color: #b52b27;
344
+ font-weight: 600;
345
+ }
346
+ .red-ui-button.dsff-dirty:not(.disabled):hover { filter: brightness(0.93); }
347
+
348
+ /* ── Dirty compact filename marker ──────────────────────────────────────── */
349
+ .dsff-compact-filename.dsff-name-dirty::before {
350
+ content: "● ";
351
+ color: #d9534f;
352
+ }
353
+
354
+ /* ── Unified modal dialog ────────────────────────────────────────────────── */
355
+ .dsff-modal-overlay {
356
+ position: fixed;
357
+ inset: 0;
358
+ background: rgba(0,0,0,0.45);
359
+ z-index: 20000;
360
+ display: flex;
361
+ align-items: center;
362
+ justify-content: center;
363
+ }
364
+ .dsff-modal {
365
+ background: var(--red-ui-secondary-background, #fff);
366
+ border: 1px solid var(--red-ui-secondary-border-color, #ccc);
367
+ border-radius: 6px;
368
+ padding: 20px 24px 18px;
369
+ max-width: 420px;
370
+ width: 90%;
371
+ box-shadow: 0 8px 32px rgba(0,0,0,0.22);
372
+ }
373
+ .dsff-modal-title {
374
+ font-weight: 600;
375
+ font-size: 0.95em;
376
+ margin: 0 0 10px;
377
+ }
378
+ .dsff-modal-body {
379
+ font-size: 0.88em;
380
+ line-height: 1.5;
381
+ margin: 0 0 16px;
382
+ opacity: 0.85;
383
+ }
384
+ .dsff-modal-body strong { font-weight: 700; }
385
+ /* Two-paragraph variant used by the unsaved-changes dialog */
386
+ .dsff-modal-msg1 {
387
+ font-size: 0.92em;
388
+ font-weight: 500;
389
+ margin: 0 0 8px;
390
+ line-height: 1.5;
391
+ }
392
+ .dsff-modal-msg2 {
393
+ font-size: 0.88em;
394
+ opacity: 0.72;
395
+ margin: 0 0 20px;
396
+ line-height: 1.5;
397
+ }
398
+ .dsff-modal-msg1 strong,
399
+ .dsff-modal-msg2 strong { font-weight: 700; }
400
+ .dsff-modal-input {
401
+ width: 100%;
402
+ box-sizing: border-box;
403
+ margin: 0 0 16px;
404
+ padding: 6px 9px;
405
+ border: 1px solid var(--red-ui-secondary-border-color, #ccc);
406
+ border-radius: 3px;
407
+ background: var(--red-ui-secondary-background, #fff);
408
+ color: var(--red-ui-primary-text, #222);
409
+ font-size: 0.92em;
410
+ display: block;
411
+ }
412
+ .dsff-modal-input:focus {
413
+ outline: none;
414
+ border-color: var(--red-ui-primary-background, #6e4db2);
415
+ }
416
+ .dsff-modal-btns {
417
+ display: flex;
418
+ gap: 8px;
419
+ justify-content: flex-end;
420
+ }
421
+ .dsff-modal-btns .red-ui-button { margin: 0; }
422
+ .dsff-modal-btn-primary {
423
+ color: var(--red-ui-primary-background, #6e4db2) !important;
424
+ border-color: var(--red-ui-primary-background, #6e4db2) !important;
425
+ font-weight: 600;
426
+ }
427
+ .dsff-modal-btn-primary:hover { background: var(--red-ui-secondary-background-alt, #f0f0f0) !important; }
428
+ .dsff-modal-btn-danger {
429
+ color: #c0392b;
430
+ border-color: #c0392b;
431
+ }
432
+ .dsff-modal-btn-danger:hover { background: #fdf2f2; }
433
+
434
+ /* ══════════════════════════════════════════════════════════════════════════
435
+ Mode-based visibility
436
+ Exactly one of dsff-compact / dsff-expanded is always on $root.
437
+ dsff-browser-view / dsff-editor-view apply only in compact mode.
438
+ dsff-file-open is set whenever a file has been loaded.
439
+ ══════════════════════════════════════════════════════════════════════════ */
440
+
441
+ /* ── COMPACT mode ────────────────────────────────────────────────────────── */
442
+
443
+ /* Show compact-specific chrome */
444
+ .dsff-compact .dsff-compact-folder { display: block; }
445
+ .dsff-compact .dsff-btn-new-compact { display: inline-block; }
446
+
447
+ /* Hide modified column — free up horizontal space */
448
+ .dsff-compact .dsff-mtime,
449
+ .dsff-compact .dsff-tree-header-mod { display: none !important; }
450
+
451
+ /* Hide all expanded-only toolbar elements */
452
+ .dsff-compact .dsff-base-label,
453
+ .dsff-compact .dsff-btn-new,
454
+ .dsff-compact .dsff-btn-wrap,
455
+ .dsff-compact .dsff-crumb { display: none !important; }
456
+
457
+ /* compact browser view: save lives in hidden $right — nothing to hide here */
458
+
459
+ /* compact editor view: show back button, filename, and save */
460
+ .dsff-compact.dsff-editor-view .dsff-btn-back { display: inline-block; }
461
+ .dsff-compact.dsff-editor-view .dsff-compact-filename { display: block; }
462
+ /* dsff-btn-save is naturally visible (no hide rule in compact mode) */
463
+
464
+ /* ── EXPANDED mode ───────────────────────────────────────────────────────── */
465
+
466
+ /* Hide compact-only elements */
467
+ .dsff-expanded .dsff-btn-back,
468
+ .dsff-expanded .dsff-compact-folder,
469
+ .dsff-expanded .dsff-btn-new-compact,
470
+ .dsff-expanded .dsff-compact-filename { display: none !important; }
471
+
472
+ /* Editor controls default hidden in expanded mode; visibility is width-driven.
473
+ The .dsff-wide class is toggled on $root by applyMode() when the available
474
+ width is >= TOOLBAR_WIDE_THRESHOLD (see plugin.js). File-open state is
475
+ deliberately NOT consulted here. */
476
+ .dsff-expanded .dsff-btn-save,
477
+ .dsff-expanded .dsff-btn-wrap,
478
+ .dsff-expanded .dsff-crumb { display: none; }
479
+
480
+ .dsff-wide .dsff-btn-save { display: inline-block; }
481
+ .dsff-wide .dsff-btn-wrap { display: inline-block; }
482
+ .dsff-wide .dsff-crumb { display: block; }
483
+
484
+ /* Editor empty state: show only in expanded mode with no file open */
485
+ .dsff-expanded:not(.dsff-file-open) .dsff-editor-empty { display: flex; }
486
+
487
+ `;
488
+ const el = document.createElement("style");
489
+ el.id = "dsff-style";
490
+ el.textContent = css;
491
+ document.head.appendChild(el);
492
+ })();