@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.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { MINUTES_PER_DAY, TOOL_DEFINITION, actionVisible, assignLanes, boolSortValue, bucketRecords, buildMonthGrid, buildUpdatedRecord, coerceInlineValue, dateOf, dateSortValue, daySlice, defangForPrompt, deriveAll, draftToRecord, embedTargetId, emptyRow, enumSortValue, errorMessage, executePresentCollection, fieldVisible, firstMissingRequiredField, isSortableField, itemIdOf, itemLabelOf, labelFieldFor, nextSortDirection, numericSortValue, resolveEnumColor, rowFromItem, shortHexId, sortItems, stringSortValue, ymdKey } from "@mulmoclaude/core/collection";
|
|
1
|
+
import { MINUTES_PER_DAY, TOOL_DEFINITION, actionVisible, assignLanes, backlinkRows, boolSortValue, bucketRecords, buildMonthGrid, buildUpdatedRecord, coerceInlineValue, dateOf, dateSortValue, daySlice, defangForPrompt, deriveAll, draftToRecord, embedTargetId, emptyRow, enumSortValue, errorMessage, executePresentCollection, fieldVisible, firstMissingRequiredField, isSortableField, itemIdOf, itemLabelOf, labelFieldFor, nextSortDirection, numericSortValue, resolveEnumColor, rowFromItem, shortHexId, sortItems, stringSortValue, ymdKey } from "@mulmoclaude/core/collection";
|
|
2
2
|
import { Fragment, Teleport, computed, createBlock, createCommentVNode, createElementBlock, createElementVNode, createTextVNode, createVNode, defineComponent, effectScope, mergeModels, nextTick, normalizeClass, normalizeStyle, onBeforeUnmount, onMounted, onUnmounted, openBlock, ref, renderList, renderSlot, resolveDynamicComponent, toDisplayString, unref, useModel, vModelCheckbox, vModelDynamic, vModelSelect, vModelText, watch, watchEffect, withCtx, withDirectives, withKeys, withModifiers } from "vue";
|
|
3
3
|
import { createI18n } from "vue-i18n";
|
|
4
4
|
import draggable from "vuedraggable";
|
|
@@ -1210,18 +1210,18 @@ var CollectionRecordModal_default = /* @__PURE__ */ defineComponent({
|
|
|
1210
1210
|
});
|
|
1211
1211
|
//#endregion
|
|
1212
1212
|
//#region src/vue/components/CollectionCalendarView.vue?vue&type=script&setup=true&lang.ts
|
|
1213
|
-
var _hoisted_1$
|
|
1213
|
+
var _hoisted_1$15 = {
|
|
1214
1214
|
class: "flex flex-col gap-3",
|
|
1215
1215
|
"data-testid": "collection-calendar"
|
|
1216
1216
|
};
|
|
1217
|
-
var _hoisted_2$
|
|
1218
|
-
var _hoisted_3$
|
|
1219
|
-
var _hoisted_4$
|
|
1220
|
-
var _hoisted_5$
|
|
1217
|
+
var _hoisted_2$14 = { class: "flex items-center gap-2" };
|
|
1218
|
+
var _hoisted_3$14 = ["aria-label"];
|
|
1219
|
+
var _hoisted_4$14 = ["aria-label"];
|
|
1220
|
+
var _hoisted_5$13 = {
|
|
1221
1221
|
class: "text-sm font-bold text-slate-800 flex-1",
|
|
1222
1222
|
"data-testid": "collection-calendar-month"
|
|
1223
1223
|
};
|
|
1224
|
-
var _hoisted_6$
|
|
1224
|
+
var _hoisted_6$12 = { class: "grid grid-cols-7 gap-1 text-[10px] font-bold text-slate-400 uppercase tracking-wider select-none" };
|
|
1225
1225
|
var _hoisted_7$10 = { class: "grid grid-cols-7 gap-1" };
|
|
1226
1226
|
var _hoisted_8$10 = [
|
|
1227
1227
|
"aria-label",
|
|
@@ -1351,23 +1351,23 @@ var CollectionCalendarView_default = /* @__PURE__ */ defineComponent({
|
|
|
1351
1351
|
viewMonth.value = now.getMonth() + 1;
|
|
1352
1352
|
}
|
|
1353
1353
|
return (_ctx, _cache) => {
|
|
1354
|
-
return openBlock(), createElementBlock("div", _hoisted_1$
|
|
1355
|
-
createElementVNode("div", _hoisted_2$
|
|
1354
|
+
return openBlock(), createElementBlock("div", _hoisted_1$15, [
|
|
1355
|
+
createElementVNode("div", _hoisted_2$14, [
|
|
1356
1356
|
createElementVNode("button", {
|
|
1357
1357
|
type: "button",
|
|
1358
1358
|
class: "h-8 w-8 flex items-center justify-center rounded text-slate-500 hover:bg-slate-100 transition-colors",
|
|
1359
1359
|
"aria-label": unref(t)("collectionsView.calendarPrevMonth"),
|
|
1360
1360
|
"data-testid": "collection-calendar-prev",
|
|
1361
1361
|
onClick: _cache[0] || (_cache[0] = ($event) => stepMonth(-1))
|
|
1362
|
-
}, [..._cache[2] || (_cache[2] = [createElementVNode("span", { class: "material-icons text-lg" }, "chevron_left", -1)])], 8, _hoisted_3$
|
|
1362
|
+
}, [..._cache[2] || (_cache[2] = [createElementVNode("span", { class: "material-icons text-lg" }, "chevron_left", -1)])], 8, _hoisted_3$14),
|
|
1363
1363
|
createElementVNode("button", {
|
|
1364
1364
|
type: "button",
|
|
1365
1365
|
class: "h-8 w-8 flex items-center justify-center rounded text-slate-500 hover:bg-slate-100 transition-colors",
|
|
1366
1366
|
"aria-label": unref(t)("collectionsView.calendarNextMonth"),
|
|
1367
1367
|
"data-testid": "collection-calendar-next",
|
|
1368
1368
|
onClick: _cache[1] || (_cache[1] = ($event) => stepMonth(1))
|
|
1369
|
-
}, [..._cache[3] || (_cache[3] = [createElementVNode("span", { class: "material-icons text-lg" }, "chevron_right", -1)])], 8, _hoisted_4$
|
|
1370
|
-
createElementVNode("h3", _hoisted_5$
|
|
1369
|
+
}, [..._cache[3] || (_cache[3] = [createElementVNode("span", { class: "material-icons text-lg" }, "chevron_right", -1)])], 8, _hoisted_4$14),
|
|
1370
|
+
createElementVNode("h3", _hoisted_5$13, toDisplayString(monthLabel.value), 1),
|
|
1371
1371
|
createElementVNode("button", {
|
|
1372
1372
|
type: "button",
|
|
1373
1373
|
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",
|
|
@@ -1375,7 +1375,7 @@ var CollectionCalendarView_default = /* @__PURE__ */ defineComponent({
|
|
|
1375
1375
|
onClick: goToday
|
|
1376
1376
|
}, toDisplayString(unref(t)("collectionsView.calendarToday")), 1)
|
|
1377
1377
|
]),
|
|
1378
|
-
createElementVNode("div", _hoisted_6$
|
|
1378
|
+
createElementVNode("div", _hoisted_6$12, [(openBlock(true), createElementBlock(Fragment, null, renderList(weekdayLabels.value, (label, idx) => {
|
|
1379
1379
|
return openBlock(), createElementBlock("div", {
|
|
1380
1380
|
key: idx,
|
|
1381
1381
|
class: "px-1 py-1 text-center"
|
|
@@ -1416,18 +1416,18 @@ var CollectionCalendarView_default = /* @__PURE__ */ defineComponent({
|
|
|
1416
1416
|
});
|
|
1417
1417
|
//#endregion
|
|
1418
1418
|
//#region src/vue/components/CollectionDayView.vue?vue&type=script&setup=true&lang.ts
|
|
1419
|
-
var _hoisted_1$
|
|
1420
|
-
var _hoisted_2$
|
|
1419
|
+
var _hoisted_1$14 = { class: "flex items-center gap-2 border-b border-slate-200 px-4 py-3" };
|
|
1420
|
+
var _hoisted_2$13 = {
|
|
1421
1421
|
class: "flex-1 text-sm font-bold text-slate-800",
|
|
1422
1422
|
"data-testid": "collection-day-view-title"
|
|
1423
1423
|
};
|
|
1424
|
-
var _hoisted_3$
|
|
1425
|
-
var _hoisted_4$
|
|
1426
|
-
var _hoisted_5$
|
|
1424
|
+
var _hoisted_3$13 = ["aria-label"];
|
|
1425
|
+
var _hoisted_4$13 = ["aria-label"];
|
|
1426
|
+
var _hoisted_5$12 = {
|
|
1427
1427
|
key: 0,
|
|
1428
1428
|
class: "px-4 py-10 text-center text-sm text-slate-400"
|
|
1429
1429
|
};
|
|
1430
|
-
var _hoisted_6$
|
|
1430
|
+
var _hoisted_6$11 = { class: "absolute -top-2 left-0 w-10 pr-1 text-right text-[10px] tabular-nums text-slate-400" };
|
|
1431
1431
|
var _hoisted_7$9 = {
|
|
1432
1432
|
class: "absolute inset-y-0 right-0",
|
|
1433
1433
|
style: { "left": "2.75rem" }
|
|
@@ -1513,6 +1513,7 @@ var CollectionDayView_default = /* @__PURE__ */ defineComponent({
|
|
|
1513
1513
|
"datetime",
|
|
1514
1514
|
"table",
|
|
1515
1515
|
"embed",
|
|
1516
|
+
"backlinks",
|
|
1516
1517
|
"image",
|
|
1517
1518
|
"markdown"
|
|
1518
1519
|
]);
|
|
@@ -1608,8 +1609,8 @@ var CollectionDayView_default = /* @__PURE__ */ defineComponent({
|
|
|
1608
1609
|
role: "dialog",
|
|
1609
1610
|
"aria-modal": "true"
|
|
1610
1611
|
}, [createElementVNode("div", { class: normalizeClass(["flex min-h-0 flex-col", __props.showDetail ? "w-80 shrink-0 border-r border-slate-200" : "w-full"]) }, [
|
|
1611
|
-
createElementVNode("div", _hoisted_1$
|
|
1612
|
-
createElementVNode("h3", _hoisted_2$
|
|
1612
|
+
createElementVNode("div", _hoisted_1$14, [
|
|
1613
|
+
createElementVNode("h3", _hoisted_2$13, toDisplayString(dayLabel.value), 1),
|
|
1613
1614
|
__props.canCreate ? (openBlock(), createElementBlock("button", {
|
|
1614
1615
|
key: 0,
|
|
1615
1616
|
type: "button",
|
|
@@ -1617,16 +1618,16 @@ var CollectionDayView_default = /* @__PURE__ */ defineComponent({
|
|
|
1617
1618
|
"aria-label": unref(t)("collectionsView.calendarCreateOn", { date: dayKey.value }),
|
|
1618
1619
|
"data-testid": "collection-day-view-create",
|
|
1619
1620
|
onClick: onCreate
|
|
1620
|
-
}, [..._cache[3] || (_cache[3] = [createElementVNode("span", { class: "material-icons text-lg" }, "add", -1)])], 8, _hoisted_3$
|
|
1621
|
+
}, [..._cache[3] || (_cache[3] = [createElementVNode("span", { class: "material-icons text-lg" }, "add", -1)])], 8, _hoisted_3$13)) : createCommentVNode("", true),
|
|
1621
1622
|
createElementVNode("button", {
|
|
1622
1623
|
type: "button",
|
|
1623
1624
|
class: "h-8 w-8 flex items-center justify-center rounded text-slate-500 hover:bg-slate-100 transition-colors",
|
|
1624
1625
|
"aria-label": unref(t)("collectionsView.dayViewClose"),
|
|
1625
1626
|
"data-testid": "collection-day-view-close",
|
|
1626
1627
|
onClick: _cache[0] || (_cache[0] = ($event) => emit("close"))
|
|
1627
|
-
}, [..._cache[4] || (_cache[4] = [createElementVNode("span", { class: "material-icons text-lg" }, "close", -1)])], 8, _hoisted_4$
|
|
1628
|
+
}, [..._cache[4] || (_cache[4] = [createElementVNode("span", { class: "material-icons text-lg" }, "close", -1)])], 8, _hoisted_4$13)
|
|
1628
1629
|
]),
|
|
1629
|
-
timedEntries.value.length === 0 && allDayEntries.value.length === 0 ? (openBlock(), createElementBlock("div", _hoisted_5$
|
|
1630
|
+
timedEntries.value.length === 0 && allDayEntries.value.length === 0 ? (openBlock(), createElementBlock("div", _hoisted_5$12, toDisplayString(unref(t)("collectionsView.dayViewEmpty")), 1)) : (openBlock(), createElementBlock("div", {
|
|
1630
1631
|
key: 1,
|
|
1631
1632
|
ref_key: "scrollEl",
|
|
1632
1633
|
ref: scrollEl,
|
|
@@ -1640,7 +1641,7 @@ var CollectionDayView_default = /* @__PURE__ */ defineComponent({
|
|
|
1640
1641
|
key: hour,
|
|
1641
1642
|
class: "absolute left-0 right-0 border-t border-slate-100",
|
|
1642
1643
|
style: normalizeStyle({ top: `${(hour - 1) * HOUR_PX}px` })
|
|
1643
|
-
}, [createElementVNode("span", _hoisted_6$
|
|
1644
|
+
}, [createElementVNode("span", _hoisted_6$11, toDisplayString(hourLabel(hour - 1)), 1)], 4);
|
|
1644
1645
|
}), 64)), createElementVNode("div", _hoisted_7$9, [(openBlock(true), createElementBlock(Fragment, null, renderList(timedEntries.value, (entry) => {
|
|
1645
1646
|
return openBlock(), createElementBlock("button", {
|
|
1646
1647
|
key: entry.id,
|
|
@@ -1675,15 +1676,15 @@ var CollectionDayView_default = /* @__PURE__ */ defineComponent({
|
|
|
1675
1676
|
});
|
|
1676
1677
|
//#endregion
|
|
1677
1678
|
//#region src/vue/components/CollectionKanbanView.vue?vue&type=script&setup=true&lang.ts
|
|
1678
|
-
var _hoisted_1$
|
|
1679
|
+
var _hoisted_1$13 = {
|
|
1679
1680
|
class: "h-full overflow-x-auto overflow-y-hidden",
|
|
1680
1681
|
"data-testid": "collection-kanban"
|
|
1681
1682
|
};
|
|
1682
|
-
var _hoisted_2$
|
|
1683
|
-
var _hoisted_3$
|
|
1684
|
-
var _hoisted_4$
|
|
1685
|
-
var _hoisted_5$
|
|
1686
|
-
var _hoisted_6$
|
|
1683
|
+
var _hoisted_2$12 = { class: "flex gap-3 h-full p-1 min-w-max" };
|
|
1684
|
+
var _hoisted_3$12 = ["data-testid"];
|
|
1685
|
+
var _hoisted_4$12 = { class: "flex items-center justify-between px-3 py-2 border-b border-slate-200" };
|
|
1686
|
+
var _hoisted_5$11 = { class: "flex items-center gap-2 min-w-0" };
|
|
1687
|
+
var _hoisted_6$10 = ["title"];
|
|
1687
1688
|
var _hoisted_7$8 = { class: "text-[11px] text-slate-400 shrink-0" };
|
|
1688
1689
|
var _hoisted_8$8 = [
|
|
1689
1690
|
"data-testid",
|
|
@@ -1717,7 +1718,10 @@ var CollectionKanbanView_default = /* @__PURE__ */ defineComponent({
|
|
|
1717
1718
|
const emit = __emit;
|
|
1718
1719
|
const { t } = useCollectionI18n();
|
|
1719
1720
|
/** The Uncategorized column uses the empty string as its sentinel value. */
|
|
1720
|
-
const groupSpec = computed(() =>
|
|
1721
|
+
const groupSpec = computed(() => {
|
|
1722
|
+
const spec = props.schema.fields[props.groupField];
|
|
1723
|
+
return spec?.type === "enum" ? spec : void 0;
|
|
1724
|
+
});
|
|
1721
1725
|
/** Declared enum values become columns in order, with a trailing
|
|
1722
1726
|
* Uncategorized column for empty/unknown values (also a drop target that
|
|
1723
1727
|
* clears the field). The Uncategorized column is omitted when the group
|
|
@@ -1764,7 +1768,10 @@ var CollectionKanbanView_default = /* @__PURE__ */ defineComponent({
|
|
|
1764
1768
|
const value = String(raw);
|
|
1765
1769
|
return (groupSpec.value?.values ?? []).includes(value) ? value : UNCATEGORIZED;
|
|
1766
1770
|
}
|
|
1767
|
-
const visibleItems = computed(() =>
|
|
1771
|
+
const visibleItems = computed(() => {
|
|
1772
|
+
const spec = groupSpec.value;
|
|
1773
|
+
return spec ? props.items.filter((item) => fieldVisible(spec, item)) : [];
|
|
1774
|
+
});
|
|
1768
1775
|
const itemsByColumnMap = computed(() => {
|
|
1769
1776
|
const map = /* @__PURE__ */ new Map();
|
|
1770
1777
|
for (const column of columns.value) map.set(column.value, []);
|
|
@@ -1796,15 +1803,15 @@ var CollectionKanbanView_default = /* @__PURE__ */ defineComponent({
|
|
|
1796
1803
|
emit("move", itemId(item), next);
|
|
1797
1804
|
}
|
|
1798
1805
|
return (_ctx, _cache) => {
|
|
1799
|
-
return openBlock(), createElementBlock("div", _hoisted_1$
|
|
1806
|
+
return openBlock(), createElementBlock("div", _hoisted_1$13, [createElementVNode("div", _hoisted_2$12, [(openBlock(true), createElementBlock(Fragment, null, renderList(columns.value, (column) => {
|
|
1800
1807
|
return openBlock(), createElementBlock("div", {
|
|
1801
1808
|
key: column.value,
|
|
1802
1809
|
"data-testid": `collection-kanban-column-${column.value || "uncategorized"}`,
|
|
1803
1810
|
class: "w-72 shrink-0 flex flex-col bg-slate-100 rounded-lg"
|
|
1804
|
-
}, [createElementVNode("div", _hoisted_4$
|
|
1811
|
+
}, [createElementVNode("div", _hoisted_4$12, [createElementVNode("div", _hoisted_5$11, [createElementVNode("span", { class: normalizeClass(["w-2 h-2 rounded-full shrink-0", unref(resolveEnumColor)(__props.schema, __props.groupField, column.value).dot]) }, null, 2), createElementVNode("span", {
|
|
1805
1812
|
class: "font-semibold text-xs text-slate-600 truncate",
|
|
1806
1813
|
title: column.label
|
|
1807
|
-
}, toDisplayString(column.label), 9, _hoisted_6$
|
|
1814
|
+
}, toDisplayString(column.label), 9, _hoisted_6$10)]), createElementVNode("span", _hoisted_7$8, toDisplayString(itemsByColumn(column.value).length), 1)]), createVNode(unref(draggable), {
|
|
1808
1815
|
"model-value": itemsByColumn(column.value),
|
|
1809
1816
|
"item-key": __props.schema.primaryKey,
|
|
1810
1817
|
group: "collection-kanban-cards",
|
|
@@ -1835,7 +1842,7 @@ var CollectionKanbanView_default = /* @__PURE__ */ defineComponent({
|
|
|
1835
1842
|
"model-value",
|
|
1836
1843
|
"item-key",
|
|
1837
1844
|
"onChange"
|
|
1838
|
-
])], 8, _hoisted_3$
|
|
1845
|
+
])], 8, _hoisted_3$12);
|
|
1839
1846
|
}), 128))])]);
|
|
1840
1847
|
};
|
|
1841
1848
|
}
|
|
@@ -1864,6 +1871,61 @@ function activatePathLink(event, path, stop = false) {
|
|
|
1864
1871
|
nav(path);
|
|
1865
1872
|
}
|
|
1866
1873
|
//#endregion
|
|
1874
|
+
//#region src/vue/components/CollectionBacklinksView.vue?vue&type=script&setup=true&lang.ts
|
|
1875
|
+
var _hoisted_1$12 = ["data-testid"];
|
|
1876
|
+
var _hoisted_2$11 = { class: "w-full text-[11px] text-slate-600 bg-white" };
|
|
1877
|
+
var _hoisted_3$11 = { class: "bg-slate-50 border-b border-slate-200 text-slate-500 font-bold uppercase tracking-wider" };
|
|
1878
|
+
var _hoisted_4$11 = { class: "divide-y divide-slate-100" };
|
|
1879
|
+
var _hoisted_5$10 = [
|
|
1880
|
+
"data-testid",
|
|
1881
|
+
"onClick",
|
|
1882
|
+
"onKeydown"
|
|
1883
|
+
];
|
|
1884
|
+
var _hoisted_6$9 = ["data-testid"];
|
|
1885
|
+
//#endregion
|
|
1886
|
+
//#region src/vue/components/CollectionBacklinksView.vue
|
|
1887
|
+
var CollectionBacklinksView_default = /* @__PURE__ */ defineComponent({
|
|
1888
|
+
__name: "CollectionBacklinksView",
|
|
1889
|
+
props: {
|
|
1890
|
+
view: {},
|
|
1891
|
+
fieldKey: {}
|
|
1892
|
+
},
|
|
1893
|
+
setup(__props) {
|
|
1894
|
+
const { t } = useCollectionI18n();
|
|
1895
|
+
return (_ctx, _cache) => {
|
|
1896
|
+
return __props.view.rows.length > 0 ? (openBlock(), createElementBlock("div", {
|
|
1897
|
+
key: 0,
|
|
1898
|
+
class: "border border-slate-200/80 rounded-xl overflow-hidden shadow-sm mt-1",
|
|
1899
|
+
"data-testid": `collections-backlinks-${__props.fieldKey}`
|
|
1900
|
+
}, [createElementVNode("table", _hoisted_2$11, [createElementVNode("thead", _hoisted_3$11, [createElementVNode("tr", null, [(openBlock(true), createElementBlock(Fragment, null, renderList(__props.view.columns, (column) => {
|
|
1901
|
+
return openBlock(), createElementBlock("th", {
|
|
1902
|
+
key: column.key,
|
|
1903
|
+
class: "text-left px-4 py-2 font-bold"
|
|
1904
|
+
}, toDisplayString(column.label), 1);
|
|
1905
|
+
}), 128))])]), createElementVNode("tbody", _hoisted_4$11, [(openBlock(true), createElementBlock(Fragment, null, renderList(__props.view.rows, (row) => {
|
|
1906
|
+
return openBlock(), createElementBlock("tr", {
|
|
1907
|
+
key: row.id,
|
|
1908
|
+
class: "group hover:bg-indigo-50/30 cursor-pointer transition-colors",
|
|
1909
|
+
role: "link",
|
|
1910
|
+
tabindex: "0",
|
|
1911
|
+
"data-testid": `collections-backlinks-${__props.fieldKey}-${row.id}`,
|
|
1912
|
+
onClick: ($event) => unref(activateRefLink)($event, __props.view.fromSlug, row.id),
|
|
1913
|
+
onKeydown: [withKeys(($event) => unref(activateRefLink)($event, __props.view.fromSlug, row.id), ["enter"]), withKeys(($event) => unref(activateRefLink)($event, __props.view.fromSlug, row.id), ["space"])]
|
|
1914
|
+
}, [(openBlock(true), createElementBlock(Fragment, null, renderList(row.cells, (cell, cellIdx) => {
|
|
1915
|
+
return openBlock(), createElementBlock("td", {
|
|
1916
|
+
key: __props.view.columns[cellIdx]?.key ?? cellIdx,
|
|
1917
|
+
class: "px-4 py-2 align-middle font-medium"
|
|
1918
|
+
}, [createElementVNode("span", { class: normalizeClass(cellIdx === 0 ? "text-indigo-600 group-hover:text-indigo-800 font-bold" : "") }, toDisplayString(cell), 3)]);
|
|
1919
|
+
}), 128))], 40, _hoisted_5$10);
|
|
1920
|
+
}), 128))])])], 8, _hoisted_1$12)) : (openBlock(), createElementBlock("span", {
|
|
1921
|
+
key: 1,
|
|
1922
|
+
class: "text-slate-400 italic",
|
|
1923
|
+
"data-testid": `collections-backlinks-${__props.fieldKey}`
|
|
1924
|
+
}, toDisplayString(unref(t)("collectionsView.noRows")), 9, _hoisted_6$9));
|
|
1925
|
+
};
|
|
1926
|
+
}
|
|
1927
|
+
});
|
|
1928
|
+
//#endregion
|
|
1867
1929
|
//#region src/vue/components/CollectionEmbedView.vue?vue&type=script&setup=true&lang.ts
|
|
1868
1930
|
var _hoisted_1$11 = [
|
|
1869
1931
|
"href",
|
|
@@ -2164,7 +2226,7 @@ var _hoisted_71$1 = [
|
|
|
2164
2226
|
"onClick"
|
|
2165
2227
|
];
|
|
2166
2228
|
var _hoisted_72$1 = {
|
|
2167
|
-
key:
|
|
2229
|
+
key: 14,
|
|
2168
2230
|
class: "text-slate-800 font-semibold"
|
|
2169
2231
|
};
|
|
2170
2232
|
var _hoisted_73$1 = {
|
|
@@ -2237,6 +2299,7 @@ var CollectionRecordPanel_default = /* @__PURE__ */ defineComponent({
|
|
|
2237
2299
|
* safe in the template. */
|
|
2238
2300
|
const detailRecord = computed(() => editing.value ? props.liveDerived ?? props.liveRecord ?? {} : props.viewing ?? {});
|
|
2239
2301
|
const embedViews = computed(() => props.render.embedViewsFor(detailRecord.value));
|
|
2302
|
+
const backlinksViews = computed(() => props.render.backlinksViewsFor(detailRecord.value));
|
|
2240
2303
|
const embedOwnerByKey = computed(() => {
|
|
2241
2304
|
const map = /* @__PURE__ */ new Map();
|
|
2242
2305
|
for (const field of Object.values(props.collection.schema.fields)) if (field.type === "embed" && field.idField) map.set(field.idField, field);
|
|
@@ -2253,11 +2316,12 @@ var CollectionRecordPanel_default = /* @__PURE__ */ defineComponent({
|
|
|
2253
2316
|
return editing.value.mode === "create" ? "collections-create" : "collections-edit";
|
|
2254
2317
|
});
|
|
2255
2318
|
/** Whether a field gets an editable control in edit mode. Toggle (a
|
|
2256
|
-
* projection of an enum that has its own input), derived (computed),
|
|
2257
|
-
* embed (a foreign record)
|
|
2258
|
-
*
|
|
2319
|
+
* projection of an enum that has its own input), derived (computed),
|
|
2320
|
+
* embed (a foreign record), and backlinks (reverse refs owned by OTHER
|
|
2321
|
+
* records) stay read-only in both modes, so the cell geometry never
|
|
2322
|
+
* changes on the view↔edit toggle. */
|
|
2259
2323
|
function isEditableType(type) {
|
|
2260
|
-
return type !== "toggle" && type !== "derived" && type !== "embed";
|
|
2324
|
+
return type !== "toggle" && type !== "derived" && type !== "embed" && type !== "backlinks";
|
|
2261
2325
|
}
|
|
2262
2326
|
/** Wide field types span the full grid width in BOTH modes — keeping
|
|
2263
2327
|
* `image` full-width here (not just when viewing) is what stops a field
|
|
@@ -2267,6 +2331,7 @@ var CollectionRecordPanel_default = /* @__PURE__ */ defineComponent({
|
|
|
2267
2331
|
"table",
|
|
2268
2332
|
"markdown",
|
|
2269
2333
|
"embed",
|
|
2334
|
+
"backlinks",
|
|
2270
2335
|
"image"
|
|
2271
2336
|
].includes(field.type) ? "col-span-full" : "col-span-1";
|
|
2272
2337
|
}
|
|
@@ -2291,7 +2356,8 @@ var CollectionRecordPanel_default = /* @__PURE__ */ defineComponent({
|
|
|
2291
2356
|
/** Required flag for an embed's per-record picker — read off the storage
|
|
2292
2357
|
* field it writes (`idField`), since the embed itself stores nothing. */
|
|
2293
2358
|
function embedPickerRequired(field) {
|
|
2294
|
-
const
|
|
2359
|
+
const idField = field.type === "embed" ? field.idField : void 0;
|
|
2360
|
+
const target = idField ? props.collection.schema.fields[idField] : void 0;
|
|
2295
2361
|
return target ? isFieldRequiredInUi(target) : false;
|
|
2296
2362
|
}
|
|
2297
2363
|
/** Tailwind fill/text/border classes tinting an enum `<select>` by its current
|
|
@@ -2568,28 +2634,32 @@ var CollectionRecordPanel_default = /* @__PURE__ */ defineComponent({
|
|
|
2568
2634
|
key: 8,
|
|
2569
2635
|
view: embedViews.value[key],
|
|
2570
2636
|
"field-key": String(key)
|
|
2571
|
-
}, null, 8, ["view", "field-key"])) : field.type === "
|
|
2637
|
+
}, null, 8, ["view", "field-key"])) : field.type === "backlinks" && backlinksViews.value[key] ? (openBlock(), createBlock(CollectionBacklinksView_default, {
|
|
2572
2638
|
key: 9,
|
|
2639
|
+
view: backlinksViews.value[key],
|
|
2640
|
+
"field-key": String(key)
|
|
2641
|
+
}, null, 8, ["view", "field-key"])) : field.type === "image" && typeof detailRecord.value[key] === "string" && detailRecord.value[key] ? (openBlock(), createElementBlock("img", {
|
|
2642
|
+
key: 10,
|
|
2573
2643
|
src: unref(resolveImageSrc)(String(detailRecord.value[key])),
|
|
2574
2644
|
alt: field.label,
|
|
2575
2645
|
class: "max-h-64 max-w-full object-contain rounded-lg border border-slate-200 bg-slate-50",
|
|
2576
2646
|
"data-testid": `collections-detail-image-${key}`
|
|
2577
2647
|
}, null, 8, _hoisted_68$1)) : field.type !== "file" && __props.render.isExternalUrl(detailRecord.value[key]) ? (openBlock(), createElementBlock("a", {
|
|
2578
|
-
key:
|
|
2648
|
+
key: 11,
|
|
2579
2649
|
href: String(detailRecord.value[key]),
|
|
2580
2650
|
target: "_blank",
|
|
2581
2651
|
rel: "noopener noreferrer",
|
|
2582
2652
|
class: "text-blue-600 hover:text-blue-800 font-semibold hover:underline break-all",
|
|
2583
2653
|
"data-testid": `collections-detail-url-${key}`
|
|
2584
2654
|
}, toDisplayString(String(detailRecord.value[key])), 9, _hoisted_69$1)) : field.type === "file" && __props.render.artifactUrl(detailRecord.value[key]) ? (openBlock(), createElementBlock("a", {
|
|
2585
|
-
key:
|
|
2655
|
+
key: 12,
|
|
2586
2656
|
href: __props.render.artifactUrl(detailRecord.value[key]) ?? void 0,
|
|
2587
2657
|
target: "_blank",
|
|
2588
2658
|
rel: "noopener noreferrer",
|
|
2589
2659
|
class: "text-blue-600 hover:text-blue-800 font-semibold hover:underline break-all",
|
|
2590
2660
|
"data-testid": `collections-detail-file-${key}`
|
|
2591
2661
|
}, toDisplayString(String(detailRecord.value[key])), 9, _hoisted_70$1)) : field.type === "file" && __props.render.fileRoutePath(detailRecord.value[key]) ? (openBlock(), createElementBlock("a", {
|
|
2592
|
-
key:
|
|
2662
|
+
key: 13,
|
|
2593
2663
|
href: __props.render.fileRoutePath(detailRecord.value[key]) ?? void 0,
|
|
2594
2664
|
class: "text-blue-600 hover:text-blue-800 font-semibold hover:underline break-all",
|
|
2595
2665
|
"data-testid": `collections-detail-file-${key}`,
|
|
@@ -3043,8 +3113,11 @@ function formatCell(value, type) {
|
|
|
3043
3113
|
}
|
|
3044
3114
|
/** Resolve the ISO 4217 code for a money field: a per-record
|
|
3045
3115
|
* `currencyField` (when present and non-blank) wins over the field's
|
|
3046
|
-
* literal `currency`.
|
|
3116
|
+
* literal `currency`. Only `money` / `derived` variants carry currency
|
|
3117
|
+
* keys; any other field resolves to undefined (the formatter's USD
|
|
3118
|
+
* fallback), as before. */
|
|
3047
3119
|
function resolveCurrency(field, record) {
|
|
3120
|
+
if (field.type !== "money" && field.type !== "derived") return void 0;
|
|
3048
3121
|
if (field.currencyField && record) {
|
|
3049
3122
|
const code = record[field.currencyField];
|
|
3050
3123
|
if (typeof code === "string" && code.trim().length > 0) return code;
|
|
@@ -3106,6 +3179,14 @@ function uniqueEmbedTargets(schema) {
|
|
|
3106
3179
|
for (const field of Object.values(schema.fields)) if (field.type === "embed" && typeof field.to === "string" && field.to.length > 0) targets.add(field.to);
|
|
3107
3180
|
return [...targets];
|
|
3108
3181
|
}
|
|
3182
|
+
/** Slugs of every SOURCE collection a `backlinks` field reverses over.
|
|
3183
|
+
* Top-level only, like `embed` (the schema rejects `backlinks` inside a
|
|
3184
|
+
* table's `of`). Mirrors the server's `uniqueBacklinkSources`. */
|
|
3185
|
+
function uniqueBacklinkSources(schema) {
|
|
3186
|
+
const sources = /* @__PURE__ */ new Set();
|
|
3187
|
+
for (const field of Object.values(schema.fields)) if (field.type === "backlinks" && field.from.length > 0) sources.add(field.from);
|
|
3188
|
+
return [...sources];
|
|
3189
|
+
}
|
|
3109
3190
|
function buildRefDisplayMap(detail) {
|
|
3110
3191
|
const { fields, primaryKey } = detail.collection.schema;
|
|
3111
3192
|
const displayField = displayFieldFor(fields, primaryKey);
|
|
@@ -3118,15 +3199,23 @@ function buildRefDisplayMap(detail) {
|
|
|
3118
3199
|
}
|
|
3119
3200
|
return map;
|
|
3120
3201
|
}
|
|
3121
|
-
|
|
3122
|
-
|
|
3202
|
+
/** Index DERIVED records by primary key — the client mirror of the
|
|
3203
|
+
* server's `loadTarget` indexing: string non-empty ids only, one record
|
|
3204
|
+
* per id (a duplicate keeps the LAST record in its FIRST position —
|
|
3205
|
+
* plain-object key semantics). Shared by the ref-record cache and the
|
|
3206
|
+
* backlinks view so every client consumer agrees with server enrichment
|
|
3207
|
+
* on which source records exist. */
|
|
3208
|
+
function derivedRecordsById(schema, items) {
|
|
3123
3209
|
const map = {};
|
|
3124
|
-
for (const item of
|
|
3210
|
+
for (const item of items) {
|
|
3125
3211
|
const slugRaw = item[schema.primaryKey];
|
|
3126
3212
|
if (typeof slugRaw === "string" && slugRaw.length > 0) map[slugRaw] = deriveAll(schema, item, {});
|
|
3127
3213
|
}
|
|
3128
3214
|
return map;
|
|
3129
3215
|
}
|
|
3216
|
+
function buildRefRecordMap(detail) {
|
|
3217
|
+
return derivedRecordsById(detail.collection.schema, detail.items);
|
|
3218
|
+
}
|
|
3130
3219
|
function sortedRefOptions(map) {
|
|
3131
3220
|
return Object.entries(map).map(([slug, display]) => ({
|
|
3132
3221
|
slug,
|
|
@@ -3149,8 +3238,64 @@ function buildEmbedOptions(schema, items) {
|
|
|
3149
3238
|
}).filter((opt) => opt.slug.length > 0).sort((left, right) => left.display.localeCompare(right.display));
|
|
3150
3239
|
}
|
|
3151
3240
|
//#endregion
|
|
3152
|
-
//#region src/vue/
|
|
3153
|
-
|
|
3241
|
+
//#region src/vue/useLinkedCollectionCaches.ts
|
|
3242
|
+
/** The de-duplicated ref + embed target slugs a schema links to. `allTargets`
|
|
3243
|
+
* is the union (each target fetched once even when both ref'd and embedded).
|
|
3244
|
+
* `backlinks` SOURCE collections ride in `embedTargets`: a backlink needs
|
|
3245
|
+
* exactly what an embed target caches (the source's schema + items in
|
|
3246
|
+
* `embedCache`), so reverse sources reuse that cache rather than adding a
|
|
3247
|
+
* parallel one. */
|
|
3248
|
+
function linkedTargets(schema) {
|
|
3249
|
+
const refTargets = new Set(uniqueRefTargets(schema));
|
|
3250
|
+
const embedTargets = /* @__PURE__ */ new Set([...uniqueEmbedTargets(schema), ...uniqueBacklinkSources(schema)]);
|
|
3251
|
+
return {
|
|
3252
|
+
refTargets,
|
|
3253
|
+
embedTargets,
|
|
3254
|
+
allTargets: [.../* @__PURE__ */ new Set([...refTargets, ...embedTargets])]
|
|
3255
|
+
};
|
|
3256
|
+
}
|
|
3257
|
+
/** Fan-out fetch that hydrates the linked-collection caches. Best-effort: a
|
|
3258
|
+
* target whose fetch *rejects* (vs. resolving `{ ok: false }`) is coerced to a
|
|
3259
|
+
* skip and must not abort the others. Returns null when a quicker subsequent
|
|
3260
|
+
* load has already moved on (stale-write guard) so the caller drops the write.
|
|
3261
|
+
* Pure + injectable (`fetchDetail`, `currentSlug`) so both paths are testable. */
|
|
3262
|
+
async function fetchLinkedCaches(targets, fetchDetail, currentSlug, expectedSlug) {
|
|
3263
|
+
const { refTargets, embedTargets, allTargets } = targets;
|
|
3264
|
+
const results = await Promise.all(allTargets.map(async (target) => {
|
|
3265
|
+
try {
|
|
3266
|
+
return {
|
|
3267
|
+
target,
|
|
3268
|
+
result: await fetchDetail(target)
|
|
3269
|
+
};
|
|
3270
|
+
} catch {
|
|
3271
|
+
return {
|
|
3272
|
+
target,
|
|
3273
|
+
result: { ok: false }
|
|
3274
|
+
};
|
|
3275
|
+
}
|
|
3276
|
+
}));
|
|
3277
|
+
if (currentSlug() !== expectedSlug) return null;
|
|
3278
|
+
const refCache = {};
|
|
3279
|
+
const refRecordCache = {};
|
|
3280
|
+
const embedCache = {};
|
|
3281
|
+
for (const { target, result } of results) {
|
|
3282
|
+
if (!result.ok) continue;
|
|
3283
|
+
if (refTargets.has(target)) {
|
|
3284
|
+
refCache[target] = buildRefDisplayMap(result.data);
|
|
3285
|
+
refRecordCache[target] = buildRefRecordMap(result.data);
|
|
3286
|
+
}
|
|
3287
|
+
if (embedTargets.has(target)) embedCache[target] = {
|
|
3288
|
+
schema: result.data.collection.schema,
|
|
3289
|
+
items: result.data.items
|
|
3290
|
+
};
|
|
3291
|
+
}
|
|
3292
|
+
return {
|
|
3293
|
+
refCache,
|
|
3294
|
+
refRecordCache,
|
|
3295
|
+
embedCache
|
|
3296
|
+
};
|
|
3297
|
+
}
|
|
3298
|
+
function useLinkedCollectionCaches(collection) {
|
|
3154
3299
|
const refCache = ref({});
|
|
3155
3300
|
const refRecordCache = ref({});
|
|
3156
3301
|
const embedCache = ref({});
|
|
@@ -3160,161 +3305,192 @@ function useCollectionRendering(collection, locale) {
|
|
|
3160
3305
|
embedCache.value = {};
|
|
3161
3306
|
}
|
|
3162
3307
|
async function loadLinkedCollections(schema, expectedSlug) {
|
|
3163
|
-
const
|
|
3164
|
-
|
|
3165
|
-
const allTargets = [.../* @__PURE__ */ new Set([...refTargets, ...embedTargets])];
|
|
3166
|
-
if (allTargets.length === 0) return;
|
|
3308
|
+
const targets = linkedTargets(schema);
|
|
3309
|
+
if (targets.allTargets.length === 0) return;
|
|
3167
3310
|
const binding = collectionUi();
|
|
3168
|
-
const
|
|
3169
|
-
|
|
3170
|
-
|
|
3171
|
-
|
|
3172
|
-
|
|
3173
|
-
};
|
|
3174
|
-
} catch {
|
|
3175
|
-
return {
|
|
3176
|
-
target,
|
|
3177
|
-
result: { ok: false }
|
|
3178
|
-
};
|
|
3179
|
-
}
|
|
3180
|
-
}));
|
|
3181
|
-
if (collection.value?.slug !== expectedSlug) return;
|
|
3182
|
-
const nextRef = {};
|
|
3183
|
-
const nextRefRecords = {};
|
|
3184
|
-
const nextEmbed = {};
|
|
3185
|
-
for (const { target, result } of results) {
|
|
3186
|
-
if (!result.ok) continue;
|
|
3187
|
-
if (refTargets.has(target)) {
|
|
3188
|
-
nextRef[target] = buildRefDisplayMap(result.data);
|
|
3189
|
-
nextRefRecords[target] = buildRefRecordMap(result.data);
|
|
3190
|
-
}
|
|
3191
|
-
if (embedTargets.has(target)) nextEmbed[target] = {
|
|
3192
|
-
schema: result.data.collection.schema,
|
|
3193
|
-
items: result.data.items
|
|
3194
|
-
};
|
|
3195
|
-
}
|
|
3196
|
-
refCache.value = nextRef;
|
|
3197
|
-
refRecordCache.value = nextRefRecords;
|
|
3198
|
-
embedCache.value = nextEmbed;
|
|
3199
|
-
}
|
|
3200
|
-
function refDisplay(targetSlug, itemSlug) {
|
|
3201
|
-
const map = refCache.value[targetSlug];
|
|
3202
|
-
return map && map[itemSlug] || itemSlug;
|
|
3203
|
-
}
|
|
3204
|
-
function refOptions(targetSlug) {
|
|
3205
|
-
const map = refCache.value[targetSlug];
|
|
3206
|
-
return map ? sortedRefOptions(map) : [];
|
|
3207
|
-
}
|
|
3208
|
-
/** Dropdown options for an `embed` field's per-record picker (`idField`):
|
|
3209
|
-
* every record in the target collection, labelled by its name/title (or
|
|
3210
|
-
* primary key). Built from `embedCache` so it works for embed targets
|
|
3211
|
-
* that aren't also `ref` targets (the profile collection, say). */
|
|
3212
|
-
function embedOptions(targetSlug) {
|
|
3213
|
-
const data = embedCache.value[targetSlug];
|
|
3214
|
-
return data ? buildEmbedOptions(data.schema, data.items) : [];
|
|
3311
|
+
const snapshot = await fetchLinkedCaches(targets, (slug) => binding.fetchCollectionDetail(slug), () => collection.value?.slug, expectedSlug);
|
|
3312
|
+
if (!snapshot) return;
|
|
3313
|
+
refCache.value = snapshot.refCache;
|
|
3314
|
+
refRecordCache.value = snapshot.refRecordCache;
|
|
3315
|
+
embedCache.value = snapshot.embedCache;
|
|
3215
3316
|
}
|
|
3216
|
-
|
|
3217
|
-
|
|
3218
|
-
|
|
3219
|
-
|
|
3220
|
-
|
|
3221
|
-
|
|
3222
|
-
|
|
3223
|
-
|
|
3224
|
-
|
|
3225
|
-
|
|
3226
|
-
|
|
3227
|
-
|
|
3228
|
-
|
|
3229
|
-
|
|
3230
|
-
|
|
3317
|
+
return {
|
|
3318
|
+
refCache,
|
|
3319
|
+
refRecordCache,
|
|
3320
|
+
embedCache,
|
|
3321
|
+
resetLinkedCaches,
|
|
3322
|
+
loadLinkedCollections
|
|
3323
|
+
};
|
|
3324
|
+
}
|
|
3325
|
+
//#endregion
|
|
3326
|
+
//#region src/vue/useCollectionRendering.renderers.ts
|
|
3327
|
+
function lookupRefDisplay(refCache, targetSlug, itemSlug) {
|
|
3328
|
+
const map = refCache[targetSlug];
|
|
3329
|
+
return map && map[itemSlug] || itemSlug;
|
|
3330
|
+
}
|
|
3331
|
+
function refOptionsFor(refCache, targetSlug) {
|
|
3332
|
+
const map = refCache[targetSlug];
|
|
3333
|
+
return map ? sortedRefOptions(map) : [];
|
|
3334
|
+
}
|
|
3335
|
+
/** Dropdown options for an `embed` field's per-record picker (`idField`):
|
|
3336
|
+
* every record in the target collection, labelled by its name/title (or
|
|
3337
|
+
* primary key). Built from `embedCache` so it works for embed targets
|
|
3338
|
+
* that aren't also `ref` targets (the profile collection, say). */
|
|
3339
|
+
function embedOptionsFor(embedCache, targetSlug) {
|
|
3340
|
+
const data = embedCache[targetSlug];
|
|
3341
|
+
return data ? buildEmbedOptions(data.schema, data.items) : [];
|
|
3342
|
+
}
|
|
3343
|
+
function resolveEmbed(field, record, embedCache) {
|
|
3344
|
+
if (field.type !== "embed" || !field.to) return {
|
|
3345
|
+
schema: null,
|
|
3346
|
+
item: null
|
|
3347
|
+
};
|
|
3348
|
+
const targetId = embedTargetId(field, record);
|
|
3349
|
+
const data = targetId ? embedCache[field.to] : void 0;
|
|
3350
|
+
if (!data) return {
|
|
3351
|
+
schema: null,
|
|
3352
|
+
item: null
|
|
3353
|
+
};
|
|
3354
|
+
const item = data.items.find((entry) => String(entry[data.schema.primaryKey] ?? "") === targetId) ?? null;
|
|
3355
|
+
return {
|
|
3356
|
+
schema: data.schema,
|
|
3357
|
+
item
|
|
3358
|
+
};
|
|
3359
|
+
}
|
|
3360
|
+
function formatEmbedValue(field, value, record, locale) {
|
|
3361
|
+
if (field.type === "money") return formatMoney(value, resolveCurrency(field, record), locale);
|
|
3362
|
+
return detailText(value);
|
|
3363
|
+
}
|
|
3364
|
+
/** Build the read-only embed view-models for one record. A function of the
|
|
3365
|
+
* open record (not a bare computed) because a per-record `idField` embed
|
|
3366
|
+
* resolves a different target per row. `schema` is the OPEN collection's
|
|
3367
|
+
* schema (null when no collection is loaded → no views). */
|
|
3368
|
+
function buildEmbedViews(schema, embedCache, record, locale) {
|
|
3369
|
+
const out = {};
|
|
3370
|
+
if (!schema) return out;
|
|
3371
|
+
for (const [key, field] of Object.entries(schema.fields)) {
|
|
3372
|
+
if (field.type !== "embed") continue;
|
|
3373
|
+
const { schema: targetSchema, item } = resolveEmbed(field, record, embedCache);
|
|
3374
|
+
const rows = [];
|
|
3375
|
+
if (targetSchema && item) for (const [subKey, subField] of Object.entries(targetSchema.fields)) {
|
|
3376
|
+
const value = item[subKey];
|
|
3377
|
+
if (value === void 0 || value === null || value === "") continue;
|
|
3378
|
+
rows.push({
|
|
3379
|
+
key: subKey,
|
|
3380
|
+
label: subField.label,
|
|
3381
|
+
type: subField.type,
|
|
3382
|
+
value,
|
|
3383
|
+
display: formatEmbedValue(subField, value, item, locale)
|
|
3384
|
+
});
|
|
3385
|
+
}
|
|
3386
|
+
out[key] = {
|
|
3387
|
+
found: Boolean(item),
|
|
3388
|
+
rows,
|
|
3389
|
+
targetSlug: field.to ?? "",
|
|
3390
|
+
recordId: embedTargetId(field, record)
|
|
3231
3391
|
};
|
|
3232
3392
|
}
|
|
3233
|
-
|
|
3234
|
-
|
|
3235
|
-
|
|
3236
|
-
|
|
3237
|
-
|
|
3238
|
-
|
|
3239
|
-
|
|
3240
|
-
|
|
3241
|
-
|
|
3242
|
-
|
|
3243
|
-
|
|
3244
|
-
|
|
3245
|
-
|
|
3246
|
-
|
|
3247
|
-
|
|
3248
|
-
|
|
3249
|
-
|
|
3250
|
-
|
|
3251
|
-
|
|
3252
|
-
|
|
3253
|
-
|
|
3254
|
-
|
|
3255
|
-
|
|
3256
|
-
|
|
3257
|
-
|
|
3393
|
+
return out;
|
|
3394
|
+
}
|
|
3395
|
+
/** One backlinks table CELL: money formatted with its currency, anything
|
|
3396
|
+
* else through the same `formatCell` list tables use — so a markdown
|
|
3397
|
+
* source column (a worklog `notes`) shows the one-line 80-char preview,
|
|
3398
|
+
* not the whole text. Unknown `display` key ⇒ plain text (fail-soft). */
|
|
3399
|
+
function formatBacklinkCell(sourceField, value, row, locale) {
|
|
3400
|
+
if (!sourceField) return detailText(value);
|
|
3401
|
+
if (sourceField.type === "money") return formatMoney(value, resolveCurrency(sourceField, row), locale);
|
|
3402
|
+
return formatCell(value, sourceField.type);
|
|
3403
|
+
}
|
|
3404
|
+
/** Build the read-only backlinks view-models for one record: for each
|
|
3405
|
+
* `backlinks` field, the rows of `from` whose `via` points at the open
|
|
3406
|
+
* record (matched via the SHARED `backlinkRows`, on source records
|
|
3407
|
+
* derived exactly like the server's enrichment — so a `display`/`filter`
|
|
3408
|
+
* on a derived column such as an invoice `total` agrees on both sides).
|
|
3409
|
+
* Source data comes out of `embedCache` (reverse sources ride the embed
|
|
3410
|
+
* fan-out — see `linkedTargets`). Fail-soft: an unloadable source or an
|
|
3411
|
+
* unknown `display` key degrades to `found: false` / a raw-key column,
|
|
3412
|
+
* never a throw. */
|
|
3413
|
+
function buildBacklinksViews(schema, embedCache, record, locale) {
|
|
3414
|
+
const out = {};
|
|
3415
|
+
if (!schema) return out;
|
|
3416
|
+
for (const [key, field] of Object.entries(schema.fields)) {
|
|
3417
|
+
if (field.type !== "backlinks") continue;
|
|
3418
|
+
const columns = field.display.map((col) => ({
|
|
3419
|
+
key: col,
|
|
3420
|
+
label: col
|
|
3421
|
+
}));
|
|
3422
|
+
const data = embedCache[field.from];
|
|
3423
|
+
if (!data) {
|
|
3258
3424
|
out[key] = {
|
|
3259
|
-
found:
|
|
3260
|
-
|
|
3261
|
-
|
|
3262
|
-
|
|
3425
|
+
found: false,
|
|
3426
|
+
columns,
|
|
3427
|
+
rows: [],
|
|
3428
|
+
fromSlug: field.from
|
|
3263
3429
|
};
|
|
3264
|
-
|
|
3265
|
-
|
|
3266
|
-
|
|
3267
|
-
|
|
3268
|
-
|
|
3269
|
-
|
|
3270
|
-
|
|
3271
|
-
|
|
3272
|
-
|
|
3273
|
-
|
|
3274
|
-
|
|
3275
|
-
|
|
3276
|
-
|
|
3277
|
-
|
|
3278
|
-
if (subField.type === "ref" && subField.to && typeof value === "string" && value.length > 0) return refDisplay(subField.to, value);
|
|
3279
|
-
return formatCell(value, subField.type);
|
|
3280
|
-
}
|
|
3281
|
-
const stepFor = stepForFieldType;
|
|
3282
|
-
function evaluateDerivedAgainstItem(field, fieldKey, item) {
|
|
3283
|
-
if (!field.formula || !collection.value) return null;
|
|
3284
|
-
const result = deriveAll(collection.value.schema, item, refRecordCache.value)[fieldKey];
|
|
3285
|
-
return typeof result === "number" && Number.isFinite(result) ? result : null;
|
|
3286
|
-
}
|
|
3287
|
-
function derivedDisplay(field, computedValue, record) {
|
|
3288
|
-
if (computedValue === null || computedValue === void 0) return "—";
|
|
3289
|
-
if (field.display === "money") return formatMoney(computedValue, resolveCurrency(field, record), locale.value);
|
|
3290
|
-
return formatCell(computedValue, field.display ?? "number");
|
|
3430
|
+
continue;
|
|
3431
|
+
}
|
|
3432
|
+
for (const column of columns) column.label = data.schema.fields[column.key]?.label ?? column.key;
|
|
3433
|
+
const selfId = String(record?.[schema.primaryKey] ?? "");
|
|
3434
|
+
const sourceById = derivedRecordsById(data.schema, data.items);
|
|
3435
|
+
out[key] = {
|
|
3436
|
+
found: true,
|
|
3437
|
+
columns,
|
|
3438
|
+
rows: backlinkRows(field, selfId, Object.values(sourceById)).map((row) => ({
|
|
3439
|
+
id: String(row[data.schema.primaryKey] ?? ""),
|
|
3440
|
+
cells: columns.map((column) => formatBacklinkCell(data.schema.fields[column.key], row[column.key], row, locale))
|
|
3441
|
+
})),
|
|
3442
|
+
fromSlug: field.from
|
|
3443
|
+
};
|
|
3291
3444
|
}
|
|
3445
|
+
return out;
|
|
3446
|
+
}
|
|
3447
|
+
function renderSubCell(subField, value, record, refCache, locale) {
|
|
3448
|
+
if (subField.type === "money") return formatMoney(value, resolveCurrency(subField, record), locale);
|
|
3449
|
+
if (subField.type === "ref" && subField.to && typeof value === "string" && value.length > 0) return lookupRefDisplay(refCache, subField.to, value);
|
|
3450
|
+
return formatCell(value, subField.type);
|
|
3451
|
+
}
|
|
3452
|
+
function evaluateDerived(field, fieldKey, item, schema, refRecords) {
|
|
3453
|
+
if (field.type !== "derived" || !schema) return null;
|
|
3454
|
+
const result = deriveAll(schema, item, refRecords)[fieldKey];
|
|
3455
|
+
return typeof result === "number" && Number.isFinite(result) ? result : null;
|
|
3456
|
+
}
|
|
3457
|
+
function renderDerived(field, computedValue, record, locale) {
|
|
3458
|
+
if (computedValue === null || computedValue === void 0) return "—";
|
|
3459
|
+
const display = field.type === "derived" ? field.display : void 0;
|
|
3460
|
+
if (display === "money") return formatMoney(computedValue, resolveCurrency(field, record), locale);
|
|
3461
|
+
return formatCell(computedValue, display ?? "number");
|
|
3462
|
+
}
|
|
3463
|
+
//#endregion
|
|
3464
|
+
//#region src/vue/useCollectionRendering.ts
|
|
3465
|
+
var STATELESS_RENDERERS = {
|
|
3466
|
+
resolveCurrency,
|
|
3467
|
+
formatMoney,
|
|
3468
|
+
formatCell,
|
|
3469
|
+
detailText,
|
|
3470
|
+
isExternalUrl,
|
|
3471
|
+
tableRows,
|
|
3472
|
+
hasTableRows,
|
|
3473
|
+
inputTypeFor,
|
|
3474
|
+
stepFor: stepForFieldType,
|
|
3475
|
+
deriveAll
|
|
3476
|
+
};
|
|
3477
|
+
function useCollectionRendering(collection, locale) {
|
|
3478
|
+
const caches = useLinkedCollectionCaches(collection);
|
|
3479
|
+
const { refCache, refRecordCache, embedCache } = caches;
|
|
3292
3480
|
return {
|
|
3293
|
-
|
|
3294
|
-
|
|
3295
|
-
|
|
3296
|
-
|
|
3297
|
-
|
|
3298
|
-
|
|
3299
|
-
|
|
3300
|
-
|
|
3301
|
-
|
|
3302
|
-
|
|
3303
|
-
|
|
3304
|
-
|
|
3305
|
-
|
|
3306
|
-
detailText,
|
|
3307
|
-
isExternalUrl,
|
|
3308
|
-
artifactUrl,
|
|
3309
|
-
fileRoutePath,
|
|
3310
|
-
tableRows,
|
|
3311
|
-
hasTableRows,
|
|
3312
|
-
formatSubCell,
|
|
3313
|
-
inputTypeFor,
|
|
3314
|
-
stepFor,
|
|
3315
|
-
deriveAll,
|
|
3316
|
-
evaluateDerivedAgainstItem,
|
|
3317
|
-
derivedDisplay
|
|
3481
|
+
...caches,
|
|
3482
|
+
...STATELESS_RENDERERS,
|
|
3483
|
+
refDisplay: (targetSlug, itemSlug) => lookupRefDisplay(refCache.value, targetSlug, itemSlug),
|
|
3484
|
+
refOptions: (targetSlug) => refOptionsFor(refCache.value, targetSlug),
|
|
3485
|
+
embedOptions: (targetSlug) => embedOptionsFor(embedCache.value, targetSlug),
|
|
3486
|
+
embedViewsFor: (record) => buildEmbedViews(collection.value?.schema ?? null, embedCache.value, record, locale.value),
|
|
3487
|
+
backlinksViewsFor: (record) => buildBacklinksViews(collection.value?.schema ?? null, embedCache.value, record, locale.value),
|
|
3488
|
+
currencySymbol: (currency) => currencySymbolForLocale(currency, locale.value),
|
|
3489
|
+
artifactUrl: (value) => collectionUi().fileAssetUrl(value),
|
|
3490
|
+
fileRoutePath: (value) => collectionUi().fileRoutePath(value),
|
|
3491
|
+
formatSubCell: (subField, value, record) => renderSubCell(subField, value, record, refCache.value, locale.value),
|
|
3492
|
+
evaluateDerivedAgainstItem: (field, fieldKey, item) => evaluateDerived(field, fieldKey, item, collection.value?.schema ?? null, refRecordCache.value),
|
|
3493
|
+
derivedDisplay: (field, computedValue, record) => renderDerived(field, computedValue, record, locale.value)
|
|
3318
3494
|
};
|
|
3319
3495
|
}
|
|
3320
3496
|
//#endregion
|
|
@@ -3805,12 +3981,12 @@ var CollectionView_default = /* @__PURE__ */ defineComponent({
|
|
|
3805
3981
|
return scalarSortValue(field, item[key]);
|
|
3806
3982
|
}
|
|
3807
3983
|
/** Derived rows sort by their display type: money/number → numeric,
|
|
3808
|
-
* date
|
|
3984
|
+
* date → epoch, anything else → the enriched value as a string. */
|
|
3809
3985
|
function derivedSortValue(field, key, item) {
|
|
3810
|
-
const
|
|
3986
|
+
const display = field.type === "derived" ? field.display : void 0;
|
|
3811
3987
|
if (display === void 0 || display === "number" || display === "money") return numericSortValue(evaluateDerivedAgainstItem(field, key, item));
|
|
3812
3988
|
const enriched = collection.value ? render.deriveAll(collection.value.schema, item, render.refRecordCache.value) : item;
|
|
3813
|
-
if (display === "date"
|
|
3989
|
+
if (display === "date") return dateSortValue(enriched[key]);
|
|
3814
3990
|
return stringSortValue(enriched[key]);
|
|
3815
3991
|
}
|
|
3816
3992
|
const sortedItems = computed(() => {
|
|
@@ -3988,10 +4164,12 @@ var CollectionView_default = /* @__PURE__ */ defineComponent({
|
|
|
3988
4164
|
function buildChatSeed(slug, message, itemId) {
|
|
3989
4165
|
const current = collection.value;
|
|
3990
4166
|
if (current?.source !== "feed") return itemId ? `/${slug} id=${itemId} ${message}` : `/${slug} ${message}`;
|
|
4167
|
+
const dataPath = current.schema.dataPath ?? `data/feeds/${slug}`;
|
|
4168
|
+
const scoped = itemId ? `(for record \`${itemId}\`) ${message}` : message;
|
|
3991
4169
|
return t("collectionsView.feedChatSeed", {
|
|
3992
4170
|
slug,
|
|
3993
|
-
dataPath
|
|
3994
|
-
message:
|
|
4171
|
+
dataPath,
|
|
4172
|
+
message: scoped
|
|
3995
4173
|
});
|
|
3996
4174
|
}
|
|
3997
4175
|
/** Start a new general-role chat seeded from the current view. */
|
|
@@ -4066,7 +4244,10 @@ var CollectionView_default = /* @__PURE__ */ defineComponent({
|
|
|
4066
4244
|
enumOriginallyEmpty.value = snapshotEmptyEnums(result.data.collection.schema, result.data.items);
|
|
4067
4245
|
await render.loadLinkedCollections(result.data.collection.schema, slug);
|
|
4068
4246
|
if (activeSlug.value !== slug) return;
|
|
4069
|
-
if (viewing.value)
|
|
4247
|
+
if (viewing.value) {
|
|
4248
|
+
const openId = String(viewing.value[result.data.collection.schema.primaryKey] ?? "");
|
|
4249
|
+
viewing.value = findItemById(openId) ?? null;
|
|
4250
|
+
}
|
|
4070
4251
|
}
|
|
4071
4252
|
function maybeAutoRefreshFeed(slug) {
|
|
4072
4253
|
if (embedded.value) return;
|
|
@@ -4080,7 +4261,7 @@ var CollectionView_default = /* @__PURE__ */ defineComponent({
|
|
|
4080
4261
|
* list table only (a whole embedded record doesn't fit a table cell,
|
|
4081
4262
|
* and it'd be identical in every row). The detail modal and the edit
|
|
4082
4263
|
* form iterate the full `schema.fields` so embeds render there too. */
|
|
4083
|
-
const listColumnFields = computed(() => collection.value ? Object.entries(collection.value.schema.fields).filter(([key, field]) => field.type !== "embed" && field.type !== "image" && key !== collection.value?.schema.primaryKey) : []);
|
|
4264
|
+
const listColumnFields = 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) : []);
|
|
4084
4265
|
/** True when the current collection declares `schema.singleton` —
|
|
4085
4266
|
* exactly one record, its primary key fixed to the declared value. */
|
|
4086
4267
|
const isSingleton = computed(() => Boolean(collection.value?.schema.singleton));
|
|
@@ -4244,7 +4425,8 @@ var CollectionView_default = /* @__PURE__ */ defineComponent({
|
|
|
4244
4425
|
}
|
|
4245
4426
|
/** Select a custom view by id (builds the `custom:<id>` mode key). */
|
|
4246
4427
|
function setCustomView(viewId) {
|
|
4247
|
-
|
|
4428
|
+
const mode = `custom:${viewId}`;
|
|
4429
|
+
view.value = mode;
|
|
4248
4430
|
}
|
|
4249
4431
|
/** A short, slug-safe id not already used by a loaded record. Collisions
|
|
4250
4432
|
* are astronomically unlikely (32 bits), but we still re-roll a few
|
|
@@ -4268,7 +4450,7 @@ var CollectionView_default = /* @__PURE__ */ defineComponent({
|
|
|
4268
4450
|
boolOriginallyPresent[key] = false;
|
|
4269
4451
|
boolTouched[key] = false;
|
|
4270
4452
|
} else if (field.type === "table") table[key] = [];
|
|
4271
|
-
else if (field.type !== "derived" && field.type !== "embed" && field.type !== "toggle") text[key] = "";
|
|
4453
|
+
else if (field.type !== "derived" && field.type !== "embed" && field.type !== "backlinks" && field.type !== "toggle") text[key] = "";
|
|
4272
4454
|
const { singleton, primaryKey } = collection.value.schema;
|
|
4273
4455
|
if (singleton) text[primaryKey] = singleton;
|
|
4274
4456
|
else if (primaryKey in text) text[primaryKey] = generateUniqueItemId(primaryKey);
|
|
@@ -4300,7 +4482,7 @@ var CollectionView_default = /* @__PURE__ */ defineComponent({
|
|
|
4300
4482
|
} else if (field.type === "table" && field.of) {
|
|
4301
4483
|
const sub = field.of;
|
|
4302
4484
|
table[key] = (Array.isArray(raw) ? raw : []).filter((row) => Boolean(row) && typeof row === "object" && !Array.isArray(row)).map((row) => rowFromItem(row, sub));
|
|
4303
|
-
} else if (field.type !== "derived" && field.type !== "embed" && field.type !== "toggle") text[key] = raw === void 0 || raw === null ? "" : String(raw);
|
|
4485
|
+
} else if (field.type !== "derived" && field.type !== "embed" && field.type !== "backlinks" && field.type !== "toggle") text[key] = raw === void 0 || raw === null ? "" : String(raw);
|
|
4304
4486
|
}
|
|
4305
4487
|
const primaryRaw = item[collection.value.schema.primaryKey];
|
|
4306
4488
|
const originalId = typeof primaryRaw === "string" ? primaryRaw : String(primaryRaw ?? "");
|
|
@@ -4402,7 +4584,8 @@ var CollectionView_default = /* @__PURE__ */ defineComponent({
|
|
|
4402
4584
|
viewing.value = null;
|
|
4403
4585
|
return;
|
|
4404
4586
|
}
|
|
4405
|
-
|
|
4587
|
+
const match = findItemById(selected) ?? null;
|
|
4588
|
+
viewing.value = match;
|
|
4406
4589
|
}
|
|
4407
4590
|
/** Title for the open-mode header: the record's primary-key value
|
|
4408
4591
|
* (e.g. `INV-2026-0001`), falling back to the collection title.
|
|
@@ -4496,19 +4679,17 @@ var CollectionView_default = /* @__PURE__ */ defineComponent({
|
|
|
4496
4679
|
/** Whether a `toggle` field reads as checked: its projected enum field
|
|
4497
4680
|
* currently equals `onValue`. The toggle stores nothing itself. */
|
|
4498
4681
|
function toggleChecked(item, field) {
|
|
4499
|
-
return field.
|
|
4682
|
+
return field.type === "toggle" && String(item[field.field] ?? "") === field.onValue;
|
|
4500
4683
|
}
|
|
4501
4684
|
/** Flip a `toggle`: write the projected enum field to `offValue` when
|
|
4502
4685
|
* currently checked, else `onValue`. Reuses the inline-edit PUT path
|
|
4503
4686
|
* (optimistic + rollback) — the toggle has no value of its own. */
|
|
4504
4687
|
function commitToggle(item, field) {
|
|
4688
|
+
if (field.type !== "toggle" || !collection.value) return;
|
|
4505
4689
|
const targetKey = field.field;
|
|
4506
|
-
if (!targetKey || !collection.value) return;
|
|
4507
4690
|
const enumField = collection.value.schema.fields[targetKey];
|
|
4508
4691
|
if (!enumField) return;
|
|
4509
|
-
|
|
4510
|
-
if (next === void 0) return;
|
|
4511
|
-
commitInlineEdit(item, targetKey, enumField, next);
|
|
4692
|
+
commitInlineEdit(item, targetKey, enumField, toggleChecked(item, field) ? field.offValue : field.onValue);
|
|
4512
4693
|
}
|
|
4513
4694
|
async function confirmDelete(item) {
|
|
4514
4695
|
if (!collection.value) return;
|