@mmlogic/components 0.3.9 → 0.3.11

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.
Files changed (50) hide show
  1. package/dist/cjs/loader.cjs.js +1 -1
  2. package/dist/cjs/mosterdcomponents.cjs.js +1 -1
  3. package/dist/cjs/{mrd-boolean-field_22.cjs.entry.js → mrd-boolean-field_23.cjs.entry.js} +684 -41
  4. package/dist/collection/collection-manifest.json +1 -0
  5. package/dist/collection/components/mrd-document-container/mrd-document-container.js +421 -0
  6. package/dist/collection/components/mrd-document-container/mrd-document-container.scss +79 -0
  7. package/dist/collection/components/mrd-document-list/mrd-document-list.js +653 -16
  8. package/dist/collection/components/mrd-document-list/mrd-document-list.scss +66 -0
  9. package/dist/collection/components/mrd-document-view/mrd-document-view.js +1 -1
  10. package/dist/collection/components/mrd-email-field/mrd-email-field.js +1 -1
  11. package/dist/collection/components/mrd-file-field/mrd-file-field.js +1 -1
  12. package/dist/collection/components/mrd-image-field/mrd-image-field.js +1 -1
  13. package/dist/collection/components/mrd-layout-section/mrd-layout-section.js +165 -14
  14. package/dist/collection/components/mrd-layout-section/mrd-layout-section.scss +0 -44
  15. package/dist/collection/components/mrd-longtext-field/mrd-longtext-field.js +1 -1
  16. package/dist/collection/components/mrd-number-field/mrd-number-field.js +1 -1
  17. package/dist/collection/components/mrd-secret-field/mrd-secret-field.js +2 -2
  18. package/dist/collection/components/mrd-text-field/mrd-text-field.js +1 -1
  19. package/dist/collection/components/mrd-textarea-field/mrd-textarea-field.js +1 -1
  20. package/dist/collection/components/mrd-time-field/mrd-time-field.js +1 -1
  21. package/dist/collection/dev/app.js +47 -0
  22. package/dist/collection/utils/i18n.js +12 -0
  23. package/dist/components/i18n.js +1 -1
  24. package/dist/components/mrd-document-container.d.ts +11 -0
  25. package/dist/components/mrd-document-container.js +1 -0
  26. package/dist/components/mrd-document-container2.js +1 -0
  27. package/dist/components/mrd-document-list2.js +1 -1
  28. package/dist/components/mrd-document-view2.js +1 -1
  29. package/dist/components/mrd-email-field2.js +1 -1
  30. package/dist/components/mrd-file-field2.js +1 -1
  31. package/dist/components/mrd-image-field2.js +1 -1
  32. package/dist/components/mrd-layout-section.js +1 -1
  33. package/dist/components/mrd-longtext-field2.js +1 -1
  34. package/dist/components/mrd-number-field2.js +1 -1
  35. package/dist/components/mrd-secret-field2.js +1 -1
  36. package/dist/components/mrd-text-field2.js +1 -1
  37. package/dist/components/mrd-textarea-field2.js +1 -1
  38. package/dist/components/mrd-time-field2.js +1 -1
  39. package/dist/esm/loader.js +1 -1
  40. package/dist/esm/mosterdcomponents.js +1 -1
  41. package/dist/esm/{mrd-boolean-field_22.entry.js → mrd-boolean-field_23.entry.js} +684 -42
  42. package/dist/mosterdcomponents/mosterdcomponents.esm.js +1 -1
  43. package/dist/mosterdcomponents/p-a9618055.entry.js +1 -0
  44. package/dist/types/components/mrd-document-container/mrd-document-container.d.ts +99 -0
  45. package/dist/types/components/mrd-document-list/mrd-document-list.d.ts +126 -1
  46. package/dist/types/components/mrd-layout-section/mrd-layout-section.d.ts +32 -5
  47. package/dist/types/components.d.ts +250 -0
  48. package/dist/types/types/client-layout.d.ts +5 -0
  49. package/package.json +1 -1
  50. package/dist/mosterdcomponents/p-b547291b.entry.js +0 -1
@@ -25,15 +25,215 @@ export class MrdDocumentList {
25
25
  this.item = null;
26
26
  /** Parent record id — required for RELATED_VIEW path building. */
27
27
  this.parentId = '';
28
+ /** Canonical parent href (`/{base}/{tenant}/{fromClass}/{parentId}`) supplied by
29
+ * the host. When the RELATED_VIEW's `inverseRelation` equals the archetype
30
+ * `container` slot, this seeds a single container so an empty dossier still
31
+ * accepts file drops and folder creation. */
32
+ this.containerHref = '';
28
33
  this.locale = navigator.language;
29
34
  this.containers = [];
30
35
  this.rootLoading = true;
31
36
  /** True when the single-container level is collapsed away (dossier context). */
32
37
  this.singleContainer = false;
38
+ /** Currently selected node id — the target for "New folder" and (later) drops. */
39
+ this.selectedId = null;
40
+ /** Node id currently highlighted as a drag-drop target (null = none). */
41
+ this.dropTargetId = null;
42
+ /** Node id showing an "uploading…" indicator while a dropped file is processed. */
43
+ this.uploadingNodeId = null;
44
+ /** Whether a drag is hovering the root drop-zone (single-container area highlight). */
45
+ this.rootDropActive = false;
33
46
  this.nodeIndex = new Map();
47
+ // ── New-folder editing state ───────────────────────────────────────────────
48
+ /** The pending node whose inline name input is open (null = not creating). */
49
+ this.pendingNode = null;
50
+ /** The node the pending folder is being created under. */
51
+ this.pendingParent = null;
52
+ /** The container the pending folder belongs to. */
53
+ this.pendingContainer = null;
54
+ /** Current text in the inline name input. */
55
+ this.pendingName = '';
56
+ /** Ref to the inline input, so we can focus it after it mounts. */
57
+ this.pendingInputEl = null;
58
+ /** Set when a fresh input needs focus on the next render. */
59
+ this.pendingNeedsFocus = false;
60
+ /** Monotonic counter for unique temporary pending-node ids. */
61
+ this.newFolderSeq = 0;
62
+ /** Last emitted canCreateFolder value, to only emit mrdCanCreate on change. */
63
+ this.lastCanCreate = false;
64
+ // ── Drag & drop (move a document into another folder) ──────────────────────
65
+ /** The document row currently being dragged. */
66
+ this.dragDoc = null;
67
+ /** Id of the node the dragged document currently lives in. */
68
+ this.dragFromId = null;
69
+ /** External file dropped onto a node, awaiting its upload to complete. */
70
+ this.pendingUpload = null;
71
+ /** The optimistic document row awaiting its self href from the create POST. */
72
+ this.lastOptimisticDoc = null;
73
+ this.startCreateFolder = () => {
74
+ const resolved = this.resolveTarget();
75
+ if (!resolved || this.pendingNode)
76
+ return;
77
+ const { target, container } = resolved;
78
+ const tempId = `${container.id}/__new__${this.newFolderSeq++}`;
79
+ const node = this.makeFolderNode(tempId, '', container.id);
80
+ node.pending = true;
81
+ node.editing = true;
82
+ target.children = [...target.children, node];
83
+ target.open = true;
84
+ this.nodeIndex.set(node.id, node);
85
+ this.pendingNode = node;
86
+ this.pendingParent = target;
87
+ this.pendingContainer = container;
88
+ this.pendingName = '';
89
+ this.pendingNeedsFocus = true;
90
+ this.bump();
91
+ };
92
+ this.onPendingInput = (e) => {
93
+ var _a;
94
+ this.pendingName = e.target.value;
95
+ if ((_a = this.pendingNode) === null || _a === void 0 ? void 0 : _a.error) {
96
+ this.pendingNode.error = undefined;
97
+ this.bump();
98
+ }
99
+ };
100
+ this.onPendingKeyDown = (e) => {
101
+ if (e.key === 'Enter') {
102
+ e.preventDefault();
103
+ this.commitFolder();
104
+ }
105
+ else if (e.key === 'Escape') {
106
+ e.preventDefault();
107
+ this.cancelFolder();
108
+ }
109
+ };
110
+ // ── Drag & drop: move a document into another folder / container ────────────
111
+ this.onDocDragStart = (e, doc, node) => {
112
+ var _a, _b, _c;
113
+ this.dragDoc = doc;
114
+ this.dragFromId = node.id;
115
+ if (e.dataTransfer) {
116
+ e.dataTransfer.effectAllowed = 'move';
117
+ // A payload is required for a drag to start in some browsers.
118
+ e.dataTransfer.setData('text/plain', (_c = (_b = (_a = doc === null || doc === void 0 ? void 0 : doc._links) === null || _a === void 0 ? void 0 : _a.self) === null || _b === void 0 ? void 0 : _b.href) !== null && _c !== void 0 ? _c : '');
119
+ }
120
+ };
121
+ this.onDocDragEnd = () => {
122
+ this.dragDoc = null;
123
+ this.dragFromId = null;
124
+ if (this.dropTargetId !== null)
125
+ this.dropTargetId = null;
126
+ };
127
+ /** A folder/container node (or a document inside it) accepts an internal move
128
+ * or an external file drop. Always claims the event so it never bubbles to the
129
+ * root drop-zone; a different node is highlighted (its own folder = no-op). */
130
+ this.onNodeDragOver = (e, node) => {
131
+ const file = this.isFileDrag(e);
132
+ if (!this.dragDoc && !file)
133
+ return;
134
+ e.preventDefault();
135
+ e.stopPropagation();
136
+ if (e.dataTransfer)
137
+ e.dataTransfer.dropEffect = file ? 'copy' : 'move';
138
+ const target = !file && node.id === this.dragFromId ? null : node.id;
139
+ if (this.dropTargetId !== target)
140
+ this.dropTargetId = target;
141
+ };
142
+ this.onNodeDragLeave = (node) => {
143
+ if (this.dropTargetId === node.id)
144
+ this.dropTargetId = null;
145
+ };
146
+ this.onNodeDrop = (e, node, container) => {
147
+ if (this.isFileDrag(e)) {
148
+ e.preventDefault();
149
+ e.stopPropagation();
150
+ this.startFileUpload(e, node, container);
151
+ return;
152
+ }
153
+ if (!this.dragDoc)
154
+ return;
155
+ e.preventDefault();
156
+ e.stopPropagation();
157
+ this.moveDocument(node, container);
158
+ this.onDocDragEnd();
159
+ };
160
+ /** Root-zone drop (dossier context): target the single container's root. */
161
+ this.onRootDragOver = (e) => {
162
+ if (!this.singleContainer)
163
+ return;
164
+ const file = this.isFileDrag(e);
165
+ if (!this.dragDoc && !file)
166
+ return;
167
+ e.preventDefault();
168
+ if (e.dataTransfer)
169
+ e.dataTransfer.dropEffect = file ? 'copy' : 'move';
170
+ if (!this.rootDropActive)
171
+ this.rootDropActive = true;
172
+ };
173
+ this.onRootDragLeave = (e) => {
174
+ const related = e.relatedTarget;
175
+ if (related && this.el.contains(related))
176
+ return; // moved onto a child, still inside
177
+ if (this.rootDropActive)
178
+ this.rootDropActive = false;
179
+ };
180
+ this.onRootDrop = (e) => {
181
+ this.rootDropActive = false;
182
+ if (!this.singleContainer)
183
+ return;
184
+ const only = this.containers[0];
185
+ if (this.isFileDrag(e)) {
186
+ e.preventDefault();
187
+ this.startFileUpload(e, only, only);
188
+ return;
189
+ }
190
+ if (!this.dragDoc)
191
+ return;
192
+ e.preventDefault();
193
+ this.moveDocument(only, only);
194
+ this.onDocDragEnd();
195
+ };
34
196
  }
35
197
  componentDidLoad() {
36
- this.emitContainerDistinct();
198
+ if (this.isSeededSingleContainer()) {
199
+ this.seedSingleContainer();
200
+ }
201
+ else {
202
+ this.emitContainerDistinct();
203
+ }
204
+ }
205
+ /** True when the view is a single-container context: a RELATED_VIEW whose
206
+ * `inverseRelation` matches the archetype `container` slot, with a known
207
+ * parent href. The container is then implied by context (not by distinct),
208
+ * so it works even with zero documents. */
209
+ isSeededSingleContainer() {
210
+ var _a, _b;
211
+ return (((_a = this.item) === null || _a === void 0 ? void 0 : _a.type) === ClientLayoutItemType.RELATED_VIEW &&
212
+ !!((_b = this.item) === null || _b === void 0 ? void 0 : _b.inverseRelation) &&
213
+ this.item.inverseRelation === this.containerField &&
214
+ !!this.containerHref);
215
+ }
216
+ /** Seed the single implied container from context and load its folders. */
217
+ seedSingleContainer() {
218
+ const node = this.makeContainerNode(this.containerHref, this.containerField, 0);
219
+ this.nodeIndex.set(node.id, node);
220
+ this.containers = [node];
221
+ this.rootLoading = false;
222
+ this.singleContainer = true;
223
+ node.open = true;
224
+ this.emitFolderDistinct(node);
225
+ }
226
+ componentDidRender() {
227
+ if (this.pendingNeedsFocus && this.pendingInputEl) {
228
+ this.pendingInputEl.focus();
229
+ this.pendingInputEl.select();
230
+ this.pendingNeedsFocus = false;
231
+ }
232
+ const can = this.canCreateFolder;
233
+ if (can !== this.lastCanCreate) {
234
+ this.lastCanCreate = can;
235
+ this.mrdCanCreate.emit(can);
236
+ }
37
237
  }
38
238
  // ── Slot field names ─────────────────────────────────────────────────────
39
239
  get containerField() {
@@ -48,12 +248,19 @@ export class MrdDocumentList {
48
248
  var _a, _b, _c;
49
249
  return (_c = (_b = (_a = this.archetype) === null || _a === void 0 ? void 0 : _a.slots) === null || _b === void 0 ? void 0 : _b.title) !== null && _c !== void 0 ? _c : 'name';
50
250
  }
251
+ get fileField() {
252
+ var _a, _b, _c;
253
+ return (_c = (_b = (_a = this.archetype) === null || _a === void 0 ? void 0 : _a.slots) === null || _b === void 0 ? void 0 : _b.file) !== null && _c !== void 0 ? _c : 'file';
254
+ }
51
255
  get dateField() {
52
256
  var _a, _b;
53
257
  return (_b = (_a = this.archetype) === null || _a === void 0 ? void 0 : _a.slots) === null || _b === void 0 ? void 0 : _b.date;
54
258
  }
55
259
  // ── Path / query building (mirrors mrd-table conventions) ────────────────
56
- /** VIEW: `/{dataClass}`; RELATED_VIEW: `/{fromClass}/{parentId}/{dataClass}`. */
260
+ /** VIEW: `/{dataClass}`; RELATED_VIEW: `/{fromClass}/{parentId}/{dataClass}`.
261
+ * Used for reads (list/distinct): `dataClass`/`relatedClass` may be an abstract
262
+ * type (e.g. "contents"), filtered down to the concrete `filterClass` via the
263
+ * `type` query param — see baseParams(). */
57
264
  buildDataPath() {
58
265
  var _a, _b, _c;
59
266
  const it = this.item;
@@ -63,6 +270,19 @@ export class MrdDocumentList {
63
270
  }
64
271
  return `/${dataClass}`;
65
272
  }
273
+ /** Path for creating a new document (file-drop). Unlike buildDataPath(), this
274
+ * must target the concrete type: `dataClass`/`relatedClass` can be an abstract
275
+ * base type (e.g. "contents") that cannot be POSTed to directly, so prefer
276
+ * `filterClass` (e.g. "documents") when present. */
277
+ buildCreatePath() {
278
+ var _a, _b, _c, _d;
279
+ const it = this.item;
280
+ const dataClass = (_c = (_b = (_a = it === null || it === void 0 ? void 0 : it.filterClass) !== null && _a !== void 0 ? _a : it === null || it === void 0 ? void 0 : it.dataClass) !== null && _b !== void 0 ? _b : it === null || it === void 0 ? void 0 : it.relatedClass) !== null && _c !== void 0 ? _c : '';
281
+ if ((it === null || it === void 0 ? void 0 : it.type) === ClientLayoutItemType.RELATED_VIEW) {
282
+ return `/${(_d = it.fromClass) !== null && _d !== void 0 ? _d : ''}/${this.parentId}/${dataClass}`;
283
+ }
284
+ return `/${dataClass}`;
285
+ }
66
286
  /** View-level static filters + filterClass, shared by every request. */
67
287
  baseParams() {
68
288
  var _a, _b, _c, _d, _e, _f, _g;
@@ -200,7 +420,7 @@ export class MrdDocumentList {
200
420
  acc.push(seg);
201
421
  let child = node.children.find(c => c.label === seg);
202
422
  if (!child) {
203
- child = this.makeFolderNode(container.id + '/' + acc.join('/'), seg);
423
+ child = this.makeFolderNode(container.id + '/' + acc.join('/'), seg, container.id);
204
424
  node.children.push(child);
205
425
  this.nodeIndex.set(child.id, child);
206
426
  }
@@ -236,9 +456,9 @@ export class MrdDocumentList {
236
456
  open: false, loading: false, foldersLoaded: false, docsLoaded: false, docs: [],
237
457
  };
238
458
  }
239
- makeFolderNode(id, label) {
459
+ makeFolderNode(id, label, containerId) {
240
460
  return {
241
- id, kind: 'folder', label,
461
+ id, kind: 'folder', label, containerId,
242
462
  hasDocs: false, docCount: 0, totalCount: 0, children: [],
243
463
  open: false, loading: false, foldersLoaded: false, docsLoaded: false, docs: [],
244
464
  };
@@ -256,6 +476,7 @@ export class MrdDocumentList {
256
476
  }
257
477
  // ── Interaction ──────────────────────────────────────────────────────────
258
478
  toggleContainer(node) {
479
+ this.selectedId = node.id;
259
480
  node.open = !node.open;
260
481
  if (node.open && !node.foldersLoaded && !node.loading) {
261
482
  this.emitFolderDistinct(node);
@@ -263,12 +484,235 @@ export class MrdDocumentList {
263
484
  this.bump();
264
485
  }
265
486
  toggleFolder(container, node) {
487
+ this.selectedId = node.id;
266
488
  node.open = !node.open;
267
489
  if (node.open && node.hasDocs && !node.docsLoaded && !node.loading) {
268
490
  this.emitDocuments(container, node);
269
491
  }
270
492
  this.bump();
271
493
  }
494
+ // ── New folder (client-side pending, materialises on document drop) ─────────
495
+ /** Resolve the node a new folder should be created under, and its container.
496
+ * Uses the current selection; falls back to the single container (dossier). */
497
+ resolveTarget() {
498
+ if (this.selectedId) {
499
+ const n = this.nodeIndex.get(this.selectedId);
500
+ if (n && n.kind === 'container')
501
+ return { target: n, container: n };
502
+ if (n && n.kind === 'folder') {
503
+ const container = n.containerId ? this.nodeIndex.get(n.containerId) : undefined;
504
+ if (container)
505
+ return { target: n, container };
506
+ }
507
+ }
508
+ if (this.singleContainer && this.containers[0]) {
509
+ const only = this.containers[0];
510
+ return { target: only, container: only };
511
+ }
512
+ return null;
513
+ }
514
+ get canCreateFolder() {
515
+ return !this.pendingNode && this.resolveTarget() !== null;
516
+ }
517
+ /** Start creating a folder under the current target. Called by the host toolbar
518
+ * (mrd-document-container). No-op when there is no valid target. */
519
+ async createFolder() {
520
+ this.startCreateFolder();
521
+ }
522
+ /** Folder path segments of a node relative to its container (derived from its id). */
523
+ segmentsOf(node, container) {
524
+ if (node.kind === 'container')
525
+ return [];
526
+ const prefix = container.id + '/';
527
+ const rest = node.id.startsWith(prefix) ? node.id.slice(prefix.length) : '';
528
+ return rest ? rest.split('/') : [];
529
+ }
530
+ commitFolder() {
531
+ const node = this.pendingNode;
532
+ const parent = this.pendingParent;
533
+ const container = this.pendingContainer;
534
+ if (!node || !parent || !container)
535
+ return;
536
+ const name = this.pendingName.trim();
537
+ if (!name) {
538
+ this.cancelFolder();
539
+ return;
540
+ }
541
+ if (name.includes('/')) {
542
+ node.error = t('folder_no_slash', this.locale);
543
+ this.bump();
544
+ return;
545
+ }
546
+ const dup = parent.children.some(c => c !== node && c.label.toLowerCase() === name.toLowerCase());
547
+ if (dup) {
548
+ node.error = t('folder_duplicate', this.locale);
549
+ this.bump();
550
+ return;
551
+ }
552
+ // Finalise: swap the temp id for the real path-derived id so a later distinct
553
+ // value with the same folder string merges onto this node.
554
+ const segs = [...this.segmentsOf(parent, container), name];
555
+ this.nodeIndex.delete(node.id);
556
+ node.id = container.id + '/' + segs.join('/');
557
+ node.fullPath = '/' + segs.join('/');
558
+ node.label = name;
559
+ node.editing = false;
560
+ node.error = undefined;
561
+ node.open = true;
562
+ this.nodeIndex.set(node.id, node);
563
+ this.selectedId = node.id;
564
+ this.clearPending();
565
+ this.bump();
566
+ }
567
+ cancelFolder() {
568
+ const node = this.pendingNode;
569
+ const parent = this.pendingParent;
570
+ if (node && parent) {
571
+ parent.children = parent.children.filter(c => c !== node);
572
+ this.nodeIndex.delete(node.id);
573
+ }
574
+ this.clearPending();
575
+ this.bump();
576
+ }
577
+ clearPending() {
578
+ this.pendingNode = null;
579
+ this.pendingParent = null;
580
+ this.pendingContainer = null;
581
+ this.pendingName = '';
582
+ this.pendingInputEl = null;
583
+ this.pendingNeedsFocus = false;
584
+ }
585
+ /** True when the drag carries external files (an OS file drop, not an internal move). */
586
+ isFileDrag(e) {
587
+ return !this.dragDoc && !!e.dataTransfer && Array.from(e.dataTransfer.types).includes('Files');
588
+ }
589
+ // ── External file drop → upload → create document ──────────────────────────
590
+ /** Begin an upload: emit mrdUpload; the host uploads and calls setFileReference. */
591
+ startFileUpload(e, node, container) {
592
+ var _a, _b;
593
+ const file = (_b = (_a = e.dataTransfer) === null || _a === void 0 ? void 0 : _a.files) === null || _b === void 0 ? void 0 : _b[0]; // single file per drop
594
+ this.dropTargetId = null;
595
+ if (!file)
596
+ return;
597
+ this.pendingUpload = { file, node, container };
598
+ node.open = true;
599
+ this.uploadingNodeId = node.id;
600
+ this.bump();
601
+ this.mrdUpload.emit({ file });
602
+ }
603
+ /** Strip a single trailing extension for the document title. */
604
+ stripExtension(name) {
605
+ return name.replace(/\.[^./\\]+$/, '');
606
+ }
607
+ /** Host callback with the uploaded file reference. Builds the create payload
608
+ * from the archetype slots and requests the new document. */
609
+ async setFileReference(uri) {
610
+ const up = this.pendingUpload;
611
+ if (!up)
612
+ return;
613
+ const { file, node, container } = up;
614
+ const folder = this.folderStringOf(node, container);
615
+ const values = {
616
+ [this.fileField]: uri,
617
+ [this.titleField]: this.stripExtension(file.name),
618
+ [this.folderField]: folder,
619
+ [this.containerField]: container.href,
620
+ };
621
+ this.mrdCreateObject.emit({ path: this.buildCreatePath(), values });
622
+ // Optimistically show the new document (no href until a reload).
623
+ const optimistic = { [this.titleField]: this.stripExtension(file.name), _optimistic: true };
624
+ const wasUnloadedEmpty = !node.docsLoaded && node.docCount === 0;
625
+ node.hasDocs = true;
626
+ node.pending = false;
627
+ node.docCount += 1;
628
+ if (wasUnloadedEmpty) {
629
+ node.docsLoaded = true;
630
+ node.docs = [optimistic];
631
+ }
632
+ else if (node.docsLoaded)
633
+ node.docs = [...node.docs, optimistic];
634
+ this.recomputeContainerTotals(container, +1);
635
+ this.lastOptimisticDoc = optimistic;
636
+ this.pendingUpload = null;
637
+ this.uploadingNodeId = null;
638
+ this.bump();
639
+ }
640
+ /** Host callback with the created document's self href, so the optimistic row
641
+ * becomes navigable without waiting for a reload. */
642
+ async setCreatedHref(href) {
643
+ var _a;
644
+ const doc = this.lastOptimisticDoc;
645
+ if (!doc || !href)
646
+ return;
647
+ doc._links = Object.assign(Object.assign({}, ((_a = doc._links) !== null && _a !== void 0 ? _a : {})), { self: { href } });
648
+ doc._optimistic = false;
649
+ this.lastOptimisticDoc = null;
650
+ this.bump();
651
+ }
652
+ /** Folder string a node represents: the raw distinct value when it holds
653
+ * documents, else derived from its path segments; null for a container root. */
654
+ folderStringOf(node, container) {
655
+ if (node.kind === 'container')
656
+ return null;
657
+ if (node.fullPath != null && node.fullPath !== '')
658
+ return node.fullPath;
659
+ const segs = this.segmentsOf(node, container);
660
+ return segs.length ? '/' + segs.join('/') : null;
661
+ }
662
+ moveDocument(toNode, toContainer) {
663
+ var _a, _b;
664
+ const doc = this.dragDoc;
665
+ const fromNode = this.dragFromId ? this.nodeIndex.get(this.dragFromId) : undefined;
666
+ if (!doc || !fromNode)
667
+ return;
668
+ const fromContainer = fromNode.kind === 'container'
669
+ ? fromNode
670
+ : (fromNode.containerId ? this.nodeIndex.get(fromNode.containerId) : undefined);
671
+ if (!fromContainer)
672
+ return;
673
+ const targetFolder = this.folderStringOf(toNode, toContainer);
674
+ const sourceFolder = this.folderStringOf(fromNode, fromContainer);
675
+ const sameContainer = toContainer === fromContainer;
676
+ // No-op: dropped back onto its own folder.
677
+ if (toNode === fromNode || (sameContainer && targetFolder === sourceFolder))
678
+ return;
679
+ // ── Optimistic tree update (fire-and-forget; no refetch) ──
680
+ fromNode.docs = fromNode.docs.filter(d => d !== doc);
681
+ fromNode.docCount = Math.max(0, fromNode.docCount - 1);
682
+ // A now-empty folder should stop showing an expand chevron (it disappears on
683
+ // the next reload anyway, since it is derived from document folder strings).
684
+ if (fromNode.docsLoaded)
685
+ fromNode.hasDocs = fromNode.docs.length > 0;
686
+ toNode.hasDocs = true;
687
+ toNode.pending = false; // a pending folder is now materialised
688
+ toNode.docCount += 1;
689
+ if (toNode.kind === 'folder')
690
+ toNode.fullPath = targetFolder !== null && targetFolder !== void 0 ? targetFolder : '';
691
+ if (toNode.docsLoaded)
692
+ toNode.docs = [...toNode.docs, doc];
693
+ this.recomputeContainerTotals(fromContainer, -1);
694
+ if (!sameContainer)
695
+ this.recomputeContainerTotals(toContainer, +1);
696
+ // ── Emit the generic update; host PATCHes the document ──
697
+ const href = (_b = (_a = doc === null || doc === void 0 ? void 0 : doc._links) === null || _a === void 0 ? void 0 : _a.self) === null || _b === void 0 ? void 0 : _b.href;
698
+ if (href) {
699
+ const values = { [this.folderField]: targetFolder };
700
+ if (!sameContainer)
701
+ values[this.containerField] = toContainer.href;
702
+ this.mrdUpdateObject.emit({ href, values });
703
+ }
704
+ this.bump();
705
+ }
706
+ /** Recompute a container's subtree totals from docCounts when its folders are
707
+ * loaded; otherwise just nudge the aggregate badge by the delta. */
708
+ recomputeContainerTotals(container, delta) {
709
+ if (container.foldersLoaded) {
710
+ this.computeTotals(container);
711
+ }
712
+ else {
713
+ container.totalCount = Math.max(0, container.totalCount + delta);
714
+ }
715
+ }
272
716
  openDoc(doc) {
273
717
  var _a, _b, _c, _d;
274
718
  const href = (_b = (_a = doc === null || doc === void 0 ? void 0 : doc._links) === null || _a === void 0 ? void 0 : _a.self) === null || _b === void 0 ? void 0 : _b.href;
@@ -291,12 +735,12 @@ export class MrdDocumentList {
291
735
  indent(depth) {
292
736
  return h("span", { class: "mrd-document-list__indent", style: { width: `${depth * 18}px` } });
293
737
  }
294
- renderDocRow(doc, depth) {
738
+ renderDocRow(doc, node, container, depth) {
295
739
  var _a, _b, _c, _d, _e;
296
740
  const label = (_b = (_a = doc === null || doc === void 0 ? void 0 : doc[this.titleField]) !== null && _a !== void 0 ? _a : doc === null || doc === void 0 ? void 0 : doc.name) !== null && _b !== void 0 ? _b : '';
297
741
  const dateRaw = this.dateField ? doc === null || doc === void 0 ? void 0 : doc[this.dateField] : undefined;
298
742
  const date = dateRaw ? formatDate(dateRaw, this.locale) : '';
299
- return (h("div", { class: "mrd-document-list__row mrd-document-list__row--doc", key: (_e = (_d = (_c = doc === null || doc === void 0 ? void 0 : doc._links) === null || _c === void 0 ? void 0 : _c.self) === null || _d === void 0 ? void 0 : _d.href) !== null && _e !== void 0 ? _e : label, onClick: () => this.openDoc(doc) }, this.indent(depth), this.renderChevron(false, true), this.renderDocIcon(), h("span", { class: "mrd-document-list__label" }, label), date && h("span", { class: "mrd-document-list__date" }, date)));
743
+ return (h("div", { class: "mrd-document-list__row mrd-document-list__row--doc", key: (_e = (_d = (_c = doc === null || doc === void 0 ? void 0 : doc._links) === null || _c === void 0 ? void 0 : _c.self) === null || _d === void 0 ? void 0 : _d.href) !== null && _e !== void 0 ? _e : label, draggable: true, onClick: () => this.openDoc(doc), onDragStart: (e) => this.onDocDragStart(e, doc, node), onDragEnd: this.onDocDragEnd, onDragOver: (e) => this.onNodeDragOver(e, node), onDragLeave: () => this.onNodeDragLeave(node), onDrop: (e) => this.onNodeDrop(e, node, container) }, this.indent(depth), this.renderChevron(false, true), this.renderDocIcon(), h("span", { class: "mrd-document-list__label" }, label), date && h("span", { class: "mrd-document-list__date" }, date)));
300
744
  }
301
745
  renderLoadingRow(depth) {
302
746
  return (h("div", { class: "mrd-document-list__loading", key: "loading" }, this.indent(depth), h("span", { class: "mrd-document-list__spinner" }), t('loading', this.locale)));
@@ -307,7 +751,7 @@ export class MrdDocumentList {
307
751
  if (node.hasDocs) {
308
752
  if (node.docsLoaded) {
309
753
  for (const doc of node.docs)
310
- out.push(this.renderDocRow(doc, depth));
754
+ out.push(this.renderDocRow(doc, node, container, depth));
311
755
  }
312
756
  else if (node.loading) {
313
757
  out.push(this.renderLoadingRow(depth));
@@ -317,14 +761,34 @@ export class MrdDocumentList {
317
761
  for (const child of node.children) {
318
762
  out.push(this.renderFolderNode(container, child, depth));
319
763
  }
764
+ // An uploading file dropped onto this node.
765
+ if (node.id === this.uploadingNodeId)
766
+ out.push(this.renderUploadingRow(depth));
320
767
  return out;
321
768
  }
769
+ renderUploadingRow(depth) {
770
+ return (h("div", { class: "mrd-document-list__loading", key: "uploading" }, this.indent(depth), h("span", { class: "mrd-document-list__spinner" }), t('upload_file', this.locale)));
771
+ }
772
+ renderFolderEditRow(node, depth) {
773
+ return (h("div", { class: "mrd-document-list__node", key: node.id }, h("div", { class: "mrd-document-list__row mrd-document-list__row--edit" }, this.indent(depth), this.renderChevron(false, true), this.renderFolderIcon(), h("input", { class: "mrd-document-list__name-input", type: "text", placeholder: t('folder_name_placeholder', this.locale), ref: el => (this.pendingInputEl = el), onInput: this.onPendingInput, onKeyDown: this.onPendingKeyDown, onBlur: () => this.cancelFolder() })), node.error && (h("div", { class: "mrd-document-list__error", style: { paddingLeft: `${depth * 18 + 52}px` } }, node.error))));
774
+ }
322
775
  renderFolderNode(container, node, depth) {
776
+ if (node.editing)
777
+ return this.renderFolderEditRow(node, depth);
323
778
  const leaf = node.children.length === 0 && !node.hasDocs;
324
- return (h("div", { class: "mrd-document-list__node", key: node.id }, h("div", { class: "mrd-document-list__row", onClick: () => this.toggleFolder(container, node) }, this.indent(depth), this.renderChevron(node.open, leaf), this.renderFolderIcon(), h("span", { class: "mrd-document-list__label" }, node.label), h("span", { class: "mrd-document-list__count" }, node.totalCount)), node.open && h("div", { class: "mrd-document-list__kids" }, this.renderChildren(container, node, depth + 1))));
779
+ const selected = node.id === this.selectedId;
780
+ const cls = 'mrd-document-list__row'
781
+ + (selected ? ' mrd-document-list__row--selected' : '')
782
+ + (node.pending ? ' mrd-document-list__row--pending' : '')
783
+ + (node.id === this.dropTargetId ? ' mrd-document-list__row--drop' : '');
784
+ return (h("div", { class: "mrd-document-list__node", key: node.id }, h("div", { class: cls, onClick: () => this.toggleFolder(container, node), onDragOver: (e) => this.onNodeDragOver(e, node), onDragLeave: () => this.onNodeDragLeave(node), onDrop: (e) => this.onNodeDrop(e, node, container) }, this.indent(depth), this.renderChevron(node.open, leaf), this.renderFolderIcon(), h("span", { class: "mrd-document-list__label" }, node.label), h("span", { class: "mrd-document-list__count" }, node.totalCount)), node.open && h("div", { class: "mrd-document-list__kids" }, this.renderChildren(container, node, depth + 1))));
325
785
  }
326
786
  renderContainerNode(node, depth) {
327
- return (h("div", { class: "mrd-document-list__node", key: node.id }, h("div", { class: "mrd-document-list__row mrd-document-list__row--dossier", onClick: () => this.toggleContainer(node) }, this.indent(depth), this.renderChevron(node.open, false), this.renderDossierIcon(), h("span", { class: "mrd-document-list__label" }, node.label), h("span", { class: "mrd-document-list__count" }, node.totalCount)), node.open && (h("div", { class: "mrd-document-list__kids" }, node.loading && !node.foldersLoaded
787
+ const selected = node.id === this.selectedId;
788
+ const cls = 'mrd-document-list__row mrd-document-list__row--dossier'
789
+ + (selected ? ' mrd-document-list__row--selected' : '')
790
+ + (node.id === this.dropTargetId ? ' mrd-document-list__row--drop' : '');
791
+ return (h("div", { class: "mrd-document-list__node", key: node.id }, h("div", { class: cls, onClick: () => this.toggleContainer(node), onDragOver: (e) => this.onNodeDragOver(e, node), onDragLeave: () => this.onNodeDragLeave(node), onDrop: (e) => this.onNodeDrop(e, node, node) }, this.indent(depth), this.renderChevron(node.open, false), this.renderDossierIcon(), h("span", { class: "mrd-document-list__label" }, node.label), h("span", { class: "mrd-document-list__count" }, node.totalCount)), node.open && (h("div", { class: "mrd-document-list__kids" }, node.loading && !node.foldersLoaded
328
792
  ? this.renderLoadingRow(depth + 1)
329
793
  : this.renderChildren(node, node, depth + 1)))));
330
794
  }
@@ -334,19 +798,25 @@ export class MrdDocumentList {
334
798
  body = this.renderLoadingRow(0);
335
799
  }
336
800
  else if (this.containers.length === 0) {
337
- body = h("div", { key: '0c4190bb86543e2d37cc1584b3495eb6eab5664c', class: "mrd-document-list__empty" }, t('no_results', this.locale));
801
+ body = h("div", { key: '9c38a2e26320a8173ba82cac412c1b32a8a1882e', class: "mrd-document-list__empty" }, t('no_results', this.locale));
338
802
  }
339
803
  else if (this.singleContainer) {
340
804
  // Dossier context: render the single container's folder tree at the root.
341
805
  const only = this.containers[0];
342
- body = only.loading && !only.foldersLoaded
343
- ? this.renderLoadingRow(0)
344
- : this.renderChildren(only, only, 0);
806
+ if (only.loading && !only.foldersLoaded) {
807
+ body = this.renderLoadingRow(0);
808
+ }
809
+ else {
810
+ const kids = this.renderChildren(only, only, 0);
811
+ body = kids.length
812
+ ? kids
813
+ : h("div", { class: "mrd-document-list__empty" }, t('doc_empty_drop', this.locale));
814
+ }
345
815
  }
346
816
  else {
347
817
  body = this.containers.map(c => this.renderContainerNode(c, 0));
348
818
  }
349
- return (h(Host, { key: '4e38d9ef5a1df137a41516a47dc5debbea6b2f35' }, h("div", { key: '6d2f162624e5bbc8650a2978a3b045c21fe13ea3', class: "mrd-document-list" }, body)));
819
+ return (h(Host, { key: 'dd83a6a609e01d21a2b5da5b3ab929f6f9315402' }, h("div", { key: 'd539d938e1efd759730cccc90c4d6c2e535db936', class: 'mrd-document-list' + (this.rootDropActive ? ' mrd-document-list--drop-active' : ''), onDragOver: this.onRootDragOver, onDragLeave: this.onRootDragLeave, onDrop: this.onRootDrop }, body)));
350
820
  }
351
821
  static get is() { return "mrd-document-list"; }
352
822
  static get encapsulation() { return "scoped"; }
@@ -431,6 +901,26 @@ export class MrdDocumentList {
431
901
  "attribute": "parent-id",
432
902
  "defaultValue": "''"
433
903
  },
904
+ "containerHref": {
905
+ "type": "string",
906
+ "mutable": false,
907
+ "complexType": {
908
+ "original": "string",
909
+ "resolved": "string",
910
+ "references": {}
911
+ },
912
+ "required": false,
913
+ "optional": false,
914
+ "docs": {
915
+ "tags": [],
916
+ "text": "Canonical parent href (`/{base}/{tenant}/{fromClass}/{parentId}`) supplied by\nthe host. When the RELATED_VIEW's `inverseRelation` equals the archetype\n`container` slot, this seeds a single container so an empty dossier still\naccepts file drops and folder creation."
917
+ },
918
+ "getter": false,
919
+ "setter": false,
920
+ "reflect": false,
921
+ "attribute": "container-href",
922
+ "defaultValue": "''"
923
+ },
434
924
  "locale": {
435
925
  "type": "string",
436
926
  "mutable": false,
@@ -457,7 +947,11 @@ export class MrdDocumentList {
457
947
  return {
458
948
  "containers": {},
459
949
  "rootLoading": {},
460
- "singleContainer": {}
950
+ "singleContainer": {},
951
+ "selectedId": {},
952
+ "dropTargetId": {},
953
+ "uploadingNodeId": {},
954
+ "rootDropActive": {}
461
955
  };
462
956
  }
463
957
  static get events() {
@@ -506,6 +1000,81 @@ export class MrdDocumentList {
506
1000
  "resolved": "{ href?: string | undefined; label: string; }",
507
1001
  "references": {}
508
1002
  }
1003
+ }, {
1004
+ "method": "mrdCanCreate",
1005
+ "name": "mrdCanCreate",
1006
+ "bubbles": true,
1007
+ "cancelable": true,
1008
+ "composed": true,
1009
+ "docs": {
1010
+ "tags": [],
1011
+ "text": "Emitted whenever the \"can create a folder here\" state changes, so a host\ntoolbar (mrd-document-container) can enable/disable its New folder button."
1012
+ },
1013
+ "complexType": {
1014
+ "original": "boolean",
1015
+ "resolved": "boolean",
1016
+ "references": {}
1017
+ }
1018
+ }, {
1019
+ "method": "mrdUpdateObject",
1020
+ "name": "mrdUpdateObject",
1021
+ "bubbles": true,
1022
+ "cancelable": true,
1023
+ "composed": true,
1024
+ "docs": {
1025
+ "tags": [],
1026
+ "text": "Generic \"update an existing object\" request \u2014 fired when a document is\ndragged into another folder/container. Host PATCHes `href` with `values`\n(no move/folder knowledge needed). `values` carries the new folder string\n(or null for the container root) and, on a cross-container move, the new\ncontainer relation href."
1027
+ },
1028
+ "complexType": {
1029
+ "original": "{ href: string; values: Record<string, unknown> }",
1030
+ "resolved": "{ href: string; values: Record<string, unknown>; }",
1031
+ "references": {
1032
+ "Record": {
1033
+ "location": "global",
1034
+ "id": "global::Record"
1035
+ }
1036
+ }
1037
+ }
1038
+ }, {
1039
+ "method": "mrdUpload",
1040
+ "name": "mrdUpload",
1041
+ "bubbles": true,
1042
+ "cancelable": true,
1043
+ "composed": true,
1044
+ "docs": {
1045
+ "tags": [],
1046
+ "text": "An external file was dropped onto a folder/container. Same flow as mrd-form:\nhost uploads the file and calls `setFileReference(uri)` with the result."
1047
+ },
1048
+ "complexType": {
1049
+ "original": "{ file: File }",
1050
+ "resolved": "{ file: File; }",
1051
+ "references": {
1052
+ "File": {
1053
+ "location": "global",
1054
+ "id": "global::File"
1055
+ }
1056
+ }
1057
+ }
1058
+ }, {
1059
+ "method": "mrdCreateObject",
1060
+ "name": "mrdCreateObject",
1061
+ "bubbles": true,
1062
+ "cancelable": true,
1063
+ "composed": true,
1064
+ "docs": {
1065
+ "tags": [],
1066
+ "text": "Fired once the dropped file is uploaded \u2014 requests creation of a new document.\nHost POSTs `values` to `path` (on a 400 it can open the create form itself)."
1067
+ },
1068
+ "complexType": {
1069
+ "original": "{ path: string; values: Record<string, unknown> }",
1070
+ "resolved": "{ path: string; values: Record<string, unknown>; }",
1071
+ "references": {
1072
+ "Record": {
1073
+ "location": "global",
1074
+ "id": "global::Record"
1075
+ }
1076
+ }
1077
+ }
509
1078
  }];
510
1079
  }
511
1080
  static get methods() {
@@ -569,6 +1138,74 @@ export class MrdDocumentList {
569
1138
  "text": "Inject the documents loaded for one folder node.",
570
1139
  "tags": []
571
1140
  }
1141
+ },
1142
+ "createFolder": {
1143
+ "complexType": {
1144
+ "signature": "() => Promise<void>",
1145
+ "parameters": [],
1146
+ "references": {
1147
+ "Promise": {
1148
+ "location": "global",
1149
+ "id": "global::Promise"
1150
+ }
1151
+ },
1152
+ "return": "Promise<void>"
1153
+ },
1154
+ "docs": {
1155
+ "text": "Start creating a folder under the current target. Called by the host toolbar\n(mrd-document-container). No-op when there is no valid target.",
1156
+ "tags": []
1157
+ }
1158
+ },
1159
+ "setFileReference": {
1160
+ "complexType": {
1161
+ "signature": "(uri: string) => Promise<void>",
1162
+ "parameters": [{
1163
+ "name": "uri",
1164
+ "type": "string",
1165
+ "docs": ""
1166
+ }],
1167
+ "references": {
1168
+ "Promise": {
1169
+ "location": "global",
1170
+ "id": "global::Promise"
1171
+ },
1172
+ "Record": {
1173
+ "location": "global",
1174
+ "id": "global::Record"
1175
+ },
1176
+ "DocRow": {
1177
+ "location": "local",
1178
+ "path": "/Users/michiel/Mosterd/mosterd-components/src/components/mrd-document-list/mrd-document-list.tsx",
1179
+ "id": "src/components/mrd-document-list/mrd-document-list.tsx::DocRow"
1180
+ }
1181
+ },
1182
+ "return": "Promise<void>"
1183
+ },
1184
+ "docs": {
1185
+ "text": "Host callback with the uploaded file reference. Builds the create payload\nfrom the archetype slots and requests the new document.",
1186
+ "tags": []
1187
+ }
1188
+ },
1189
+ "setCreatedHref": {
1190
+ "complexType": {
1191
+ "signature": "(href: string) => Promise<void>",
1192
+ "parameters": [{
1193
+ "name": "href",
1194
+ "type": "string",
1195
+ "docs": ""
1196
+ }],
1197
+ "references": {
1198
+ "Promise": {
1199
+ "location": "global",
1200
+ "id": "global::Promise"
1201
+ }
1202
+ },
1203
+ "return": "Promise<void>"
1204
+ },
1205
+ "docs": {
1206
+ "text": "Host callback with the created document's self href, so the optimistic row\nbecomes navigable without waiting for a reload.",
1207
+ "tags": []
1208
+ }
572
1209
  }
573
1210
  };
574
1211
  }