@shamar/adonis 0.1.2 → 0.1.3

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.
@@ -818,9 +818,12 @@
818
818
 
819
819
  openRecord() {
820
820
  if (!this.value || !this.detailUrlBase) return;
821
+ const url = this.readonly
822
+ ? `${this.detailUrlBase}/${this.value}`
823
+ : `${this.detailUrlBase}/${this.value}/edit`;
821
824
  window.ShamarUI.openDialog({
822
- url: `${this.detailUrlBase}/${this.value}/edit`,
823
- title: this.label || 'Edit record',
825
+ url,
826
+ title: this.label || (this.readonly ? 'View record' : 'Edit record'),
824
827
  slug: this.relatedResource,
825
828
  });
826
829
  },
@@ -1119,9 +1122,12 @@
1119
1122
 
1120
1123
  openRecord(item) {
1121
1124
  if (!item?.id || !this.detailUrlBase) return;
1125
+ const url = this.readonly
1126
+ ? `${this.detailUrlBase}/${item.id}`
1127
+ : `${this.detailUrlBase}/${item.id}/edit`;
1122
1128
  window.ShamarUI.openDialog({
1123
- url: `${this.detailUrlBase}/${item.id}/edit`,
1124
- title: item.label || 'Edit record',
1129
+ url,
1130
+ title: item.label || (this.readonly ? 'View record' : 'Edit record'),
1125
1131
  slug: this.relatedResource,
1126
1132
  });
1127
1133
  },
@@ -1453,10 +1459,30 @@
1453
1459
  function createShamarM2mTable(cfg) {
1454
1460
  const self = createShamarM2m(cfg);
1455
1461
  self.addOpen = false;
1462
+ self.listUrl = cfg.listUrl || null;
1463
+ self.columns = Array.isArray(cfg.columns) ? cfg.columns : [];
1464
+ self.listHeaders = Array.isArray(cfg.listHeaders) ? cfg.listHeaders : [];
1465
+ self.rows = [];
1466
+ self.total = 0;
1467
+ self.page = 1;
1468
+ self.pageCount = 1;
1469
+ self.perPage = String(cfg.perPage || 10);
1470
+ self.listSearch = '';
1471
+ self.listFilters = [];
1472
+ self.listSort = cfg.defaultSort || '';
1473
+ self.listDirection = cfg.defaultDirection || 'asc';
1474
+ self.listLoading = false;
1475
+ self.filterPanelOpen = false;
1476
+ self._listAbort = null;
1477
+
1456
1478
  const prevInit = self.init;
1457
1479
  const prevDestroy = self.destroy;
1458
1480
  const prevPick = self.pick;
1459
1481
  const prevCreateAndEdit = self.createAndEdit;
1482
+ const prevAttach = self.attachHasMany;
1483
+ const prevDetach = self.detachHasMany;
1484
+ const prevOpenRecord = self.openRecord;
1485
+
1460
1486
  self.init = function initTable() {
1461
1487
  prevInit.call(this);
1462
1488
  this._tablePickHandler = (event) => {
@@ -1467,13 +1493,190 @@
1467
1493
  }
1468
1494
  };
1469
1495
  window.addEventListener('shamar-m2m-pick', this._tablePickHandler);
1496
+ if (this.listUrl) {
1497
+ this.loadList();
1498
+ }
1470
1499
  };
1500
+
1471
1501
  self.destroy = function destroyTable() {
1472
1502
  prevDestroy.call(this);
1473
1503
  if (this._tablePickHandler) {
1474
1504
  window.removeEventListener('shamar-m2m-pick', this._tablePickHandler);
1475
1505
  }
1506
+ if (this._listAbort) this._listAbort.abort();
1507
+ };
1508
+
1509
+ Object.defineProperty(self, 'filterableHeaders', {
1510
+ get() {
1511
+ return (this.listHeaders || []).filter(
1512
+ (header) =>
1513
+ header.filter_kind === 'boolean' ||
1514
+ (header.filter_kind === 'select' && Array.isArray(header.options)),
1515
+ );
1516
+ },
1517
+ });
1518
+
1519
+ Object.defineProperty(self, 'availableResults', {
1520
+ get() {
1521
+ const linked = new Set([
1522
+ ...(this.selected || []).map((item) => String(item.id)),
1523
+ ...(this.rows || []).map((row) => String(row.id)),
1524
+ ]);
1525
+ return (this.results || []).filter((item) => !linked.has(String(item.id)));
1526
+ },
1527
+ });
1528
+
1529
+ self.cellAlignment = function cellAlignment(columnName) {
1530
+ const column = (this.columns || []).find((entry) => entry.name === columnName);
1531
+ return column?.alignmentClass || '';
1532
+ };
1533
+
1534
+ self.buildListUrl = function buildListUrl() {
1535
+ const url = new URL(this.listUrl, window.location.origin);
1536
+ if (this.listSearch) url.searchParams.set('search', this.listSearch);
1537
+ if (this.listSort) {
1538
+ url.searchParams.set('sort', this.listSort);
1539
+ if (this.listDirection === 'asc' || this.listDirection === 'desc') {
1540
+ url.searchParams.set('direction', this.listDirection);
1541
+ }
1542
+ }
1543
+ url.searchParams.set('page', String(this.page || 1));
1544
+ url.searchParams.set('perPage', String(this.perPage || 10));
1545
+ if (this.listFilters?.length) {
1546
+ url.searchParams.set('filters', JSON.stringify(this.listFilters));
1547
+ }
1548
+ return `${url.pathname}${url.search}`;
1549
+ };
1550
+
1551
+ self.loadList = async function loadList() {
1552
+ if (!this.listUrl) return;
1553
+ if (this._listAbort) this._listAbort.abort();
1554
+ const ctl = new AbortController();
1555
+ this._listAbort = ctl;
1556
+ this.listLoading = true;
1557
+ try {
1558
+ const response = await fetch(this.buildListUrl(), {
1559
+ signal: ctl.signal,
1560
+ credentials: 'same-origin',
1561
+ headers: { Accept: 'application/json' },
1562
+ });
1563
+ if (!response.ok) throw new Error('list failed');
1564
+ const data = await response.json();
1565
+ this.rows = Array.isArray(data.rows) ? data.rows : [];
1566
+ this.total = Number(data.total) || 0;
1567
+ this.page = Number(data.page) || 1;
1568
+ this.pageCount = Math.max(1, Number(data.pageCount) || 1);
1569
+ this.perPage = String(data.perPage || this.perPage || 10);
1570
+ if (Array.isArray(data.columns) && data.columns.length) {
1571
+ this.columns = data.columns;
1572
+ }
1573
+ if (Array.isArray(data.listHeaders)) {
1574
+ this.listHeaders = data.listHeaders;
1575
+ }
1576
+ if (data.sort) this.listSort = data.sort;
1577
+ if (data.direction) this.listDirection = data.direction;
1578
+ } catch (error) {
1579
+ if (error.name !== 'AbortError') {
1580
+ this.rows = [];
1581
+ this.total = 0;
1582
+ showToast('error', {
1583
+ title: 'Error',
1584
+ message: 'Unable to load related records.',
1585
+ });
1586
+ }
1587
+ } finally {
1588
+ this.listLoading = false;
1589
+ }
1476
1590
  };
1591
+
1592
+ self.applyListSearch = function applyListSearch() {
1593
+ this.page = 1;
1594
+ this.loadList();
1595
+ };
1596
+
1597
+ self.setListPage = function setListPage(page) {
1598
+ const next = Math.max(1, Number(page) || 1);
1599
+ this.page = next;
1600
+ this.loadList();
1601
+ };
1602
+
1603
+ self.toggleListSort = function toggleListSort(field) {
1604
+ if (!field) return;
1605
+ if (this.listSort === field) {
1606
+ this.listDirection = this.listDirection === 'asc' ? 'desc' : 'asc';
1607
+ } else {
1608
+ this.listSort = field;
1609
+ this.listDirection = 'asc';
1610
+ }
1611
+ this.page = 1;
1612
+ this.loadList();
1613
+ };
1614
+
1615
+ self.hasFilter = function hasFilter(header, value) {
1616
+ const field = header.filterField || header.name;
1617
+ return this.listFilters.some(
1618
+ (chip) => chip.field === field && String(chip.value) === String(value),
1619
+ );
1620
+ };
1621
+
1622
+ self.toggleBooleanFilter = function toggleBooleanFilter(header, value) {
1623
+ const field = header.filterField || header.name;
1624
+ const existing = this.listFilters.find((chip) => chip.field === field);
1625
+ if (existing && String(existing.value) === String(value)) {
1626
+ this.listFilters = this.listFilters.filter((chip) => chip.field !== field);
1627
+ } else {
1628
+ this.listFilters = [
1629
+ ...this.listFilters.filter((chip) => chip.field !== field),
1630
+ {
1631
+ field,
1632
+ op: '=',
1633
+ value,
1634
+ label: `${header.label}: ${value ? 'Yes' : 'No'}`,
1635
+ },
1636
+ ];
1637
+ }
1638
+ this.page = 1;
1639
+ this.loadList();
1640
+ };
1641
+
1642
+ self.toggleSelectFilter = function toggleSelectFilter(header, opt) {
1643
+ const field = header.filterField || header.name;
1644
+ const value = opt.value;
1645
+ if (this.hasFilter(header, value)) {
1646
+ this.listFilters = this.listFilters.filter(
1647
+ (chip) => !(chip.field === field && String(chip.value) === String(value)),
1648
+ );
1649
+ } else {
1650
+ this.listFilters = [
1651
+ ...this.listFilters.filter((chip) => chip.field !== field),
1652
+ {
1653
+ field,
1654
+ op: '=',
1655
+ value,
1656
+ label: `${header.label}: ${opt.label}`,
1657
+ },
1658
+ ];
1659
+ }
1660
+ this.page = 1;
1661
+ this.loadList();
1662
+ };
1663
+
1664
+ self.removeListFilter = function removeListFilter(chip) {
1665
+ this.listFilters = this.listFilters.filter(
1666
+ (entry) =>
1667
+ !(entry.field === chip.field && String(entry.value) === String(chip.value)),
1668
+ );
1669
+ this.page = 1;
1670
+ this.loadList();
1671
+ };
1672
+
1673
+ self.clearListFilters = function clearListFilters() {
1674
+ this.listFilters = [];
1675
+ this.filterPanelOpen = false;
1676
+ this.page = 1;
1677
+ this.loadList();
1678
+ };
1679
+
1477
1680
  self.toggleAdd = function toggleAdd() {
1478
1681
  this.addOpen = !this.addOpen;
1479
1682
  if (this.addOpen) {
@@ -1482,18 +1685,76 @@
1482
1685
  this.$nextTick?.(() => this.$refs.addInput?.focus());
1483
1686
  }
1484
1687
  };
1688
+
1485
1689
  self.closeAdd = function closeAdd() {
1486
1690
  this.addOpen = false;
1487
1691
  this.query = '';
1488
1692
  };
1693
+
1489
1694
  self.pick = function pickTable(item) {
1695
+ if (
1696
+ this.listUrl &&
1697
+ item?.id != null &&
1698
+ this.rows.some((row) => String(row.id) === String(item.id))
1699
+ ) {
1700
+ this.closeAdd();
1701
+ return;
1702
+ }
1490
1703
  prevPick.call(this, item);
1491
1704
  this.closeAdd();
1492
1705
  };
1706
+
1707
+ self.attachHasMany = async function attachHasManyTable(item) {
1708
+ await prevAttach.call(this, item);
1709
+ if (this.listUrl) {
1710
+ this.page = 1;
1711
+ await this.loadList();
1712
+ }
1713
+ };
1714
+
1715
+ self.detachHasMany = async function detachHasManyTable(relatedId) {
1716
+ await prevDetach.call(this, relatedId);
1717
+ if (this.listUrl) {
1718
+ await this.loadList();
1719
+ }
1720
+ };
1721
+
1722
+ self.openRecord = function openRecordTable(item) {
1723
+ if (!item?.id || !this.detailUrlBase) return;
1724
+ const url = this.readonly
1725
+ ? `${this.detailUrlBase}/${item.id}`
1726
+ : `${this.detailUrlBase}/${item.id}/edit`;
1727
+ window.ShamarUI.openDialog({
1728
+ url,
1729
+ title: item.label || (this.readonly ? 'View record' : 'Edit record'),
1730
+ slug: this.relatedResource,
1731
+ onResult: () => {
1732
+ if (this.listUrl) this.loadList();
1733
+ },
1734
+ });
1735
+ };
1736
+
1493
1737
  self.createAndEdit = function createAndEditTable() {
1494
1738
  this.closeAdd();
1495
- prevCreateAndEdit.call(this);
1739
+ if (!this.createUrl) return;
1740
+ const q = (this.query || '').trim();
1741
+ const url = q
1742
+ ? `${this.createUrl}${this.createUrl.includes('?') ? '&' : '?'}name=${encodeURIComponent(q)}`
1743
+ : this.createUrl;
1744
+ const title = q ? `New ${this.singularLabel}` : `Create ${this.singularLabel}`;
1745
+ window.ShamarUI.openDialog({
1746
+ url,
1747
+ title,
1748
+ slug: this.relatedResource,
1749
+ onResult: (detail) => {
1750
+ if (detail?.id) {
1751
+ prevPick.call(this, { id: detail.id, label: detail.label });
1752
+ }
1753
+ if (this.listUrl) this.loadList();
1754
+ },
1755
+ });
1496
1756
  };
1757
+
1497
1758
  return self;
1498
1759
  }
1499
1760
 
@@ -3134,12 +3395,36 @@
3134
3395
  applyListHrefs();
3135
3396
  bindListViewSwitcher();
3136
3397
  bindListRefresh();
3398
+ bindListRowNavigation();
3137
3399
  bindMediaUploads();
3138
3400
  bindFormSaveShortcut();
3139
3401
  bindFormAutosave();
3140
3402
  bindRecordPagerNav();
3141
3403
  });
3142
3404
 
3405
+ function bindListRowNavigation() {
3406
+ document.addEventListener('click', (event) => {
3407
+ if (!(event instanceof MouseEvent) || event.button !== 0) return;
3408
+ if (event.defaultPrevented) return;
3409
+ if (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return;
3410
+ const target = event.target;
3411
+ if (!(target instanceof Element)) return;
3412
+ if (
3413
+ target.closest(
3414
+ 'a, button, input, select, textarea, label, [data-shamar-stop-row-nav]',
3415
+ )
3416
+ ) {
3417
+ return;
3418
+ }
3419
+ const row = target.closest('tr[data-shamar-row-href]');
3420
+ if (!row) return;
3421
+ const href = row.getAttribute('data-shamar-row-href');
3422
+ if (!href) return;
3423
+ event.preventDefault();
3424
+ window.location.assign(href);
3425
+ });
3426
+ }
3427
+
3143
3428
  function relationConfigsFromForm(form) {
3144
3429
  const configs = [];
3145
3430
  if (!(form instanceof HTMLFormElement)) return configs;
package/dist/config.d.ts CHANGED
@@ -71,6 +71,33 @@ export interface ShamarConfig {
71
71
  };
72
72
  /** Default branding inherited by panels without their own. */
73
73
  branding?: PanelConfig['branding'];
74
+ /**
75
+ * Optional REST / OpenAPI docs settings (consumed by `@shamar/rest`).
76
+ * Prefer nesting here or use a separate `config/shamar_rest.ts`.
77
+ */
78
+ rest?: {
79
+ enabled?: boolean;
80
+ openapiPath?: string;
81
+ docs?: {
82
+ enabled?: boolean;
83
+ path?: string;
84
+ ui?: 'scalar';
85
+ };
86
+ openapi?: {
87
+ title?: string;
88
+ version?: string;
89
+ description?: string;
90
+ servers?: Array<{
91
+ url: string;
92
+ description?: string;
93
+ }>;
94
+ };
95
+ security?: boolean;
96
+ discover?: false | {
97
+ prefixes?: string[];
98
+ exclude?: string[];
99
+ };
100
+ };
74
101
  }
75
102
  export declare function defineConfig(config: ShamarConfig): ShamarConfig & {
76
103
  panels: PanelConfig[];
@@ -1 +1 @@
1
- {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,iBAAiB,EACjB,YAAY,EACZ,YAAY,EACb,MAAM,kBAAkB,CAAC;AAC1B,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAChD,OAAO,KAAK,EACV,WAAW,EACX,WAAW,EACX,QAAQ,EAET,MAAM,cAAc,CAAC;AACtB,OAAO,EAAE,KAAK,IAAI,WAAW,EAAE,KAAK,YAAY,EAAE,MAAM,cAAc,CAAC;AACvE,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AAEtD,MAAM,MAAM,SAAS,GAAG,OAAO,GAAG,UAAU,CAAC;AAE7C,MAAM,WAAW,YAAY;IAC3B,+DAA+D;IAC/D,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,+CAA+C;IAC/C,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,gEAAgE;IAChE,GAAG,CAAC,EAAE,SAAS,CAAC;IAChB,0EAA0E;IAC1E,OAAO,CAAC,EAAE,WAAW,GAAG,CAAC,MAAM,WAAW,CAAC,CAAC;IAC5C;;;OAGG;IACH,SAAS,CAAC,EAAE,KAAK,CAAC,OAAO,QAAQ,CAAC,CAAC;IACnC,+CAA+C;IAC/C,MAAM,CAAC,EAAE,KAAK,CAAC,WAAW,GAAG,YAAY,CAAC,CAAC;IAC3C,IAAI,CAAC,EAAE;QACL,KAAK,CAAC,EAAE,MAAM,OAAO,CAAC;YAAE,OAAO,EAAE,OAAO,CAAA;SAAE,CAAC,CAAC;QAC5C,2EAA2E;QAC3E,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,oEAAoE;QACpE,UAAU,CAAC,EAAE,MAAM,CAAC;QACpB,8CAA8C;QAC9C,KAAK,CAAC,EAAE,MAAM,CAAC;QACf;;;WAGG;QACH,QAAQ,CAAC,EAAE,OAAO,CAAC;QACnB,wDAAwD;QACxD,iBAAiB,CAAC,EAAE,OAAO,CAAC;QAC5B,qEAAqE;QACrE,SAAS,CAAC,EAAE,iBAAiB,CAAC,WAAW,CAAC,CAAC;QAC3C,4CAA4C;QAC5C,YAAY,CAAC,EAAE,YAAY,CAAC;QAC5B,sEAAsE;QACtE,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC;QACvC;;;;;;;;WAQG;QACH,OAAO,CAAC,EAAE;YACR,OAAO,EAAE,CACP,SAAS,EAAE,MAAM,EACjB,GAAG,EAAE,iBAAiB,KACnB,YAAY,GAAG,IAAI,GAAG,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC,CAAC;YACxD;;;;eAIG;YACH,UAAU,CAAC,EAAE,OAAO,CAAC;YACrB;;;eAGG;YACH,yBAAyB,CAAC,EAAE,OAAO,CAAC;SACrC,CAAC;QACF,iEAAiE;QACjE,WAAW,CAAC,EAAE,CAAC,GAAG,EAAE,iBAAiB,KAAK,YAAY,GAAG,IAAI,GAAG,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC,CAAC;KAC9F,CAAC;IACF,8DAA8D;IAC9D,QAAQ,CAAC,EAAE,WAAW,CAAC,UAAU,CAAC,CAAC;CACpC;AAED,wBAAgB,YAAY,CAAC,MAAM,EAAE,YAAY,GAAG,YAAY,GAAG;IAAE,MAAM,EAAE,WAAW,EAAE,CAAA;CAAE,CAU3F;AAED,OAAO,EAAE,WAAW,IAAI,KAAK,EAAE,CAAC"}
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAYA,OAAO,EAAE,KAAK,IAAI,WAAW,EAAqB,MAAM,cAAc,CAAC;AA2EvE,MAAM,UAAU,YAAY,CAAC,MAAoB;IAC/C,MAAM,MAAM,GAAG,eAAe,CAAC,MAAM,CAAC,CAAC;IACvC,OAAO;QACL,IAAI,EAAE,MAAM,CAAC,CAAC,CAAC,EAAE,IAAI,IAAI,QAAQ;QACjC,SAAS,EAAE,aAAa;QACxB,GAAG,EAAE,OAAO;QACZ,SAAS,EAAE,MAAM,CAAC,CAAC,CAAC,EAAE,SAAS,IAAI,EAAE;QACrC,GAAG,MAAM;QACT,MAAM;KACP,CAAC;AACJ,CAAC;AAED,OAAO,EAAE,WAAW,IAAI,KAAK,EAAE,CAAC;AAEhC,SAAS,eAAe,CAAC,MAAoB;IAC3C,IAAI,MAAM,CAAC,MAAM,IAAI,MAAM,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAC9C,OAAO,MAAM,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE;YACjC,MAAM,KAAK,GAAG,OAAQ,KAAsB,CAAC,KAAK,KAAK,UAAU;gBAC/D,CAAC,CAAE,KAAsB,CAAC,KAAK,EAAE;gBACjC,CAAC,CAAE,KAAqB,CAAC;YAC3B,OAAO;gBACL,GAAG,KAAK;gBACR,GAAG,EAAE,KAAK,CAAC,GAAG,IAAI,MAAM,CAAC,GAAG;gBAC5B,QAAQ,EAAE,KAAK,CAAC,QAAQ,IAAI,MAAM,CAAC,QAAQ;gBAC3C,SAAS,EAAE,CAAC,GAAG,CAAC,KAAK,CAAC,SAAS,IAAI,EAAE,CAAC,CAAC;aACxC,CAAC;QACJ,CAAC,CAAC,CAAC;IACL,CAAC;IAED,OAAO;QACL;YACE,EAAE,EAAE,OAAO;YACX,IAAI,EAAE,MAAM,CAAC,IAAI,IAAI,QAAQ;YAC7B,GAAG,EAAE,MAAM,CAAC,GAAG;YACf,QAAQ,EAAE,MAAM,CAAC,QAAQ;YACzB,SAAS,EAAE,CAAC,GAAG,CAAC,MAAM,CAAC,SAAS,IAAI,EAAE,CAAC,CAAC;SACzC;KACF,CAAC;AACJ,CAAC"}
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAYA,OAAO,EAAE,KAAK,IAAI,WAAW,EAAqB,MAAM,cAAc,CAAC;AAqGvE,MAAM,UAAU,YAAY,CAAC,MAAoB;IAC/C,MAAM,MAAM,GAAG,eAAe,CAAC,MAAM,CAAC,CAAC;IACvC,OAAO;QACL,IAAI,EAAE,MAAM,CAAC,CAAC,CAAC,EAAE,IAAI,IAAI,QAAQ;QACjC,SAAS,EAAE,aAAa;QACxB,GAAG,EAAE,OAAO;QACZ,SAAS,EAAE,MAAM,CAAC,CAAC,CAAC,EAAE,SAAS,IAAI,EAAE;QACrC,GAAG,MAAM;QACT,MAAM;KACP,CAAC;AACJ,CAAC;AAED,OAAO,EAAE,WAAW,IAAI,KAAK,EAAE,CAAC;AAEhC,SAAS,eAAe,CAAC,MAAoB;IAC3C,IAAI,MAAM,CAAC,MAAM,IAAI,MAAM,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAC9C,OAAO,MAAM,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE;YACjC,MAAM,KAAK,GAAG,OAAQ,KAAsB,CAAC,KAAK,KAAK,UAAU;gBAC/D,CAAC,CAAE,KAAsB,CAAC,KAAK,EAAE;gBACjC,CAAC,CAAE,KAAqB,CAAC;YAC3B,OAAO;gBACL,GAAG,KAAK;gBACR,GAAG,EAAE,KAAK,CAAC,GAAG,IAAI,MAAM,CAAC,GAAG;gBAC5B,QAAQ,EAAE,KAAK,CAAC,QAAQ,IAAI,MAAM,CAAC,QAAQ;gBAC3C,SAAS,EAAE,CAAC,GAAG,CAAC,KAAK,CAAC,SAAS,IAAI,EAAE,CAAC,CAAC;aACxC,CAAC;QACJ,CAAC,CAAC,CAAC;IACL,CAAC;IAED,OAAO;QACL;YACE,EAAE,EAAE,OAAO;YACX,IAAI,EAAE,MAAM,CAAC,IAAI,IAAI,QAAQ;YAC7B,GAAG,EAAE,MAAM,CAAC,GAAG;YACf,QAAQ,EAAE,MAAM,CAAC,QAAQ;YACzB,SAAS,EAAE,CAAC,GAAG,CAAC,MAAM,CAAC,SAAS,IAAI,EAAE,CAAC,CAAC;SACzC;KACF,CAAC;AACJ,CAAC"}
@@ -36,6 +36,7 @@ export declare class AdminController {
36
36
  formState(ctx: ShamarHttpContext): Promise<unknown>;
37
37
  summary(ctx: ShamarHttpContext): Promise<unknown>;
38
38
  relationSearch(ctx: ShamarHttpContext): Promise<unknown>;
39
+ relationTable(ctx: ShamarHttpContext): Promise<unknown>;
39
40
  relationQuickCreate(ctx: ShamarHttpContext): Promise<unknown>;
40
41
  relationAttach(ctx: ShamarHttpContext): Promise<unknown>;
41
42
  relationDetach(ctx: ShamarHttpContext): Promise<unknown>;
@@ -1 +1 @@
1
- {"version":3,"file":"admin_controller.d.ts","sourceRoot":"","sources":["../../src/controllers/admin_controller.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"admin_controller.d.ts","sourceRoot":"","sources":["../../src/controllers/admin_controller.ts"],"names":[],"mappings":"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"}
@@ -1,4 +1,4 @@
1
- import { resolveGridItemStyle, isValidationException } from '@shamar/core';
1
+ import { resolveGridItemStyle, isValidationException, relationUsesListTable } from '@shamar/core';
2
2
  import { Authorizer as AuthorizerClass } from '@shamar/cherubim';
3
3
  import { ResourceController } from '../controller.js';
4
4
  import { authRequired, assertResourceAccess, buildAuthContext, ForbiddenError, resourcePolicyFlags, respondForbidden, respondUnauthorized, UnauthorizedError, } from '../shamar/auth.js';
@@ -9,6 +9,7 @@ import { formSections, formSchemaTree, detailSections, detailSchemaTree, normali
9
9
  import { evaluateFormState, formClientFields } from '../form-state.js';
10
10
  import { buildRelationUiConfig, collectRelationIds, isRelationField, recordSummaryLabel, resolveRelatedMeta, relationTitleAttribute, } from '../shamar/relation-fields.js';
11
11
  import { hydrateRecordsForDisplay } from '../shamar/display-relations.js';
12
+ import { buildRelationTableRows, relationTableListMeta, } from '../shamar/relation-table.js';
12
13
  export class AdminController {
13
14
  panel;
14
15
  config;
@@ -277,6 +278,7 @@ export class AdminController {
277
278
  if (this.wantsJson(ctx, options?.asJson)) {
278
279
  return ctx.response.json(record);
279
280
  }
281
+ const embed = this.isEmbed(ctx);
280
282
  const title = recordTitle(meta, record);
281
283
  const policy = resourcePolicyFlags(this.authorizer, authCtx, this.registry, meta, record);
282
284
  const shell = this.shellOpts(ctx, authCtx, {
@@ -298,11 +300,14 @@ export class AdminController {
298
300
  resource: meta,
299
301
  record,
300
302
  id: record.id,
303
+ embed,
301
304
  detailSchema: detailSchemaTree(meta),
302
305
  detailSections: detailSections(meta),
303
306
  relationUi,
304
307
  formErrors: {},
305
- recordPager: await this.resolveRecordPager(ctx, meta, String(record.id), 'show'),
308
+ recordPager: embed
309
+ ? null
310
+ : await this.resolveRecordPager(ctx, meta, String(record.id), 'show'),
306
311
  resolveGridItemStyle,
307
312
  recordActions: visibleRowActions(meta, policy, record),
308
313
  });
@@ -434,6 +439,66 @@ export class AdminController {
434
439
  });
435
440
  return ctx.response.json({ results });
436
441
  }
442
+ async relationTable(ctx) {
443
+ const meta = this.requireResource(ctx);
444
+ const authResult = await this.ensureResourceAction(ctx, meta, 'viewAny', undefined, true);
445
+ if (!this.isAuthContext(authResult))
446
+ return authResult;
447
+ const authCtx = authResult;
448
+ const fieldName = String(ctx.request.input('field') ?? '');
449
+ const field = meta.fields.find((entry) => entry.name === fieldName);
450
+ if (!field?.relation || field.relation.kind !== 'hasMany' || !field.relation.foreignKey) {
451
+ return ctx.response.badRequest({ message: 'Relation table requires a hasMany field' });
452
+ }
453
+ const parentId = ctx.request.input('parentId');
454
+ if (!parentId) {
455
+ return ctx.response.badRequest({ message: 'parentId is required' });
456
+ }
457
+ const related = resolveRelatedMeta(this.registry, field.relation);
458
+ const relatedAuth = await this.ensureResourceAction(ctx, related, 'viewAny', undefined, true);
459
+ if (!this.isAuthContext(relatedAuth))
460
+ return relatedAuth;
461
+ const { columnConfigs, columns, listHeaders } = relationTableListMeta(related, field.relation.foreignKey);
462
+ const rawQuery = this.listQuery(ctx);
463
+ const hasFiltersParam = ctx.request.input('filters') !== undefined && ctx.request.input('filters') !== null;
464
+ const normalized = normalizeListQuery(rawQuery, { perPage: 10 });
465
+ if (!hasFiltersParam && related.defaultFilters?.length) {
466
+ normalized.filters = resolveDefaultFilters(related, listHeaders);
467
+ }
468
+ else if (normalized.filters?.length) {
469
+ normalized.filters = labelListFilters(normalized.filters, listHeaders);
470
+ }
471
+ if (!normalized.sort && related.defaultSort) {
472
+ normalized.sort = related.defaultSort.field;
473
+ normalized.direction = related.defaultSort.direction;
474
+ }
475
+ const query = this.withPolicyScope(authCtx, related, {
476
+ ...normalized,
477
+ scope: {
478
+ ...(normalized.scope ?? {}),
479
+ [field.relation.foreignKey]: String(parentId),
480
+ },
481
+ });
482
+ const result = await this.resources.index(related, query);
483
+ await hydrateRecordsForDisplay(related, result.items, this.registry, this.panel.adapter);
484
+ decorateRevokedStatus(result.items);
485
+ const titleAttribute = relationTitleAttribute(field.relation);
486
+ const rows = buildRelationTableRows(related, columnConfigs, result.items, this.basePath, (record) => recordSummaryLabel(related, record, titleAttribute));
487
+ return ctx.response.json({
488
+ items: result.items,
489
+ rows,
490
+ total: result.total,
491
+ page: result.page,
492
+ perPage: result.perPage,
493
+ pageCount: result.pageCount,
494
+ columns,
495
+ listHeaders,
496
+ filters: normalized.filters ?? [],
497
+ sort: normalized.sort ?? null,
498
+ direction: normalized.direction ?? null,
499
+ search: normalized.search ?? '',
500
+ });
501
+ }
437
502
  async relationQuickCreate(ctx) {
438
503
  const meta = this.requireResource(ctx);
439
504
  const authResult = await this.ensureResourceAction(ctx, meta, 'create', undefined, true);
@@ -973,7 +1038,16 @@ export class AdminController {
973
1038
  const titleAttribute = relationTitleAttribute(field.relation);
974
1039
  const widget = field.relation.widget ?? 'combobox';
975
1040
  let initialItems = [];
976
- if (field.relation.kind === 'hasMany' && record?.id != null && field.relation.foreignKey) {
1041
+ const skipPreload = field.relation.kind === 'hasMany' &&
1042
+ widget === 'table' &&
1043
+ record?.id != null &&
1044
+ !!field.relation.foreignKey &&
1045
+ relationUsesListTable(field.relation);
1046
+ if (skipPreload) {
1047
+ // RelationTable loads linked rows via GET relation-table (search/page/filters).
1048
+ initialItems = [];
1049
+ }
1050
+ else if (field.relation.kind === 'hasMany' && record?.id != null && field.relation.foreignKey) {
977
1051
  initialItems = await this.panel.adapter.search(related, {
978
1052
  titleAttribute,
979
1053
  limit: field.relation.preloadLimit ?? 100,