@mulmoclaude/collection-plugin 0.7.5 → 0.8.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.
- package/dist/style.css +14 -0
- package/dist/vue/components/CollectionBacklinksView.vue.d.ts +9 -0
- package/dist/vue/components/CollectionBacklinksView.vue.d.ts.map +1 -0
- package/dist/vue/components/CollectionKanbanView.vue.d.ts.map +1 -1
- package/dist/vue/components/CollectionRecordPanel.vue.d.ts +24 -2
- package/dist/vue/components/CollectionRecordPanel.vue.d.ts.map +1 -1
- package/dist/vue/components/CollectionView.vue.d.ts.map +1 -1
- package/dist/vue/useCollectionRendering.d.ts +3 -2
- package/dist/vue/useCollectionRendering.d.ts.map +1 -1
- package/dist/vue/useCollectionRendering.helpers.d.ts +14 -1
- package/dist/vue/useCollectionRendering.helpers.d.ts.map +1 -1
- package/dist/vue/useCollectionRendering.renderers.d.ts +32 -0
- package/dist/vue/useCollectionRendering.renderers.d.ts.map +1 -0
- package/dist/vue/useLinkedCollectionCaches.d.ts +37 -0
- package/dist/vue/useLinkedCollectionCaches.d.ts.map +1 -0
- package/dist/vue.cjs +396 -215
- package/dist/vue.cjs.map +1 -1
- package/dist/vue.js +397 -216
- package/dist/vue.js.map +1 -1
- package/package.json +5 -5
package/dist/vue.cjs
CHANGED
|
@@ -1234,18 +1234,18 @@ var CollectionRecordModal_default = /* @__PURE__ */ (0, vue.defineComponent)({
|
|
|
1234
1234
|
});
|
|
1235
1235
|
//#endregion
|
|
1236
1236
|
//#region src/vue/components/CollectionCalendarView.vue?vue&type=script&setup=true&lang.ts
|
|
1237
|
-
var _hoisted_1$
|
|
1237
|
+
var _hoisted_1$15 = {
|
|
1238
1238
|
class: "flex flex-col gap-3",
|
|
1239
1239
|
"data-testid": "collection-calendar"
|
|
1240
1240
|
};
|
|
1241
|
-
var _hoisted_2$
|
|
1242
|
-
var _hoisted_3$
|
|
1243
|
-
var _hoisted_4$
|
|
1244
|
-
var _hoisted_5$
|
|
1241
|
+
var _hoisted_2$14 = { class: "flex items-center gap-2" };
|
|
1242
|
+
var _hoisted_3$14 = ["aria-label"];
|
|
1243
|
+
var _hoisted_4$14 = ["aria-label"];
|
|
1244
|
+
var _hoisted_5$13 = {
|
|
1245
1245
|
class: "text-sm font-bold text-slate-800 flex-1",
|
|
1246
1246
|
"data-testid": "collection-calendar-month"
|
|
1247
1247
|
};
|
|
1248
|
-
var _hoisted_6$
|
|
1248
|
+
var _hoisted_6$12 = { class: "grid grid-cols-7 gap-1 text-[10px] font-bold text-slate-400 uppercase tracking-wider select-none" };
|
|
1249
1249
|
var _hoisted_7$10 = { class: "grid grid-cols-7 gap-1" };
|
|
1250
1250
|
var _hoisted_8$10 = [
|
|
1251
1251
|
"aria-label",
|
|
@@ -1375,23 +1375,23 @@ var CollectionCalendarView_default = /* @__PURE__ */ (0, vue.defineComponent)({
|
|
|
1375
1375
|
viewMonth.value = now.getMonth() + 1;
|
|
1376
1376
|
}
|
|
1377
1377
|
return (_ctx, _cache) => {
|
|
1378
|
-
return (0, vue.openBlock)(), (0, vue.createElementBlock)("div", _hoisted_1$
|
|
1379
|
-
(0, vue.createElementVNode)("div", _hoisted_2$
|
|
1378
|
+
return (0, vue.openBlock)(), (0, vue.createElementBlock)("div", _hoisted_1$15, [
|
|
1379
|
+
(0, vue.createElementVNode)("div", _hoisted_2$14, [
|
|
1380
1380
|
(0, vue.createElementVNode)("button", {
|
|
1381
1381
|
type: "button",
|
|
1382
1382
|
class: "h-8 w-8 flex items-center justify-center rounded text-slate-500 hover:bg-slate-100 transition-colors",
|
|
1383
1383
|
"aria-label": (0, vue.unref)(t)("collectionsView.calendarPrevMonth"),
|
|
1384
1384
|
"data-testid": "collection-calendar-prev",
|
|
1385
1385
|
onClick: _cache[0] || (_cache[0] = ($event) => stepMonth(-1))
|
|
1386
|
-
}, [..._cache[2] || (_cache[2] = [(0, vue.createElementVNode)("span", { class: "material-icons text-lg" }, "chevron_left", -1)])], 8, _hoisted_3$
|
|
1386
|
+
}, [..._cache[2] || (_cache[2] = [(0, vue.createElementVNode)("span", { class: "material-icons text-lg" }, "chevron_left", -1)])], 8, _hoisted_3$14),
|
|
1387
1387
|
(0, vue.createElementVNode)("button", {
|
|
1388
1388
|
type: "button",
|
|
1389
1389
|
class: "h-8 w-8 flex items-center justify-center rounded text-slate-500 hover:bg-slate-100 transition-colors",
|
|
1390
1390
|
"aria-label": (0, vue.unref)(t)("collectionsView.calendarNextMonth"),
|
|
1391
1391
|
"data-testid": "collection-calendar-next",
|
|
1392
1392
|
onClick: _cache[1] || (_cache[1] = ($event) => stepMonth(1))
|
|
1393
|
-
}, [..._cache[3] || (_cache[3] = [(0, vue.createElementVNode)("span", { class: "material-icons text-lg" }, "chevron_right", -1)])], 8, _hoisted_4$
|
|
1394
|
-
(0, vue.createElementVNode)("h3", _hoisted_5$
|
|
1393
|
+
}, [..._cache[3] || (_cache[3] = [(0, vue.createElementVNode)("span", { class: "material-icons text-lg" }, "chevron_right", -1)])], 8, _hoisted_4$14),
|
|
1394
|
+
(0, vue.createElementVNode)("h3", _hoisted_5$13, (0, vue.toDisplayString)(monthLabel.value), 1),
|
|
1395
1395
|
(0, vue.createElementVNode)("button", {
|
|
1396
1396
|
type: "button",
|
|
1397
1397
|
class: "h-8 px-2.5 flex items-center gap-1 rounded border border-slate-200 bg-white text-slate-600 hover:bg-slate-50 text-xs font-bold transition-colors",
|
|
@@ -1399,7 +1399,7 @@ var CollectionCalendarView_default = /* @__PURE__ */ (0, vue.defineComponent)({
|
|
|
1399
1399
|
onClick: goToday
|
|
1400
1400
|
}, (0, vue.toDisplayString)((0, vue.unref)(t)("collectionsView.calendarToday")), 1)
|
|
1401
1401
|
]),
|
|
1402
|
-
(0, vue.createElementVNode)("div", _hoisted_6$
|
|
1402
|
+
(0, vue.createElementVNode)("div", _hoisted_6$12, [((0, vue.openBlock)(true), (0, vue.createElementBlock)(vue.Fragment, null, (0, vue.renderList)(weekdayLabels.value, (label, idx) => {
|
|
1403
1403
|
return (0, vue.openBlock)(), (0, vue.createElementBlock)("div", {
|
|
1404
1404
|
key: idx,
|
|
1405
1405
|
class: "px-1 py-1 text-center"
|
|
@@ -1440,18 +1440,18 @@ var CollectionCalendarView_default = /* @__PURE__ */ (0, vue.defineComponent)({
|
|
|
1440
1440
|
});
|
|
1441
1441
|
//#endregion
|
|
1442
1442
|
//#region src/vue/components/CollectionDayView.vue?vue&type=script&setup=true&lang.ts
|
|
1443
|
-
var _hoisted_1$
|
|
1444
|
-
var _hoisted_2$
|
|
1443
|
+
var _hoisted_1$14 = { class: "flex items-center gap-2 border-b border-slate-200 px-4 py-3" };
|
|
1444
|
+
var _hoisted_2$13 = {
|
|
1445
1445
|
class: "flex-1 text-sm font-bold text-slate-800",
|
|
1446
1446
|
"data-testid": "collection-day-view-title"
|
|
1447
1447
|
};
|
|
1448
|
-
var _hoisted_3$
|
|
1449
|
-
var _hoisted_4$
|
|
1450
|
-
var _hoisted_5$
|
|
1448
|
+
var _hoisted_3$13 = ["aria-label"];
|
|
1449
|
+
var _hoisted_4$13 = ["aria-label"];
|
|
1450
|
+
var _hoisted_5$12 = {
|
|
1451
1451
|
key: 0,
|
|
1452
1452
|
class: "px-4 py-10 text-center text-sm text-slate-400"
|
|
1453
1453
|
};
|
|
1454
|
-
var _hoisted_6$
|
|
1454
|
+
var _hoisted_6$11 = { class: "absolute -top-2 left-0 w-10 pr-1 text-right text-[10px] tabular-nums text-slate-400" };
|
|
1455
1455
|
var _hoisted_7$9 = {
|
|
1456
1456
|
class: "absolute inset-y-0 right-0",
|
|
1457
1457
|
style: { "left": "2.75rem" }
|
|
@@ -1537,6 +1537,7 @@ var CollectionDayView_default = /* @__PURE__ */ (0, vue.defineComponent)({
|
|
|
1537
1537
|
"datetime",
|
|
1538
1538
|
"table",
|
|
1539
1539
|
"embed",
|
|
1540
|
+
"backlinks",
|
|
1540
1541
|
"image",
|
|
1541
1542
|
"markdown"
|
|
1542
1543
|
]);
|
|
@@ -1632,8 +1633,8 @@ var CollectionDayView_default = /* @__PURE__ */ (0, vue.defineComponent)({
|
|
|
1632
1633
|
role: "dialog",
|
|
1633
1634
|
"aria-modal": "true"
|
|
1634
1635
|
}, [(0, vue.createElementVNode)("div", { class: (0, vue.normalizeClass)(["flex min-h-0 flex-col", __props.showDetail ? "w-80 shrink-0 border-r border-slate-200" : "w-full"]) }, [
|
|
1635
|
-
(0, vue.createElementVNode)("div", _hoisted_1$
|
|
1636
|
-
(0, vue.createElementVNode)("h3", _hoisted_2$
|
|
1636
|
+
(0, vue.createElementVNode)("div", _hoisted_1$14, [
|
|
1637
|
+
(0, vue.createElementVNode)("h3", _hoisted_2$13, (0, vue.toDisplayString)(dayLabel.value), 1),
|
|
1637
1638
|
__props.canCreate ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("button", {
|
|
1638
1639
|
key: 0,
|
|
1639
1640
|
type: "button",
|
|
@@ -1641,16 +1642,16 @@ var CollectionDayView_default = /* @__PURE__ */ (0, vue.defineComponent)({
|
|
|
1641
1642
|
"aria-label": (0, vue.unref)(t)("collectionsView.calendarCreateOn", { date: dayKey.value }),
|
|
1642
1643
|
"data-testid": "collection-day-view-create",
|
|
1643
1644
|
onClick: onCreate
|
|
1644
|
-
}, [..._cache[3] || (_cache[3] = [(0, vue.createElementVNode)("span", { class: "material-icons text-lg" }, "add", -1)])], 8, _hoisted_3$
|
|
1645
|
+
}, [..._cache[3] || (_cache[3] = [(0, vue.createElementVNode)("span", { class: "material-icons text-lg" }, "add", -1)])], 8, _hoisted_3$13)) : (0, vue.createCommentVNode)("", true),
|
|
1645
1646
|
(0, vue.createElementVNode)("button", {
|
|
1646
1647
|
type: "button",
|
|
1647
1648
|
class: "h-8 w-8 flex items-center justify-center rounded text-slate-500 hover:bg-slate-100 transition-colors",
|
|
1648
1649
|
"aria-label": (0, vue.unref)(t)("collectionsView.dayViewClose"),
|
|
1649
1650
|
"data-testid": "collection-day-view-close",
|
|
1650
1651
|
onClick: _cache[0] || (_cache[0] = ($event) => emit("close"))
|
|
1651
|
-
}, [..._cache[4] || (_cache[4] = [(0, vue.createElementVNode)("span", { class: "material-icons text-lg" }, "close", -1)])], 8, _hoisted_4$
|
|
1652
|
+
}, [..._cache[4] || (_cache[4] = [(0, vue.createElementVNode)("span", { class: "material-icons text-lg" }, "close", -1)])], 8, _hoisted_4$13)
|
|
1652
1653
|
]),
|
|
1653
|
-
timedEntries.value.length === 0 && allDayEntries.value.length === 0 ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("div", _hoisted_5$
|
|
1654
|
+
timedEntries.value.length === 0 && allDayEntries.value.length === 0 ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("div", _hoisted_5$12, (0, vue.toDisplayString)((0, vue.unref)(t)("collectionsView.dayViewEmpty")), 1)) : ((0, vue.openBlock)(), (0, vue.createElementBlock)("div", {
|
|
1654
1655
|
key: 1,
|
|
1655
1656
|
ref_key: "scrollEl",
|
|
1656
1657
|
ref: scrollEl,
|
|
@@ -1664,7 +1665,7 @@ var CollectionDayView_default = /* @__PURE__ */ (0, vue.defineComponent)({
|
|
|
1664
1665
|
key: hour,
|
|
1665
1666
|
class: "absolute left-0 right-0 border-t border-slate-100",
|
|
1666
1667
|
style: (0, vue.normalizeStyle)({ top: `${(hour - 1) * HOUR_PX}px` })
|
|
1667
|
-
}, [(0, vue.createElementVNode)("span", _hoisted_6$
|
|
1668
|
+
}, [(0, vue.createElementVNode)("span", _hoisted_6$11, (0, vue.toDisplayString)(hourLabel(hour - 1)), 1)], 4);
|
|
1668
1669
|
}), 64)), (0, vue.createElementVNode)("div", _hoisted_7$9, [((0, vue.openBlock)(true), (0, vue.createElementBlock)(vue.Fragment, null, (0, vue.renderList)(timedEntries.value, (entry) => {
|
|
1669
1670
|
return (0, vue.openBlock)(), (0, vue.createElementBlock)("button", {
|
|
1670
1671
|
key: entry.id,
|
|
@@ -1699,15 +1700,15 @@ var CollectionDayView_default = /* @__PURE__ */ (0, vue.defineComponent)({
|
|
|
1699
1700
|
});
|
|
1700
1701
|
//#endregion
|
|
1701
1702
|
//#region src/vue/components/CollectionKanbanView.vue?vue&type=script&setup=true&lang.ts
|
|
1702
|
-
var _hoisted_1$
|
|
1703
|
+
var _hoisted_1$13 = {
|
|
1703
1704
|
class: "h-full overflow-x-auto overflow-y-hidden",
|
|
1704
1705
|
"data-testid": "collection-kanban"
|
|
1705
1706
|
};
|
|
1706
|
-
var _hoisted_2$
|
|
1707
|
-
var _hoisted_3$
|
|
1708
|
-
var _hoisted_4$
|
|
1709
|
-
var _hoisted_5$
|
|
1710
|
-
var _hoisted_6$
|
|
1707
|
+
var _hoisted_2$12 = { class: "flex gap-3 h-full p-1 min-w-max" };
|
|
1708
|
+
var _hoisted_3$12 = ["data-testid"];
|
|
1709
|
+
var _hoisted_4$12 = { class: "flex items-center justify-between px-3 py-2 border-b border-slate-200" };
|
|
1710
|
+
var _hoisted_5$11 = { class: "flex items-center gap-2 min-w-0" };
|
|
1711
|
+
var _hoisted_6$10 = ["title"];
|
|
1711
1712
|
var _hoisted_7$8 = { class: "text-[11px] text-slate-400 shrink-0" };
|
|
1712
1713
|
var _hoisted_8$8 = [
|
|
1713
1714
|
"data-testid",
|
|
@@ -1741,7 +1742,10 @@ var CollectionKanbanView_default = /* @__PURE__ */ (0, vue.defineComponent)({
|
|
|
1741
1742
|
const emit = __emit;
|
|
1742
1743
|
const { t } = useCollectionI18n();
|
|
1743
1744
|
/** The Uncategorized column uses the empty string as its sentinel value. */
|
|
1744
|
-
const groupSpec = (0, vue.computed)(() =>
|
|
1745
|
+
const groupSpec = (0, vue.computed)(() => {
|
|
1746
|
+
const spec = props.schema.fields[props.groupField];
|
|
1747
|
+
return spec?.type === "enum" ? spec : void 0;
|
|
1748
|
+
});
|
|
1745
1749
|
/** Declared enum values become columns in order, with a trailing
|
|
1746
1750
|
* Uncategorized column for empty/unknown values (also a drop target that
|
|
1747
1751
|
* clears the field). The Uncategorized column is omitted when the group
|
|
@@ -1788,7 +1792,10 @@ var CollectionKanbanView_default = /* @__PURE__ */ (0, vue.defineComponent)({
|
|
|
1788
1792
|
const value = String(raw);
|
|
1789
1793
|
return (groupSpec.value?.values ?? []).includes(value) ? value : UNCATEGORIZED;
|
|
1790
1794
|
}
|
|
1791
|
-
const visibleItems = (0, vue.computed)(() =>
|
|
1795
|
+
const visibleItems = (0, vue.computed)(() => {
|
|
1796
|
+
const spec = groupSpec.value;
|
|
1797
|
+
return spec ? props.items.filter((item) => (0, _mulmoclaude_core_collection.fieldVisible)(spec, item)) : [];
|
|
1798
|
+
});
|
|
1792
1799
|
const itemsByColumnMap = (0, vue.computed)(() => {
|
|
1793
1800
|
const map = /* @__PURE__ */ new Map();
|
|
1794
1801
|
for (const column of columns.value) map.set(column.value, []);
|
|
@@ -1820,15 +1827,15 @@ var CollectionKanbanView_default = /* @__PURE__ */ (0, vue.defineComponent)({
|
|
|
1820
1827
|
emit("move", itemId(item), next);
|
|
1821
1828
|
}
|
|
1822
1829
|
return (_ctx, _cache) => {
|
|
1823
|
-
return (0, vue.openBlock)(), (0, vue.createElementBlock)("div", _hoisted_1$
|
|
1830
|
+
return (0, vue.openBlock)(), (0, vue.createElementBlock)("div", _hoisted_1$13, [(0, vue.createElementVNode)("div", _hoisted_2$12, [((0, vue.openBlock)(true), (0, vue.createElementBlock)(vue.Fragment, null, (0, vue.renderList)(columns.value, (column) => {
|
|
1824
1831
|
return (0, vue.openBlock)(), (0, vue.createElementBlock)("div", {
|
|
1825
1832
|
key: column.value,
|
|
1826
1833
|
"data-testid": `collection-kanban-column-${column.value || "uncategorized"}`,
|
|
1827
1834
|
class: "w-72 shrink-0 flex flex-col bg-slate-100 rounded-lg"
|
|
1828
|
-
}, [(0, vue.createElementVNode)("div", _hoisted_4$
|
|
1835
|
+
}, [(0, vue.createElementVNode)("div", _hoisted_4$12, [(0, vue.createElementVNode)("div", _hoisted_5$11, [(0, vue.createElementVNode)("span", { class: (0, vue.normalizeClass)(["w-2 h-2 rounded-full shrink-0", (0, vue.unref)(_mulmoclaude_core_collection.resolveEnumColor)(__props.schema, __props.groupField, column.value).dot]) }, null, 2), (0, vue.createElementVNode)("span", {
|
|
1829
1836
|
class: "font-semibold text-xs text-slate-600 truncate",
|
|
1830
1837
|
title: column.label
|
|
1831
|
-
}, (0, vue.toDisplayString)(column.label), 9, _hoisted_6$
|
|
1838
|
+
}, (0, vue.toDisplayString)(column.label), 9, _hoisted_6$10)]), (0, vue.createElementVNode)("span", _hoisted_7$8, (0, vue.toDisplayString)(itemsByColumn(column.value).length), 1)]), (0, vue.createVNode)((0, vue.unref)(vuedraggable.default), {
|
|
1832
1839
|
"model-value": itemsByColumn(column.value),
|
|
1833
1840
|
"item-key": __props.schema.primaryKey,
|
|
1834
1841
|
group: "collection-kanban-cards",
|
|
@@ -1859,7 +1866,7 @@ var CollectionKanbanView_default = /* @__PURE__ */ (0, vue.defineComponent)({
|
|
|
1859
1866
|
"model-value",
|
|
1860
1867
|
"item-key",
|
|
1861
1868
|
"onChange"
|
|
1862
|
-
])], 8, _hoisted_3$
|
|
1869
|
+
])], 8, _hoisted_3$12);
|
|
1863
1870
|
}), 128))])]);
|
|
1864
1871
|
};
|
|
1865
1872
|
}
|
|
@@ -1888,6 +1895,61 @@ function activatePathLink(event, path, stop = false) {
|
|
|
1888
1895
|
nav(path);
|
|
1889
1896
|
}
|
|
1890
1897
|
//#endregion
|
|
1898
|
+
//#region src/vue/components/CollectionBacklinksView.vue?vue&type=script&setup=true&lang.ts
|
|
1899
|
+
var _hoisted_1$12 = ["data-testid"];
|
|
1900
|
+
var _hoisted_2$11 = { class: "w-full text-[11px] text-slate-600 bg-white" };
|
|
1901
|
+
var _hoisted_3$11 = { class: "bg-slate-50 border-b border-slate-200 text-slate-500 font-bold uppercase tracking-wider" };
|
|
1902
|
+
var _hoisted_4$11 = { class: "divide-y divide-slate-100" };
|
|
1903
|
+
var _hoisted_5$10 = [
|
|
1904
|
+
"data-testid",
|
|
1905
|
+
"onClick",
|
|
1906
|
+
"onKeydown"
|
|
1907
|
+
];
|
|
1908
|
+
var _hoisted_6$9 = ["data-testid"];
|
|
1909
|
+
//#endregion
|
|
1910
|
+
//#region src/vue/components/CollectionBacklinksView.vue
|
|
1911
|
+
var CollectionBacklinksView_default = /* @__PURE__ */ (0, vue.defineComponent)({
|
|
1912
|
+
__name: "CollectionBacklinksView",
|
|
1913
|
+
props: {
|
|
1914
|
+
view: {},
|
|
1915
|
+
fieldKey: {}
|
|
1916
|
+
},
|
|
1917
|
+
setup(__props) {
|
|
1918
|
+
const { t } = useCollectionI18n();
|
|
1919
|
+
return (_ctx, _cache) => {
|
|
1920
|
+
return __props.view.rows.length > 0 ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("div", {
|
|
1921
|
+
key: 0,
|
|
1922
|
+
class: "border border-slate-200/80 rounded-xl overflow-hidden shadow-sm mt-1",
|
|
1923
|
+
"data-testid": `collections-backlinks-${__props.fieldKey}`
|
|
1924
|
+
}, [(0, vue.createElementVNode)("table", _hoisted_2$11, [(0, vue.createElementVNode)("thead", _hoisted_3$11, [(0, vue.createElementVNode)("tr", null, [((0, vue.openBlock)(true), (0, vue.createElementBlock)(vue.Fragment, null, (0, vue.renderList)(__props.view.columns, (column) => {
|
|
1925
|
+
return (0, vue.openBlock)(), (0, vue.createElementBlock)("th", {
|
|
1926
|
+
key: column.key,
|
|
1927
|
+
class: "text-left px-4 py-2 font-bold"
|
|
1928
|
+
}, (0, vue.toDisplayString)(column.label), 1);
|
|
1929
|
+
}), 128))])]), (0, vue.createElementVNode)("tbody", _hoisted_4$11, [((0, vue.openBlock)(true), (0, vue.createElementBlock)(vue.Fragment, null, (0, vue.renderList)(__props.view.rows, (row) => {
|
|
1930
|
+
return (0, vue.openBlock)(), (0, vue.createElementBlock)("tr", {
|
|
1931
|
+
key: row.id,
|
|
1932
|
+
class: "group hover:bg-indigo-50/30 cursor-pointer transition-colors",
|
|
1933
|
+
role: "link",
|
|
1934
|
+
tabindex: "0",
|
|
1935
|
+
"data-testid": `collections-backlinks-${__props.fieldKey}-${row.id}`,
|
|
1936
|
+
onClick: ($event) => (0, vue.unref)(activateRefLink)($event, __props.view.fromSlug, row.id),
|
|
1937
|
+
onKeydown: [(0, vue.withKeys)(($event) => (0, vue.unref)(activateRefLink)($event, __props.view.fromSlug, row.id), ["enter"]), (0, vue.withKeys)(($event) => (0, vue.unref)(activateRefLink)($event, __props.view.fromSlug, row.id), ["space"])]
|
|
1938
|
+
}, [((0, vue.openBlock)(true), (0, vue.createElementBlock)(vue.Fragment, null, (0, vue.renderList)(row.cells, (cell, cellIdx) => {
|
|
1939
|
+
return (0, vue.openBlock)(), (0, vue.createElementBlock)("td", {
|
|
1940
|
+
key: __props.view.columns[cellIdx]?.key ?? cellIdx,
|
|
1941
|
+
class: "px-4 py-2 align-middle font-medium"
|
|
1942
|
+
}, [(0, vue.createElementVNode)("span", { class: (0, vue.normalizeClass)(cellIdx === 0 ? "text-indigo-600 group-hover:text-indigo-800 font-bold" : "") }, (0, vue.toDisplayString)(cell), 3)]);
|
|
1943
|
+
}), 128))], 40, _hoisted_5$10);
|
|
1944
|
+
}), 128))])])], 8, _hoisted_1$12)) : ((0, vue.openBlock)(), (0, vue.createElementBlock)("span", {
|
|
1945
|
+
key: 1,
|
|
1946
|
+
class: "text-slate-400 italic",
|
|
1947
|
+
"data-testid": `collections-backlinks-${__props.fieldKey}`
|
|
1948
|
+
}, (0, vue.toDisplayString)((0, vue.unref)(t)("collectionsView.noRows")), 9, _hoisted_6$9));
|
|
1949
|
+
};
|
|
1950
|
+
}
|
|
1951
|
+
});
|
|
1952
|
+
//#endregion
|
|
1891
1953
|
//#region src/vue/components/CollectionEmbedView.vue?vue&type=script&setup=true&lang.ts
|
|
1892
1954
|
var _hoisted_1$11 = [
|
|
1893
1955
|
"href",
|
|
@@ -2188,7 +2250,7 @@ var _hoisted_71$1 = [
|
|
|
2188
2250
|
"onClick"
|
|
2189
2251
|
];
|
|
2190
2252
|
var _hoisted_72$1 = {
|
|
2191
|
-
key:
|
|
2253
|
+
key: 14,
|
|
2192
2254
|
class: "text-slate-800 font-semibold"
|
|
2193
2255
|
};
|
|
2194
2256
|
var _hoisted_73$1 = {
|
|
@@ -2261,6 +2323,7 @@ var CollectionRecordPanel_default = /* @__PURE__ */ (0, vue.defineComponent)({
|
|
|
2261
2323
|
* safe in the template. */
|
|
2262
2324
|
const detailRecord = (0, vue.computed)(() => editing.value ? props.liveDerived ?? props.liveRecord ?? {} : props.viewing ?? {});
|
|
2263
2325
|
const embedViews = (0, vue.computed)(() => props.render.embedViewsFor(detailRecord.value));
|
|
2326
|
+
const backlinksViews = (0, vue.computed)(() => props.render.backlinksViewsFor(detailRecord.value));
|
|
2264
2327
|
const embedOwnerByKey = (0, vue.computed)(() => {
|
|
2265
2328
|
const map = /* @__PURE__ */ new Map();
|
|
2266
2329
|
for (const field of Object.values(props.collection.schema.fields)) if (field.type === "embed" && field.idField) map.set(field.idField, field);
|
|
@@ -2277,11 +2340,12 @@ var CollectionRecordPanel_default = /* @__PURE__ */ (0, vue.defineComponent)({
|
|
|
2277
2340
|
return editing.value.mode === "create" ? "collections-create" : "collections-edit";
|
|
2278
2341
|
});
|
|
2279
2342
|
/** Whether a field gets an editable control in edit mode. Toggle (a
|
|
2280
|
-
* projection of an enum that has its own input), derived (computed),
|
|
2281
|
-
* embed (a foreign record)
|
|
2282
|
-
*
|
|
2343
|
+
* projection of an enum that has its own input), derived (computed),
|
|
2344
|
+
* embed (a foreign record), and backlinks (reverse refs owned by OTHER
|
|
2345
|
+
* records) stay read-only in both modes, so the cell geometry never
|
|
2346
|
+
* changes on the view↔edit toggle. */
|
|
2283
2347
|
function isEditableType(type) {
|
|
2284
|
-
return type !== "toggle" && type !== "derived" && type !== "embed";
|
|
2348
|
+
return type !== "toggle" && type !== "derived" && type !== "embed" && type !== "backlinks";
|
|
2285
2349
|
}
|
|
2286
2350
|
/** Wide field types span the full grid width in BOTH modes — keeping
|
|
2287
2351
|
* `image` full-width here (not just when viewing) is what stops a field
|
|
@@ -2291,6 +2355,7 @@ var CollectionRecordPanel_default = /* @__PURE__ */ (0, vue.defineComponent)({
|
|
|
2291
2355
|
"table",
|
|
2292
2356
|
"markdown",
|
|
2293
2357
|
"embed",
|
|
2358
|
+
"backlinks",
|
|
2294
2359
|
"image"
|
|
2295
2360
|
].includes(field.type) ? "col-span-full" : "col-span-1";
|
|
2296
2361
|
}
|
|
@@ -2315,7 +2380,8 @@ var CollectionRecordPanel_default = /* @__PURE__ */ (0, vue.defineComponent)({
|
|
|
2315
2380
|
/** Required flag for an embed's per-record picker — read off the storage
|
|
2316
2381
|
* field it writes (`idField`), since the embed itself stores nothing. */
|
|
2317
2382
|
function embedPickerRequired(field) {
|
|
2318
|
-
const
|
|
2383
|
+
const idField = field.type === "embed" ? field.idField : void 0;
|
|
2384
|
+
const target = idField ? props.collection.schema.fields[idField] : void 0;
|
|
2319
2385
|
return target ? isFieldRequiredInUi(target) : false;
|
|
2320
2386
|
}
|
|
2321
2387
|
/** Tailwind fill/text/border classes tinting an enum `<select>` by its current
|
|
@@ -2592,28 +2658,32 @@ var CollectionRecordPanel_default = /* @__PURE__ */ (0, vue.defineComponent)({
|
|
|
2592
2658
|
key: 8,
|
|
2593
2659
|
view: embedViews.value[key],
|
|
2594
2660
|
"field-key": String(key)
|
|
2595
|
-
}, null, 8, ["view", "field-key"])) : field.type === "
|
|
2661
|
+
}, null, 8, ["view", "field-key"])) : field.type === "backlinks" && backlinksViews.value[key] ? ((0, vue.openBlock)(), (0, vue.createBlock)(CollectionBacklinksView_default, {
|
|
2596
2662
|
key: 9,
|
|
2663
|
+
view: backlinksViews.value[key],
|
|
2664
|
+
"field-key": String(key)
|
|
2665
|
+
}, null, 8, ["view", "field-key"])) : field.type === "image" && typeof detailRecord.value[key] === "string" && detailRecord.value[key] ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("img", {
|
|
2666
|
+
key: 10,
|
|
2597
2667
|
src: (0, vue.unref)(resolveImageSrc)(String(detailRecord.value[key])),
|
|
2598
2668
|
alt: field.label,
|
|
2599
2669
|
class: "max-h-64 max-w-full object-contain rounded-lg border border-slate-200 bg-slate-50",
|
|
2600
2670
|
"data-testid": `collections-detail-image-${key}`
|
|
2601
2671
|
}, null, 8, _hoisted_68$1)) : field.type !== "file" && __props.render.isExternalUrl(detailRecord.value[key]) ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("a", {
|
|
2602
|
-
key:
|
|
2672
|
+
key: 11,
|
|
2603
2673
|
href: String(detailRecord.value[key]),
|
|
2604
2674
|
target: "_blank",
|
|
2605
2675
|
rel: "noopener noreferrer",
|
|
2606
2676
|
class: "text-blue-600 hover:text-blue-800 font-semibold hover:underline break-all",
|
|
2607
2677
|
"data-testid": `collections-detail-url-${key}`
|
|
2608
2678
|
}, (0, vue.toDisplayString)(String(detailRecord.value[key])), 9, _hoisted_69$1)) : field.type === "file" && __props.render.artifactUrl(detailRecord.value[key]) ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("a", {
|
|
2609
|
-
key:
|
|
2679
|
+
key: 12,
|
|
2610
2680
|
href: __props.render.artifactUrl(detailRecord.value[key]) ?? void 0,
|
|
2611
2681
|
target: "_blank",
|
|
2612
2682
|
rel: "noopener noreferrer",
|
|
2613
2683
|
class: "text-blue-600 hover:text-blue-800 font-semibold hover:underline break-all",
|
|
2614
2684
|
"data-testid": `collections-detail-file-${key}`
|
|
2615
2685
|
}, (0, vue.toDisplayString)(String(detailRecord.value[key])), 9, _hoisted_70$1)) : field.type === "file" && __props.render.fileRoutePath(detailRecord.value[key]) ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("a", {
|
|
2616
|
-
key:
|
|
2686
|
+
key: 13,
|
|
2617
2687
|
href: __props.render.fileRoutePath(detailRecord.value[key]) ?? void 0,
|
|
2618
2688
|
class: "text-blue-600 hover:text-blue-800 font-semibold hover:underline break-all",
|
|
2619
2689
|
"data-testid": `collections-detail-file-${key}`,
|
|
@@ -3067,8 +3137,11 @@ function formatCell(value, type) {
|
|
|
3067
3137
|
}
|
|
3068
3138
|
/** Resolve the ISO 4217 code for a money field: a per-record
|
|
3069
3139
|
* `currencyField` (when present and non-blank) wins over the field's
|
|
3070
|
-
* literal `currency`.
|
|
3140
|
+
* literal `currency`. Only `money` / `derived` variants carry currency
|
|
3141
|
+
* keys; any other field resolves to undefined (the formatter's USD
|
|
3142
|
+
* fallback), as before. */
|
|
3071
3143
|
function resolveCurrency(field, record) {
|
|
3144
|
+
if (field.type !== "money" && field.type !== "derived") return void 0;
|
|
3072
3145
|
if (field.currencyField && record) {
|
|
3073
3146
|
const code = record[field.currencyField];
|
|
3074
3147
|
if (typeof code === "string" && code.trim().length > 0) return code;
|
|
@@ -3130,6 +3203,14 @@ function uniqueEmbedTargets(schema) {
|
|
|
3130
3203
|
for (const field of Object.values(schema.fields)) if (field.type === "embed" && typeof field.to === "string" && field.to.length > 0) targets.add(field.to);
|
|
3131
3204
|
return [...targets];
|
|
3132
3205
|
}
|
|
3206
|
+
/** Slugs of every SOURCE collection a `backlinks` field reverses over.
|
|
3207
|
+
* Top-level only, like `embed` (the schema rejects `backlinks` inside a
|
|
3208
|
+
* table's `of`). Mirrors the server's `uniqueBacklinkSources`. */
|
|
3209
|
+
function uniqueBacklinkSources(schema) {
|
|
3210
|
+
const sources = /* @__PURE__ */ new Set();
|
|
3211
|
+
for (const field of Object.values(schema.fields)) if (field.type === "backlinks" && field.from.length > 0) sources.add(field.from);
|
|
3212
|
+
return [...sources];
|
|
3213
|
+
}
|
|
3133
3214
|
function buildRefDisplayMap(detail) {
|
|
3134
3215
|
const { fields, primaryKey } = detail.collection.schema;
|
|
3135
3216
|
const displayField = displayFieldFor(fields, primaryKey);
|
|
@@ -3142,15 +3223,23 @@ function buildRefDisplayMap(detail) {
|
|
|
3142
3223
|
}
|
|
3143
3224
|
return map;
|
|
3144
3225
|
}
|
|
3145
|
-
|
|
3146
|
-
|
|
3226
|
+
/** Index DERIVED records by primary key — the client mirror of the
|
|
3227
|
+
* server's `loadTarget` indexing: string non-empty ids only, one record
|
|
3228
|
+
* per id (a duplicate keeps the LAST record in its FIRST position —
|
|
3229
|
+
* plain-object key semantics). Shared by the ref-record cache and the
|
|
3230
|
+
* backlinks view so every client consumer agrees with server enrichment
|
|
3231
|
+
* on which source records exist. */
|
|
3232
|
+
function derivedRecordsById(schema, items) {
|
|
3147
3233
|
const map = {};
|
|
3148
|
-
for (const item of
|
|
3234
|
+
for (const item of items) {
|
|
3149
3235
|
const slugRaw = item[schema.primaryKey];
|
|
3150
3236
|
if (typeof slugRaw === "string" && slugRaw.length > 0) map[slugRaw] = (0, _mulmoclaude_core_collection.deriveAll)(schema, item, {});
|
|
3151
3237
|
}
|
|
3152
3238
|
return map;
|
|
3153
3239
|
}
|
|
3240
|
+
function buildRefRecordMap(detail) {
|
|
3241
|
+
return derivedRecordsById(detail.collection.schema, detail.items);
|
|
3242
|
+
}
|
|
3154
3243
|
function sortedRefOptions(map) {
|
|
3155
3244
|
return Object.entries(map).map(([slug, display]) => ({
|
|
3156
3245
|
slug,
|
|
@@ -3173,8 +3262,64 @@ function buildEmbedOptions(schema, items) {
|
|
|
3173
3262
|
}).filter((opt) => opt.slug.length > 0).sort((left, right) => left.display.localeCompare(right.display));
|
|
3174
3263
|
}
|
|
3175
3264
|
//#endregion
|
|
3176
|
-
//#region src/vue/
|
|
3177
|
-
|
|
3265
|
+
//#region src/vue/useLinkedCollectionCaches.ts
|
|
3266
|
+
/** The de-duplicated ref + embed target slugs a schema links to. `allTargets`
|
|
3267
|
+
* is the union (each target fetched once even when both ref'd and embedded).
|
|
3268
|
+
* `backlinks` SOURCE collections ride in `embedTargets`: a backlink needs
|
|
3269
|
+
* exactly what an embed target caches (the source's schema + items in
|
|
3270
|
+
* `embedCache`), so reverse sources reuse that cache rather than adding a
|
|
3271
|
+
* parallel one. */
|
|
3272
|
+
function linkedTargets(schema) {
|
|
3273
|
+
const refTargets = new Set(uniqueRefTargets(schema));
|
|
3274
|
+
const embedTargets = /* @__PURE__ */ new Set([...uniqueEmbedTargets(schema), ...uniqueBacklinkSources(schema)]);
|
|
3275
|
+
return {
|
|
3276
|
+
refTargets,
|
|
3277
|
+
embedTargets,
|
|
3278
|
+
allTargets: [.../* @__PURE__ */ new Set([...refTargets, ...embedTargets])]
|
|
3279
|
+
};
|
|
3280
|
+
}
|
|
3281
|
+
/** Fan-out fetch that hydrates the linked-collection caches. Best-effort: a
|
|
3282
|
+
* target whose fetch *rejects* (vs. resolving `{ ok: false }`) is coerced to a
|
|
3283
|
+
* skip and must not abort the others. Returns null when a quicker subsequent
|
|
3284
|
+
* load has already moved on (stale-write guard) so the caller drops the write.
|
|
3285
|
+
* Pure + injectable (`fetchDetail`, `currentSlug`) so both paths are testable. */
|
|
3286
|
+
async function fetchLinkedCaches(targets, fetchDetail, currentSlug, expectedSlug) {
|
|
3287
|
+
const { refTargets, embedTargets, allTargets } = targets;
|
|
3288
|
+
const results = await Promise.all(allTargets.map(async (target) => {
|
|
3289
|
+
try {
|
|
3290
|
+
return {
|
|
3291
|
+
target,
|
|
3292
|
+
result: await fetchDetail(target)
|
|
3293
|
+
};
|
|
3294
|
+
} catch {
|
|
3295
|
+
return {
|
|
3296
|
+
target,
|
|
3297
|
+
result: { ok: false }
|
|
3298
|
+
};
|
|
3299
|
+
}
|
|
3300
|
+
}));
|
|
3301
|
+
if (currentSlug() !== expectedSlug) return null;
|
|
3302
|
+
const refCache = {};
|
|
3303
|
+
const refRecordCache = {};
|
|
3304
|
+
const embedCache = {};
|
|
3305
|
+
for (const { target, result } of results) {
|
|
3306
|
+
if (!result.ok) continue;
|
|
3307
|
+
if (refTargets.has(target)) {
|
|
3308
|
+
refCache[target] = buildRefDisplayMap(result.data);
|
|
3309
|
+
refRecordCache[target] = buildRefRecordMap(result.data);
|
|
3310
|
+
}
|
|
3311
|
+
if (embedTargets.has(target)) embedCache[target] = {
|
|
3312
|
+
schema: result.data.collection.schema,
|
|
3313
|
+
items: result.data.items
|
|
3314
|
+
};
|
|
3315
|
+
}
|
|
3316
|
+
return {
|
|
3317
|
+
refCache,
|
|
3318
|
+
refRecordCache,
|
|
3319
|
+
embedCache
|
|
3320
|
+
};
|
|
3321
|
+
}
|
|
3322
|
+
function useLinkedCollectionCaches(collection) {
|
|
3178
3323
|
const refCache = (0, vue.ref)({});
|
|
3179
3324
|
const refRecordCache = (0, vue.ref)({});
|
|
3180
3325
|
const embedCache = (0, vue.ref)({});
|
|
@@ -3184,161 +3329,192 @@ function useCollectionRendering(collection, locale) {
|
|
|
3184
3329
|
embedCache.value = {};
|
|
3185
3330
|
}
|
|
3186
3331
|
async function loadLinkedCollections(schema, expectedSlug) {
|
|
3187
|
-
const
|
|
3188
|
-
|
|
3189
|
-
const allTargets = [.../* @__PURE__ */ new Set([...refTargets, ...embedTargets])];
|
|
3190
|
-
if (allTargets.length === 0) return;
|
|
3332
|
+
const targets = linkedTargets(schema);
|
|
3333
|
+
if (targets.allTargets.length === 0) return;
|
|
3191
3334
|
const binding = collectionUi();
|
|
3192
|
-
const
|
|
3193
|
-
|
|
3194
|
-
|
|
3195
|
-
|
|
3196
|
-
|
|
3197
|
-
};
|
|
3198
|
-
} catch {
|
|
3199
|
-
return {
|
|
3200
|
-
target,
|
|
3201
|
-
result: { ok: false }
|
|
3202
|
-
};
|
|
3203
|
-
}
|
|
3204
|
-
}));
|
|
3205
|
-
if (collection.value?.slug !== expectedSlug) return;
|
|
3206
|
-
const nextRef = {};
|
|
3207
|
-
const nextRefRecords = {};
|
|
3208
|
-
const nextEmbed = {};
|
|
3209
|
-
for (const { target, result } of results) {
|
|
3210
|
-
if (!result.ok) continue;
|
|
3211
|
-
if (refTargets.has(target)) {
|
|
3212
|
-
nextRef[target] = buildRefDisplayMap(result.data);
|
|
3213
|
-
nextRefRecords[target] = buildRefRecordMap(result.data);
|
|
3214
|
-
}
|
|
3215
|
-
if (embedTargets.has(target)) nextEmbed[target] = {
|
|
3216
|
-
schema: result.data.collection.schema,
|
|
3217
|
-
items: result.data.items
|
|
3218
|
-
};
|
|
3219
|
-
}
|
|
3220
|
-
refCache.value = nextRef;
|
|
3221
|
-
refRecordCache.value = nextRefRecords;
|
|
3222
|
-
embedCache.value = nextEmbed;
|
|
3223
|
-
}
|
|
3224
|
-
function refDisplay(targetSlug, itemSlug) {
|
|
3225
|
-
const map = refCache.value[targetSlug];
|
|
3226
|
-
return map && map[itemSlug] || itemSlug;
|
|
3227
|
-
}
|
|
3228
|
-
function refOptions(targetSlug) {
|
|
3229
|
-
const map = refCache.value[targetSlug];
|
|
3230
|
-
return map ? sortedRefOptions(map) : [];
|
|
3231
|
-
}
|
|
3232
|
-
/** Dropdown options for an `embed` field's per-record picker (`idField`):
|
|
3233
|
-
* every record in the target collection, labelled by its name/title (or
|
|
3234
|
-
* primary key). Built from `embedCache` so it works for embed targets
|
|
3235
|
-
* that aren't also `ref` targets (the profile collection, say). */
|
|
3236
|
-
function embedOptions(targetSlug) {
|
|
3237
|
-
const data = embedCache.value[targetSlug];
|
|
3238
|
-
return data ? buildEmbedOptions(data.schema, data.items) : [];
|
|
3335
|
+
const snapshot = await fetchLinkedCaches(targets, (slug) => binding.fetchCollectionDetail(slug), () => collection.value?.slug, expectedSlug);
|
|
3336
|
+
if (!snapshot) return;
|
|
3337
|
+
refCache.value = snapshot.refCache;
|
|
3338
|
+
refRecordCache.value = snapshot.refRecordCache;
|
|
3339
|
+
embedCache.value = snapshot.embedCache;
|
|
3239
3340
|
}
|
|
3240
|
-
|
|
3241
|
-
|
|
3242
|
-
|
|
3243
|
-
|
|
3244
|
-
|
|
3245
|
-
|
|
3246
|
-
|
|
3247
|
-
|
|
3248
|
-
|
|
3249
|
-
|
|
3250
|
-
|
|
3251
|
-
|
|
3252
|
-
|
|
3253
|
-
|
|
3254
|
-
|
|
3341
|
+
return {
|
|
3342
|
+
refCache,
|
|
3343
|
+
refRecordCache,
|
|
3344
|
+
embedCache,
|
|
3345
|
+
resetLinkedCaches,
|
|
3346
|
+
loadLinkedCollections
|
|
3347
|
+
};
|
|
3348
|
+
}
|
|
3349
|
+
//#endregion
|
|
3350
|
+
//#region src/vue/useCollectionRendering.renderers.ts
|
|
3351
|
+
function lookupRefDisplay(refCache, targetSlug, itemSlug) {
|
|
3352
|
+
const map = refCache[targetSlug];
|
|
3353
|
+
return map && map[itemSlug] || itemSlug;
|
|
3354
|
+
}
|
|
3355
|
+
function refOptionsFor(refCache, targetSlug) {
|
|
3356
|
+
const map = refCache[targetSlug];
|
|
3357
|
+
return map ? sortedRefOptions(map) : [];
|
|
3358
|
+
}
|
|
3359
|
+
/** Dropdown options for an `embed` field's per-record picker (`idField`):
|
|
3360
|
+
* every record in the target collection, labelled by its name/title (or
|
|
3361
|
+
* primary key). Built from `embedCache` so it works for embed targets
|
|
3362
|
+
* that aren't also `ref` targets (the profile collection, say). */
|
|
3363
|
+
function embedOptionsFor(embedCache, targetSlug) {
|
|
3364
|
+
const data = embedCache[targetSlug];
|
|
3365
|
+
return data ? buildEmbedOptions(data.schema, data.items) : [];
|
|
3366
|
+
}
|
|
3367
|
+
function resolveEmbed(field, record, embedCache) {
|
|
3368
|
+
if (field.type !== "embed" || !field.to) return {
|
|
3369
|
+
schema: null,
|
|
3370
|
+
item: null
|
|
3371
|
+
};
|
|
3372
|
+
const targetId = (0, _mulmoclaude_core_collection.embedTargetId)(field, record);
|
|
3373
|
+
const data = targetId ? embedCache[field.to] : void 0;
|
|
3374
|
+
if (!data) return {
|
|
3375
|
+
schema: null,
|
|
3376
|
+
item: null
|
|
3377
|
+
};
|
|
3378
|
+
const item = data.items.find((entry) => String(entry[data.schema.primaryKey] ?? "") === targetId) ?? null;
|
|
3379
|
+
return {
|
|
3380
|
+
schema: data.schema,
|
|
3381
|
+
item
|
|
3382
|
+
};
|
|
3383
|
+
}
|
|
3384
|
+
function formatEmbedValue(field, value, record, locale) {
|
|
3385
|
+
if (field.type === "money") return formatMoney(value, resolveCurrency(field, record), locale);
|
|
3386
|
+
return detailText(value);
|
|
3387
|
+
}
|
|
3388
|
+
/** Build the read-only embed view-models for one record. A function of the
|
|
3389
|
+
* open record (not a bare computed) because a per-record `idField` embed
|
|
3390
|
+
* resolves a different target per row. `schema` is the OPEN collection's
|
|
3391
|
+
* schema (null when no collection is loaded → no views). */
|
|
3392
|
+
function buildEmbedViews(schema, embedCache, record, locale) {
|
|
3393
|
+
const out = {};
|
|
3394
|
+
if (!schema) return out;
|
|
3395
|
+
for (const [key, field] of Object.entries(schema.fields)) {
|
|
3396
|
+
if (field.type !== "embed") continue;
|
|
3397
|
+
const { schema: targetSchema, item } = resolveEmbed(field, record, embedCache);
|
|
3398
|
+
const rows = [];
|
|
3399
|
+
if (targetSchema && item) for (const [subKey, subField] of Object.entries(targetSchema.fields)) {
|
|
3400
|
+
const value = item[subKey];
|
|
3401
|
+
if (value === void 0 || value === null || value === "") continue;
|
|
3402
|
+
rows.push({
|
|
3403
|
+
key: subKey,
|
|
3404
|
+
label: subField.label,
|
|
3405
|
+
type: subField.type,
|
|
3406
|
+
value,
|
|
3407
|
+
display: formatEmbedValue(subField, value, item, locale)
|
|
3408
|
+
});
|
|
3409
|
+
}
|
|
3410
|
+
out[key] = {
|
|
3411
|
+
found: Boolean(item),
|
|
3412
|
+
rows,
|
|
3413
|
+
targetSlug: field.to ?? "",
|
|
3414
|
+
recordId: (0, _mulmoclaude_core_collection.embedTargetId)(field, record)
|
|
3255
3415
|
};
|
|
3256
3416
|
}
|
|
3257
|
-
|
|
3258
|
-
|
|
3259
|
-
|
|
3260
|
-
|
|
3261
|
-
|
|
3262
|
-
|
|
3263
|
-
|
|
3264
|
-
|
|
3265
|
-
|
|
3266
|
-
|
|
3267
|
-
|
|
3268
|
-
|
|
3269
|
-
|
|
3270
|
-
|
|
3271
|
-
|
|
3272
|
-
|
|
3273
|
-
|
|
3274
|
-
|
|
3275
|
-
|
|
3276
|
-
|
|
3277
|
-
|
|
3278
|
-
|
|
3279
|
-
|
|
3280
|
-
|
|
3281
|
-
|
|
3417
|
+
return out;
|
|
3418
|
+
}
|
|
3419
|
+
/** One backlinks table CELL: money formatted with its currency, anything
|
|
3420
|
+
* else through the same `formatCell` list tables use — so a markdown
|
|
3421
|
+
* source column (a worklog `notes`) shows the one-line 80-char preview,
|
|
3422
|
+
* not the whole text. Unknown `display` key ⇒ plain text (fail-soft). */
|
|
3423
|
+
function formatBacklinkCell(sourceField, value, row, locale) {
|
|
3424
|
+
if (!sourceField) return detailText(value);
|
|
3425
|
+
if (sourceField.type === "money") return formatMoney(value, resolveCurrency(sourceField, row), locale);
|
|
3426
|
+
return formatCell(value, sourceField.type);
|
|
3427
|
+
}
|
|
3428
|
+
/** Build the read-only backlinks view-models for one record: for each
|
|
3429
|
+
* `backlinks` field, the rows of `from` whose `via` points at the open
|
|
3430
|
+
* record (matched via the SHARED `backlinkRows`, on source records
|
|
3431
|
+
* derived exactly like the server's enrichment — so a `display`/`filter`
|
|
3432
|
+
* on a derived column such as an invoice `total` agrees on both sides).
|
|
3433
|
+
* Source data comes out of `embedCache` (reverse sources ride the embed
|
|
3434
|
+
* fan-out — see `linkedTargets`). Fail-soft: an unloadable source or an
|
|
3435
|
+
* unknown `display` key degrades to `found: false` / a raw-key column,
|
|
3436
|
+
* never a throw. */
|
|
3437
|
+
function buildBacklinksViews(schema, embedCache, record, locale) {
|
|
3438
|
+
const out = {};
|
|
3439
|
+
if (!schema) return out;
|
|
3440
|
+
for (const [key, field] of Object.entries(schema.fields)) {
|
|
3441
|
+
if (field.type !== "backlinks") continue;
|
|
3442
|
+
const columns = field.display.map((col) => ({
|
|
3443
|
+
key: col,
|
|
3444
|
+
label: col
|
|
3445
|
+
}));
|
|
3446
|
+
const data = embedCache[field.from];
|
|
3447
|
+
if (!data) {
|
|
3282
3448
|
out[key] = {
|
|
3283
|
-
found:
|
|
3284
|
-
|
|
3285
|
-
|
|
3286
|
-
|
|
3449
|
+
found: false,
|
|
3450
|
+
columns,
|
|
3451
|
+
rows: [],
|
|
3452
|
+
fromSlug: field.from
|
|
3287
3453
|
};
|
|
3288
|
-
|
|
3289
|
-
|
|
3290
|
-
|
|
3291
|
-
|
|
3292
|
-
|
|
3293
|
-
|
|
3294
|
-
|
|
3295
|
-
|
|
3296
|
-
|
|
3297
|
-
|
|
3298
|
-
|
|
3299
|
-
|
|
3300
|
-
|
|
3301
|
-
|
|
3302
|
-
if (subField.type === "ref" && subField.to && typeof value === "string" && value.length > 0) return refDisplay(subField.to, value);
|
|
3303
|
-
return formatCell(value, subField.type);
|
|
3304
|
-
}
|
|
3305
|
-
const stepFor = stepForFieldType;
|
|
3306
|
-
function evaluateDerivedAgainstItem(field, fieldKey, item) {
|
|
3307
|
-
if (!field.formula || !collection.value) return null;
|
|
3308
|
-
const result = (0, _mulmoclaude_core_collection.deriveAll)(collection.value.schema, item, refRecordCache.value)[fieldKey];
|
|
3309
|
-
return typeof result === "number" && Number.isFinite(result) ? result : null;
|
|
3310
|
-
}
|
|
3311
|
-
function derivedDisplay(field, computedValue, record) {
|
|
3312
|
-
if (computedValue === null || computedValue === void 0) return "—";
|
|
3313
|
-
if (field.display === "money") return formatMoney(computedValue, resolveCurrency(field, record), locale.value);
|
|
3314
|
-
return formatCell(computedValue, field.display ?? "number");
|
|
3454
|
+
continue;
|
|
3455
|
+
}
|
|
3456
|
+
for (const column of columns) column.label = data.schema.fields[column.key]?.label ?? column.key;
|
|
3457
|
+
const selfId = String(record?.[schema.primaryKey] ?? "");
|
|
3458
|
+
const sourceById = derivedRecordsById(data.schema, data.items);
|
|
3459
|
+
out[key] = {
|
|
3460
|
+
found: true,
|
|
3461
|
+
columns,
|
|
3462
|
+
rows: (0, _mulmoclaude_core_collection.backlinkRows)(field, selfId, Object.values(sourceById)).map((row) => ({
|
|
3463
|
+
id: String(row[data.schema.primaryKey] ?? ""),
|
|
3464
|
+
cells: columns.map((column) => formatBacklinkCell(data.schema.fields[column.key], row[column.key], row, locale))
|
|
3465
|
+
})),
|
|
3466
|
+
fromSlug: field.from
|
|
3467
|
+
};
|
|
3315
3468
|
}
|
|
3469
|
+
return out;
|
|
3470
|
+
}
|
|
3471
|
+
function renderSubCell(subField, value, record, refCache, locale) {
|
|
3472
|
+
if (subField.type === "money") return formatMoney(value, resolveCurrency(subField, record), locale);
|
|
3473
|
+
if (subField.type === "ref" && subField.to && typeof value === "string" && value.length > 0) return lookupRefDisplay(refCache, subField.to, value);
|
|
3474
|
+
return formatCell(value, subField.type);
|
|
3475
|
+
}
|
|
3476
|
+
function evaluateDerived(field, fieldKey, item, schema, refRecords) {
|
|
3477
|
+
if (field.type !== "derived" || !schema) return null;
|
|
3478
|
+
const result = (0, _mulmoclaude_core_collection.deriveAll)(schema, item, refRecords)[fieldKey];
|
|
3479
|
+
return typeof result === "number" && Number.isFinite(result) ? result : null;
|
|
3480
|
+
}
|
|
3481
|
+
function renderDerived(field, computedValue, record, locale) {
|
|
3482
|
+
if (computedValue === null || computedValue === void 0) return "—";
|
|
3483
|
+
const display = field.type === "derived" ? field.display : void 0;
|
|
3484
|
+
if (display === "money") return formatMoney(computedValue, resolveCurrency(field, record), locale);
|
|
3485
|
+
return formatCell(computedValue, display ?? "number");
|
|
3486
|
+
}
|
|
3487
|
+
//#endregion
|
|
3488
|
+
//#region src/vue/useCollectionRendering.ts
|
|
3489
|
+
var STATELESS_RENDERERS = {
|
|
3490
|
+
resolveCurrency,
|
|
3491
|
+
formatMoney,
|
|
3492
|
+
formatCell,
|
|
3493
|
+
detailText,
|
|
3494
|
+
isExternalUrl,
|
|
3495
|
+
tableRows,
|
|
3496
|
+
hasTableRows,
|
|
3497
|
+
inputTypeFor,
|
|
3498
|
+
stepFor: stepForFieldType,
|
|
3499
|
+
deriveAll: _mulmoclaude_core_collection.deriveAll
|
|
3500
|
+
};
|
|
3501
|
+
function useCollectionRendering(collection, locale) {
|
|
3502
|
+
const caches = useLinkedCollectionCaches(collection);
|
|
3503
|
+
const { refCache, refRecordCache, embedCache } = caches;
|
|
3316
3504
|
return {
|
|
3317
|
-
|
|
3318
|
-
|
|
3319
|
-
|
|
3320
|
-
|
|
3321
|
-
|
|
3322
|
-
|
|
3323
|
-
|
|
3324
|
-
|
|
3325
|
-
|
|
3326
|
-
|
|
3327
|
-
|
|
3328
|
-
|
|
3329
|
-
|
|
3330
|
-
detailText,
|
|
3331
|
-
isExternalUrl,
|
|
3332
|
-
artifactUrl,
|
|
3333
|
-
fileRoutePath,
|
|
3334
|
-
tableRows,
|
|
3335
|
-
hasTableRows,
|
|
3336
|
-
formatSubCell,
|
|
3337
|
-
inputTypeFor,
|
|
3338
|
-
stepFor,
|
|
3339
|
-
deriveAll: _mulmoclaude_core_collection.deriveAll,
|
|
3340
|
-
evaluateDerivedAgainstItem,
|
|
3341
|
-
derivedDisplay
|
|
3505
|
+
...caches,
|
|
3506
|
+
...STATELESS_RENDERERS,
|
|
3507
|
+
refDisplay: (targetSlug, itemSlug) => lookupRefDisplay(refCache.value, targetSlug, itemSlug),
|
|
3508
|
+
refOptions: (targetSlug) => refOptionsFor(refCache.value, targetSlug),
|
|
3509
|
+
embedOptions: (targetSlug) => embedOptionsFor(embedCache.value, targetSlug),
|
|
3510
|
+
embedViewsFor: (record) => buildEmbedViews(collection.value?.schema ?? null, embedCache.value, record, locale.value),
|
|
3511
|
+
backlinksViewsFor: (record) => buildBacklinksViews(collection.value?.schema ?? null, embedCache.value, record, locale.value),
|
|
3512
|
+
currencySymbol: (currency) => currencySymbolForLocale(currency, locale.value),
|
|
3513
|
+
artifactUrl: (value) => collectionUi().fileAssetUrl(value),
|
|
3514
|
+
fileRoutePath: (value) => collectionUi().fileRoutePath(value),
|
|
3515
|
+
formatSubCell: (subField, value, record) => renderSubCell(subField, value, record, refCache.value, locale.value),
|
|
3516
|
+
evaluateDerivedAgainstItem: (field, fieldKey, item) => evaluateDerived(field, fieldKey, item, collection.value?.schema ?? null, refRecordCache.value),
|
|
3517
|
+
derivedDisplay: (field, computedValue, record) => renderDerived(field, computedValue, record, locale.value)
|
|
3342
3518
|
};
|
|
3343
3519
|
}
|
|
3344
3520
|
//#endregion
|
|
@@ -3829,12 +4005,12 @@ var CollectionView_default = /* @__PURE__ */ (0, vue.defineComponent)({
|
|
|
3829
4005
|
return scalarSortValue(field, item[key]);
|
|
3830
4006
|
}
|
|
3831
4007
|
/** Derived rows sort by their display type: money/number → numeric,
|
|
3832
|
-
* date
|
|
4008
|
+
* date → epoch, anything else → the enriched value as a string. */
|
|
3833
4009
|
function derivedSortValue(field, key, item) {
|
|
3834
|
-
const
|
|
4010
|
+
const display = field.type === "derived" ? field.display : void 0;
|
|
3835
4011
|
if (display === void 0 || display === "number" || display === "money") return (0, _mulmoclaude_core_collection.numericSortValue)(evaluateDerivedAgainstItem(field, key, item));
|
|
3836
4012
|
const enriched = collection.value ? render.deriveAll(collection.value.schema, item, render.refRecordCache.value) : item;
|
|
3837
|
-
if (display === "date"
|
|
4013
|
+
if (display === "date") return (0, _mulmoclaude_core_collection.dateSortValue)(enriched[key]);
|
|
3838
4014
|
return (0, _mulmoclaude_core_collection.stringSortValue)(enriched[key]);
|
|
3839
4015
|
}
|
|
3840
4016
|
const sortedItems = (0, vue.computed)(() => {
|
|
@@ -4012,10 +4188,12 @@ var CollectionView_default = /* @__PURE__ */ (0, vue.defineComponent)({
|
|
|
4012
4188
|
function buildChatSeed(slug, message, itemId) {
|
|
4013
4189
|
const current = collection.value;
|
|
4014
4190
|
if (current?.source !== "feed") return itemId ? `/${slug} id=${itemId} ${message}` : `/${slug} ${message}`;
|
|
4191
|
+
const dataPath = current.schema.dataPath ?? `data/feeds/${slug}`;
|
|
4192
|
+
const scoped = itemId ? `(for record \`${itemId}\`) ${message}` : message;
|
|
4015
4193
|
return t("collectionsView.feedChatSeed", {
|
|
4016
4194
|
slug,
|
|
4017
|
-
dataPath
|
|
4018
|
-
message:
|
|
4195
|
+
dataPath,
|
|
4196
|
+
message: scoped
|
|
4019
4197
|
});
|
|
4020
4198
|
}
|
|
4021
4199
|
/** Start a new general-role chat seeded from the current view. */
|
|
@@ -4090,7 +4268,10 @@ var CollectionView_default = /* @__PURE__ */ (0, vue.defineComponent)({
|
|
|
4090
4268
|
enumOriginallyEmpty.value = snapshotEmptyEnums(result.data.collection.schema, result.data.items);
|
|
4091
4269
|
await render.loadLinkedCollections(result.data.collection.schema, slug);
|
|
4092
4270
|
if (activeSlug.value !== slug) return;
|
|
4093
|
-
if (viewing.value)
|
|
4271
|
+
if (viewing.value) {
|
|
4272
|
+
const openId = String(viewing.value[result.data.collection.schema.primaryKey] ?? "");
|
|
4273
|
+
viewing.value = findItemById(openId) ?? null;
|
|
4274
|
+
}
|
|
4094
4275
|
}
|
|
4095
4276
|
function maybeAutoRefreshFeed(slug) {
|
|
4096
4277
|
if (embedded.value) return;
|
|
@@ -4104,7 +4285,7 @@ var CollectionView_default = /* @__PURE__ */ (0, vue.defineComponent)({
|
|
|
4104
4285
|
* list table only (a whole embedded record doesn't fit a table cell,
|
|
4105
4286
|
* and it'd be identical in every row). The detail modal and the edit
|
|
4106
4287
|
* form iterate the full `schema.fields` so embeds render there too. */
|
|
4107
|
-
const listColumnFields = (0, vue.computed)(() => collection.value ? Object.entries(collection.value.schema.fields).filter(([key, field]) => field.type !== "embed" && field.type !== "image" && key !== collection.value?.schema.primaryKey) : []);
|
|
4288
|
+
const listColumnFields = (0, vue.computed)(() => collection.value ? Object.entries(collection.value.schema.fields).filter(([key, field]) => field.type !== "embed" && field.type !== "backlinks" && field.type !== "image" && key !== collection.value?.schema.primaryKey) : []);
|
|
4108
4289
|
/** True when the current collection declares `schema.singleton` —
|
|
4109
4290
|
* exactly one record, its primary key fixed to the declared value. */
|
|
4110
4291
|
const isSingleton = (0, vue.computed)(() => Boolean(collection.value?.schema.singleton));
|
|
@@ -4268,7 +4449,8 @@ var CollectionView_default = /* @__PURE__ */ (0, vue.defineComponent)({
|
|
|
4268
4449
|
}
|
|
4269
4450
|
/** Select a custom view by id (builds the `custom:<id>` mode key). */
|
|
4270
4451
|
function setCustomView(viewId) {
|
|
4271
|
-
|
|
4452
|
+
const mode = `custom:${viewId}`;
|
|
4453
|
+
view.value = mode;
|
|
4272
4454
|
}
|
|
4273
4455
|
/** A short, slug-safe id not already used by a loaded record. Collisions
|
|
4274
4456
|
* are astronomically unlikely (32 bits), but we still re-roll a few
|
|
@@ -4292,7 +4474,7 @@ var CollectionView_default = /* @__PURE__ */ (0, vue.defineComponent)({
|
|
|
4292
4474
|
boolOriginallyPresent[key] = false;
|
|
4293
4475
|
boolTouched[key] = false;
|
|
4294
4476
|
} else if (field.type === "table") table[key] = [];
|
|
4295
|
-
else if (field.type !== "derived" && field.type !== "embed" && field.type !== "toggle") text[key] = "";
|
|
4477
|
+
else if (field.type !== "derived" && field.type !== "embed" && field.type !== "backlinks" && field.type !== "toggle") text[key] = "";
|
|
4296
4478
|
const { singleton, primaryKey } = collection.value.schema;
|
|
4297
4479
|
if (singleton) text[primaryKey] = singleton;
|
|
4298
4480
|
else if (primaryKey in text) text[primaryKey] = generateUniqueItemId(primaryKey);
|
|
@@ -4324,7 +4506,7 @@ var CollectionView_default = /* @__PURE__ */ (0, vue.defineComponent)({
|
|
|
4324
4506
|
} else if (field.type === "table" && field.of) {
|
|
4325
4507
|
const sub = field.of;
|
|
4326
4508
|
table[key] = (Array.isArray(raw) ? raw : []).filter((row) => Boolean(row) && typeof row === "object" && !Array.isArray(row)).map((row) => (0, _mulmoclaude_core_collection.rowFromItem)(row, sub));
|
|
4327
|
-
} else if (field.type !== "derived" && field.type !== "embed" && field.type !== "toggle") text[key] = raw === void 0 || raw === null ? "" : String(raw);
|
|
4509
|
+
} else if (field.type !== "derived" && field.type !== "embed" && field.type !== "backlinks" && field.type !== "toggle") text[key] = raw === void 0 || raw === null ? "" : String(raw);
|
|
4328
4510
|
}
|
|
4329
4511
|
const primaryRaw = item[collection.value.schema.primaryKey];
|
|
4330
4512
|
const originalId = typeof primaryRaw === "string" ? primaryRaw : String(primaryRaw ?? "");
|
|
@@ -4426,7 +4608,8 @@ var CollectionView_default = /* @__PURE__ */ (0, vue.defineComponent)({
|
|
|
4426
4608
|
viewing.value = null;
|
|
4427
4609
|
return;
|
|
4428
4610
|
}
|
|
4429
|
-
|
|
4611
|
+
const match = findItemById(selected) ?? null;
|
|
4612
|
+
viewing.value = match;
|
|
4430
4613
|
}
|
|
4431
4614
|
/** Title for the open-mode header: the record's primary-key value
|
|
4432
4615
|
* (e.g. `INV-2026-0001`), falling back to the collection title.
|
|
@@ -4520,19 +4703,17 @@ var CollectionView_default = /* @__PURE__ */ (0, vue.defineComponent)({
|
|
|
4520
4703
|
/** Whether a `toggle` field reads as checked: its projected enum field
|
|
4521
4704
|
* currently equals `onValue`. The toggle stores nothing itself. */
|
|
4522
4705
|
function toggleChecked(item, field) {
|
|
4523
|
-
return field.
|
|
4706
|
+
return field.type === "toggle" && String(item[field.field] ?? "") === field.onValue;
|
|
4524
4707
|
}
|
|
4525
4708
|
/** Flip a `toggle`: write the projected enum field to `offValue` when
|
|
4526
4709
|
* currently checked, else `onValue`. Reuses the inline-edit PUT path
|
|
4527
4710
|
* (optimistic + rollback) — the toggle has no value of its own. */
|
|
4528
4711
|
function commitToggle(item, field) {
|
|
4712
|
+
if (field.type !== "toggle" || !collection.value) return;
|
|
4529
4713
|
const targetKey = field.field;
|
|
4530
|
-
if (!targetKey || !collection.value) return;
|
|
4531
4714
|
const enumField = collection.value.schema.fields[targetKey];
|
|
4532
4715
|
if (!enumField) return;
|
|
4533
|
-
|
|
4534
|
-
if (next === void 0) return;
|
|
4535
|
-
commitInlineEdit(item, targetKey, enumField, next);
|
|
4716
|
+
commitInlineEdit(item, targetKey, enumField, toggleChecked(item, field) ? field.offValue : field.onValue);
|
|
4536
4717
|
}
|
|
4537
4718
|
async function confirmDelete(item) {
|
|
4538
4719
|
if (!collection.value) return;
|