@mmlogic/components 0.1.14 → 0.1.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.
Files changed (36) hide show
  1. package/dist/cjs/{format-BT6-_W7X.js → format-IFzg0q-6.js} +13 -1
  2. package/dist/cjs/loader.cjs.js +1 -1
  3. package/dist/cjs/mosterdcomponents.cjs.js +1 -1
  4. package/dist/cjs/mrd-boolean-field_16.cjs.entry.js +53 -20
  5. package/dist/cjs/mrd-table.cjs.entry.js +134 -15
  6. package/dist/collection/components/mrd-field/mrd-field.js +2 -2
  7. package/dist/collection/components/mrd-form/mrd-form.js +53 -20
  8. package/dist/collection/components/mrd-relation-field/mrd-relation-field.js +3 -3
  9. package/dist/collection/components/mrd-table/mrd-table.js +134 -14
  10. package/dist/collection/components/mrd-table/mrd-table.scss +88 -27
  11. package/dist/collection/dev/app.js +8 -6
  12. package/dist/collection/utils/format.js +1 -1
  13. package/dist/collection/utils/i18n.js +12 -0
  14. package/dist/components/format.js +1 -1
  15. package/dist/components/i18n.js +1 -1
  16. package/dist/components/mrd-form.js +1 -1
  17. package/dist/components/mrd-relation-field2.js +1 -1
  18. package/dist/components/mrd-table.js +1 -1
  19. package/dist/esm/{format-beKIX2qZ.js → format-Cc9kQ1j-.js} +13 -1
  20. package/dist/esm/loader.js +1 -1
  21. package/dist/esm/mosterdcomponents.js +1 -1
  22. package/dist/esm/mrd-boolean-field_16.entry.js +53 -20
  23. package/dist/esm/mrd-table.entry.js +134 -15
  24. package/dist/mosterdcomponents/mosterdcomponents.esm.js +1 -1
  25. package/dist/mosterdcomponents/p-1e0d88fd.entry.js +1 -0
  26. package/dist/mosterdcomponents/p-829b9b6f.entry.js +1 -0
  27. package/dist/mosterdcomponents/p-Cc9kQ1j-.js +1 -0
  28. package/dist/types/components/mrd-field/mrd-field.d.ts +1 -0
  29. package/dist/types/components/mrd-form/mrd-form.d.ts +9 -4
  30. package/dist/types/components/mrd-relation-field/mrd-relation-field.d.ts +1 -0
  31. package/dist/types/components/mrd-table/mrd-table.d.ts +11 -0
  32. package/dist/types/components.d.ts +6 -6
  33. package/package.json +1 -1
  34. package/dist/mosterdcomponents/p-33e56d76.entry.js +0 -1
  35. package/dist/mosterdcomponents/p-80d6eff2.entry.js +0 -1
  36. package/dist/mosterdcomponents/p-beKIX2qZ.js +0 -1
@@ -30,6 +30,7 @@ export class MrdForm {
30
30
  this.formValues = {};
31
31
  this.errors = {};
32
32
  this.submitted = false;
33
+ this.initialValues = {};
33
34
  this.handleFieldChange = (e) => {
34
35
  const { name, value } = e.detail;
35
36
  const prevHref = this.getHref(this.formValues[name]);
@@ -80,8 +81,9 @@ export class MrdForm {
80
81
  };
81
82
  }
82
83
  componentWillLoad() {
83
- var _a;
84
- this.formValues = Object.assign({}, ((_a = this.values) !== null && _a !== void 0 ? _a : {}));
84
+ var _a, _b;
85
+ this.initialValues = Object.assign({}, ((_a = this.values) !== null && _a !== void 0 ? _a : {}));
86
+ this.formValues = Object.assign({}, ((_b = this.values) !== null && _b !== void 0 ? _b : {}));
85
87
  }
86
88
  componentDidLoad() {
87
89
  // Apply reference pre-fill and emit mrdFetchAll for dependent DROPDOWN fields.
@@ -94,6 +96,7 @@ export class MrdForm {
94
96
  /** Sync formValues when the values prop is set from outside after mount
95
97
  * (e.g. when pre-filling an existing record in edit mode). */
96
98
  valuesChanged(newValues) {
99
+ this.initialValues = Object.assign({}, (newValues !== null && newValues !== void 0 ? newValues : {}));
97
100
  this.formValues = Object.assign({}, (newValues !== null && newValues !== void 0 ? newValues : {}));
98
101
  this.applyReferenceValue();
99
102
  this.errors = {};
@@ -221,10 +224,42 @@ export class MrdForm {
221
224
  this.errors = newErrors;
222
225
  return Object.keys(newErrors).length === 0;
223
226
  }
224
- /** Build a clean submit payload containing only layout fields in API format.
225
- * - Only fields defined in the layout are included (system fields are excluded).
226
- * - Relation values stored as RelationSearchResult { id, label } are unwrapped
227
- * to just the href string (the `id`), matching what the API expects.
227
+ normalizeFieldValue(value) {
228
+ return (value === '' || value == null) ? null : value;
229
+ }
230
+ normalizeRelationValue(value) {
231
+ if (value == null || value === '')
232
+ return null;
233
+ if (typeof value === 'string')
234
+ return value || null;
235
+ if (Array.isArray(value)) {
236
+ return value.map(v => typeof v === 'object' && v !== null && 'id' in v ? v.id : String(v));
237
+ }
238
+ if (typeof value === 'object' && 'id' in value) {
239
+ return value.id || null;
240
+ }
241
+ return null;
242
+ }
243
+ deepEqual(a, b) {
244
+ if (a === b)
245
+ return true;
246
+ if (a == null && b == null)
247
+ return true;
248
+ if (a == null || b == null)
249
+ return false;
250
+ if (Array.isArray(a) && Array.isArray(b)) {
251
+ if (a.length !== b.length)
252
+ return false;
253
+ const sa = [...a].sort();
254
+ const sb = [...b].sort();
255
+ return JSON.stringify(sa) === JSON.stringify(sb);
256
+ }
257
+ return JSON.stringify(a) === JSON.stringify(b);
258
+ }
259
+ /** Build a submit payload containing only fields that changed relative to
260
+ * initialValues. For a new record (POST) initialValues is empty, so every
261
+ * non-null field is included. For edit (PATCH) only modified fields are sent,
262
+ * including fields explicitly cleared to null.
228
263
  */
229
264
  buildSubmitPayload() {
230
265
  var _a, _b;
@@ -234,23 +269,21 @@ export class MrdForm {
234
269
  if (item.type === ClientLayoutItemType.FIELD && item.field) {
235
270
  const name = item.field.name;
236
271
  const value = this.formValues[name];
237
- // Skip file/image fields that are still uploading (value is a File object)
238
272
  if (value instanceof File)
239
273
  continue;
240
- // Omit empty strings for optional fields
241
- if (value !== '')
242
- payload[name] = value !== null && value !== void 0 ? value : null;
274
+ const current = this.normalizeFieldValue(value);
275
+ const initial = this.normalizeFieldValue(this.initialValues[name]);
276
+ if (this.deepEqual(current, initial))
277
+ continue;
278
+ payload[name] = current;
243
279
  }
244
280
  else if (item.type === ClientLayoutItemType.RELATION && item.relation) {
245
281
  const name = item.relation.name;
246
- const value = this.formValues[name];
247
- if (value !== null && typeof value === 'object' && !Array.isArray(value) && 'id' in value) {
248
- // RelationSearchResult { id: href, label } → plain href
249
- payload[name] = value.id;
250
- }
251
- else if (value !== '' && value !== undefined) {
252
- payload[name] = value !== null && value !== void 0 ? value : null;
253
- }
282
+ const current = this.normalizeRelationValue(this.formValues[name]);
283
+ const initial = this.normalizeRelationValue(this.initialValues[name]);
284
+ if (this.deepEqual(current, initial))
285
+ continue;
286
+ payload[name] = current;
254
287
  }
255
288
  }
256
289
  return payload;
@@ -473,8 +506,8 @@ export class MrdForm {
473
506
  "text": ""
474
507
  },
475
508
  "complexType": {
476
- "original": "{ query: string; relatedClass: string }",
477
- "resolved": "{ query: string; relatedClass: string; }",
509
+ "original": "{ name: string; query: string; relatedClass: string }",
510
+ "resolved": "{ name: string; query: string; relatedClass: string; }",
478
511
  "references": {}
479
512
  }
480
513
  }, {
@@ -70,7 +70,7 @@ export class MrdRelationField {
70
70
  this.isLoading = true;
71
71
  this.showResults = true;
72
72
  this.searchDebounce = setTimeout(() => {
73
- this.mrdSearch.emit({ query, relatedClass: this.mostSignificantClass });
73
+ this.mrdSearch.emit({ name: this.name, query, relatedClass: this.mostSignificantClass });
74
74
  }, 300);
75
75
  }
76
76
  else {
@@ -543,8 +543,8 @@ export class MrdRelationField {
543
543
  "text": ""
544
544
  },
545
545
  "complexType": {
546
- "original": "{ query: string; relatedClass: string }",
547
- "resolved": "{ query: string; relatedClass: string; }",
546
+ "original": "{ name: string; query: string; relatedClass: string }",
547
+ "resolved": "{ name: string; query: string; relatedClass: string; }",
548
548
  "references": {}
549
549
  }
550
550
  }, {
@@ -17,6 +17,7 @@ export class MrdTable {
17
17
  this.pendingPages = new Set();
18
18
  this.debounceTimer = null;
19
19
  this.outsideClickHandler = null;
20
+ this.keydownHandler = null;
20
21
  // ── Props ──────────────────────────────────────────────────────────────────
21
22
  this.columns = [];
22
23
  /** Direct rows (non-paginated mode, used when totalElements === 0). */
@@ -55,6 +56,8 @@ export class MrdTable {
55
56
  this.popupPos = { top: 0, left: 0 };
56
57
  /** Current scroll offset of the scroll container — drives pagination footer. */
57
58
  this.scrollTop = 0;
59
+ /** Full text shown in the TEXTBLOCK expand modal (null = closed). */
60
+ this.textblockModal = null;
58
61
  this.handleScroll = (e) => {
59
62
  const scroller = e.currentTarget;
60
63
  const scrollTop = scroller.scrollTop;
@@ -131,6 +134,10 @@ export class MrdTable {
131
134
  document.removeEventListener('click', this.outsideClickHandler);
132
135
  this.outsideClickHandler = null;
133
136
  }
137
+ if (this.keydownHandler) {
138
+ document.removeEventListener('keydown', this.keydownHandler);
139
+ this.keydownHandler = null;
140
+ }
134
141
  }
135
142
  componentDidRender() {
136
143
  if (this.colWidths.length === 0 && this.loadedPages.size > 0 && this.totalElements > 0) {
@@ -275,7 +282,26 @@ export class MrdTable {
275
282
  const defaultOperator = (TEXT_TYPES.has(dataType) || dataType === 'RELATION')
276
283
  ? 'startsWith'
277
284
  : undefined;
278
- this.pendingFilter = existing ? Object.assign({}, existing) : { field: name, dataType, operator: defaultOperator };
285
+ // For DATETIME, stored values are UTC ISO strings; convert back to local
286
+ // "YYYY-MM-DD" dates so the date inputs show what the user originally entered.
287
+ // If from and to cover the same local day it was an exact-date filter — restore
288
+ // to exact mode so the user sees the single-date input again.
289
+ if (dataType === 'DATETIME' && existing) {
290
+ const display = Object.assign({}, existing);
291
+ if (typeof display.from === 'string' && display.from)
292
+ display.from = this.utcISOToLocalDate(display.from);
293
+ if (typeof display.to === 'string' && display.to)
294
+ display.to = this.utcISOToLocalDateExclusiveEnd(display.to);
295
+ if (display.from && display.to && display.from === display.to) {
296
+ this.pendingFilter = Object.assign(Object.assign({}, display), { value: display.from, from: undefined, to: undefined });
297
+ }
298
+ else {
299
+ this.pendingFilter = display;
300
+ }
301
+ }
302
+ else {
303
+ this.pendingFilter = existing ? Object.assign({}, existing) : { field: name, dataType, operator: defaultOperator };
304
+ }
279
305
  this.openFilterCol = name;
280
306
  // Close on outside click — re-register to replace any stale handler
281
307
  if (this.outsideClickHandler)
@@ -295,6 +321,23 @@ export class MrdTable {
295
321
  this.outsideClickHandler = null;
296
322
  }
297
323
  }
324
+ openTextblockModal(text) {
325
+ this.textblockModal = text;
326
+ if (this.keydownHandler)
327
+ document.removeEventListener('keydown', this.keydownHandler);
328
+ this.keydownHandler = (ev) => {
329
+ if (ev.key === 'Escape')
330
+ this.closeTextblockModal();
331
+ };
332
+ document.addEventListener('keydown', this.keydownHandler);
333
+ }
334
+ closeTextblockModal() {
335
+ this.textblockModal = null;
336
+ if (this.keydownHandler) {
337
+ document.removeEventListener('keydown', this.keydownHandler);
338
+ this.keydownHandler = null;
339
+ }
340
+ }
298
341
  setPending(key, val) {
299
342
  this.pendingFilter = Object.assign(Object.assign({}, this.pendingFilter), { [key]: val });
300
343
  }
@@ -318,18 +361,70 @@ export class MrdTable {
318
361
  return true;
319
362
  return false;
320
363
  }
364
+ // Convert a local "YYYY-MM-DD" date string to the UTC ISO string at the
365
+ // start of that local day (midnight). new Date(y, m, d) uses local time.
366
+ dateLocalToUTCStart(dateStr) {
367
+ if (!dateStr)
368
+ return dateStr;
369
+ const [year, month, day] = dateStr.split('-').map(Number);
370
+ return new Date(year, month - 1, day).toISOString().replace(/\.\d{3}Z$/, 'Z');
371
+ }
372
+ // Start of the day AFTER the given local date (exclusive range end).
373
+ dateLocalToUTCEndExclusive(dateStr) {
374
+ if (!dateStr)
375
+ return dateStr;
376
+ const [year, month, day] = dateStr.split('-').map(Number);
377
+ return new Date(year, month - 1, day + 1).toISOString().replace(/\.\d{3}Z$/, 'Z');
378
+ }
379
+ // Convert a stored UTC ISO string back to the local "YYYY-MM-DD" date.
380
+ utcISOToLocalDate(utcStr) {
381
+ if (!utcStr)
382
+ return utcStr;
383
+ const d = new Date(utcStr);
384
+ if (isNaN(d.getTime()))
385
+ return utcStr;
386
+ return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
387
+ }
388
+ // The stored "to" value is the exclusive end (midnight of the next day).
389
+ // Subtract one day to recover the local date the user entered.
390
+ utcISOToLocalDateExclusiveEnd(utcStr) {
391
+ if (!utcStr)
392
+ return utcStr;
393
+ const d = new Date(utcStr);
394
+ if (isNaN(d.getTime()))
395
+ return utcStr;
396
+ d.setDate(d.getDate() - 1);
397
+ return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
398
+ }
321
399
  applyFilter() {
322
400
  const f = this.pendingFilter;
323
401
  if (!(f === null || f === void 0 ? void 0 : f.field)) {
324
402
  this.closeFilterPopup();
325
403
  return;
326
404
  }
405
+ // For DATETIME fields the user enters local dates; convert to UTC ISO strings.
406
+ // Exact date → range covering the full local day (from = midnight, to = next midnight).
407
+ // "to" is always the exclusive end (midnight of the next local day).
408
+ let normalized = Object.assign({}, f);
409
+ if (f.dataType === 'DATETIME') {
410
+ if (typeof normalized.value === 'string' && normalized.value) {
411
+ normalized.from = this.dateLocalToUTCStart(normalized.value);
412
+ normalized.to = this.dateLocalToUTCEndExclusive(normalized.value);
413
+ normalized.value = undefined;
414
+ }
415
+ else {
416
+ if (typeof normalized.from === 'string' && normalized.from)
417
+ normalized.from = this.dateLocalToUTCStart(normalized.from);
418
+ if (typeof normalized.to === 'string' && normalized.to)
419
+ normalized.to = this.dateLocalToUTCEndExclusive(normalized.to);
420
+ }
421
+ }
327
422
  const next = new Map(this.activeFilters);
328
- if (this.filterHasValue(f)) {
329
- next.set(f.field, f);
423
+ if (this.filterHasValue(normalized)) {
424
+ next.set(normalized.field, normalized);
330
425
  }
331
426
  else {
332
- next.delete(f.field);
427
+ next.delete(normalized.field);
333
428
  }
334
429
  this.activeFilters = next;
335
430
  this.closeFilterPopup();
@@ -374,7 +469,7 @@ export class MrdTable {
374
469
  }
375
470
  // ── Render: filter popup ───────────────────────────────────────────────────
376
471
  renderFilterEditor(col) {
377
- var _a, _b, _c, _d, _e, _f, _g;
472
+ var _a, _b, _c, _d, _e, _f, _g, _h;
378
473
  const pf = (_a = this.pendingFilter) !== null && _a !== void 0 ? _a : {};
379
474
  const dataType = this.colDataType(col);
380
475
  if (NO_FILTER_TYPES.has(dataType)) {
@@ -406,12 +501,14 @@ export class MrdTable {
406
501
  const rangeMode = pf.from !== undefined || pf.to !== undefined;
407
502
  return (h("div", { class: "mrd-table__filter-editor" }, h("div", { class: "mrd-table__filter-radio-group mrd-table__filter-radio-group--inline" }, h("label", { class: "mrd-table__filter-radio-label" }, h("input", { type: "radio", name: `nm-${this.openFilterCol}`, checked: !rangeMode, onChange: () => { this.pendingFilter = Object.assign(Object.assign({}, pf), { from: undefined, to: undefined }); } }), t('filter_exact', this.locale)), h("label", { class: "mrd-table__filter-radio-label" }, h("input", { type: "radio", name: `nm-${this.openFilterCol}`, checked: rangeMode, onChange: () => { this.pendingFilter = Object.assign(Object.assign({}, pf), { value: undefined, from: null, to: null }); } }), t('filter_range', this.locale))), !rangeMode ? (h("input", { type: "number", class: "mrd-table__filter-input", value: pf.value != null ? String(pf.value) : '', onInput: (e) => this.setPending('value', e.target.value) })) : (h("div", { class: "mrd-table__filter-range" }, h("input", { type: "number", class: "mrd-table__filter-input", placeholder: t('filter_from', this.locale), value: pf.from != null ? String(pf.from) : '', onInput: (e) => this.setPending('from', e.target.value) }), h("span", { class: "mrd-table__filter-range-sep" }, "\u2013"), h("input", { type: "number", class: "mrd-table__filter-input", placeholder: t('filter_to', this.locale), value: pf.to != null ? String(pf.to) : '', onInput: (e) => this.setPending('to', e.target.value) })))));
408
503
  }
504
+ if (dataType === 'DATETIME') {
505
+ const rangeMode = pf.from !== undefined || pf.to !== undefined;
506
+ return (h("div", { class: "mrd-table__filter-editor" }, h("div", { class: "mrd-table__filter-radio-group mrd-table__filter-radio-group--inline" }, h("label", { class: "mrd-table__filter-radio-label" }, h("input", { type: "radio", name: `dt-${this.openFilterCol}`, checked: !rangeMode, onChange: () => { this.pendingFilter = Object.assign(Object.assign({}, pf), { from: undefined, to: undefined }); } }), t('filter_exact', this.locale)), h("label", { class: "mrd-table__filter-radio-label" }, h("input", { type: "radio", name: `dt-${this.openFilterCol}`, checked: rangeMode, onChange: () => { this.pendingFilter = Object.assign(Object.assign({}, pf), { value: undefined, from: null, to: null }); } }), t('filter_range', this.locale))), !rangeMode ? (h("input", { type: "date", class: "mrd-table__filter-input", value: String((_g = pf.value) !== null && _g !== void 0 ? _g : ''), onInput: (e) => this.setPending('value', e.target.value) })) : (h("div", { class: "mrd-table__filter-range mrd-table__filter-range--stacked" }, h("label", { class: "mrd-table__filter-range-label" }, t('filter_from', this.locale)), h("input", { type: "date", class: "mrd-table__filter-input", value: pf.from != null ? String(pf.from) : '', onInput: (e) => this.setPending('from', e.target.value) }), h("label", { class: "mrd-table__filter-range-label" }, t('filter_to', this.locale)), h("input", { type: "date", class: "mrd-table__filter-input", value: pf.to != null ? String(pf.to) : '', onInput: (e) => this.setPending('to', e.target.value) })))));
507
+ }
409
508
  if (DATE_TYPES.has(dataType)) {
410
- const inputType = dataType === 'DATE' ? 'date'
411
- : dataType === 'DATETIME' ? 'datetime-local'
412
- : 'time';
509
+ const inputType = dataType === 'DATE' ? 'date' : 'time';
413
510
  const rangeMode = pf.from !== undefined || pf.to !== undefined;
414
- return (h("div", { class: "mrd-table__filter-editor" }, h("div", { class: "mrd-table__filter-radio-group mrd-table__filter-radio-group--inline" }, h("label", { class: "mrd-table__filter-radio-label" }, h("input", { type: "radio", name: `dt-${this.openFilterCol}`, checked: !rangeMode, onChange: () => { this.pendingFilter = Object.assign(Object.assign({}, pf), { from: undefined, to: undefined }); } }), t('filter_exact', this.locale)), h("label", { class: "mrd-table__filter-radio-label" }, h("input", { type: "radio", name: `dt-${this.openFilterCol}`, checked: rangeMode, onChange: () => { this.pendingFilter = Object.assign(Object.assign({}, pf), { value: undefined, from: null, to: null }); } }), t('filter_range', this.locale))), !rangeMode ? (h("input", { type: inputType, class: "mrd-table__filter-input", value: String((_g = pf.value) !== null && _g !== void 0 ? _g : ''), onInput: (e) => this.setPending('value', e.target.value) })) : (h("div", { class: "mrd-table__filter-range" }, h("input", { type: inputType, class: "mrd-table__filter-input", placeholder: t('filter_from', this.locale), value: pf.from != null ? String(pf.from) : '', onInput: (e) => this.setPending('from', e.target.value) }), h("input", { type: inputType, class: "mrd-table__filter-input", placeholder: t('filter_to', this.locale), value: pf.to != null ? String(pf.to) : '', onInput: (e) => this.setPending('to', e.target.value) })))));
511
+ return (h("div", { class: "mrd-table__filter-editor" }, h("div", { class: "mrd-table__filter-radio-group mrd-table__filter-radio-group--inline" }, h("label", { class: "mrd-table__filter-radio-label" }, h("input", { type: "radio", name: `dt-${this.openFilterCol}`, checked: !rangeMode, onChange: () => { this.pendingFilter = Object.assign(Object.assign({}, pf), { from: undefined, to: undefined }); } }), t('filter_exact', this.locale)), h("label", { class: "mrd-table__filter-radio-label" }, h("input", { type: "radio", name: `dt-${this.openFilterCol}`, checked: rangeMode, onChange: () => { this.pendingFilter = Object.assign(Object.assign({}, pf), { value: undefined, from: null, to: null }); } }), t('filter_range', this.locale))), !rangeMode ? (h("input", { type: inputType, class: "mrd-table__filter-input", value: String((_h = pf.value) !== null && _h !== void 0 ? _h : ''), onInput: (e) => this.setPending('value', e.target.value) })) : (h("div", { class: "mrd-table__filter-range" }, h("input", { type: inputType, class: "mrd-table__filter-input", placeholder: t('filter_from', this.locale), value: pf.from != null ? String(pf.from) : '', onInput: (e) => this.setPending('from', e.target.value) }), h("input", { type: inputType, class: "mrd-table__filter-input", placeholder: t('filter_to', this.locale), value: pf.to != null ? String(pf.to) : '', onInput: (e) => this.setPending('to', e.target.value) })))));
415
512
  }
416
513
  return null;
417
514
  }
@@ -464,6 +561,15 @@ export class MrdTable {
464
561
  this.mrdDownload.emit({ href, fileName });
465
562
  } }, h("svg", { class: "mrd-table__file-icon", viewBox: "0 0 24 24", "aria-hidden": "true" }, h("path", { fill: "currentColor", d: "M14 2H6c-1.1 0-2 .9-2 2v16c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V8l-6-6zm-1 7V3.5L18.5 9H13zm-3 8l-3-3 1.41-1.41L10 14.17l4.59-4.58L16 11l-6 6z" })), t('download', this.locale))) : ''));
466
563
  }
564
+ const TEXTBLOCK_MAX = 200;
565
+ if (dataType === 'TEXTBLOCK') {
566
+ const full = CellRenderer.render(col, row, this.locale);
567
+ if (full.length <= TEXTBLOCK_MAX) {
568
+ return h("td", { class: "mrd-table__cell" }, full);
569
+ }
570
+ const preview = full.slice(0, TEXTBLOCK_MAX) + '…';
571
+ return (h("td", { class: "mrd-table__cell" }, preview, h("button", { class: "mrd-table__textblock-btn", onClick: (e) => { e.stopPropagation(); this.openTextblockModal(full); }, "aria-label": t('textblock_show_more', this.locale) }, "\u22EF")));
572
+ }
467
573
  const value = CellRenderer.render(col, row, this.locale);
468
574
  return (h("td", { class: `mrd-table__cell${isNumeric ? ' mrd-table__cell--numeric' : ''}` }, value));
469
575
  }
@@ -478,8 +584,13 @@ export class MrdTable {
478
584
  var _a, _b, _c, _d;
479
585
  const name = this.colName(col);
480
586
  const isFiltered = this.activeFilters.has(name);
481
- return (h("th", { class: `mrd-table__header${isFiltered ? ' mrd-table__header--filtered' : ''}` }, h("span", { class: "mrd-table__header-label" }, (_d = (_b = (_a = col.field) === null || _a === void 0 ? void 0 : _a.label) !== null && _b !== void 0 ? _b : (_c = col.relation) === null || _c === void 0 ? void 0 : _c.label) !== null && _d !== void 0 ? _d : ''), this.filterMode && (h("button", { class: `mrd-table__header-filter-btn${isFiltered ? ' mrd-table__header-filter-btn--active' : ''}`, onClick: (e) => this.handleFilterOpen(col, e) }, "\u25BE"))));
482
- }))), h("tbody", null, (_b = this.rows) === null || _b === void 0 ? void 0 : _b.map((row, i) => (h("tr", { class: "mrd-table__row mrd-table__row--clickable", style: { background: i % 2 === 0 ? '' : 'var(--mrd-color-neutral-100)' }, onClick: () => this.mrdRowClick.emit(row) }, this.columns.map(col => this.renderCell(col, row))))))), (!this.rows || this.rows.length === 0) && (h("p", { class: "mrd-table__empty" }, t('no_results', this.locale)))), this.renderFooter((_c = this.rows) === null || _c === void 0 ? void 0 : _c.length), this.renderFilterPopup()));
587
+ const cls = [
588
+ 'mrd-table__header',
589
+ isFiltered ? 'mrd-table__header--filtered' : '',
590
+ this.filterMode ? 'mrd-table__header--sortable' : '',
591
+ ].filter(Boolean).join(' ');
592
+ return (h("th", { class: cls, onClick: this.filterMode ? (e) => this.handleFilterOpen(col, e) : undefined }, h("span", { class: "mrd-table__header-label" }, (_d = (_b = (_a = col.field) === null || _a === void 0 ? void 0 : _a.label) !== null && _b !== void 0 ? _b : (_c = col.relation) === null || _c === void 0 ? void 0 : _c.label) !== null && _d !== void 0 ? _d : ''), isFiltered && this.renderFilterIcon()));
593
+ }))), h("tbody", null, (_b = this.rows) === null || _b === void 0 ? void 0 : _b.map((row, i) => (h("tr", { class: "mrd-table__row mrd-table__row--clickable", style: { background: i % 2 === 0 ? '' : 'var(--mrd-color-neutral-100)' }, onClick: () => this.mrdRowClick.emit(row) }, this.columns.map(col => this.renderCell(col, row))))))), (!this.rows || this.rows.length === 0) && (h("p", { class: "mrd-table__empty" }, t('no_results', this.locale)))), this.renderFooter((_c = this.rows) === null || _c === void 0 ? void 0 : _c.length), this.renderFilterPopup(), this.renderTextblockModal()));
483
594
  }
484
595
  // ── Paginated / virtual-scroll mode ────────────────────────────────────
485
596
  // Derive the authoritative row count from loaded pages:
@@ -521,8 +632,16 @@ export class MrdTable {
521
632
  isActive ? `mrd-table__header--sorted-${this.sortDir}` : '',
522
633
  isFiltered ? 'mrd-table__header--filtered' : '',
523
634
  ].filter(Boolean).join(' ');
524
- return (h("th", { class: cls, style: this.colWidths[idx] ? { width: `${this.colWidths[idx]}px` } : undefined, onClick: () => this.handleSortClick(col) }, h("span", { class: "mrd-table__header-label" }, (_d = (_b = (_a = col.field) === null || _a === void 0 ? void 0 : _a.label) !== null && _b !== void 0 ? _b : (_c = col.relation) === null || _c === void 0 ? void 0 : _c.label) !== null && _d !== void 0 ? _d : ''), h("span", { class: "mrd-table__sort-icon", "aria-hidden": "true" }, isActive ? (this.sortDir === 'asc' ? '▲' : '▼') : '⇅'), this.filterMode && (h("button", { class: `mrd-table__header-filter-btn${isFiltered ? ' mrd-table__header-filter-btn--active' : ''}`, onClick: (e) => { e.stopPropagation(); this.handleFilterOpen(col, e); } }, "\u25BE"))));
525
- }))), h("tbody", null, topSpacerHeight > 0 && (h("tr", { class: "mrd-table__spacer", style: { height: `${topSpacerHeight}px` } }, h("td", { colSpan: colCount }))), renderedRows, bottomSpacerHeight > 0 && (h("tr", { class: "mrd-table__spacer", style: { height: `${bottomSpacerHeight}px` } }, h("td", { colSpan: colCount })))))), effectiveTotal === 0 && this.loadedPages.has(0) && (h("p", { class: "mrd-table__empty" }, t('no_results', this.locale))), effectiveTotal > 0 && this.renderFooter(undefined, effectiveTotal), this.renderFilterPopup()));
635
+ return (h("th", { class: cls, style: this.colWidths[idx] ? { width: `${this.colWidths[idx]}px` } : undefined, onClick: (e) => this.filterMode ? this.handleFilterOpen(col, e) : this.handleSortClick(col) }, h("span", { class: "mrd-table__header-label" }, (_d = (_b = (_a = col.field) === null || _a === void 0 ? void 0 : _a.label) !== null && _b !== void 0 ? _b : (_c = col.relation) === null || _c === void 0 ? void 0 : _c.label) !== null && _d !== void 0 ? _d : ''), isActive && (h("span", { class: "mrd-table__sort-icon", "aria-hidden": "true" }, this.sortDir === 'asc' ? '▲' : '▼')), !isActive && !this.filterMode && (h("span", { class: "mrd-table__sort-icon", "aria-hidden": "true" }, "\u21C5")), isFiltered && this.renderFilterIcon()));
636
+ }))), h("tbody", null, topSpacerHeight > 0 && (h("tr", { class: "mrd-table__spacer", style: { height: `${topSpacerHeight}px` } }, h("td", { colSpan: colCount }))), renderedRows, bottomSpacerHeight > 0 && (h("tr", { class: "mrd-table__spacer", style: { height: `${bottomSpacerHeight}px` } }, h("td", { colSpan: colCount })))))), effectiveTotal === 0 && this.loadedPages.has(0) && (h("p", { class: "mrd-table__empty" }, t('no_results', this.locale))), effectiveTotal > 0 && this.renderFooter(undefined, effectiveTotal), this.renderFilterPopup(), this.renderTextblockModal()));
637
+ }
638
+ renderFilterIcon() {
639
+ return (h("span", { class: "mrd-table__filter-icon", "aria-hidden": "true" }, h("svg", { viewBox: "0 0 24 24", width: "14", height: "14", fill: "currentColor" }, h("path", { d: "M10 18h4v-2h-4v2zM3 6v2h18V6H3zm3 7h12v-2H6v2z" }))));
640
+ }
641
+ renderTextblockModal() {
642
+ if (this.textblockModal === null)
643
+ return null;
644
+ return (h("div", { class: "mrd-table__modal-backdrop", onClick: () => this.closeTextblockModal(), role: "dialog", "aria-modal": "true" }, h("div", { class: "mrd-table__modal", onClick: (e) => e.stopPropagation() }, h("button", { class: "mrd-table__modal-close", onClick: () => this.closeTextblockModal(), "aria-label": t('close', this.locale) }, "\u2715"), h("p", { class: "mrd-table__modal-text" }, this.textblockModal))));
526
645
  }
527
646
  static get is() { return "mrd-table"; }
528
647
  static get encapsulation() { return "scoped"; }
@@ -747,7 +866,8 @@ export class MrdTable {
747
866
  "openFilterCol": {},
748
867
  "pendingFilter": {},
749
868
  "popupPos": {},
750
- "scrollTop": {}
869
+ "scrollTop": {},
870
+ "textblockModal": {}
751
871
  };
752
872
  }
753
873
  static get events() {
@@ -73,7 +73,7 @@
73
73
  }
74
74
 
75
75
  .mrd-table__sort-icon {
76
- font-size: 0.65rem;
76
+ font-size: 0.85rem;
77
77
  opacity: 0.4;
78
78
  vertical-align: middle;
79
79
  }
@@ -84,6 +84,14 @@
84
84
  color: var(--mrd-color-primary);
85
85
  }
86
86
 
87
+ .mrd-table__filter-icon {
88
+ display: inline-flex;
89
+ align-items: center;
90
+ vertical-align: middle;
91
+ margin-left: var(--mrd-space-1);
92
+ color: var(--mrd-color-primary);
93
+ }
94
+
87
95
  /* ── Data rows ─────────────────────────────────────────────────────────── */
88
96
  /* Background is set via inline style based on absolute row index,
89
97
  so zebra striping stays correct even with virtual spacer rows. */
@@ -273,37 +281,12 @@
273
281
  pointer-events: none;
274
282
  }
275
283
 
276
- /* ── Header: filtered indicator + filter button ─────────────────────────── */
284
+ /* ── Header: filtered indicator ─────────────────────────────────────────── */
277
285
  .mrd-table__header--filtered {
278
286
  color: var(--mrd-color-primary);
279
287
  border-bottom-color: var(--mrd-color-primary);
280
288
  }
281
289
 
282
- .mrd-table__header-filter-btn {
283
- display: inline-flex;
284
- align-items: center;
285
- justify-content: center;
286
- margin-left: var(--mrd-space-1);
287
- padding: 0 3px;
288
- background: transparent;
289
- border: none;
290
- border-radius: 3px;
291
- cursor: pointer;
292
- color: var(--mrd-color-neutral-500);
293
- font-size: 0.8rem;
294
- line-height: 1;
295
- vertical-align: middle;
296
- }
297
-
298
- .mrd-table__header-filter-btn:hover {
299
- background: var(--mrd-color-neutral-200);
300
- color: var(--mrd-color-neutral-800);
301
- }
302
-
303
- .mrd-table__header-filter-btn--active {
304
- color: var(--mrd-color-primary);
305
- }
306
-
307
290
  /* ── Filter popup ───────────────────────────────────────────────────────── */
308
291
  .mrd-table__filter-popup {
309
292
  position: fixed;
@@ -433,6 +416,18 @@
433
416
  flex-shrink: 0;
434
417
  }
435
418
 
419
+ .mrd-table__filter-range--stacked {
420
+ flex-direction: column;
421
+ align-items: stretch;
422
+ gap: var(--mrd-space-2);
423
+ }
424
+
425
+ .mrd-table__filter-range-label {
426
+ font-size: var(--mrd-font-size-xs);
427
+ color: var(--mrd-color-neutral-500);
428
+ margin-bottom: 2px;
429
+ }
430
+
436
431
  /* Radio group */
437
432
  .mrd-table__filter-radio-group {
438
433
  display: flex;
@@ -576,3 +571,69 @@
576
571
  height: 1rem;
577
572
  }
578
573
 
574
+ /* ── TEXTBLOCK expand button ────────────────────────────────────────────── */
575
+ .mrd-table__textblock-btn {
576
+ display: inline;
577
+ background: none;
578
+ border: none;
579
+ padding: 0 0 0 var(--mrd-space-1);
580
+ cursor: pointer;
581
+ color: var(--mrd-color-primary);
582
+ font-size: var(--mrd-font-size-sm);
583
+ font-family: inherit;
584
+ line-height: inherit;
585
+ vertical-align: middle;
586
+ }
587
+
588
+ .mrd-table__textblock-btn:hover {
589
+ color: var(--mrd-color-primary-dark);
590
+ }
591
+
592
+ /* ── TEXTBLOCK modal ────────────────────────────────────────────────────── */
593
+ .mrd-table__modal-backdrop {
594
+ position: fixed;
595
+ inset: 0;
596
+ background: rgba(0, 0, 0, 0.4);
597
+ z-index: var(--mrd-z-modal, 300);
598
+ display: flex;
599
+ align-items: center;
600
+ justify-content: center;
601
+ }
602
+
603
+ .mrd-table__modal {
604
+ background: #fff;
605
+ border-radius: var(--mrd-radius-md, 0.5rem);
606
+ padding: var(--mrd-space-6);
607
+ max-width: min(600px, 90vw);
608
+ max-height: 70vh;
609
+ overflow-y: auto;
610
+ position: relative;
611
+ box-shadow: var(--mrd-shadow-lg);
612
+ }
613
+
614
+ .mrd-table__modal-close {
615
+ position: absolute;
616
+ top: var(--mrd-space-3);
617
+ right: var(--mrd-space-3);
618
+ background: none;
619
+ border: none;
620
+ cursor: pointer;
621
+ font-size: 1.25rem;
622
+ line-height: 1;
623
+ color: var(--mrd-color-text-muted, #6b7280);
624
+ padding: 0;
625
+ }
626
+
627
+ .mrd-table__modal-close:hover {
628
+ color: var(--mrd-color-text, #111827);
629
+ }
630
+
631
+ .mrd-table__modal-text {
632
+ margin: 0;
633
+ padding-right: var(--mrd-space-6);
634
+ white-space: pre-wrap;
635
+ word-break: break-word;
636
+ font-size: var(--mrd-font-size-sm);
637
+ line-height: 1.6;
638
+ }
639
+
@@ -431,14 +431,16 @@ function renderForm(layout, record = null, selfHref = null) {
431
431
 
432
432
  form.addEventListener('mrdSearch', async (e) => {
433
433
  logEvent('mrdSearch (live)', e.detail);
434
- const { query, relatedClass } = e.detail;
435
- const meta = _relationMeta[relatedClass];
436
- if (!meta || !query || query.length < 2) return;
434
+ const { name, query, relatedClass } = e.detail;
435
+ if (!query || query.length < 2) return;
437
436
 
437
+ // relatedClass here is actually mostSignificantClass (the URL segment).
438
+ // Use name to find the exact field, avoiding collisions when multiple
439
+ // fields share the same mostSignificantClass (e.g. 'companies').
438
440
  try {
439
- const results = await apiSearchRelation(authGetToken(), _selectedTenant, meta.mostSignificantClass, query);
440
- const host = Array.from(document.querySelectorAll('mrd-relation-field'))
441
- .find(el => el.name === meta.name);
441
+ const results = await apiSearchRelation(authGetToken(), _selectedTenant, relatedClass, query);
442
+ const host = Array.from(form.querySelectorAll('mrd-relation-field'))
443
+ .find(el => el.name === name);
442
444
  if (host && typeof host.setSearchResults === 'function') {
443
445
  host.setSearchResults(results);
444
446
  }
@@ -20,7 +20,7 @@ export function formatPercentage(value, locale, decimalPrecision = 2) {
20
20
  style: 'percent',
21
21
  minimumFractionDigits: decimalPrecision,
22
22
  maximumFractionDigits: decimalPrecision,
23
- }).format(value / 100);
23
+ }).format(value);
24
24
  }
25
25
  export function formatDate(value, locale) {
26
26
  if (!value)
@@ -53,6 +53,9 @@ const translations = {
53
53
  filter_select_none: 'Geen',
54
54
  filter_search_value: 'Zoekwaarde...',
55
55
  filter_no_support: 'Geen filtering beschikbaar voor dit veldtype.',
56
+ // mrd-table textblock
57
+ textblock_show_more: 'Meer tonen',
58
+ close: 'Sluiten',
56
59
  },
57
60
  en: {
58
61
  required: 'This field is required',
@@ -108,6 +111,9 @@ const translations = {
108
111
  filter_select_none: 'None',
109
112
  filter_search_value: 'Search value...',
110
113
  filter_no_support: 'Filtering is not available for this field type.',
114
+ // mrd-table textblock
115
+ textblock_show_more: 'Show more',
116
+ close: 'Close',
111
117
  },
112
118
  ar: {
113
119
  required: 'هذا الحقل مطلوب',
@@ -163,6 +169,9 @@ const translations = {
163
169
  filter_select_none: 'لا شيء',
164
170
  filter_search_value: 'قيمة البحث...',
165
171
  filter_no_support: 'التصفية غير متاحة لهذا النوع من الحقول.',
172
+ // mrd-table textblock
173
+ textblock_show_more: 'عرض المزيد',
174
+ close: 'إغلاق',
166
175
  },
167
176
  fr: {
168
177
  required: 'Ce champ est obligatoire',
@@ -218,6 +227,9 @@ const translations = {
218
227
  filter_select_none: 'Aucun',
219
228
  filter_search_value: 'Valeur de recherche...',
220
229
  filter_no_support: "Le filtrage n'est pas disponible pour ce type de champ.",
230
+ // mrd-table textblock
231
+ textblock_show_more: 'Voir plus',
232
+ close: 'Fermer',
221
233
  },
222
234
  };
223
235
  export function t(key, locale) {
@@ -1 +1 @@
1
- function n(n,i,t){return null==n||isNaN(n)?"":new Intl.NumberFormat(i,t).format(n)}function i(n,i,t){return null==n||isNaN(n)?"":new Intl.NumberFormat(t,{style:"currency",currency:i,minimumFractionDigits:2,maximumFractionDigits:2}).format(n)}function t(n,i,t=2){return null==n||isNaN(n)?"":new Intl.NumberFormat(i,{style:"percent",minimumFractionDigits:t,maximumFractionDigits:t}).format(n/100)}function e(n,i){if(!n)return"";const t="string"==typeof n?new Date(n):n;return isNaN(t.getTime())?"":new Intl.DateTimeFormat(i,{year:"numeric",month:"2-digit",day:"2-digit"}).format(t)}function r(n,i){if(!n)return"";const t="string"==typeof n?new Date(n):n;return isNaN(t.getTime())?"":new Intl.DateTimeFormat(i,{year:"numeric",month:"2-digit",day:"2-digit",hour:"2-digit",minute:"2-digit"}).format(t)}function u(n,i){if(!n)return"";const[t,e]=n.split(":").map(Number),r=new Date;return r.setHours(t,e,0,0),new Intl.DateTimeFormat(i,{hour:"2-digit",minute:"2-digit"}).format(r)}function a(n,i){var t,e,r,u;if(!n||!n.trim())return null;const a=new Intl.NumberFormat(i).formatToParts(1234567.89),o=null!==(e=null===(t=a.find((n=>"group"===n.type)))||void 0===t?void 0:t.value)&&void 0!==e?e:",",l=null!==(u=null===(r=a.find((n=>"decimal"===n.type)))||void 0===r?void 0:r.value)&&void 0!==u?u:".",s=n.replace(new RegExp(`\\${o}`,"g"),"").replace(new RegExp(`\\${l}`),".").replace("%","").trim(),c=parseFloat(s);return isNaN(c)?null:c}export{u as a,r as b,e as c,i as d,t as e,n as f,a as p}
1
+ function n(n,i,t){return null==n||isNaN(n)?"":new Intl.NumberFormat(i,t).format(n)}function i(n,i,t){return null==n||isNaN(n)?"":new Intl.NumberFormat(t,{style:"currency",currency:i,minimumFractionDigits:2,maximumFractionDigits:2}).format(n)}function t(n,i,t=2){return null==n||isNaN(n)?"":new Intl.NumberFormat(i,{style:"percent",minimumFractionDigits:t,maximumFractionDigits:t}).format(n)}function e(n,i){if(!n)return"";const t="string"==typeof n?new Date(n):n;return isNaN(t.getTime())?"":new Intl.DateTimeFormat(i,{year:"numeric",month:"2-digit",day:"2-digit"}).format(t)}function r(n,i){if(!n)return"";const t="string"==typeof n?new Date(n):n;return isNaN(t.getTime())?"":new Intl.DateTimeFormat(i,{year:"numeric",month:"2-digit",day:"2-digit",hour:"2-digit",minute:"2-digit"}).format(t)}function u(n,i){if(!n)return"";const[t,e]=n.split(":").map(Number),r=new Date;return r.setHours(t,e,0,0),new Intl.DateTimeFormat(i,{hour:"2-digit",minute:"2-digit"}).format(r)}function a(n,i){var t,e,r,u;if(!n||!n.trim())return null;const a=new Intl.NumberFormat(i).formatToParts(1234567.89),o=null!==(e=null===(t=a.find((n=>"group"===n.type)))||void 0===t?void 0:t.value)&&void 0!==e?e:",",l=null!==(u=null===(r=a.find((n=>"decimal"===n.type)))||void 0===r?void 0:r.value)&&void 0!==u?u:".",s=n.replace(new RegExp(`\\${o}`,"g"),"").replace(new RegExp(`\\${l}`),".").replace("%","").trim(),c=parseFloat(s);return isNaN(c)?null:c}export{u as a,r as b,e as c,i as d,t as e,n as f,a as p}