@mmlogic/components 0.1.29 → 0.2.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.
@@ -13,7 +13,7 @@ const NUMERIC_TYPES = new Set(['INTEGER', 'DECIMAL', 'PERCENTAGE', 'CURRENCY']);
13
13
  const DATE_TYPES = new Set(['DATE', 'DATETIME', 'TIME']);
14
14
  const NO_FILTER_TYPES = new Set(['FILE', 'IMAGE']);
15
15
  /** Column types that cannot be sorted or filtered (not stored in PostgreSQL). */
16
- const NON_INTERACTIVE_TYPES = new Set(['LONGTEXT', 'JSON']);
16
+ const NON_INTERACTIVE_TYPES = new Set(['LONGTEXT', 'JSON', 'FILE', 'IMAGE']);
17
17
  export class MrdTable {
18
18
  constructor() {
19
19
  // ── Non-state internals ────────────────────────────────────────────────────
@@ -185,25 +185,25 @@ export class MrdTable {
185
185
  return this.sortDir === 'desc' ? `${this.sortField},desc` : this.sortField;
186
186
  }
187
187
  colName(col) {
188
- var _a, _b, _c, _d;
189
- return (_d = (_b = (_a = col.field) === null || _a === void 0 ? void 0 : _a.name) !== null && _b !== void 0 ? _b : (_c = col.relation) === null || _c === void 0 ? void 0 : _c.name) !== null && _d !== void 0 ? _d : '';
188
+ var _a;
189
+ return (_a = col.name) !== null && _a !== void 0 ? _a : '';
190
190
  }
191
191
  colDataType(col) {
192
- var _a, _b;
192
+ var _a;
193
193
  if (col.type === 'RELATION')
194
194
  return 'RELATION';
195
- return (_b = (_a = col.field) === null || _a === void 0 ? void 0 : _a.dataType) !== null && _b !== void 0 ? _b : 'TEXT';
195
+ return (_a = col.dataType) !== null && _a !== void 0 ? _a : 'TEXT';
196
196
  }
197
197
  // ── Aggregation helpers ────────────────────────────────────────────────────
198
198
  buildAggregationParams() {
199
199
  var _a;
200
200
  const groups = { sum: [], avg: [], count: [] };
201
201
  for (const col of this.columns) {
202
- if (col.type !== 'FIELD' || !((_a = col.field) === null || _a === void 0 ? void 0 : _a.aggregate))
202
+ if (col.type !== 'FIELD' || !col.aggregate)
203
203
  continue;
204
- const fn = col.field.aggregate.toLowerCase();
204
+ const fn = col.aggregate.toLowerCase();
205
205
  if (fn in groups)
206
- groups[fn].push(col.field.name);
206
+ groups[fn].push((_a = col.name) !== null && _a !== void 0 ? _a : '');
207
207
  }
208
208
  const params = {};
209
209
  if (groups.sum.length)
@@ -221,19 +221,19 @@ export class MrdTable {
221
221
  }
222
222
  renderAggregationValue(col) {
223
223
  var _a, _b;
224
- if (col.type !== 'FIELD' || !((_a = col.field) === null || _a === void 0 ? void 0 : _a.aggregate) || !this.aggregations)
224
+ if (col.type !== 'FIELD' || !col.aggregate || !this.aggregations)
225
225
  return '';
226
- const fn = col.field.aggregate.toLowerCase();
227
- const val = (_b = this.aggregations[fn]) === null || _b === void 0 ? void 0 : _b[col.field.name];
226
+ const fn = col.aggregate.toLowerCase();
227
+ const val = (_a = this.aggregations[fn]) === null || _a === void 0 ? void 0 : _a[(_b = col.name) !== null && _b !== void 0 ? _b : ''];
228
228
  if (val == null)
229
229
  return '';
230
- const dt = col.field.dataType;
230
+ const dt = col.dataType;
231
231
  if (dt === 'INTEGER')
232
232
  return formatNumber(val, this.locale, { maximumFractionDigits: 0 });
233
233
  if (dt === 'PERCENTAGE')
234
234
  return formatPercentage(val, this.locale);
235
- if (dt === 'CURRENCY' && col.field.currencyCode)
236
- return formatCurrency(val, col.field.currencyCode, this.locale);
235
+ if (dt === 'CURRENCY' && col.currencyCode)
236
+ return formatCurrency(val, col.currencyCode, this.locale);
237
237
  return formatNumber(val, this.locale);
238
238
  }
239
239
  // ── Reset pagination ───────────────────────────────────────────────────────
@@ -578,7 +578,7 @@ export class MrdTable {
578
578
  }
579
579
  // ── Render: filter popup ───────────────────────────────────────────────────
580
580
  renderFilterEditor(col) {
581
- var _a, _b, _c, _d, _e, _f, _g, _h;
581
+ var _a, _b, _c, _d, _e, _f, _g;
582
582
  const pf = (_a = this.pendingFilter) !== null && _a !== void 0 ? _a : {};
583
583
  const dataType = this.colDataType(col);
584
584
  if (NO_FILTER_TYPES.has(dataType)) {
@@ -594,19 +594,19 @@ export class MrdTable {
594
594
  ].map(opt => (h("label", { class: "mrd-table__filter-radio-label" }, h("input", { type: "radio", name: `bf-${this.openFilterCol}`, checked: !noValueOp && pf.value === opt.value, onChange: () => { this.pendingFilter = Object.assign(Object.assign({}, pf), { operator: undefined, value: opt.value }); } }), t(opt.labelKey, this.locale)))), h("label", { class: "mrd-table__filter-radio-label" }, h("input", { type: "radio", name: `bf-${this.openFilterCol}`, checked: boolOp === 'isEmpty', onChange: () => { this.pendingFilter = Object.assign(Object.assign({}, pf), { operator: 'isEmpty', value: undefined }); } }), t('filter_is_empty', this.locale)), h("label", { class: "mrd-table__filter-radio-label" }, h("input", { type: "radio", name: `bf-${this.openFilterCol}`, checked: boolOp === 'isNotEmpty', onChange: () => { this.pendingFilter = Object.assign(Object.assign({}, pf), { operator: 'isNotEmpty', value: undefined }); } }), t('filter_is_not_empty', this.locale))));
595
595
  }
596
596
  if (dataType === 'LIST') {
597
- const items = (_c = (_b = col.field) === null || _b === void 0 ? void 0 : _b.listItems) !== null && _c !== void 0 ? _c : [];
598
- const selected = (_d = pf.values) !== null && _d !== void 0 ? _d : [];
597
+ const items = (_b = col.listItems) !== null && _b !== void 0 ? _b : [];
598
+ const selected = (_c = pf.values) !== null && _c !== void 0 ? _c : [];
599
599
  return (h("div", { class: "mrd-table__filter-list" }, h("div", { class: "mrd-table__filter-list-controls" }, h("button", { class: "mrd-table__filter-list-btn", onClick: () => { this.pendingFilter = Object.assign(Object.assign({}, pf), { values: items.map(i => i.key) }); } }, t('filter_select_all', this.locale)), h("button", { class: "mrd-table__filter-list-btn", onClick: () => { this.pendingFilter = Object.assign(Object.assign({}, pf), { values: [] }); } }, t('filter_select_none', this.locale))), items.map(item => (h("label", { class: "mrd-table__filter-checkbox-label" }, h("input", { type: "checkbox", checked: selected.includes(item.key), onChange: (e) => this.togglePendingValue(item.key, e.target.checked) }), item.label)))));
600
600
  }
601
601
  if (TEXT_TYPES.has(dataType) || dataType === 'RELATION') {
602
- const op = (_e = pf.operator) !== null && _e !== void 0 ? _e : 'startsWith';
602
+ const op = (_d = pf.operator) !== null && _d !== void 0 ? _d : 'startsWith';
603
603
  const noInput = op === 'isEmpty' || op === 'isNotEmpty';
604
604
  return (h("div", { class: "mrd-table__filter-editor" }, h("select", { class: "mrd-table__filter-select", onChange: (e) => this.setPending('operator', e.target.value) }, [
605
605
  { val: 'startsWith', labelKey: 'filter_starts_with' },
606
606
  { val: 'equals', labelKey: 'filter_equals' },
607
607
  { val: 'isEmpty', labelKey: 'filter_is_empty' },
608
608
  { val: 'isNotEmpty', labelKey: 'filter_is_not_empty' },
609
- ].map(o => h("option", { value: o.val, selected: op === o.val }, t(o.labelKey, this.locale)))), !noInput && (h("input", { type: "text", class: "mrd-table__filter-input", value: String((_f = pf.value) !== null && _f !== void 0 ? _f : ''), placeholder: t('filter_search_value', this.locale), onInput: (e) => this.setPending('value', e.target.value) }))));
609
+ ].map(o => h("option", { value: o.val, selected: op === o.val }, t(o.labelKey, this.locale)))), !noInput && (h("input", { type: "text", class: "mrd-table__filter-input", value: String((_e = pf.value) !== null && _e !== void 0 ? _e : ''), placeholder: t('filter_search_value', this.locale), onInput: (e) => this.setPending('value', e.target.value) }))));
610
610
  }
611
611
  if (NUMERIC_TYPES.has(dataType)) {
612
612
  const numOp = pf.operator;
@@ -634,7 +634,7 @@ export class MrdTable {
634
634
  else {
635
635
  this.pendingFilter = Object.assign(Object.assign({}, pf), { operator: undefined });
636
636
  }
637
- } }, h("option", { value: "", selected: !noInput }, t('filter_has_value', this.locale)), h("option", { value: "isEmpty", selected: dtOp === 'isEmpty' }, t('filter_is_empty', this.locale)), h("option", { value: "isNotEmpty", selected: dtOp === 'isNotEmpty' }, t('filter_is_not_empty', this.locale))), !noInput && (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) })))))));
637
+ } }, h("option", { value: "", selected: !noInput }, t('filter_has_value', this.locale)), h("option", { value: "isEmpty", selected: dtOp === 'isEmpty' }, t('filter_is_empty', this.locale)), h("option", { value: "isNotEmpty", selected: dtOp === 'isNotEmpty' }, t('filter_is_not_empty', this.locale))), !noInput && (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((_f = pf.value) !== null && _f !== void 0 ? _f : ''), 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) })))))));
638
638
  }
639
639
  if (DATE_TYPES.has(dataType)) {
640
640
  const inputType = dataType === 'DATE' ? 'date' : 'time';
@@ -649,18 +649,18 @@ export class MrdTable {
649
649
  else {
650
650
  this.pendingFilter = Object.assign(Object.assign({}, pf), { operator: undefined });
651
651
  }
652
- } }, h("option", { value: "", selected: !noInput }, t('filter_has_value', this.locale)), h("option", { value: "isEmpty", selected: dtdOp === 'isEmpty' }, t('filter_is_empty', this.locale)), h("option", { value: "isNotEmpty", selected: dtdOp === 'isNotEmpty' }, t('filter_is_not_empty', this.locale))), !noInput && (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) })))))));
652
+ } }, h("option", { value: "", selected: !noInput }, t('filter_has_value', this.locale)), h("option", { value: "isEmpty", selected: dtdOp === 'isEmpty' }, t('filter_is_empty', this.locale)), h("option", { value: "isNotEmpty", selected: dtdOp === 'isNotEmpty' }, t('filter_is_not_empty', this.locale))), !noInput && (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) })))))));
653
653
  }
654
654
  return null;
655
655
  }
656
656
  renderFilterPopup() {
657
- var _a, _b, _c, _d;
657
+ var _a;
658
658
  if (!this.openFilterCol || !this.pendingFilter)
659
659
  return null;
660
660
  const col = this.columns.find(c => this.colName(c) === this.openFilterCol);
661
661
  if (!col)
662
662
  return null;
663
- const 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.openFilterCol;
663
+ const label = (_a = col.label) !== null && _a !== void 0 ? _a : this.openFilterCol;
664
664
  const sortActive = this.sortField === this.openFilterCol;
665
665
  return (h("div", { class: "mrd-table__filter-popup", style: { top: `${this.popupPos.top}px`, left: `${this.popupPos.left}px` }, onClick: (e) => e.stopPropagation() }, h("div", { class: "mrd-table__filter-popup-header" }, h("span", { class: "mrd-table__filter-popup-title" }, label), h("button", { class: "mrd-table__filter-close", onClick: () => this.closeFilterPopup() }, "\u2715")), h("div", { class: "mrd-table__filter-section" }, h("div", { class: "mrd-table__filter-section-label" }, t('filter_sorting', this.locale)), h("div", { class: "mrd-table__filter-sort-buttons" }, h("button", { class: `mrd-table__filter-sort-btn${sortActive && this.sortDir === 'asc' ? ' mrd-table__filter-sort-btn--active' : ''}`, onClick: () => this.applySort(col, 'asc') }, "\u25B2 ", t('filter_ascending', this.locale)), h("button", { class: `mrd-table__filter-sort-btn${sortActive && this.sortDir === 'desc' ? ' mrd-table__filter-sort-btn--active' : ''}`, onClick: () => this.applySort(col, 'desc') }, "\u25BC ", t('filter_descending', this.locale)))), h("div", { class: "mrd-table__filter-divider" }), h("div", { class: "mrd-table__filter-section" }, h("div", { class: "mrd-table__filter-section-label" }, t('filter_section', this.locale)), this.renderFilterEditor(col)), h("div", { class: "mrd-table__filter-popup-footer" }, h("button", { class: "mrd-table__filter-btn mrd-table__filter-btn--clear", onClick: () => this.clearFilter() }, t('filter_clear', this.locale)), h("button", { class: "mrd-table__filter-btn mrd-table__filter-btn--apply", onClick: () => this.applyFilter() }, t('filter_apply', this.locale)))));
666
666
  }
@@ -687,13 +687,13 @@ export class MrdTable {
687
687
  }
688
688
  // ── Render: cell ──────────────────────────────────────────────────────────
689
689
  renderCell(col, row) {
690
- var _a, _b, _c, _d, _e, _f;
690
+ var _a, _b, _c;
691
691
  const numericTypes = new Set(['INTEGER', 'DECIMAL', 'PERCENTAGE', 'CURRENCY']);
692
- const dataType = (_b = (_a = col.field) === null || _a === void 0 ? void 0 : _a.dataType) !== null && _b !== void 0 ? _b : '';
692
+ const dataType = (_a = col.dataType) !== null && _a !== void 0 ? _a : '';
693
693
  const isNumeric = col.type === 'FIELD' && numericTypes.has(dataType);
694
694
  const isFile = col.type === 'FIELD' && (dataType === 'FILE' || dataType === 'IMAGE');
695
695
  if (isFile) {
696
- const name = (_d = (_c = col.field) === null || _c === void 0 ? void 0 : _c.name) !== null && _d !== void 0 ? _d : '';
696
+ const name = (_b = col.name) !== null && _b !== void 0 ? _b : '';
697
697
  const fileVal = row === null || row === void 0 ? void 0 : row[name];
698
698
  const href = fileVal === null || fileVal === void 0 ? void 0 : fileVal.href;
699
699
  const fileName = fileVal === null || fileVal === void 0 ? void 0 : fileVal.fileName;
@@ -703,7 +703,7 @@ export class MrdTable {
703
703
  } }, 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))) : ''));
704
704
  }
705
705
  if (dataType === 'JSON') {
706
- const name = (_f = (_e = col.field) === null || _e === void 0 ? void 0 : _e.name) !== null && _f !== void 0 ? _f : '';
706
+ const name = (_c = col.name) !== null && _c !== void 0 ? _c : '';
707
707
  const rawValue = name ? row === null || row === void 0 ? void 0 : row[name] : undefined;
708
708
  if (rawValue == null || rawValue === '')
709
709
  return h("td", { class: "mrd-table__cell" });
@@ -727,12 +727,12 @@ export class MrdTable {
727
727
  renderTotalsRow() {
728
728
  if (!this.aggregations)
729
729
  return null;
730
- if (!this.columns.some(c => { var _a; return c.type === 'FIELD' && ((_a = c.field) === null || _a === void 0 ? void 0 : _a.aggregate); }))
730
+ if (!this.columns.some(c => c.type === 'FIELD' && c.aggregate))
731
731
  return null;
732
732
  return (h("tfoot", null, h("tr", { class: "mrd-table__totals-row" }, this.columns.map(col => {
733
- var _a, _b;
733
+ var _a;
734
734
  const val = this.renderAggregationValue(col);
735
- const isNumeric = col.type === 'FIELD' && NUMERIC_TYPES.has((_b = (_a = col.field) === null || _a === void 0 ? void 0 : _a.dataType) !== null && _b !== void 0 ? _b : '');
735
+ const isNumeric = col.type === 'FIELD' && NUMERIC_TYPES.has((_a = col.dataType) !== null && _a !== void 0 ? _a : '');
736
736
  return (h("td", { class: `mrd-table__totals-cell${isNumeric ? ' mrd-table__totals-cell--numeric' : ''}` }, val));
737
737
  }))));
738
738
  }
@@ -744,7 +744,7 @@ export class MrdTable {
744
744
  // ── Non-paginated mode ──────────────────────────────────────────────────
745
745
  if (this.totalElements === 0) {
746
746
  return (h(Host, null, this.renderToolbar(), h("div", { class: "mrd-table" }, h("table", { class: "mrd-table__table" }, h("thead", null, h("tr", null, this.columns.map(col => {
747
- var _a, _b, _c, _d;
747
+ var _a;
748
748
  const name = this.colName(col);
749
749
  const isFiltered = this.activeFilters.has(name);
750
750
  const cls = [
@@ -752,7 +752,7 @@ export class MrdTable {
752
752
  isFiltered ? 'mrd-table__header--filtered' : '',
753
753
  this.filterMode ? 'mrd-table__header--sortable' : '',
754
754
  ].filter(Boolean).join(' ');
755
- 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()));
755
+ return (h("th", { class: cls, onClick: this.filterMode ? (e) => this.handleFilterOpen(col, e) : undefined }, h("span", { class: "mrd-table__header-label" }, (_a = col.label) !== null && _a !== void 0 ? _a : ''), isFiltered && this.renderFilterIcon()));
756
756
  }))), 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.renderTotalsRow()), (!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(), this.renderJsonModal()));
757
757
  }
758
758
  // ── Paginated / virtual-scroll mode ────────────────────────────────────
@@ -785,7 +785,7 @@ export class MrdTable {
785
785
  }
786
786
  }
787
787
  return (h(Host, null, this.renderToolbar(), h("div", { class: "mrd-table__scroll", style: { height: `${this.tableHeight}px` }, onScroll: this.handleScroll }, h("table", { class: "mrd-table__table", style: tableStyle }, h("thead", null, h("tr", null, this.columns.map((col, idx) => {
788
- var _a, _b, _c, _d;
788
+ var _a;
789
789
  const name = this.colName(col);
790
790
  const isActive = this.sortField === name;
791
791
  const isFiltered = this.activeFilters.has(name);
@@ -796,7 +796,7 @@ export class MrdTable {
796
796
  isActive ? `mrd-table__header--sorted-${this.sortDir}` : '',
797
797
  isFiltered ? 'mrd-table__header--filtered' : '',
798
798
  ].filter(Boolean).join(' ');
799
- return (h("th", { class: cls, style: this.colWidths[idx] ? { width: `${this.colWidths[idx]}px` } : undefined, onClick: isInteractive ? ((e) => this.filterMode ? this.handleFilterOpen(col, e) : this.handleSortClick(col)) : 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 : ''), isInteractive && isActive && (h("span", { class: "mrd-table__sort-icon", "aria-hidden": "true" }, this.sortDir === 'asc' ? '▲' : '▼')), isInteractive && !isActive && !this.filterMode && (h("span", { class: "mrd-table__sort-icon", "aria-hidden": "true" }, "\u21C5")), isInteractive && isFiltered && this.renderFilterIcon()));
799
+ return (h("th", { class: cls, style: this.colWidths[idx] ? { width: `${this.colWidths[idx]}px` } : undefined, onClick: isInteractive ? ((e) => this.filterMode ? this.handleFilterOpen(col, e) : this.handleSortClick(col)) : undefined }, h("span", { class: "mrd-table__header-label" }, (_a = col.label) !== null && _a !== void 0 ? _a : ''), isInteractive && isActive && (h("span", { class: "mrd-table__sort-icon", "aria-hidden": "true" }, this.sortDir === 'asc' ? '▲' : '▼')), isInteractive && !isActive && !this.filterMode && (h("span", { class: "mrd-table__sort-icon", "aria-hidden": "true" }, "\u21C5")), isInteractive && isFiltered && this.renderFilterIcon()));
800
800
  }))), 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 })))), this.renderTotalsRow())), 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()));
801
801
  }
802
802
  renderFilterIcon() {
@@ -102,106 +102,18 @@ async function apiSearchRelation(token, tenantCode, mostSignificantClass, query)
102
102
 
103
103
  /* =====================================================================
104
104
  LAYOUT MAPPERS
105
- Translate flat API layout responses to nested ClientLayoutItem format
106
- expected by mrd-form.
105
+ mrd-form and mrd-field now accept the flat API format directly.
106
+ No transformation needed — pass the raw layout through as-is.
107
107
  ===================================================================== */
108
108
 
109
109
  /**
110
- * Map a single flat API item to the nested ClientLayoutItem structure.
111
- * Used by both mapApiLayoutToMrdForm (form) and mapApiColumns (table).
112
- */
113
- function mapApiItem(item) {
114
- if (item.type === 'FIELD') {
115
- if (item.field) return item; // already nested — pass through
116
- return {
117
- type: 'FIELD',
118
- field: {
119
- name: item.name,
120
- label: item.label,
121
- dataType: item.dataType,
122
- required: !!item.required,
123
- multiple: !!item.multiple,
124
- header: !!item.header,
125
- defaultValue: item.defaultValue ?? null,
126
- listItems: item.listItems ?? null,
127
- aggregate: item.aggregate ?? null,
128
- },
129
- };
130
- }
131
- if (item.type === 'RELATION') {
132
- if (item.relation) return item; // already nested — pass through
133
- return {
134
- type: 'RELATION',
135
- relation: {
136
- name: item.name,
137
- label: item.label,
138
- relatedClass: item.relatedClass,
139
- mostSignificantClass: item.mostSignificantClass ?? null,
140
- displayType: item.editBehavior ?? 'SEARCH',
141
- editBehavior: item.editBehavior ?? null,
142
- commonRelation: item.commonRelation ?? null,
143
- required: !!item.required,
144
- multiple: !!item.multiple,
145
- defaultValue: item.defaultValue ?? null,
146
- },
147
- };
148
- }
149
- if (Array.isArray(item.items)) {
150
- return { ...item, items: item.items.map(mapApiItem) };
151
- }
152
- return item;
153
- }
154
-
155
- /**
156
- * Map API layout (OBJECT_FORM_DASHBOARD shape) → ClientLayout for mrd-form.
157
- *
158
- * IMPORTANT: always map editBehavior and commonRelation from the API response —
159
- * omitting them causes relation fields to fall back to SEARCH mode.
160
- * _relationMeta must be keyed by BOTH relatedClass AND mostSignificantClass
161
- * because mrdSearch sends mostSignificantClass as the lookup key.
110
+ * Extract a ClientLayout from the raw API form response.
111
+ * The API sends flat items (name, dataType, relatedClass at root level)
112
+ * which mrd-form/mrd-field read directly.
162
113
  */
163
114
  function mapApiLayoutToMrdForm(raw) {
164
- if (!raw || !Array.isArray(raw.items)) return raw;
165
-
166
- function mapItem(item) {
167
- if (item.type === 'FIELD') {
168
- return {
169
- type: 'FIELD',
170
- field: {
171
- name: item.name,
172
- label: item.label,
173
- dataType: item.dataType,
174
- required: !!item.required,
175
- multiple: !!item.multiple,
176
- header: !!item.header,
177
- defaultValue: item.defaultValue ?? null,
178
- listItems: item.listItems ?? null,
179
- },
180
- };
181
- }
182
- if (item.type === 'RELATION') {
183
- return {
184
- type: 'RELATION',
185
- relation: {
186
- name: item.name,
187
- label: item.label,
188
- relatedClass: item.relatedClass,
189
- mostSignificantClass: item.mostSignificantClass ?? null,
190
- displayType: item.editBehavior ?? 'SEARCH',
191
- editBehavior: item.editBehavior ?? null,
192
- commonRelation: item.commonRelation ?? null,
193
- required: !!item.required,
194
- multiple: !!item.multiple,
195
- defaultValue: item.defaultValue ?? null,
196
- },
197
- };
198
- }
199
- // SECTION / GROUP — recurse into children
200
- if (Array.isArray(item.items)) {
201
- return { ...item, items: item.items.map(mapItem) };
202
- }
203
- return item;
204
- }
205
-
206
- return { items: raw.items.map(mapItem) };
115
+ if (!raw) return raw;
116
+ // Unwrap OBJECT_FORM_DASHBOARD: keep items at top level, drop type wrapper
117
+ if (Array.isArray(raw.items)) return raw;
118
+ return raw;
207
119
  }
@@ -260,7 +260,7 @@ async function loadTable() {
260
260
  // Strip any ?page= from href so we control pagination ourselves
261
261
  _tableDataHref = dataHref.replace(/([?&])page=\d+/, '').replace(/\?$/, '');
262
262
 
263
- const columns = (view.values ?? []).map(mapApiItem);
263
+ const columns = view.values ?? [];
264
264
  const defaultSort = view.defaultSort ?? '';
265
265
  const viewLabel = layoutItem?.label ?? viewKey ?? '';
266
266
  const alternativeViews = layoutItem?.alternativeViews ?? [];
@@ -407,7 +407,7 @@ function renderTable(columns, totalElements, pageSize, page0Rows, dataHref, defa
407
407
  const meta = page0.page ?? {};
408
408
  const rows0 = Object.values(page0._embedded ?? {})[0] ?? [];
409
409
 
410
- table.columns = (newView.values ?? []).map(mapApiItem);
410
+ table.columns = newView.values ?? [];
411
411
  table.defaultSort = newDefaultSort;
412
412
  table.totalElements = meta.totalElements ?? 0;
413
413
  table.pageSize = meta.size ?? 20;
@@ -455,7 +455,7 @@ async function loadRecord(row) {
455
455
 
456
456
  const formHref = recordResp.body?._links?.form?.href ?? null;
457
457
  const layout = await apiFetchForm(authGetToken(), _selectedTenant, _selectedType.pluralName, formHref);
458
- renderForm(layout, recordResp.body, selfHref);
458
+ await renderForm(layout, recordResp.body, selfHref);
459
459
  } catch (err) {
460
460
  formPanel.innerHTML = `<span style="color:var(--mrd-color-danger)">❌ ${escHtml(err.message)}</span>`;
461
461
  }
@@ -476,7 +476,7 @@ async function loadForm() {
476
476
 
477
477
  try {
478
478
  const layout = await apiFetchForm(authGetToken(), _selectedTenant, pluralName);
479
- renderForm(layout);
479
+ await renderForm(layout);
480
480
  } catch (err) {
481
481
  formPanel.innerHTML = `<span style="color:var(--mrd-color-danger)">❌ ${escHtml(err.message)}</span>`;
482
482
  }
@@ -489,21 +489,21 @@ async function loadForm() {
489
489
  * @param record - existing record for edit mode (null = new record)
490
490
  * @param selfHref - absolute URL for PATCH; null = POST (new record)
491
491
  */
492
- function renderForm(layout, record = null, selfHref = null) {
492
+ async function renderForm(layout, record = null, selfHref = null) {
493
493
  // Build relation metadata map keyed by both relatedClass and mostSignificantClass
494
494
  // so lookups work regardless of which value arrives in events (mrdSearch uses mostSignificantClass).
495
495
  _relationMeta = {};
496
496
  function collectRelations(items) {
497
497
  if (!Array.isArray(items)) return;
498
498
  items.forEach(item => {
499
- if (item.type === 'RELATION' && item.relation) {
499
+ if (item.type === 'RELATION' && item.relatedClass) {
500
500
  const meta = {
501
- name: item.relation.name,
502
- mostSignificantClass: item.relation.mostSignificantClass,
501
+ name: item.name,
502
+ mostSignificantClass: item.mostSignificantClass,
503
503
  };
504
- _relationMeta[item.relation.relatedClass] = meta;
505
- if (item.relation.mostSignificantClass) {
506
- _relationMeta[item.relation.mostSignificantClass] = meta;
504
+ _relationMeta[item.relatedClass] = meta;
505
+ if (item.mostSignificantClass) {
506
+ _relationMeta[item.mostSignificantClass] = meta;
507
507
  }
508
508
  }
509
509
  if (Array.isArray(item.items)) collectRelations(item.items);
@@ -534,12 +534,19 @@ function renderForm(layout, record = null, selfHref = null) {
534
534
  const formPanel = document.getElementById('panel-form');
535
535
  formPanel.innerHTML = `<mrd-form id="live-form" locale="${_locale}"></mrd-form>`;
536
536
 
537
- customElements.whenDefined('mrd-form').then(() => {
538
- const form = document.getElementById('live-form');
539
- form.layout = layout;
540
- if (record) form.values = initialValues;
537
+ const form = document.getElementById('live-form');
538
+ // Wait for Stencil to fully initialize this specific instance before setting props.
539
+ // componentOnReady() resolves after componentDidLoad — more reliable than customElements.whenDefined.
540
+ if (typeof form.componentOnReady === 'function') {
541
+ await form.componentOnReady();
542
+ } else {
543
+ await customElements.whenDefined('mrd-form');
544
+ }
545
+
546
+ form.layout = layout;
547
+ if (record) form.values = initialValues;
541
548
 
542
- // Upload files immediately on selection; write binary URI back via setFieldValue
549
+ // Upload files immediately on selection; write binary URI back via setFieldValue
543
550
  form.addEventListener('mrdChange', async (e) => {
544
551
  const { name, value } = e.detail;
545
552
  if (!(value instanceof File)) return;
@@ -645,7 +652,6 @@ function renderForm(layout, record = null, selfHref = null) {
645
652
  console.error('[mrdFetchAll] failed:', err);
646
653
  }
647
654
  });
648
- });
649
655
  }
650
656
 
651
657
  /* =====================================================================
@@ -745,6 +751,17 @@ document.addEventListener('DOMContentLoaded', async () => {
745
751
  logEvent('mrdDownload', e.detail);
746
752
  });
747
753
 
754
+ ftSection.addEventListener('mrdSearch', (e) => {
755
+ logEvent('mrdSearch', e.detail);
756
+ const { query, dataClass } = e.detail;
757
+ const mock = [
758
+ { id: '/data/demo/1', label: 'Alice Johnson', description: 'Senior Engineer' },
759
+ { id: '/data/demo/2', label: 'Bob van der Berg', description: 'Product Manager' },
760
+ { id: '/data/demo/3', label: 'Carol Martínez', description: 'UX Designer' },
761
+ ].filter(r => r.label.toLowerCase().includes(query.toLowerCase()));
762
+ ftSection.setSearchResults(mock, dataClass);
763
+ });
764
+
748
765
  authRestoreSession();
749
766
  await authHandleCallback();
750
767