@mulmoclaude/collection-plugin 0.2.2 → 0.4.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/chunk-CKQMccvm.cjs +28 -0
- package/dist/core/actionVisible.d.ts +1 -0
- package/dist/core/actionVisible.d.ts.map +1 -0
- package/dist/core/calendarGrid.d.ts +1 -0
- package/dist/core/calendarGrid.d.ts.map +1 -0
- package/dist/core/deriveAll.d.ts +2 -1
- package/dist/core/deriveAll.d.ts.map +1 -0
- package/dist/core/derivedFormula.d.ts +1 -0
- package/dist/core/derivedFormula.d.ts.map +1 -0
- package/dist/core/draft.d.ts +3 -2
- package/dist/core/draft.d.ts.map +1 -0
- package/dist/core/enumColors.d.ts +2 -1
- package/dist/core/enumColors.d.ts.map +1 -0
- package/dist/core/errorMessage.d.ts +5 -0
- package/dist/core/errorMessage.d.ts.map +1 -0
- package/dist/core/itemLabel.d.ts +2 -1
- package/dist/core/itemLabel.d.ts.map +1 -0
- package/dist/core/presentCollection.d.ts +2 -1
- package/dist/core/presentCollection.d.ts.map +1 -0
- package/dist/core/promptSafety.d.ts +2 -0
- package/dist/core/promptSafety.d.ts.map +1 -0
- package/dist/core/schema.d.ts +1 -0
- package/dist/core/schema.d.ts.map +1 -0
- package/dist/core/shortHexId.d.ts +9 -0
- package/dist/core/shortHexId.d.ts.map +1 -0
- package/dist/core/sortItems.d.ts +2 -1
- package/dist/core/sortItems.d.ts.map +1 -0
- package/dist/core/uiTypes.d.ts +54 -1
- package/dist/core/uiTypes.d.ts.map +1 -0
- package/dist/{deriveAll-B5spcUIb.js → deriveAll-BHcs1erT.js} +2 -20
- package/dist/deriveAll-BHcs1erT.js.map +1 -0
- package/dist/{deriveAll-Do0v6Xjb.cjs → deriveAll-BJ0Lvm1Q.cjs} +1 -31
- package/dist/deriveAll-BJ0Lvm1Q.cjs.map +1 -0
- package/dist/index.cjs +49 -712
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +15 -11
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +4 -670
- package/dist/index.js.map +1 -1
- package/dist/promptSafety-BSO2gdsg.cjs +961 -0
- package/dist/promptSafety-BSO2gdsg.cjs.map +1 -0
- package/dist/promptSafety-DShN_VCG.js +698 -0
- package/dist/promptSafety-DShN_VCG.js.map +1 -0
- package/dist/schema-DyfSfVzh.js +21 -0
- package/dist/schema-DyfSfVzh.js.map +1 -0
- package/dist/schema-LrmrgW12.cjs +32 -0
- package/dist/schema-LrmrgW12.cjs.map +1 -0
- package/dist/server/atomic.d.ts +1 -0
- package/dist/server/atomic.d.ts.map +1 -0
- package/dist/server/delete.d.ts +2 -1
- package/dist/server/delete.d.ts.map +1 -0
- package/dist/server/derive.d.ts +4 -3
- package/dist/server/derive.d.ts.map +1 -0
- package/dist/server/discoveredCollection.d.ts +2 -1
- package/dist/server/discoveredCollection.d.ts.map +1 -0
- package/dist/server/discovery.d.ts +4 -3
- package/dist/server/discovery.d.ts.map +1 -0
- package/dist/server/host.d.ts +1 -0
- package/dist/server/host.d.ts.map +1 -0
- package/dist/server/index.d.ts +12 -11
- package/dist/server/index.d.ts.map +1 -0
- package/dist/server/io.d.ts +3 -2
- package/dist/server/io.d.ts.map +1 -0
- package/dist/server/paths.d.ts +1 -0
- package/dist/server/paths.d.ts.map +1 -0
- package/dist/server/spawn.d.ts +3 -2
- package/dist/server/spawn.d.ts.map +1 -0
- package/dist/server/templatePath.d.ts +1 -0
- package/dist/server/templatePath.d.ts.map +1 -0
- package/dist/server/util.d.ts +1 -0
- package/dist/server/util.d.ts.map +1 -0
- package/dist/server/validate.d.ts +3 -2
- package/dist/server/validate.d.ts.map +1 -0
- package/dist/server/views.d.ts +3 -2
- package/dist/server/views.d.ts.map +1 -0
- package/dist/server.cjs +6 -26
- package/dist/server.cjs.map +1 -1
- package/dist/server.js +2 -1
- package/dist/server.js.map +1 -1
- package/dist/style.css +2664 -0
- package/dist/vue/collectionViewMode.d.ts +12 -0
- package/dist/vue/collectionViewMode.d.ts.map +1 -0
- package/dist/vue/components/CollectionCalendarView.vue.d.ts +27 -0
- package/dist/vue/components/CollectionCalendarView.vue.d.ts.map +1 -0
- package/dist/vue/components/CollectionCustomView.vue.d.ts +9 -0
- package/dist/vue/components/CollectionCustomView.vue.d.ts.map +1 -0
- package/dist/vue/components/CollectionDayView.vue.d.ts +40 -0
- package/dist/vue/components/CollectionDayView.vue.d.ts.map +1 -0
- package/dist/vue/components/CollectionEmbedView.vue.d.ts +9 -0
- package/dist/vue/components/CollectionEmbedView.vue.d.ts.map +1 -0
- package/dist/vue/components/CollectionKanbanView.vue.d.ts +23 -0
- package/dist/vue/components/CollectionKanbanView.vue.d.ts.map +1 -0
- package/dist/vue/components/CollectionRecordModal.vue.d.ts +18 -0
- package/dist/vue/components/CollectionRecordModal.vue.d.ts.map +1 -0
- package/dist/vue/components/CollectionRecordPanel.vue.d.ts +46 -0
- package/dist/vue/components/CollectionRecordPanel.vue.d.ts.map +1 -0
- package/dist/vue/components/CollectionView.vue.d.ts +41 -0
- package/dist/vue/components/CollectionView.vue.d.ts.map +1 -0
- package/dist/vue/components/CollectionViewConfigModal.vue.d.ts +16 -0
- package/dist/vue/components/CollectionViewConfigModal.vue.d.ts.map +1 -0
- package/dist/vue/components/CollectionsIndexView.vue.d.ts +4 -0
- package/dist/vue/components/CollectionsIndexView.vue.d.ts.map +1 -0
- package/dist/vue/components/FeedsView.vue.d.ts +4 -0
- package/dist/vue/components/FeedsView.vue.d.ts.map +1 -0
- package/dist/vue/index.d.ts +16 -0
- package/dist/vue/index.d.ts.map +1 -0
- package/dist/vue/lang/de.d.ts +4 -0
- package/dist/vue/lang/de.d.ts.map +1 -0
- package/dist/vue/lang/en.d.ts +98 -0
- package/dist/vue/lang/en.d.ts.map +1 -0
- package/dist/vue/lang/es.d.ts +4 -0
- package/dist/vue/lang/es.d.ts.map +1 -0
- package/dist/vue/lang/fr.d.ts +4 -0
- package/dist/vue/lang/fr.d.ts.map +1 -0
- package/dist/vue/lang/index.d.ts +111 -0
- package/dist/vue/lang/index.d.ts.map +1 -0
- package/dist/vue/lang/ja.d.ts +4 -0
- package/dist/vue/lang/ja.d.ts.map +1 -0
- package/dist/vue/lang/ko.d.ts +4 -0
- package/dist/vue/lang/ko.d.ts.map +1 -0
- package/dist/vue/lang/ptBR.d.ts +4 -0
- package/dist/vue/lang/ptBR.d.ts.map +1 -0
- package/dist/vue/lang/zh.d.ts +4 -0
- package/dist/vue/lang/zh.d.ts.map +1 -0
- package/dist/vue/uiContext.d.ts +169 -0
- package/dist/vue/uiContext.d.ts.map +1 -0
- package/dist/vue/useCollectionRendering.d.ts +30 -0
- package/dist/vue/useCollectionRendering.d.ts.map +1 -0
- package/dist/vue.cjs +4713 -0
- package/dist/vue.cjs.map +1 -0
- package/dist/vue.js +4693 -0
- package/dist/vue.js.map +1 -0
- package/package.json +21 -6
- package/dist/deriveAll-B5spcUIb.js.map +0 -1
- package/dist/deriveAll-Do0v6Xjb.cjs.map +0 -1
package/dist/vue.cjs
ADDED
|
@@ -0,0 +1,4713 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
const require_chunk = require("./chunk-CKQMccvm.cjs");
|
|
3
|
+
const require_promptSafety = require("./promptSafety-BSO2gdsg.cjs");
|
|
4
|
+
const require_deriveAll = require("./deriveAll-BJ0Lvm1Q.cjs");
|
|
5
|
+
let vue = require("vue");
|
|
6
|
+
let vue_i18n = require("vue-i18n");
|
|
7
|
+
let vuedraggable = require("vuedraggable");
|
|
8
|
+
vuedraggable = require_chunk.__toESM(vuedraggable, 1);
|
|
9
|
+
//#region src/vue/uiContext.ts
|
|
10
|
+
var current = null;
|
|
11
|
+
/** Wire the collection view layer to a host. Call once at app startup. */
|
|
12
|
+
function configureCollectionUi(capabilities) {
|
|
13
|
+
current = capabilities;
|
|
14
|
+
}
|
|
15
|
+
function collectionUi() {
|
|
16
|
+
if (current === null) throw new Error("@mulmoclaude/collection-plugin/vue: configureCollectionUi() was not called by the host");
|
|
17
|
+
return current;
|
|
18
|
+
}
|
|
19
|
+
//#endregion
|
|
20
|
+
//#region src/vue/useCollectionRendering.ts
|
|
21
|
+
function useCollectionRendering(collection, locale) {
|
|
22
|
+
const refCache = (0, vue.ref)({});
|
|
23
|
+
const refRecordCache = (0, vue.ref)({});
|
|
24
|
+
const embedCache = (0, vue.ref)({});
|
|
25
|
+
function resetLinkedCaches() {
|
|
26
|
+
refCache.value = {};
|
|
27
|
+
refRecordCache.value = {};
|
|
28
|
+
embedCache.value = {};
|
|
29
|
+
}
|
|
30
|
+
function uniqueRefTargets(schema) {
|
|
31
|
+
const targets = /* @__PURE__ */ new Set();
|
|
32
|
+
const walk = (fields) => {
|
|
33
|
+
for (const field of Object.values(fields)) {
|
|
34
|
+
if (field.type === "ref" && typeof field.to === "string" && field.to.length > 0) targets.add(field.to);
|
|
35
|
+
if (field.type === "table" && field.of) walk(field.of);
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
walk(schema.fields);
|
|
39
|
+
return [...targets];
|
|
40
|
+
}
|
|
41
|
+
function uniqueEmbedTargets(schema) {
|
|
42
|
+
const targets = /* @__PURE__ */ new Set();
|
|
43
|
+
for (const field of Object.values(schema.fields)) if (field.type === "embed" && typeof field.to === "string" && field.to.length > 0) targets.add(field.to);
|
|
44
|
+
return [...targets];
|
|
45
|
+
}
|
|
46
|
+
function buildRefDisplayMap(detail) {
|
|
47
|
+
const { fields, primaryKey } = detail.collection.schema;
|
|
48
|
+
const displayField = "name" in fields ? "name" : "title" in fields ? "title" : primaryKey;
|
|
49
|
+
const map = {};
|
|
50
|
+
for (const item of detail.items) {
|
|
51
|
+
const slugRaw = item[primaryKey];
|
|
52
|
+
if (typeof slugRaw !== "string" || slugRaw.length === 0) continue;
|
|
53
|
+
const displayRaw = item[displayField];
|
|
54
|
+
map[slugRaw] = typeof displayRaw === "string" && displayRaw.length > 0 ? displayRaw : slugRaw;
|
|
55
|
+
}
|
|
56
|
+
return map;
|
|
57
|
+
}
|
|
58
|
+
function buildRefRecordMap(detail) {
|
|
59
|
+
const { schema } = detail.collection;
|
|
60
|
+
const map = {};
|
|
61
|
+
for (const item of detail.items) {
|
|
62
|
+
const slugRaw = item[schema.primaryKey];
|
|
63
|
+
if (typeof slugRaw === "string" && slugRaw.length > 0) map[slugRaw] = require_deriveAll.deriveAll(schema, item, {});
|
|
64
|
+
}
|
|
65
|
+
return map;
|
|
66
|
+
}
|
|
67
|
+
async function loadLinkedCollections(schema, expectedSlug) {
|
|
68
|
+
const refTargets = new Set(uniqueRefTargets(schema));
|
|
69
|
+
const embedTargets = new Set(uniqueEmbedTargets(schema));
|
|
70
|
+
const allTargets = [...new Set([...refTargets, ...embedTargets])];
|
|
71
|
+
if (allTargets.length === 0) return;
|
|
72
|
+
const binding = collectionUi();
|
|
73
|
+
const results = await Promise.all(allTargets.map(async (target) => {
|
|
74
|
+
try {
|
|
75
|
+
return {
|
|
76
|
+
target,
|
|
77
|
+
result: await binding.fetchCollectionDetail(target)
|
|
78
|
+
};
|
|
79
|
+
} catch {
|
|
80
|
+
return {
|
|
81
|
+
target,
|
|
82
|
+
result: { ok: false }
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
}));
|
|
86
|
+
if (collection.value?.slug !== expectedSlug) return;
|
|
87
|
+
const nextRef = {};
|
|
88
|
+
const nextRefRecords = {};
|
|
89
|
+
const nextEmbed = {};
|
|
90
|
+
for (const { target, result } of results) {
|
|
91
|
+
if (!result.ok) continue;
|
|
92
|
+
if (refTargets.has(target)) {
|
|
93
|
+
nextRef[target] = buildRefDisplayMap(result.data);
|
|
94
|
+
nextRefRecords[target] = buildRefRecordMap(result.data);
|
|
95
|
+
}
|
|
96
|
+
if (embedTargets.has(target)) nextEmbed[target] = {
|
|
97
|
+
schema: result.data.collection.schema,
|
|
98
|
+
items: result.data.items
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
refCache.value = nextRef;
|
|
102
|
+
refRecordCache.value = nextRefRecords;
|
|
103
|
+
embedCache.value = nextEmbed;
|
|
104
|
+
}
|
|
105
|
+
function refDisplay(targetSlug, itemSlug) {
|
|
106
|
+
const map = refCache.value[targetSlug];
|
|
107
|
+
return map && map[itemSlug] || itemSlug;
|
|
108
|
+
}
|
|
109
|
+
function refOptions(targetSlug) {
|
|
110
|
+
const map = refCache.value[targetSlug];
|
|
111
|
+
if (!map) return [];
|
|
112
|
+
return Object.entries(map).map(([slug, display]) => ({
|
|
113
|
+
slug,
|
|
114
|
+
display
|
|
115
|
+
})).sort((left, right) => left.display.localeCompare(right.display));
|
|
116
|
+
}
|
|
117
|
+
function resolveEmbed(field) {
|
|
118
|
+
if (field.type !== "embed" || !field.to || !field.id) return {
|
|
119
|
+
schema: null,
|
|
120
|
+
item: null
|
|
121
|
+
};
|
|
122
|
+
const data = embedCache.value[field.to];
|
|
123
|
+
if (!data) return {
|
|
124
|
+
schema: null,
|
|
125
|
+
item: null
|
|
126
|
+
};
|
|
127
|
+
const item = data.items.find((entry) => String(entry[data.schema.primaryKey] ?? "") === field.id) ?? null;
|
|
128
|
+
return {
|
|
129
|
+
schema: data.schema,
|
|
130
|
+
item
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
function embedValue(field, value, record) {
|
|
134
|
+
if (field.type === "money") return formatMoney(value, resolveCurrency(field, record), locale.value);
|
|
135
|
+
return detailText(value);
|
|
136
|
+
}
|
|
137
|
+
const embedViews = (0, vue.computed)(() => {
|
|
138
|
+
const out = {};
|
|
139
|
+
if (!collection.value) return out;
|
|
140
|
+
for (const [key, field] of Object.entries(collection.value.schema.fields)) {
|
|
141
|
+
if (field.type !== "embed") continue;
|
|
142
|
+
const { schema, item } = resolveEmbed(field);
|
|
143
|
+
const rows = [];
|
|
144
|
+
if (schema && item) for (const [subKey, subField] of Object.entries(schema.fields)) {
|
|
145
|
+
const value = item[subKey];
|
|
146
|
+
if (value === void 0 || value === null || value === "") continue;
|
|
147
|
+
rows.push({
|
|
148
|
+
key: subKey,
|
|
149
|
+
label: subField.label,
|
|
150
|
+
type: subField.type,
|
|
151
|
+
value,
|
|
152
|
+
display: embedValue(subField, value, item)
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
out[key] = {
|
|
156
|
+
found: Boolean(item),
|
|
157
|
+
rows,
|
|
158
|
+
targetSlug: field.to ?? "",
|
|
159
|
+
recordId: field.id ?? ""
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
return out;
|
|
163
|
+
});
|
|
164
|
+
function resolveCurrency(field, record) {
|
|
165
|
+
if (field.currencyField && record) {
|
|
166
|
+
const code = record[field.currencyField];
|
|
167
|
+
if (typeof code === "string" && code.trim().length > 0) return code;
|
|
168
|
+
}
|
|
169
|
+
return field.currency;
|
|
170
|
+
}
|
|
171
|
+
function currencySymbol(currency) {
|
|
172
|
+
const code = currency && currency.length > 0 ? currency : "USD";
|
|
173
|
+
try {
|
|
174
|
+
return new Intl.NumberFormat(locale.value, {
|
|
175
|
+
style: "currency",
|
|
176
|
+
currency: code
|
|
177
|
+
}).formatToParts(0).find((entry) => entry.type === "currency")?.value ?? code;
|
|
178
|
+
} catch {
|
|
179
|
+
return code;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
function formatMoney(value, currency, displayLocale) {
|
|
183
|
+
if (value === void 0 || value === "") return "—";
|
|
184
|
+
const amount = typeof value === "number" ? value : Number(value);
|
|
185
|
+
if (!Number.isFinite(amount)) return String(value);
|
|
186
|
+
const currencyCode = currency && currency.length > 0 ? currency : "USD";
|
|
187
|
+
try {
|
|
188
|
+
return new Intl.NumberFormat(displayLocale, {
|
|
189
|
+
style: "currency",
|
|
190
|
+
currency: currencyCode
|
|
191
|
+
}).format(amount);
|
|
192
|
+
} catch {
|
|
193
|
+
return String(amount);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
function formatCell(value, type) {
|
|
197
|
+
if (value === void 0 || value === null || value === "") return "—";
|
|
198
|
+
if (type === "markdown" && typeof value === "string") return value.length > 80 ? `${value.slice(0, 80)}…` : value;
|
|
199
|
+
if (typeof value === "string" || typeof value === "number") return String(value);
|
|
200
|
+
return JSON.stringify(value);
|
|
201
|
+
}
|
|
202
|
+
function isExternalUrl(value) {
|
|
203
|
+
return typeof value === "string" && /^https?:\/\//i.test(value);
|
|
204
|
+
}
|
|
205
|
+
function artifactUrl(value) {
|
|
206
|
+
return collectionUi().fileAssetUrl(value);
|
|
207
|
+
}
|
|
208
|
+
function fileRoutePath(value) {
|
|
209
|
+
return collectionUi().fileRoutePath(value);
|
|
210
|
+
}
|
|
211
|
+
function detailText(value) {
|
|
212
|
+
if (value === void 0 || value === null || value === "") return "—";
|
|
213
|
+
return String(value);
|
|
214
|
+
}
|
|
215
|
+
function tableRows(value) {
|
|
216
|
+
if (!Array.isArray(value)) return [];
|
|
217
|
+
return value.filter((row) => Boolean(row) && typeof row === "object" && !Array.isArray(row));
|
|
218
|
+
}
|
|
219
|
+
function hasTableRows(value) {
|
|
220
|
+
return tableRows(value).length > 0;
|
|
221
|
+
}
|
|
222
|
+
function formatSubCell(subField, value, record) {
|
|
223
|
+
if (subField.type === "money") return formatMoney(value, resolveCurrency(subField, record), locale.value);
|
|
224
|
+
if (subField.type === "ref" && subField.to && typeof value === "string" && value.length > 0) return refDisplay(subField.to, value);
|
|
225
|
+
return formatCell(value, subField.type);
|
|
226
|
+
}
|
|
227
|
+
function inputTypeFor(type) {
|
|
228
|
+
if (type === "email") return "email";
|
|
229
|
+
if (type === "number") return "number";
|
|
230
|
+
if (type === "money") return "number";
|
|
231
|
+
if (type === "date") return "date";
|
|
232
|
+
if (type === "datetime") return "datetime-local";
|
|
233
|
+
return "text";
|
|
234
|
+
}
|
|
235
|
+
function evaluateDerivedAgainstItem(field, fieldKey, item) {
|
|
236
|
+
if (!field.formula || !collection.value) return null;
|
|
237
|
+
const result = require_deriveAll.deriveAll(collection.value.schema, item, refRecordCache.value)[fieldKey];
|
|
238
|
+
return typeof result === "number" && Number.isFinite(result) ? result : null;
|
|
239
|
+
}
|
|
240
|
+
function derivedDisplay(field, computedValue, record) {
|
|
241
|
+
if (computedValue === null || computedValue === void 0) return "—";
|
|
242
|
+
if (field.display === "money") return formatMoney(computedValue, resolveCurrency(field, record), locale.value);
|
|
243
|
+
return formatCell(computedValue, field.display ?? "number");
|
|
244
|
+
}
|
|
245
|
+
return {
|
|
246
|
+
refCache,
|
|
247
|
+
refRecordCache,
|
|
248
|
+
embedCache,
|
|
249
|
+
resetLinkedCaches,
|
|
250
|
+
loadLinkedCollections,
|
|
251
|
+
refDisplay,
|
|
252
|
+
refOptions,
|
|
253
|
+
embedViews,
|
|
254
|
+
resolveCurrency,
|
|
255
|
+
currencySymbol,
|
|
256
|
+
formatMoney,
|
|
257
|
+
formatCell,
|
|
258
|
+
detailText,
|
|
259
|
+
isExternalUrl,
|
|
260
|
+
artifactUrl,
|
|
261
|
+
fileRoutePath,
|
|
262
|
+
tableRows,
|
|
263
|
+
hasTableRows,
|
|
264
|
+
formatSubCell,
|
|
265
|
+
inputTypeFor,
|
|
266
|
+
deriveAll: require_deriveAll.deriveAll,
|
|
267
|
+
evaluateDerivedAgainstItem,
|
|
268
|
+
derivedDisplay
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
//#endregion
|
|
272
|
+
//#region src/vue/collectionViewMode.ts
|
|
273
|
+
var STORAGE_KEY = "collection_view_modes";
|
|
274
|
+
var SORT_STORAGE_KEY = "collection_sorts";
|
|
275
|
+
var BUILT_IN_MODES = [
|
|
276
|
+
"table",
|
|
277
|
+
"calendar",
|
|
278
|
+
"kanban"
|
|
279
|
+
];
|
|
280
|
+
/** A persisted mode is valid if it's a known built-in OR any `custom:<id>`
|
|
281
|
+
* key (the id is validated against the live schema at render time, so an
|
|
282
|
+
* unknown custom id simply collapses to the table there). Takes `unknown`
|
|
283
|
+
* and type-guards `string` first: a corrupted localStorage entry could hold a
|
|
284
|
+
* number/object, and calling `.startsWith` on that would throw. */
|
|
285
|
+
function isValidViewMode(value) {
|
|
286
|
+
return typeof value === "string" && (BUILT_IN_MODES.includes(value) || value.startsWith("custom:"));
|
|
287
|
+
}
|
|
288
|
+
function readAll() {
|
|
289
|
+
try {
|
|
290
|
+
const raw = localStorage.getItem(STORAGE_KEY);
|
|
291
|
+
if (!raw) return {};
|
|
292
|
+
const parsed = JSON.parse(raw);
|
|
293
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
|
|
294
|
+
} catch {
|
|
295
|
+
return {};
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
function readCollectionViewMode(slug) {
|
|
299
|
+
const stored = readAll()[slug];
|
|
300
|
+
return isValidViewMode(stored) ? stored : null;
|
|
301
|
+
}
|
|
302
|
+
function writeCollectionViewMode(slug, view) {
|
|
303
|
+
try {
|
|
304
|
+
const all = readAll();
|
|
305
|
+
all[slug] = view;
|
|
306
|
+
localStorage.setItem(STORAGE_KEY, JSON.stringify(all));
|
|
307
|
+
} catch {}
|
|
308
|
+
}
|
|
309
|
+
function isSortState(value) {
|
|
310
|
+
if (!value || typeof value !== "object") return false;
|
|
311
|
+
const rec = value;
|
|
312
|
+
return typeof rec.field === "string" && (rec.direction === "asc" || rec.direction === "desc");
|
|
313
|
+
}
|
|
314
|
+
function readAllSorts() {
|
|
315
|
+
try {
|
|
316
|
+
const raw = localStorage.getItem(SORT_STORAGE_KEY);
|
|
317
|
+
if (!raw) return {};
|
|
318
|
+
const parsed = JSON.parse(raw);
|
|
319
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return {};
|
|
320
|
+
const out = {};
|
|
321
|
+
for (const [slug, value] of Object.entries(parsed)) if (isSortState(value)) out[slug] = value;
|
|
322
|
+
return out;
|
|
323
|
+
} catch {
|
|
324
|
+
return {};
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
function readCollectionSort(slug) {
|
|
328
|
+
return readAllSorts()[slug] ?? null;
|
|
329
|
+
}
|
|
330
|
+
/** Persist (or, when `sort` is null, clear) the slug's active column sort. */
|
|
331
|
+
function writeCollectionSort(slug, sort) {
|
|
332
|
+
try {
|
|
333
|
+
const all = Object.fromEntries(Object.entries(readAllSorts()).filter(([key]) => key !== slug));
|
|
334
|
+
if (sort) all[slug] = sort;
|
|
335
|
+
localStorage.setItem(SORT_STORAGE_KEY, JSON.stringify(all));
|
|
336
|
+
} catch {}
|
|
337
|
+
}
|
|
338
|
+
//#endregion
|
|
339
|
+
//#region src/vue/components/CollectionRecordModal.vue
|
|
340
|
+
var CollectionRecordModal_default = /* @__PURE__ */ (0, vue.defineComponent)({
|
|
341
|
+
__name: "CollectionRecordModal",
|
|
342
|
+
emits: ["close"],
|
|
343
|
+
setup(__props, { emit: __emit }) {
|
|
344
|
+
const emit = __emit;
|
|
345
|
+
const dialogEl = (0, vue.ref)(null);
|
|
346
|
+
let previouslyFocused = null;
|
|
347
|
+
const FOCUSABLE_SELECTOR = [
|
|
348
|
+
"a[href]",
|
|
349
|
+
"button:not([disabled])",
|
|
350
|
+
"input:not([disabled])",
|
|
351
|
+
"select:not([disabled])",
|
|
352
|
+
"textarea:not([disabled])",
|
|
353
|
+
"[tabindex]:not([tabindex=\"-1\"])"
|
|
354
|
+
].join(",");
|
|
355
|
+
/** Visible, focusable controls inside the dialog, in DOM order. */
|
|
356
|
+
function focusableItems() {
|
|
357
|
+
if (!dialogEl.value) return [];
|
|
358
|
+
return Array.from(dialogEl.value.querySelectorAll(FOCUSABLE_SELECTOR)).filter((node) => node.offsetParent !== null);
|
|
359
|
+
}
|
|
360
|
+
/** Trap Tab / Shift+Tab inside the dialog so focus can't escape to the
|
|
361
|
+
* page behind the overlay. Wraps at both ends; the dialog container
|
|
362
|
+
* itself (tabindex -1) counts as "before the first item". */
|
|
363
|
+
function onTab(event) {
|
|
364
|
+
const items = focusableItems();
|
|
365
|
+
if (items.length === 0) {
|
|
366
|
+
event.preventDefault();
|
|
367
|
+
dialogEl.value?.focus();
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
const [first] = items;
|
|
371
|
+
const last = items[items.length - 1];
|
|
372
|
+
const active = document.activeElement;
|
|
373
|
+
if (event.shiftKey) {
|
|
374
|
+
if (active === first || active === dialogEl.value) {
|
|
375
|
+
event.preventDefault();
|
|
376
|
+
last.focus();
|
|
377
|
+
}
|
|
378
|
+
} else if (active === last) {
|
|
379
|
+
event.preventDefault();
|
|
380
|
+
first.focus();
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
(0, vue.onMounted)(async () => {
|
|
384
|
+
previouslyFocused = document.activeElement instanceof HTMLElement ? document.activeElement : null;
|
|
385
|
+
await (0, vue.nextTick)();
|
|
386
|
+
dialogEl.value?.focus();
|
|
387
|
+
});
|
|
388
|
+
(0, vue.onBeforeUnmount)(() => {
|
|
389
|
+
previouslyFocused?.focus?.();
|
|
390
|
+
});
|
|
391
|
+
return (_ctx, _cache) => {
|
|
392
|
+
return (0, vue.openBlock)(), (0, vue.createBlock)(vue.Teleport, { to: "body" }, [(0, vue.createElementVNode)("div", {
|
|
393
|
+
class: "fixed inset-0 z-40 flex items-center justify-center bg-slate-900/40 p-4",
|
|
394
|
+
"data-testid": "collections-record-modal",
|
|
395
|
+
onClick: _cache[1] || (_cache[1] = (0, vue.withModifiers)(($event) => emit("close"), ["self"]))
|
|
396
|
+
}, [(0, vue.createElementVNode)("div", {
|
|
397
|
+
ref_key: "dialogEl",
|
|
398
|
+
ref: dialogEl,
|
|
399
|
+
class: "flex max-h-[85vh] w-full max-w-2xl flex-col overflow-hidden rounded-2xl bg-white shadow-xl focus:outline-none",
|
|
400
|
+
role: "dialog",
|
|
401
|
+
"aria-modal": "true",
|
|
402
|
+
tabindex: "-1",
|
|
403
|
+
onKeydown: [_cache[0] || (_cache[0] = (0, vue.withKeys)(($event) => emit("close"), ["esc"])), (0, vue.withKeys)(onTab, ["tab"])]
|
|
404
|
+
}, [(0, vue.renderSlot)(_ctx.$slots, "default")], 544)])]);
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
});
|
|
408
|
+
//#endregion
|
|
409
|
+
//#region src/vue/lang/index.ts
|
|
410
|
+
var i18n = (0, vue_i18n.createI18n)({
|
|
411
|
+
legacy: false,
|
|
412
|
+
locale: "en",
|
|
413
|
+
fallbackLocale: "en",
|
|
414
|
+
messages: {
|
|
415
|
+
en: {
|
|
416
|
+
collectionsView: {
|
|
417
|
+
addCollectionLabel: "Collection",
|
|
418
|
+
addCollectionPrompt: "Help me create a new collection. First read `config/helps/collection-skills.md` for the schema-driven collection conventions. Then use the `presentForm` tool (do not use AskUserQuestion) to ask me what kind of data I want to track, and author the schema.json and SKILL.md from my answers.",
|
|
419
|
+
addFeedTitle: "Add a feed",
|
|
420
|
+
addFeedHint: "Paste a feed or API URL — I'll fetch it and work out the title and fields for you.",
|
|
421
|
+
addFeedPrompt: "Add a new data-source feed from this URL: {url}\n\nFirst Read `config/helps/feeds.md` and follow it exactly. Fetch that URL yourself, inspect the response to infer a sensible title and fields, then author `feeds/<slug>/schema.json` as the help describes — do NOT ask me any questions; work it all out from the data. Opening the feed loads its items automatically; when you're done, tell me it's registered (no need to mention Refresh).",
|
|
422
|
+
title: "Collections",
|
|
423
|
+
backToIndex: "Back to collections",
|
|
424
|
+
indexEmpty: "No collections installed. Star a skill that ships a schema from the Skills page to see it here.",
|
|
425
|
+
editItem: "Edit",
|
|
426
|
+
openItem: "Open {id}",
|
|
427
|
+
confirmDelete: "Delete this item? This cannot be undone.",
|
|
428
|
+
deleteFeed: "Delete feed",
|
|
429
|
+
confirmDeleteFeed: "Delete the \"{title}\" feed and all its fetched records? This cannot be undone.",
|
|
430
|
+
deleteCollection: "Delete collection",
|
|
431
|
+
confirmDeleteCollection: "Delete the entire \"{title}\" collection, including all its records? A restorable backup is archived first.",
|
|
432
|
+
itemsEmpty: "No items yet. Click + to add one.",
|
|
433
|
+
notFound: "Collection not found",
|
|
434
|
+
loadFailed: "Failed to load",
|
|
435
|
+
requiredField: "This field is required",
|
|
436
|
+
selectPlaceholder: "Select…",
|
|
437
|
+
inlineSaveFailed: "Couldn't save change: {error}",
|
|
438
|
+
addRow: "Add row",
|
|
439
|
+
removeRow: "Remove row",
|
|
440
|
+
noRows: "No rows yet",
|
|
441
|
+
tableSummary: "{count} items",
|
|
442
|
+
embedMissing: "No “{id}” record found in {collection}.",
|
|
443
|
+
embedCreate: "Set it up",
|
|
444
|
+
searchPlaceholder: "Search records…",
|
|
445
|
+
searchSummary: "Showing {shown} of {total}",
|
|
446
|
+
noMatchingItems: "No matching items",
|
|
447
|
+
clearSearch: "Clear search",
|
|
448
|
+
sortBy: "Sort by {field}",
|
|
449
|
+
openCollection: "Open {title}",
|
|
450
|
+
createTitle: "Add new",
|
|
451
|
+
derivedLabel: "Derived",
|
|
452
|
+
embedMissingTitle: "Embedded reference missing",
|
|
453
|
+
chat: "Chat",
|
|
454
|
+
refreshFeed: "Refresh",
|
|
455
|
+
refreshFailed: "Refresh failed: {error}",
|
|
456
|
+
feedChatSeed: "The \"{slug}\" feed is defined by the schema at `feeds/{slug}/schema.json` and its records live in `{dataPath}/` (one `<id>.json` per record). Using that schema and data, respond to this request: {message}",
|
|
457
|
+
feedsTitle: "Data-source feeds",
|
|
458
|
+
feedsEmpty: "No feeds registered yet.",
|
|
459
|
+
chatTitle: "Start a chat",
|
|
460
|
+
chatPlaceholder: "Describe what you want to do with this collection…",
|
|
461
|
+
chatStart: "Start chat",
|
|
462
|
+
viewToggle: "View",
|
|
463
|
+
viewTable: "Table",
|
|
464
|
+
viewCalendar: "Calendar",
|
|
465
|
+
calendarFieldLabel: "Calendar date field",
|
|
466
|
+
calendarPrevMonth: "Previous month",
|
|
467
|
+
calendarNextMonth: "Next month",
|
|
468
|
+
calendarToday: "Today",
|
|
469
|
+
calendarNoDate: "No date",
|
|
470
|
+
calendarCreateOn: "Create on {date}",
|
|
471
|
+
dayViewOpen: "Open day view for {date}",
|
|
472
|
+
dayViewAllDay: "All day",
|
|
473
|
+
dayViewEmpty: "No items on this day",
|
|
474
|
+
dayViewClose: "Close day view",
|
|
475
|
+
viewKanban: "Kanban",
|
|
476
|
+
kanbanFieldLabel: "Kanban group field",
|
|
477
|
+
kanbanUncategorized: "Uncategorized",
|
|
478
|
+
kanbanOpenCard: "Open {label}",
|
|
479
|
+
addView: "Add view",
|
|
480
|
+
config: {
|
|
481
|
+
open: "Collection settings",
|
|
482
|
+
title: "{title} · settings",
|
|
483
|
+
viewsHeading: "Custom views",
|
|
484
|
+
deleteView: "Delete {label}",
|
|
485
|
+
confirmDelete: "Delete the \"{label}\" view? This removes its HTML file and unregisters it.",
|
|
486
|
+
empty: "No custom views yet."
|
|
487
|
+
},
|
|
488
|
+
customViewLoading: "Loading view…",
|
|
489
|
+
customViewError: "Couldn't load this view: {error}",
|
|
490
|
+
addViewPrompt: "I want to add a custom view to the \"{title}\" collection. Ask me what I want to see or edit, then author the HTML view file at {base}/views/your-view.html and register it in {base}/schema.json under `views[]` (capabilities [\"read\"] for a read-only view, [\"read\",\"write\"] if it edits records). Follow the custom-view help for the data contract.",
|
|
491
|
+
repair: "Repair",
|
|
492
|
+
dataIssuesDetected: "{count} record file(s) have data problems and may be missing from this view.",
|
|
493
|
+
repairPrompt: "The \"{title}\" collection has {count} record file(s) with data problems that keep them from appearing. Fix each one — Read the file, correct it, then Write it back:\n{issues}\n\nWhen you're done, call presentCollection to confirm the records load.",
|
|
494
|
+
source: {
|
|
495
|
+
user: "User",
|
|
496
|
+
project: "Project"
|
|
497
|
+
}
|
|
498
|
+
},
|
|
499
|
+
common: {
|
|
500
|
+
add: "Add",
|
|
501
|
+
cancel: "Cancel",
|
|
502
|
+
loading: "Loading...",
|
|
503
|
+
no: "No",
|
|
504
|
+
remove: "Remove",
|
|
505
|
+
save: "Save",
|
|
506
|
+
saving: "Saving...",
|
|
507
|
+
yes: "Yes"
|
|
508
|
+
}
|
|
509
|
+
},
|
|
510
|
+
ja: {
|
|
511
|
+
collectionsView: {
|
|
512
|
+
addCollectionLabel: "コレクション",
|
|
513
|
+
addCollectionPrompt: "新しいコレクションを作成したいです。まず `config/helps/collection-skills.md` を読んでスキーマ駆動コレクションの規約を確認してください。次に `presentForm` ツールを使って(AskUserQuestion は使わないで)どんなデータを管理したいか質問し、その回答をもとに schema.json と SKILL.md を作成してください。",
|
|
514
|
+
addFeedTitle: "フィードを追加",
|
|
515
|
+
addFeedHint: "フィードまたは API の URL を貼り付けてください。取得してタイトルとフィールドを自動で判断します。",
|
|
516
|
+
addFeedPrompt: "次の URL からデータソースフィードを追加してください: {url}\n\nまず `config/helps/feeds.md` を読み、その指示に正確に従ってください。その URL を自分で取得してレスポンスを調べ、ふさわしいタイトルとフィールドを推測し、ヘルプの説明どおりに `feeds/<slug>/schema.json` を作成してください。私には質問せず、データからすべて判断してください。フィードを開くとアイテムは自動で読み込まれます。完了したら登録できたことを伝えてください(Refresh を押すよう案内する必要はありません)。",
|
|
517
|
+
title: "コレクション",
|
|
518
|
+
backToIndex: "コレクション一覧に戻る",
|
|
519
|
+
indexEmpty: "インストール済みのコレクションがありません。スキーマを含むスキルを Skills ページからスター付けすると、ここに表示されます。",
|
|
520
|
+
editItem: "編集",
|
|
521
|
+
openItem: "{id} を開く",
|
|
522
|
+
confirmDelete: "この項目を削除しますか?元に戻せません。",
|
|
523
|
+
deleteFeed: "フィードを削除",
|
|
524
|
+
confirmDeleteFeed: "フィード「{title}」と取得済みのすべてのレコードを削除しますか?この操作は取り消せません。",
|
|
525
|
+
deleteCollection: "コレクションを削除",
|
|
526
|
+
confirmDeleteCollection: "コレクション「{title}」とそのすべてのレコードを削除しますか?削除前に復元可能なバックアップが保存されます。",
|
|
527
|
+
itemsEmpty: "まだ項目がありません。+ を押して追加してください。",
|
|
528
|
+
notFound: "コレクションが見つかりません",
|
|
529
|
+
loadFailed: "読み込みに失敗しました",
|
|
530
|
+
requiredField: "この項目は必須です",
|
|
531
|
+
selectPlaceholder: "選択…",
|
|
532
|
+
inlineSaveFailed: "変更を保存できませんでした: {error}",
|
|
533
|
+
addRow: "行を追加",
|
|
534
|
+
removeRow: "行を削除",
|
|
535
|
+
noRows: "行がありません",
|
|
536
|
+
tableSummary: "{count}件",
|
|
537
|
+
embedMissing: "{collection} に「{id}」のレコードが見つかりません。",
|
|
538
|
+
embedCreate: "設定する",
|
|
539
|
+
searchPlaceholder: "レコードを検索…",
|
|
540
|
+
searchSummary: "{total} 件中 {shown} 件を表示",
|
|
541
|
+
noMatchingItems: "一致する項目がありません",
|
|
542
|
+
clearSearch: "検索をクリア",
|
|
543
|
+
sortBy: "{field}で並べ替え",
|
|
544
|
+
openCollection: "{title} を開く",
|
|
545
|
+
createTitle: "新規追加",
|
|
546
|
+
derivedLabel: "計算値",
|
|
547
|
+
embedMissingTitle: "埋め込み参照が見つかりません",
|
|
548
|
+
chat: "チャット",
|
|
549
|
+
refreshFeed: "更新",
|
|
550
|
+
refreshFailed: "更新に失敗しました: {error}",
|
|
551
|
+
feedChatSeed: "フィード「{slug}」はスキーマ `feeds/{slug}/schema.json` で定義され、レコードは `{dataPath}/`(1 レコードにつき `<id>.json` 1 ファイル)に保存されています。このスキーマとデータを使って、次のリクエストに応えてください: {message}",
|
|
552
|
+
feedsTitle: "データソースフィード",
|
|
553
|
+
feedsEmpty: "登録されたフィードはありません。",
|
|
554
|
+
chatTitle: "チャットを開始",
|
|
555
|
+
chatPlaceholder: "このコレクションで行いたいことを入力してください…",
|
|
556
|
+
chatStart: "チャットを開始",
|
|
557
|
+
viewToggle: "表示",
|
|
558
|
+
viewTable: "テーブル",
|
|
559
|
+
viewCalendar: "カレンダー",
|
|
560
|
+
calendarFieldLabel: "カレンダーの日付フィールド",
|
|
561
|
+
calendarPrevMonth: "前の月",
|
|
562
|
+
calendarNextMonth: "次の月",
|
|
563
|
+
calendarToday: "今日",
|
|
564
|
+
calendarNoDate: "日付なし",
|
|
565
|
+
calendarCreateOn: "{date} に作成",
|
|
566
|
+
dayViewOpen: "{date} の日表示を開く",
|
|
567
|
+
dayViewAllDay: "終日",
|
|
568
|
+
dayViewEmpty: "この日の項目はありません",
|
|
569
|
+
dayViewClose: "日表示を閉じる",
|
|
570
|
+
viewKanban: "カンバン",
|
|
571
|
+
kanbanFieldLabel: "カンバンのグループフィールド",
|
|
572
|
+
kanbanUncategorized: "未分類",
|
|
573
|
+
kanbanOpenCard: "{label} を開く",
|
|
574
|
+
addView: "ビューを追加",
|
|
575
|
+
config: {
|
|
576
|
+
open: "コレクション設定",
|
|
577
|
+
title: "{title} · 設定",
|
|
578
|
+
viewsHeading: "カスタムビュー",
|
|
579
|
+
deleteView: "{label} を削除",
|
|
580
|
+
confirmDelete: "ビュー「{label}」を削除しますか? HTML ファイルを削除し、登録を解除します。",
|
|
581
|
+
empty: "カスタムビューはまだありません。"
|
|
582
|
+
},
|
|
583
|
+
customViewLoading: "ビューを読み込み中…",
|
|
584
|
+
customViewError: "このビューを読み込めませんでした: {error}",
|
|
585
|
+
addViewPrompt: "「{title}」コレクションにカスタムビューを追加したい。何を見たい/編集したいか質問してから、HTML ビューファイルを {base}/views/your-view.html に作成し、{base}/schema.json の `views[]` に登録して(読み取り専用なら capabilities [\"read\"]、レコードを編集するなら [\"read\",\"write\"])。データ契約は custom-view ヘルプに従ってください。",
|
|
586
|
+
repair: "修復",
|
|
587
|
+
dataIssuesDetected: "{count} 件のレコードファイルにデータの問題があり、この表示に出てこない可能性があります。",
|
|
588
|
+
repairPrompt: "コレクション「{title}」に、表示されない原因となるデータの問題を持つレコードファイルが {count} 件あります。それぞれを修正してください — ファイルを Read し、修正してから Write し直します:\n{issues}\n\n完了したら presentCollection を呼び出して、レコードが読み込めることを確認してください。",
|
|
589
|
+
source: {
|
|
590
|
+
user: "ユーザー",
|
|
591
|
+
project: "プロジェクト"
|
|
592
|
+
}
|
|
593
|
+
},
|
|
594
|
+
common: {
|
|
595
|
+
add: "追加",
|
|
596
|
+
cancel: "キャンセル",
|
|
597
|
+
loading: "読み込み中...",
|
|
598
|
+
no: "いいえ",
|
|
599
|
+
remove: "削除",
|
|
600
|
+
save: "保存",
|
|
601
|
+
saving: "保存中...",
|
|
602
|
+
yes: "はい"
|
|
603
|
+
}
|
|
604
|
+
},
|
|
605
|
+
zh: {
|
|
606
|
+
collectionsView: {
|
|
607
|
+
addCollectionLabel: "集合",
|
|
608
|
+
addCollectionPrompt: "帮我创建一个新的集合。请先阅读 `config/helps/collection-skills.md` 了解基于 schema 的集合约定。然后使用 `presentForm` 工具(不要使用 AskUserQuestion)询问我想跟踪哪种数据,并根据我的回答编写 schema.json 和 SKILL.md。",
|
|
609
|
+
addFeedTitle: "添加订阅源",
|
|
610
|
+
addFeedHint: "粘贴订阅源或 API 的 URL,我会抓取并自动推断标题和字段。",
|
|
611
|
+
addFeedPrompt: "从以下 URL 添加一个数据源订阅:{url}\n\n请先阅读 `config/helps/feeds.md` 并严格按其说明操作。自己抓取该 URL 并检查响应,推断合适的标题和字段,然后按帮助说明编写 `feeds/<slug>/schema.json`。不要向我提问——完全根据数据判断。打开订阅源会自动加载条目。完成后告诉我已注册(无需提示点击 Refresh)。",
|
|
612
|
+
title: "集合",
|
|
613
|
+
backToIndex: "返回集合列表",
|
|
614
|
+
indexEmpty: "尚未安装任何集合。在「技能」页面对带有 schema 的技能加星即可在此显示。",
|
|
615
|
+
editItem: "编辑",
|
|
616
|
+
openItem: "打开 {id}",
|
|
617
|
+
confirmDelete: "删除此项?此操作无法撤销。",
|
|
618
|
+
deleteFeed: "删除订阅源",
|
|
619
|
+
confirmDeleteFeed: "删除订阅源「{title}」及其所有已抓取的记录?此操作无法撤销。",
|
|
620
|
+
deleteCollection: "删除集合",
|
|
621
|
+
confirmDeleteCollection: "删除整个“{title}”集合及其所有记录?删除前会先归档一份可恢复的备份。",
|
|
622
|
+
itemsEmpty: "暂无项目。点击 + 添加一个。",
|
|
623
|
+
notFound: "未找到集合",
|
|
624
|
+
loadFailed: "加载失败",
|
|
625
|
+
requiredField: "此字段为必填项",
|
|
626
|
+
selectPlaceholder: "请选择…",
|
|
627
|
+
inlineSaveFailed: "无法保存更改:{error}",
|
|
628
|
+
addRow: "添加行",
|
|
629
|
+
removeRow: "删除行",
|
|
630
|
+
noRows: "暂无行",
|
|
631
|
+
tableSummary: "{count} 项",
|
|
632
|
+
embedMissing: "在 {collection} 中找不到「{id}」记录。",
|
|
633
|
+
embedCreate: "去设置",
|
|
634
|
+
searchPlaceholder: "搜索记录…",
|
|
635
|
+
searchSummary: "显示 {total} 条中的 {shown} 条",
|
|
636
|
+
noMatchingItems: "没有匹配的项目",
|
|
637
|
+
clearSearch: "清除搜索",
|
|
638
|
+
sortBy: "按{field}排序",
|
|
639
|
+
openCollection: "打开 {title}",
|
|
640
|
+
createTitle: "新增",
|
|
641
|
+
derivedLabel: "派生",
|
|
642
|
+
embedMissingTitle: "缺少嵌入引用",
|
|
643
|
+
chat: "对话",
|
|
644
|
+
refreshFeed: "刷新",
|
|
645
|
+
refreshFailed: "刷新失败:{error}",
|
|
646
|
+
feedChatSeed: "订阅源“{slug}”由 schema `feeds/{slug}/schema.json` 定义,其记录保存在 `{dataPath}/`(每条记录一个 `<id>.json` 文件)。请使用该 schema 和数据来响应以下请求:{message}",
|
|
647
|
+
feedsTitle: "数据源订阅",
|
|
648
|
+
feedsEmpty: "尚未注册任何订阅源。",
|
|
649
|
+
chatTitle: "开始对话",
|
|
650
|
+
chatPlaceholder: "描述你想对这个集合做什么…",
|
|
651
|
+
chatStart: "开始对话",
|
|
652
|
+
viewToggle: "视图",
|
|
653
|
+
viewTable: "表格",
|
|
654
|
+
viewCalendar: "日历",
|
|
655
|
+
calendarFieldLabel: "日历日期字段",
|
|
656
|
+
calendarPrevMonth: "上个月",
|
|
657
|
+
calendarNextMonth: "下个月",
|
|
658
|
+
calendarToday: "今天",
|
|
659
|
+
calendarNoDate: "无日期",
|
|
660
|
+
calendarCreateOn: "在 {date} 创建",
|
|
661
|
+
dayViewOpen: "打开 {date} 的日视图",
|
|
662
|
+
dayViewAllDay: "全天",
|
|
663
|
+
dayViewEmpty: "当天没有项目",
|
|
664
|
+
dayViewClose: "关闭日视图",
|
|
665
|
+
viewKanban: "看板",
|
|
666
|
+
kanbanFieldLabel: "看板分组字段",
|
|
667
|
+
kanbanUncategorized: "未分类",
|
|
668
|
+
kanbanOpenCard: "打开 {label}",
|
|
669
|
+
addView: "添加视图",
|
|
670
|
+
config: {
|
|
671
|
+
open: "集合设置",
|
|
672
|
+
title: "{title} · 设置",
|
|
673
|
+
viewsHeading: "自定义视图",
|
|
674
|
+
deleteView: "删除 {label}",
|
|
675
|
+
confirmDelete: "删除视图“{label}”?这将删除其 HTML 文件并取消注册。",
|
|
676
|
+
empty: "暂无自定义视图。"
|
|
677
|
+
},
|
|
678
|
+
customViewLoading: "正在加载视图…",
|
|
679
|
+
customViewError: "无法加载此视图:{error}",
|
|
680
|
+
addViewPrompt: "我想为“{title}”集合添加一个自定义视图。先问我想查看或编辑什么,然后在 {base}/views/your-view.html 创建 HTML 视图文件,并在 {base}/schema.json 的 `views[]` 中注册(只读视图用 capabilities [\"read\"],需要编辑记录则用 [\"read\",\"write\"])。数据契约请遵循 custom-view 帮助文档。",
|
|
681
|
+
repair: "修复",
|
|
682
|
+
dataIssuesDetected: "有 {count} 个记录文件存在数据问题,可能未显示在此视图中。",
|
|
683
|
+
repairPrompt: "集合 {title} 有 {count} 个记录文件存在导致无法显示的数据问题。请逐一修复——用 Read 读取该文件,更正后再用 Write 写回:\n{issues}\n\n完成后,调用 presentCollection 确认记录可以正常加载。",
|
|
684
|
+
source: {
|
|
685
|
+
user: "用户",
|
|
686
|
+
project: "项目"
|
|
687
|
+
}
|
|
688
|
+
},
|
|
689
|
+
common: {
|
|
690
|
+
add: "添加",
|
|
691
|
+
cancel: "取消",
|
|
692
|
+
loading: "加载中...",
|
|
693
|
+
no: "否",
|
|
694
|
+
remove: "移除",
|
|
695
|
+
save: "保存",
|
|
696
|
+
saving: "保存中...",
|
|
697
|
+
yes: "是"
|
|
698
|
+
}
|
|
699
|
+
},
|
|
700
|
+
ko: {
|
|
701
|
+
collectionsView: {
|
|
702
|
+
addCollectionLabel: "컬렉션",
|
|
703
|
+
addCollectionPrompt: "새 컬렉션을 만들고 싶어요. 먼저 `config/helps/collection-skills.md`를 읽고 스키마 기반 컬렉션 규칙을 확인하세요. 그런 다음 `presentForm` 도구를 사용해(AskUserQuestion은 사용하지 말고) 어떤 데이터를 관리하고 싶은지 물어보고, 제 답변을 바탕으로 schema.json과 SKILL.md를 작성해 주세요.",
|
|
704
|
+
addFeedTitle: "피드 추가",
|
|
705
|
+
addFeedHint: "피드 또는 API URL을 붙여넣으세요. 가져와서 제목과 필드를 자동으로 추론합니다.",
|
|
706
|
+
addFeedPrompt: "다음 URL에서 데이터 소스 피드를 추가하세요: {url}\n\n먼저 `config/helps/feeds.md`를 읽고 그대로 따르세요. 해당 URL을 직접 가져와 응답을 살펴보고 적절한 제목과 필드를 추론한 다음, 도움말 설명대로 `feeds/<slug>/schema.json`을 작성하세요. 저에게 질문하지 말고 데이터에서 모두 판단하세요. 피드를 열면 항목이 자동으로 로드됩니다. 완료되면 등록되었다고 알려주세요(Refresh를 누르라고 안내할 필요 없음).",
|
|
707
|
+
title: "컬렉션",
|
|
708
|
+
backToIndex: "컬렉션 목록으로 돌아가기",
|
|
709
|
+
indexEmpty: "설치된 컬렉션이 없습니다. Skills 페이지에서 스키마를 포함한 스킬에 별표를 추가하면 여기에 표시됩니다.",
|
|
710
|
+
editItem: "편집",
|
|
711
|
+
openItem: "{id} 열기",
|
|
712
|
+
confirmDelete: "이 항목을 삭제하시겠습니까? 되돌릴 수 없습니다.",
|
|
713
|
+
deleteFeed: "피드 삭제",
|
|
714
|
+
confirmDeleteFeed: "\"{title}\" 피드와 가져온 모든 레코드를 삭제하시겠습니까? 이 작업은 되돌릴 수 없습니다.",
|
|
715
|
+
deleteCollection: "컬렉션 삭제",
|
|
716
|
+
confirmDeleteCollection: "\"{title}\" 컬렉션과 모든 레코드를 삭제하시겠습니까? 삭제 전에 복원 가능한 백업이 보관됩니다.",
|
|
717
|
+
itemsEmpty: "아직 항목이 없습니다. + 를 눌러 추가하세요.",
|
|
718
|
+
notFound: "컬렉션을 찾을 수 없습니다",
|
|
719
|
+
loadFailed: "불러오기에 실패했습니다",
|
|
720
|
+
requiredField: "이 필드는 필수입니다",
|
|
721
|
+
selectPlaceholder: "선택…",
|
|
722
|
+
inlineSaveFailed: "변경 사항을 저장하지 못했습니다: {error}",
|
|
723
|
+
addRow: "행 추가",
|
|
724
|
+
removeRow: "행 삭제",
|
|
725
|
+
noRows: "행이 없습니다",
|
|
726
|
+
tableSummary: "{count}개",
|
|
727
|
+
embedMissing: "{collection}에 '{id}' 레코드가 없습니다.",
|
|
728
|
+
embedCreate: "설정하기",
|
|
729
|
+
searchPlaceholder: "레코드 검색…",
|
|
730
|
+
searchSummary: "{total}개 중 {shown}개 표시",
|
|
731
|
+
noMatchingItems: "일치하는 항목이 없습니다",
|
|
732
|
+
clearSearch: "검색 지우기",
|
|
733
|
+
sortBy: "{field} 기준 정렬",
|
|
734
|
+
openCollection: "{title} 열기",
|
|
735
|
+
createTitle: "새로 추가",
|
|
736
|
+
derivedLabel: "파생",
|
|
737
|
+
embedMissingTitle: "임베드된 참조 없음",
|
|
738
|
+
chat: "채팅",
|
|
739
|
+
refreshFeed: "새로고침",
|
|
740
|
+
refreshFailed: "새로고침 실패: {error}",
|
|
741
|
+
feedChatSeed: "“{slug}” 피드는 스키마 `feeds/{slug}/schema.json`로 정의되며, 레코드는 `{dataPath}/`(레코드당 `<id>.json` 파일 하나)에 저장됩니다. 이 스키마와 데이터를 사용하여 다음 요청에 응답하세요: {message}",
|
|
742
|
+
feedsTitle: "데이터 소스 피드",
|
|
743
|
+
feedsEmpty: "등록된 피드가 없습니다.",
|
|
744
|
+
chatTitle: "채팅 시작",
|
|
745
|
+
chatPlaceholder: "이 컬렉션으로 하고 싶은 작업을 설명하세요…",
|
|
746
|
+
chatStart: "채팅 시작",
|
|
747
|
+
viewToggle: "보기",
|
|
748
|
+
viewTable: "표",
|
|
749
|
+
viewCalendar: "캘린더",
|
|
750
|
+
calendarFieldLabel: "캘린더 날짜 필드",
|
|
751
|
+
calendarPrevMonth: "이전 달",
|
|
752
|
+
calendarNextMonth: "다음 달",
|
|
753
|
+
calendarToday: "오늘",
|
|
754
|
+
calendarNoDate: "날짜 없음",
|
|
755
|
+
calendarCreateOn: "{date}에 생성",
|
|
756
|
+
dayViewOpen: "{date} 일 보기 열기",
|
|
757
|
+
dayViewAllDay: "종일",
|
|
758
|
+
dayViewEmpty: "이 날에는 항목이 없습니다",
|
|
759
|
+
dayViewClose: "일 보기 닫기",
|
|
760
|
+
viewKanban: "칸반",
|
|
761
|
+
kanbanFieldLabel: "칸반 그룹 필드",
|
|
762
|
+
kanbanUncategorized: "미분류",
|
|
763
|
+
kanbanOpenCard: "{label} 열기",
|
|
764
|
+
addView: "보기 추가",
|
|
765
|
+
config: {
|
|
766
|
+
open: "컬렉션 설정",
|
|
767
|
+
title: "{title} · 설정",
|
|
768
|
+
viewsHeading: "사용자 지정 보기",
|
|
769
|
+
deleteView: "{label} 삭제",
|
|
770
|
+
confirmDelete: "“{label}” 보기를 삭제할까요? HTML 파일을 삭제하고 등록을 해제합니다.",
|
|
771
|
+
empty: "아직 사용자 지정 보기가 없습니다."
|
|
772
|
+
},
|
|
773
|
+
customViewLoading: "보기를 불러오는 중…",
|
|
774
|
+
customViewError: "이 보기를 불러오지 못했습니다: {error}",
|
|
775
|
+
addViewPrompt: "{title} 컬렉션에 사용자 지정 보기를 추가하고 싶어요. 무엇을 보거나 편집하고 싶은지 먼저 물어본 뒤, {base}/views/your-view.html 에 HTML 보기 파일을 만들고 {base}/schema.json의 `views[]`에 등록해 주세요(읽기 전용 보기는 capabilities [\"read\"], 레코드를 편집하면 [\"read\",\"write\"]). 데이터 계약은 custom-view 도움말을 따르세요.",
|
|
776
|
+
repair: "복구",
|
|
777
|
+
dataIssuesDetected: "{count}개의 레코드 파일에 데이터 문제가 있어 이 보기에 표시되지 않을 수 있습니다.",
|
|
778
|
+
repairPrompt: "{title} 컬렉션에 표시되지 않는 원인이 되는 데이터 문제가 있는 레코드 파일이 {count}개 있습니다. 각 파일을 수정하세요 — Read로 파일을 읽고 수정한 뒤 Write로 다시 저장하세요:\n{issues}\n\n완료되면 presentCollection을 호출하여 레코드가 로드되는지 확인하세요.",
|
|
779
|
+
source: {
|
|
780
|
+
user: "사용자",
|
|
781
|
+
project: "프로젝트"
|
|
782
|
+
}
|
|
783
|
+
},
|
|
784
|
+
common: {
|
|
785
|
+
add: "추가",
|
|
786
|
+
cancel: "취소",
|
|
787
|
+
loading: "불러오는 중...",
|
|
788
|
+
no: "아니오",
|
|
789
|
+
remove: "삭제",
|
|
790
|
+
save: "저장",
|
|
791
|
+
saving: "저장 중...",
|
|
792
|
+
yes: "예"
|
|
793
|
+
}
|
|
794
|
+
},
|
|
795
|
+
es: {
|
|
796
|
+
collectionsView: {
|
|
797
|
+
addCollectionLabel: "Colección",
|
|
798
|
+
addCollectionPrompt: "Ayúdame a crear una nueva colección. Primero lee `config/helps/collection-skills.md` para conocer las convenciones de las colecciones basadas en esquemas. Luego usa la herramienta `presentForm` (no uses AskUserQuestion) para preguntarme qué tipo de datos quiero registrar, y crea el schema.json y el SKILL.md a partir de mis respuestas.",
|
|
799
|
+
addFeedTitle: "Añadir un feed",
|
|
800
|
+
addFeedHint: "Pega la URL de un feed o una API; la obtendré y deduciré el título y los campos por ti.",
|
|
801
|
+
addFeedPrompt: "Añade una nueva fuente de datos (feed) desde esta URL: {url}\n\nPrimero lee `config/helps/feeds.md` y síguelo exactamente. Obtén esa URL tú mismo, inspecciona la respuesta para deducir un título adecuado y los campos, y luego crea `feeds/<slug>/schema.json` como indica la ayuda. NO me hagas preguntas: dedúcelo todo a partir de los datos. Al abrir el feed sus elementos se cargan automáticamente; cuando termines, dime que está registrado (no hace falta mencionar Refresh).",
|
|
802
|
+
title: "Colecciones",
|
|
803
|
+
backToIndex: "Volver a colecciones",
|
|
804
|
+
indexEmpty: "No hay colecciones instaladas. Marca con estrella una skill que incluya un schema desde la página Skills para verla aquí.",
|
|
805
|
+
editItem: "Editar",
|
|
806
|
+
openItem: "Abrir {id}",
|
|
807
|
+
confirmDelete: "¿Eliminar este elemento? Esta acción no se puede deshacer.",
|
|
808
|
+
deleteFeed: "Eliminar feed",
|
|
809
|
+
confirmDeleteFeed: "¿Eliminar el feed \"{title}\" y todos sus registros descargados? Esta acción no se puede deshacer.",
|
|
810
|
+
deleteCollection: "Eliminar colección",
|
|
811
|
+
confirmDeleteCollection: "¿Eliminar toda la colección \"{title}\", incluidos todos sus registros? Antes se archiva una copia de seguridad restaurable.",
|
|
812
|
+
itemsEmpty: "Aún no hay elementos. Pulsa + para añadir uno.",
|
|
813
|
+
notFound: "Colección no encontrada",
|
|
814
|
+
loadFailed: "Error al cargar",
|
|
815
|
+
requiredField: "Este campo es obligatorio",
|
|
816
|
+
selectPlaceholder: "Seleccionar…",
|
|
817
|
+
inlineSaveFailed: "No se pudo guardar el cambio: {error}",
|
|
818
|
+
addRow: "Añadir fila",
|
|
819
|
+
removeRow: "Quitar fila",
|
|
820
|
+
noRows: "Aún no hay filas",
|
|
821
|
+
tableSummary: "{count} elementos",
|
|
822
|
+
embedMissing: "No se encontró el registro «{id}» en {collection}.",
|
|
823
|
+
embedCreate: "Configurarlo",
|
|
824
|
+
searchPlaceholder: "Buscar registros…",
|
|
825
|
+
searchSummary: "Mostrando {shown} de {total}",
|
|
826
|
+
noMatchingItems: "No hay elementos coincidentes",
|
|
827
|
+
clearSearch: "Borrar búsqueda",
|
|
828
|
+
sortBy: "Ordenar por {field}",
|
|
829
|
+
openCollection: "Abrir {title}",
|
|
830
|
+
createTitle: "Añadir nuevo",
|
|
831
|
+
derivedLabel: "Derivado",
|
|
832
|
+
embedMissingTitle: "Falta la referencia incrustada",
|
|
833
|
+
chat: "Chat",
|
|
834
|
+
refreshFeed: "Actualizar",
|
|
835
|
+
refreshFailed: "Error al actualizar: {error}",
|
|
836
|
+
feedChatSeed: "El feed «{slug}» está definido por el esquema `feeds/{slug}/schema.json` y sus registros se guardan en `{dataPath}/` (un archivo `<id>.json` por registro). Usa ese esquema y esos datos para responder a esta solicitud: {message}",
|
|
837
|
+
feedsTitle: "Fuentes de datos",
|
|
838
|
+
feedsEmpty: "Aún no hay fuentes registradas.",
|
|
839
|
+
chatTitle: "Iniciar un chat",
|
|
840
|
+
chatPlaceholder: "Describe qué quieres hacer con esta colección…",
|
|
841
|
+
chatStart: "Iniciar chat",
|
|
842
|
+
viewToggle: "Vista",
|
|
843
|
+
viewTable: "Tabla",
|
|
844
|
+
viewCalendar: "Calendario",
|
|
845
|
+
calendarFieldLabel: "Campo de fecha del calendario",
|
|
846
|
+
calendarPrevMonth: "Mes anterior",
|
|
847
|
+
calendarNextMonth: "Mes siguiente",
|
|
848
|
+
calendarToday: "Hoy",
|
|
849
|
+
calendarNoDate: "Sin fecha",
|
|
850
|
+
calendarCreateOn: "Crear el {date}",
|
|
851
|
+
dayViewOpen: "Abrir vista de día para {date}",
|
|
852
|
+
dayViewAllDay: "Todo el día",
|
|
853
|
+
dayViewEmpty: "No hay elementos este día",
|
|
854
|
+
dayViewClose: "Cerrar vista de día",
|
|
855
|
+
viewKanban: "Kanban",
|
|
856
|
+
kanbanFieldLabel: "Campo de agrupación Kanban",
|
|
857
|
+
kanbanUncategorized: "Sin categoría",
|
|
858
|
+
kanbanOpenCard: "Abrir {label}",
|
|
859
|
+
addView: "Añadir vista",
|
|
860
|
+
config: {
|
|
861
|
+
open: "Ajustes de la colección",
|
|
862
|
+
title: "{title} · ajustes",
|
|
863
|
+
viewsHeading: "Vistas personalizadas",
|
|
864
|
+
deleteView: "Eliminar {label}",
|
|
865
|
+
confirmDelete: "¿Eliminar la vista \"{label}\"? Se borrará su archivo HTML y se anulará su registro.",
|
|
866
|
+
empty: "Aún no hay vistas personalizadas."
|
|
867
|
+
},
|
|
868
|
+
customViewLoading: "Cargando vista…",
|
|
869
|
+
customViewError: "No se pudo cargar esta vista: {error}",
|
|
870
|
+
addViewPrompt: "Quiero añadir una vista personalizada a la colección «{title}». Pregúntame qué quiero ver o editar, luego crea el archivo HTML de la vista en {base}/views/your-view.html y regístralo en {base}/schema.json dentro de `views[]` (capabilities [\"read\"] para una vista de solo lectura, [\"read\",\"write\"] si edita registros). Sigue la ayuda custom-view para el contrato de datos.",
|
|
871
|
+
repair: "Reparar",
|
|
872
|
+
dataIssuesDetected: "{count} archivo(s) de registro tienen problemas de datos y podrían no aparecer en esta vista.",
|
|
873
|
+
repairPrompt: "La colección {title} tiene {count} archivo(s) de registro con problemas de datos que impiden que aparezcan. Corrige cada uno: lee el archivo con Read, corrígelo y vuelve a escribirlo con Write:\n{issues}\n\nCuando termines, llama a presentCollection para confirmar que los registros se cargan.",
|
|
874
|
+
source: {
|
|
875
|
+
user: "Usuario",
|
|
876
|
+
project: "Proyecto"
|
|
877
|
+
}
|
|
878
|
+
},
|
|
879
|
+
common: {
|
|
880
|
+
add: "Añadir",
|
|
881
|
+
cancel: "Cancelar",
|
|
882
|
+
loading: "Cargando...",
|
|
883
|
+
no: "No",
|
|
884
|
+
remove: "Quitar",
|
|
885
|
+
save: "Guardar",
|
|
886
|
+
saving: "Guardando...",
|
|
887
|
+
yes: "Sí"
|
|
888
|
+
}
|
|
889
|
+
},
|
|
890
|
+
"pt-BR": {
|
|
891
|
+
collectionsView: {
|
|
892
|
+
addCollectionLabel: "Coleção",
|
|
893
|
+
addCollectionPrompt: "Ajude-me a criar uma nova coleção. Primeiro leia `config/helps/collection-skills.md` para conhecer as convenções de coleções baseadas em esquema. Depois use a ferramenta `presentForm` (não use AskUserQuestion) para perguntar que tipo de dados quero acompanhar, e crie o schema.json e o SKILL.md a partir das minhas respostas.",
|
|
894
|
+
addFeedTitle: "Adicionar um feed",
|
|
895
|
+
addFeedHint: "Cole a URL de um feed ou API; vou buscá-la e deduzir o título e os campos para você.",
|
|
896
|
+
addFeedPrompt: "Adicione um novo feed de fonte de dados a partir desta URL: {url}\n\nPrimeiro leia `config/helps/feeds.md` e siga-o exatamente. Busque essa URL você mesmo, inspecione a resposta para deduzir um título adequado e os campos, e então crie `feeds/<slug>/schema.json` como a ajuda descreve. NÃO me faça perguntas: deduza tudo a partir dos dados. Ao abrir o feed os itens são carregados automaticamente; quando terminar, diga-me que está registrado (não precisa mencionar Refresh).",
|
|
897
|
+
title: "Coleções",
|
|
898
|
+
backToIndex: "Voltar para coleções",
|
|
899
|
+
indexEmpty: "Nenhuma coleção instalada. Marque com estrela uma skill que inclua um schema na página Skills para vê-la aqui.",
|
|
900
|
+
editItem: "Editar",
|
|
901
|
+
openItem: "Abrir {id}",
|
|
902
|
+
confirmDelete: "Excluir este item? Esta ação não pode ser desfeita.",
|
|
903
|
+
deleteFeed: "Excluir feed",
|
|
904
|
+
confirmDeleteFeed: "Excluir o feed \"{title}\" e todos os seus registros baixados? Esta ação não pode ser desfeita.",
|
|
905
|
+
deleteCollection: "Excluir coleção",
|
|
906
|
+
confirmDeleteCollection: "Excluir toda a coleção \"{title}\", incluindo todos os seus registros? Um backup restaurável é arquivado antes.",
|
|
907
|
+
itemsEmpty: "Ainda não há itens. Clique em + para adicionar um.",
|
|
908
|
+
notFound: "Coleção não encontrada",
|
|
909
|
+
loadFailed: "Falha ao carregar",
|
|
910
|
+
requiredField: "Este campo é obrigatório",
|
|
911
|
+
selectPlaceholder: "Selecionar…",
|
|
912
|
+
inlineSaveFailed: "Não foi possível salvar a alteração: {error}",
|
|
913
|
+
addRow: "Adicionar linha",
|
|
914
|
+
removeRow: "Remover linha",
|
|
915
|
+
noRows: "Ainda não há linhas",
|
|
916
|
+
tableSummary: "{count} itens",
|
|
917
|
+
embedMissing: "Nenhum registro '{id}' encontrado em {collection}.",
|
|
918
|
+
embedCreate: "Configurar",
|
|
919
|
+
searchPlaceholder: "Buscar registros…",
|
|
920
|
+
searchSummary: "Mostrando {shown} de {total}",
|
|
921
|
+
noMatchingItems: "Nenhum item correspondente",
|
|
922
|
+
clearSearch: "Limpar busca",
|
|
923
|
+
sortBy: "Ordenar por {field}",
|
|
924
|
+
openCollection: "Abrir {title}",
|
|
925
|
+
createTitle: "Adicionar novo",
|
|
926
|
+
derivedLabel: "Derivado",
|
|
927
|
+
embedMissingTitle: "Referência incorporada ausente",
|
|
928
|
+
chat: "Chat",
|
|
929
|
+
refreshFeed: "Atualizar",
|
|
930
|
+
refreshFailed: "Falha ao atualizar: {error}",
|
|
931
|
+
feedChatSeed: "O feed \"{slug}\" é definido pelo esquema `feeds/{slug}/schema.json` e seus registros ficam em `{dataPath}/` (um arquivo `<id>.json` por registro). Use esse esquema e esses dados para responder a esta solicitação: {message}",
|
|
932
|
+
feedsTitle: "Feeds de dados",
|
|
933
|
+
feedsEmpty: "Nenhum feed registrado ainda.",
|
|
934
|
+
chatTitle: "Iniciar um chat",
|
|
935
|
+
chatPlaceholder: "Descreva o que você quer fazer com esta coleção…",
|
|
936
|
+
chatStart: "Iniciar chat",
|
|
937
|
+
viewToggle: "Visualização",
|
|
938
|
+
viewTable: "Tabela",
|
|
939
|
+
viewCalendar: "Calendário",
|
|
940
|
+
calendarFieldLabel: "Campo de data do calendário",
|
|
941
|
+
calendarPrevMonth: "Mês anterior",
|
|
942
|
+
calendarNextMonth: "Próximo mês",
|
|
943
|
+
calendarToday: "Hoje",
|
|
944
|
+
calendarNoDate: "Sem data",
|
|
945
|
+
calendarCreateOn: "Criar em {date}",
|
|
946
|
+
dayViewOpen: "Abrir visualização do dia para {date}",
|
|
947
|
+
dayViewAllDay: "Dia inteiro",
|
|
948
|
+
dayViewEmpty: "Nenhum item neste dia",
|
|
949
|
+
dayViewClose: "Fechar visualização do dia",
|
|
950
|
+
viewKanban: "Kanban",
|
|
951
|
+
kanbanFieldLabel: "Campo de agrupamento do Kanban",
|
|
952
|
+
kanbanUncategorized: "Sem categoria",
|
|
953
|
+
kanbanOpenCard: "Abrir {label}",
|
|
954
|
+
addView: "Adicionar visualização",
|
|
955
|
+
config: {
|
|
956
|
+
open: "Configurações da coleção",
|
|
957
|
+
title: "{title} · configurações",
|
|
958
|
+
viewsHeading: "Visualizações personalizadas",
|
|
959
|
+
deleteView: "Excluir {label}",
|
|
960
|
+
confirmDelete: "Excluir a visualização \"{label}\"? Isso remove o arquivo HTML e cancela o registro.",
|
|
961
|
+
empty: "Ainda não há visualizações personalizadas."
|
|
962
|
+
},
|
|
963
|
+
customViewLoading: "Carregando visualização…",
|
|
964
|
+
customViewError: "Não foi possível carregar esta visualização: {error}",
|
|
965
|
+
addViewPrompt: "Quero adicionar uma visualização personalizada à coleção \"{title}\". Pergunte o que eu quero ver ou editar, depois crie o arquivo HTML da visualização em {base}/views/your-view.html e registre-o em {base}/schema.json em `views[]` (capabilities [\"read\"] para uma visualização somente leitura, [\"read\",\"write\"] se editar registros). Siga a ajuda custom-view para o contrato de dados.",
|
|
966
|
+
repair: "Reparar",
|
|
967
|
+
dataIssuesDetected: "{count} arquivo(s) de registro têm problemas de dados e podem não aparecer nesta visualização.",
|
|
968
|
+
repairPrompt: "A coleção {title} tem {count} arquivo(s) de registro com problemas de dados que impedem que apareçam. Corrija cada um: leia o arquivo com Read, corrija-o e grave-o novamente com Write:\n{issues}\n\nQuando terminar, chame presentCollection para confirmar que os registros carregam.",
|
|
969
|
+
source: {
|
|
970
|
+
user: "Usuário",
|
|
971
|
+
project: "Projeto"
|
|
972
|
+
}
|
|
973
|
+
},
|
|
974
|
+
common: {
|
|
975
|
+
add: "Adicionar",
|
|
976
|
+
cancel: "Cancelar",
|
|
977
|
+
loading: "Carregando...",
|
|
978
|
+
no: "Não",
|
|
979
|
+
remove: "Remover",
|
|
980
|
+
save: "Salvar",
|
|
981
|
+
saving: "Salvando...",
|
|
982
|
+
yes: "Sim"
|
|
983
|
+
}
|
|
984
|
+
},
|
|
985
|
+
fr: {
|
|
986
|
+
collectionsView: {
|
|
987
|
+
addCollectionLabel: "Collection",
|
|
988
|
+
addCollectionPrompt: "Aide-moi à créer une nouvelle collection. Lis d'abord `config/helps/collection-skills.md` pour les conventions des collections basées sur un schéma. Utilise ensuite l'outil `presentForm` (n'utilise pas AskUserQuestion) pour me demander quel type de données je veux suivre, et crée le schema.json et le SKILL.md à partir de mes réponses.",
|
|
989
|
+
addFeedTitle: "Ajouter un flux",
|
|
990
|
+
addFeedHint: "Collez l'URL d'un flux ou d'une API ; je la récupère et déduis le titre et les champs pour vous.",
|
|
991
|
+
addFeedPrompt: "Ajoute un nouveau flux de source de données depuis cette URL : {url}\n\nLis d'abord `config/helps/feeds.md` et suis-le exactement. Récupère cette URL toi-même, inspecte la réponse pour déduire un titre pertinent et les champs, puis crée `feeds/<slug>/schema.json` comme l'explique l'aide. Ne me pose AUCUNE question — déduis tout à partir des données. À l'ouverture du flux, ses éléments se chargent automatiquement ; quand tu as terminé, dis-moi qu'il est enregistré (inutile de mentionner Refresh).",
|
|
992
|
+
title: "Collections",
|
|
993
|
+
backToIndex: "Retour aux collections",
|
|
994
|
+
indexEmpty: "Aucune collection installée. Mettez une étoile sur une compétence avec un schema depuis la page Skills pour la voir ici.",
|
|
995
|
+
editItem: "Modifier",
|
|
996
|
+
openItem: "Ouvrir {id}",
|
|
997
|
+
confirmDelete: "Supprimer cet élément ? Cette action est irréversible.",
|
|
998
|
+
deleteFeed: "Supprimer le flux",
|
|
999
|
+
confirmDeleteFeed: "Supprimer le flux « {title} » et tous ses enregistrements récupérés ? Cette action est irréversible.",
|
|
1000
|
+
deleteCollection: "Supprimer la collection",
|
|
1001
|
+
confirmDeleteCollection: "Supprimer toute la collection « {title} », y compris tous ses enregistrements ? Une sauvegarde restaurable est archivée au préalable.",
|
|
1002
|
+
itemsEmpty: "Aucun élément pour l'instant. Cliquez sur + pour en ajouter un.",
|
|
1003
|
+
notFound: "Collection introuvable",
|
|
1004
|
+
loadFailed: "Échec du chargement",
|
|
1005
|
+
requiredField: "Ce champ est obligatoire",
|
|
1006
|
+
selectPlaceholder: "Sélectionner…",
|
|
1007
|
+
inlineSaveFailed: "Impossible d'enregistrer la modification : {error}",
|
|
1008
|
+
addRow: "Ajouter une ligne",
|
|
1009
|
+
removeRow: "Supprimer la ligne",
|
|
1010
|
+
noRows: "Aucune ligne pour l'instant",
|
|
1011
|
+
tableSummary: "{count} éléments",
|
|
1012
|
+
embedMissing: "Aucun enregistrement « {id} » trouvé dans {collection}.",
|
|
1013
|
+
embedCreate: "Le configurer",
|
|
1014
|
+
searchPlaceholder: "Rechercher des enregistrements…",
|
|
1015
|
+
searchSummary: "Affichage de {shown} sur {total}",
|
|
1016
|
+
noMatchingItems: "Aucun élément correspondant",
|
|
1017
|
+
clearSearch: "Effacer la recherche",
|
|
1018
|
+
sortBy: "Trier par {field}",
|
|
1019
|
+
openCollection: "Ouvrir {title}",
|
|
1020
|
+
createTitle: "Ajouter",
|
|
1021
|
+
derivedLabel: "Calculé",
|
|
1022
|
+
embedMissingTitle: "Référence intégrée manquante",
|
|
1023
|
+
chat: "Discussion",
|
|
1024
|
+
refreshFeed: "Actualiser",
|
|
1025
|
+
refreshFailed: "Échec de l'actualisation : {error}",
|
|
1026
|
+
feedChatSeed: "Le flux « {slug} » est défini par le schéma `feeds/{slug}/schema.json` et ses enregistrements se trouvent dans `{dataPath}/` (un fichier `<id>.json` par enregistrement). Utilise ce schéma et ces données pour répondre à cette demande : {message}",
|
|
1027
|
+
feedsTitle: "Flux de données",
|
|
1028
|
+
feedsEmpty: "Aucun flux enregistré pour le moment.",
|
|
1029
|
+
chatTitle: "Démarrer une discussion",
|
|
1030
|
+
chatPlaceholder: "Décrivez ce que vous voulez faire avec cette collection…",
|
|
1031
|
+
chatStart: "Démarrer la discussion",
|
|
1032
|
+
viewToggle: "Affichage",
|
|
1033
|
+
viewTable: "Tableau",
|
|
1034
|
+
viewCalendar: "Calendrier",
|
|
1035
|
+
calendarFieldLabel: "Champ de date du calendrier",
|
|
1036
|
+
calendarPrevMonth: "Mois précédent",
|
|
1037
|
+
calendarNextMonth: "Mois suivant",
|
|
1038
|
+
calendarToday: "Aujourd'hui",
|
|
1039
|
+
calendarNoDate: "Sans date",
|
|
1040
|
+
calendarCreateOn: "Créer le {date}",
|
|
1041
|
+
dayViewOpen: "Ouvrir la vue du jour pour {date}",
|
|
1042
|
+
dayViewAllDay: "Toute la journée",
|
|
1043
|
+
dayViewEmpty: "Aucun élément ce jour",
|
|
1044
|
+
dayViewClose: "Fermer la vue du jour",
|
|
1045
|
+
viewKanban: "Kanban",
|
|
1046
|
+
kanbanFieldLabel: "Champ de regroupement Kanban",
|
|
1047
|
+
kanbanUncategorized: "Non classé",
|
|
1048
|
+
kanbanOpenCard: "Ouvrir {label}",
|
|
1049
|
+
addView: "Ajouter une vue",
|
|
1050
|
+
config: {
|
|
1051
|
+
open: "Paramètres de la collection",
|
|
1052
|
+
title: "{title} · paramètres",
|
|
1053
|
+
viewsHeading: "Vues personnalisées",
|
|
1054
|
+
deleteView: "Supprimer {label}",
|
|
1055
|
+
confirmDelete: "Supprimer la vue « {label} » ? Cela supprime son fichier HTML et la désenregistre.",
|
|
1056
|
+
empty: "Aucune vue personnalisée pour le moment."
|
|
1057
|
+
},
|
|
1058
|
+
customViewLoading: "Chargement de la vue…",
|
|
1059
|
+
customViewError: "Impossible de charger cette vue : {error}",
|
|
1060
|
+
addViewPrompt: "Je veux ajouter une vue personnalisée à la collection « {title} ». Demande-moi ce que je veux voir ou modifier, puis crée le fichier HTML de la vue dans {base}/views/your-view.html et enregistre-le dans {base}/schema.json sous `views[]` (capabilities [\"read\"] pour une vue en lecture seule, [\"read\",\"write\"] si elle modifie des enregistrements). Suis l'aide custom-view pour le contrat de données.",
|
|
1061
|
+
repair: "Réparer",
|
|
1062
|
+
dataIssuesDetected: "{count} fichier(s) d'enregistrement présentent des problèmes de données et peuvent être absents de cette vue.",
|
|
1063
|
+
repairPrompt: "La collection « {title} » comporte {count} fichier(s) d'enregistrement présentant des problèmes de données qui les empêchent d'apparaître. Corrigez chacun : lisez le fichier avec Read, corrigez-le, puis réécrivez-le avec Write :\n{issues}\n\nUne fois terminé, appelez presentCollection pour confirmer que les enregistrements se chargent.",
|
|
1064
|
+
source: {
|
|
1065
|
+
user: "Utilisateur",
|
|
1066
|
+
project: "Projet"
|
|
1067
|
+
}
|
|
1068
|
+
},
|
|
1069
|
+
common: {
|
|
1070
|
+
add: "Ajouter",
|
|
1071
|
+
cancel: "Annuler",
|
|
1072
|
+
loading: "Chargement...",
|
|
1073
|
+
no: "Non",
|
|
1074
|
+
remove: "Supprimer",
|
|
1075
|
+
save: "Enregistrer",
|
|
1076
|
+
saving: "Enregistrement...",
|
|
1077
|
+
yes: "Oui"
|
|
1078
|
+
}
|
|
1079
|
+
},
|
|
1080
|
+
de: {
|
|
1081
|
+
collectionsView: {
|
|
1082
|
+
addCollectionLabel: "Sammlung",
|
|
1083
|
+
addCollectionPrompt: "Hilf mir, eine neue Sammlung zu erstellen. Lies zuerst `config/helps/collection-skills.md` für die Konventionen schemabasierter Sammlungen. Verwende dann das Tool `presentForm` (nutze nicht AskUserQuestion), um mich zu fragen, welche Art von Daten ich verfolgen möchte, und erstelle die schema.json und SKILL.md aus meinen Antworten.",
|
|
1084
|
+
addFeedTitle: "Feed hinzufügen",
|
|
1085
|
+
addFeedHint: "Füge die URL eines Feeds oder einer API ein; ich rufe sie ab und ermittle Titel und Felder für dich.",
|
|
1086
|
+
addFeedPrompt: "Füge einen neuen Datenquellen-Feed von dieser URL hinzu: {url}\n\nLies zuerst `config/helps/feeds.md` und befolge es genau. Rufe diese URL selbst ab, untersuche die Antwort, um einen sinnvollen Titel und die Felder abzuleiten, und erstelle dann `feeds/<slug>/schema.json` wie in der Hilfe beschrieben. Stelle mir KEINE Fragen - leite alles aus den Daten ab. Beim Öffnen des Feeds werden seine Einträge automatisch geladen; sag mir Bescheid, wenn er registriert ist (Refresh muss nicht erwähnt werden).",
|
|
1087
|
+
title: "Sammlungen",
|
|
1088
|
+
backToIndex: "Zurück zu Sammlungen",
|
|
1089
|
+
indexEmpty: "Keine Sammlungen installiert. Markiere auf der Skills-Seite eine Skill mit Schema, um sie hier zu sehen.",
|
|
1090
|
+
editItem: "Bearbeiten",
|
|
1091
|
+
openItem: "{id} öffnen",
|
|
1092
|
+
confirmDelete: "Diesen Eintrag löschen? Das kann nicht rückgängig gemacht werden.",
|
|
1093
|
+
deleteFeed: "Feed löschen",
|
|
1094
|
+
confirmDeleteFeed: "Den Feed {title} und alle abgerufenen Datensätze löschen? Dies kann nicht rückgängig gemacht werden.",
|
|
1095
|
+
deleteCollection: "Sammlung löschen",
|
|
1096
|
+
confirmDeleteCollection: "Die gesamte Sammlung „{title}“ einschließlich aller Datensätze löschen? Zuvor wird eine wiederherstellbare Sicherung archiviert.",
|
|
1097
|
+
itemsEmpty: "Noch keine Einträge. Klicke auf +, um einen hinzuzufügen.",
|
|
1098
|
+
notFound: "Sammlung nicht gefunden",
|
|
1099
|
+
loadFailed: "Laden fehlgeschlagen",
|
|
1100
|
+
requiredField: "Dieses Feld ist erforderlich",
|
|
1101
|
+
selectPlaceholder: "Auswählen…",
|
|
1102
|
+
inlineSaveFailed: "Änderung konnte nicht gespeichert werden: {error}",
|
|
1103
|
+
addRow: "Zeile hinzufügen",
|
|
1104
|
+
removeRow: "Zeile entfernen",
|
|
1105
|
+
noRows: "Noch keine Zeilen",
|
|
1106
|
+
tableSummary: "{count} Einträge",
|
|
1107
|
+
embedMissing: "Kein Datensatz {id} in {collection} gefunden.",
|
|
1108
|
+
embedCreate: "Einrichten",
|
|
1109
|
+
searchPlaceholder: "Datensätze suchen…",
|
|
1110
|
+
searchSummary: "{shown} von {total} werden angezeigt",
|
|
1111
|
+
noMatchingItems: "Keine passenden Einträge",
|
|
1112
|
+
clearSearch: "Suche zurücksetzen",
|
|
1113
|
+
sortBy: "Nach {field} sortieren",
|
|
1114
|
+
openCollection: "{title} öffnen",
|
|
1115
|
+
createTitle: "Neu hinzufügen",
|
|
1116
|
+
derivedLabel: "Abgeleitet",
|
|
1117
|
+
embedMissingTitle: "Eingebettete Referenz fehlt",
|
|
1118
|
+
chat: "Chat",
|
|
1119
|
+
refreshFeed: "Aktualisieren",
|
|
1120
|
+
refreshFailed: "Aktualisierung fehlgeschlagen: {error}",
|
|
1121
|
+
feedChatSeed: "Der Feed {slug} ist durch das Schema `feeds/{slug}/schema.json` definiert und seine Datensätze liegen in `{dataPath}/` (eine `<id>.json`-Datei pro Datensatz). Nutze dieses Schema und diese Daten, um auf die folgende Anfrage zu antworten: {message}",
|
|
1122
|
+
feedsTitle: "Datenquellen-Feeds",
|
|
1123
|
+
feedsEmpty: "Noch keine Feeds registriert.",
|
|
1124
|
+
chatTitle: "Chat starten",
|
|
1125
|
+
chatPlaceholder: "Beschreibe, was du mit dieser Sammlung tun möchtest…",
|
|
1126
|
+
chatStart: "Chat starten",
|
|
1127
|
+
viewToggle: "Ansicht",
|
|
1128
|
+
viewTable: "Tabelle",
|
|
1129
|
+
viewCalendar: "Kalender",
|
|
1130
|
+
calendarFieldLabel: "Kalender-Datumsfeld",
|
|
1131
|
+
calendarPrevMonth: "Voriger Monat",
|
|
1132
|
+
calendarNextMonth: "Nächster Monat",
|
|
1133
|
+
calendarToday: "Heute",
|
|
1134
|
+
calendarNoDate: "Kein Datum",
|
|
1135
|
+
calendarCreateOn: "Am {date} erstellen",
|
|
1136
|
+
dayViewOpen: "Tagesansicht für {date} öffnen",
|
|
1137
|
+
dayViewAllDay: "Ganztägig",
|
|
1138
|
+
dayViewEmpty: "Keine Einträge an diesem Tag",
|
|
1139
|
+
dayViewClose: "Tagesansicht schließen",
|
|
1140
|
+
viewKanban: "Kanban",
|
|
1141
|
+
kanbanFieldLabel: "Kanban-Gruppierungsfeld",
|
|
1142
|
+
kanbanUncategorized: "Nicht kategorisiert",
|
|
1143
|
+
kanbanOpenCard: "{label} öffnen",
|
|
1144
|
+
addView: "Ansicht hinzufügen",
|
|
1145
|
+
config: {
|
|
1146
|
+
open: "Sammlungseinstellungen",
|
|
1147
|
+
title: "{title} · Einstellungen",
|
|
1148
|
+
viewsHeading: "Benutzerdefinierte Ansichten",
|
|
1149
|
+
deleteView: "{label} löschen",
|
|
1150
|
+
confirmDelete: "Ansicht {label} löschen? Die HTML-Datei wird entfernt und die Registrierung aufgehoben.",
|
|
1151
|
+
empty: "Noch keine benutzerdefinierten Ansichten."
|
|
1152
|
+
},
|
|
1153
|
+
customViewLoading: "Ansicht wird geladen…",
|
|
1154
|
+
customViewError: "Diese Ansicht konnte nicht geladen werden: {error}",
|
|
1155
|
+
addViewPrompt: "Ich möchte der Sammlung {title} eine benutzerdefinierte Ansicht hinzufügen. Frag mich, was ich sehen oder bearbeiten möchte, erstelle dann die HTML-Ansichtsdatei unter {base}/views/your-view.html und registriere sie in {base}/schema.json unter `views[]` (capabilities [\"read\"] für eine schreibgeschützte Ansicht, [\"read\",\"write\"] wenn sie Datensätze bearbeitet). Folge der custom-view-Hilfe für den Datenvertrag.",
|
|
1156
|
+
repair: "Reparieren",
|
|
1157
|
+
dataIssuesDetected: "{count} Datensatzdatei(en) haben Datenprobleme und fehlen möglicherweise in dieser Ansicht.",
|
|
1158
|
+
repairPrompt: "Die Sammlung {title} hat {count} Datensatzdatei(en) mit Datenproblemen, die ihr Erscheinen verhindern. Korrigiere jede — die Datei mit Read lesen, korrigieren und mit Write zurückschreiben:\n{issues}\n\nRufe anschließend presentCollection auf, um zu bestätigen, dass die Datensätze geladen werden.",
|
|
1159
|
+
source: {
|
|
1160
|
+
user: "Benutzer",
|
|
1161
|
+
project: "Projekt"
|
|
1162
|
+
}
|
|
1163
|
+
},
|
|
1164
|
+
common: {
|
|
1165
|
+
add: "Hinzufügen",
|
|
1166
|
+
cancel: "Abbrechen",
|
|
1167
|
+
loading: "Wird geladen...",
|
|
1168
|
+
no: "Nein",
|
|
1169
|
+
remove: "Entfernen",
|
|
1170
|
+
save: "Speichern",
|
|
1171
|
+
saving: "Wird gespeichert...",
|
|
1172
|
+
yes: "Ja"
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
1176
|
+
});
|
|
1177
|
+
var syncScope = (0, vue.effectScope)(true);
|
|
1178
|
+
var syncing = false;
|
|
1179
|
+
/** Mirror this instance's locale to the host's (via the binding) exactly once,
|
|
1180
|
+
* in a detached effect so it lives for the app's lifetime rather than a single
|
|
1181
|
+
* component's. Called lazily on the first `useT()` — by then App.vue's setup has
|
|
1182
|
+
* configured the binding, so `collectionUi()` resolves. */
|
|
1183
|
+
function ensureLocaleSync() {
|
|
1184
|
+
if (syncing) return;
|
|
1185
|
+
syncing = true;
|
|
1186
|
+
syncScope.run(() => {
|
|
1187
|
+
(0, vue.watchEffect)(() => {
|
|
1188
|
+
i18n.global.locale.value = collectionUi().localeTag();
|
|
1189
|
+
});
|
|
1190
|
+
});
|
|
1191
|
+
}
|
|
1192
|
+
/** The plugin's i18n composable — a drop-in for vue-i18n's `useI18n()` over the
|
|
1193
|
+
* plugin's own self-contained instance. Returns `{ t, locale }` (destructured at
|
|
1194
|
+
* the call site, exactly like `useI18n()`), with `t` reading the plugin's keys
|
|
1195
|
+
* and `locale` the reactive tag for date/number formatting. */
|
|
1196
|
+
function useCollectionI18n() {
|
|
1197
|
+
ensureLocaleSync();
|
|
1198
|
+
return {
|
|
1199
|
+
t: i18n.global.t,
|
|
1200
|
+
locale: i18n.global.locale
|
|
1201
|
+
};
|
|
1202
|
+
}
|
|
1203
|
+
//#endregion
|
|
1204
|
+
//#region src/vue/components/CollectionEmbedView.vue?vue&type=script&setup=true&lang.ts
|
|
1205
|
+
var _hoisted_1$9 = { class: "flex items-center justify-between text-[10px] font-bold text-indigo-600/90 tracking-wider uppercase" };
|
|
1206
|
+
var _hoisted_2$9 = { class: "flex items-center gap-1.5" };
|
|
1207
|
+
var _hoisted_3$9 = { class: "bg-indigo-100/60 text-indigo-700 px-1.5 py-0.5 rounded font-mono font-medium lowercase" };
|
|
1208
|
+
var _hoisted_4$9 = { class: "grid gap-x-4 gap-y-3 grid-cols-2" };
|
|
1209
|
+
var _hoisted_5$8 = { class: "text-[10px] font-semibold text-slate-400 uppercase tracking-wide" };
|
|
1210
|
+
var _hoisted_6$8 = ["data-testid"];
|
|
1211
|
+
var _hoisted_7$8 = {
|
|
1212
|
+
key: 0,
|
|
1213
|
+
class: "material-icons text-emerald-600 text-sm align-middle"
|
|
1214
|
+
};
|
|
1215
|
+
var _hoisted_8$8 = {
|
|
1216
|
+
key: 1,
|
|
1217
|
+
class: "text-slate-300"
|
|
1218
|
+
};
|
|
1219
|
+
var _hoisted_9$8 = {
|
|
1220
|
+
key: 1,
|
|
1221
|
+
class: "whitespace-pre-wrap font-normal text-slate-600"
|
|
1222
|
+
};
|
|
1223
|
+
var _hoisted_10$8 = { key: 2 };
|
|
1224
|
+
var _hoisted_11$8 = ["data-testid"];
|
|
1225
|
+
var _hoisted_12$7 = { class: "flex items-start gap-3" };
|
|
1226
|
+
var _hoisted_13$6 = { class: "flex-1 min-w-0" };
|
|
1227
|
+
var _hoisted_14$5 = { class: "text-xs font-semibold text-red-800 uppercase tracking-wider mb-1" };
|
|
1228
|
+
var _hoisted_15$5 = ["data-testid"];
|
|
1229
|
+
//#endregion
|
|
1230
|
+
//#region src/vue/components/CollectionEmbedView.vue
|
|
1231
|
+
var CollectionEmbedView_default = /* @__PURE__ */ (0, vue.defineComponent)({
|
|
1232
|
+
__name: "CollectionEmbedView",
|
|
1233
|
+
props: {
|
|
1234
|
+
view: {},
|
|
1235
|
+
fieldKey: {}
|
|
1236
|
+
},
|
|
1237
|
+
setup(__props) {
|
|
1238
|
+
const { t } = useCollectionI18n();
|
|
1239
|
+
return (_ctx, _cache) => {
|
|
1240
|
+
const _component_router_link = (0, vue.resolveComponent)("router-link");
|
|
1241
|
+
return __props.view.found ? ((0, vue.openBlock)(), (0, vue.createBlock)(_component_router_link, {
|
|
1242
|
+
key: 0,
|
|
1243
|
+
to: {
|
|
1244
|
+
path: `/collections/${__props.view.targetSlug}`,
|
|
1245
|
+
query: { selected: __props.view.recordId }
|
|
1246
|
+
},
|
|
1247
|
+
class: "group block relative rounded-xl border border-slate-200 bg-slate-50/50 p-4 pl-5 space-y-3 hover:bg-indigo-50/20 hover:border-indigo-200 transition-all duration-300 shadow-sm",
|
|
1248
|
+
"data-testid": `collections-embed-${__props.fieldKey}`
|
|
1249
|
+
}, {
|
|
1250
|
+
default: (0, vue.withCtx)(() => [
|
|
1251
|
+
_cache[1] || (_cache[1] = (0, vue.createElementVNode)("div", { class: "absolute left-0 top-0 bottom-0 w-1 bg-indigo-500 rounded-l-xl transition-all duration-300 group-hover:w-1.5 group-hover:bg-indigo-600" }, null, -1)),
|
|
1252
|
+
(0, vue.createElementVNode)("div", _hoisted_1$9, [(0, vue.createElementVNode)("div", _hoisted_2$9, [_cache[0] || (_cache[0] = (0, vue.createElementVNode)("span", { class: "material-icons text-sm" }, "link", -1)), (0, vue.createElementVNode)("span", null, (0, vue.toDisplayString)(__props.view.targetSlug), 1)]), (0, vue.createElementVNode)("span", _hoisted_3$9, (0, vue.toDisplayString)(__props.view.recordId), 1)]),
|
|
1253
|
+
(0, vue.createElementVNode)("div", _hoisted_4$9, [((0, vue.openBlock)(true), (0, vue.createElementBlock)(vue.Fragment, null, (0, vue.renderList)(__props.view.rows, (row) => {
|
|
1254
|
+
return (0, vue.openBlock)(), (0, vue.createElementBlock)("div", {
|
|
1255
|
+
key: row.key,
|
|
1256
|
+
class: "space-y-0.5"
|
|
1257
|
+
}, [(0, vue.createElementVNode)("div", _hoisted_5$8, (0, vue.toDisplayString)(row.label), 1), (0, vue.createElementVNode)("div", {
|
|
1258
|
+
class: "text-xs text-slate-700 font-medium break-words",
|
|
1259
|
+
"data-testid": `collections-embed-${__props.fieldKey}-${row.key}`
|
|
1260
|
+
}, [row.type === "boolean" ? ((0, vue.openBlock)(), (0, vue.createElementBlock)(vue.Fragment, { key: 0 }, [row.value === true ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("span", _hoisted_7$8, "check_circle")) : ((0, vue.openBlock)(), (0, vue.createElementBlock)("span", _hoisted_8$8, "—"))], 64)) : row.type === "markdown" ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("p", _hoisted_9$8, (0, vue.toDisplayString)(row.display), 1)) : ((0, vue.openBlock)(), (0, vue.createElementBlock)("span", _hoisted_10$8, (0, vue.toDisplayString)(row.display), 1))], 8, _hoisted_6$8)]);
|
|
1261
|
+
}), 128))])
|
|
1262
|
+
]),
|
|
1263
|
+
_: 1
|
|
1264
|
+
}, 8, ["to", "data-testid"])) : ((0, vue.openBlock)(), (0, vue.createElementBlock)("div", {
|
|
1265
|
+
key: 1,
|
|
1266
|
+
class: "relative rounded-xl border border-red-100 bg-red-50/30 p-4 pl-5 shadow-sm",
|
|
1267
|
+
"data-testid": `collections-embed-${__props.fieldKey}`
|
|
1268
|
+
}, [_cache[4] || (_cache[4] = (0, vue.createElementVNode)("div", { class: "absolute left-0 top-0 bottom-0 w-1 bg-red-400 rounded-l-xl" }, null, -1)), (0, vue.createElementVNode)("div", _hoisted_12$7, [_cache[3] || (_cache[3] = (0, vue.createElementVNode)("span", { class: "material-icons text-red-500 text-lg mt-0.5" }, "error_outline", -1)), (0, vue.createElementVNode)("div", _hoisted_13$6, [
|
|
1269
|
+
(0, vue.createElementVNode)("p", _hoisted_14$5, (0, vue.toDisplayString)((0, vue.unref)(t)("collectionsView.embedMissingTitle")), 1),
|
|
1270
|
+
(0, vue.createElementVNode)("p", {
|
|
1271
|
+
class: "text-xs text-red-600",
|
|
1272
|
+
"data-testid": `collections-embed-missing-${__props.fieldKey}`
|
|
1273
|
+
}, (0, vue.toDisplayString)((0, vue.unref)(t)("collectionsView.embedMissing", {
|
|
1274
|
+
collection: __props.view.targetSlug,
|
|
1275
|
+
id: __props.view.recordId
|
|
1276
|
+
})), 9, _hoisted_15$5),
|
|
1277
|
+
__props.view.targetSlug ? ((0, vue.openBlock)(), (0, vue.createBlock)(_component_router_link, {
|
|
1278
|
+
key: 0,
|
|
1279
|
+
to: { path: `/collections/${__props.view.targetSlug}` },
|
|
1280
|
+
class: "inline-flex items-center gap-0.5 text-xs text-indigo-600 hover:text-indigo-800 font-semibold mt-2 hover:underline"
|
|
1281
|
+
}, {
|
|
1282
|
+
default: (0, vue.withCtx)(() => [(0, vue.createElementVNode)("span", null, (0, vue.toDisplayString)((0, vue.unref)(t)("collectionsView.embedCreate")), 1), _cache[2] || (_cache[2] = (0, vue.createElementVNode)("span", { class: "material-icons text-xs" }, "arrow_forward", -1))]),
|
|
1283
|
+
_: 1
|
|
1284
|
+
}, 8, ["to"])) : (0, vue.createCommentVNode)("", true)
|
|
1285
|
+
])])], 8, _hoisted_11$8));
|
|
1286
|
+
};
|
|
1287
|
+
}
|
|
1288
|
+
});
|
|
1289
|
+
//#endregion
|
|
1290
|
+
//#region src/vue/components/CollectionCalendarView.vue?vue&type=script&setup=true&lang.ts
|
|
1291
|
+
var _hoisted_1$8 = {
|
|
1292
|
+
class: "flex flex-col gap-3",
|
|
1293
|
+
"data-testid": "collection-calendar"
|
|
1294
|
+
};
|
|
1295
|
+
var _hoisted_2$8 = { class: "flex items-center gap-2" };
|
|
1296
|
+
var _hoisted_3$8 = ["aria-label"];
|
|
1297
|
+
var _hoisted_4$8 = ["aria-label"];
|
|
1298
|
+
var _hoisted_5$7 = {
|
|
1299
|
+
class: "text-sm font-bold text-slate-800 flex-1",
|
|
1300
|
+
"data-testid": "collection-calendar-month"
|
|
1301
|
+
};
|
|
1302
|
+
var _hoisted_6$7 = { class: "grid grid-cols-7 gap-1 text-[10px] font-bold text-slate-400 uppercase tracking-wider select-none" };
|
|
1303
|
+
var _hoisted_7$7 = { class: "grid grid-cols-7 gap-1" };
|
|
1304
|
+
var _hoisted_8$7 = [
|
|
1305
|
+
"aria-label",
|
|
1306
|
+
"data-testid",
|
|
1307
|
+
"onClick",
|
|
1308
|
+
"onKeydown"
|
|
1309
|
+
];
|
|
1310
|
+
var _hoisted_9$7 = { class: "flex items-center justify-end" };
|
|
1311
|
+
var _hoisted_10$7 = ["data-testid", "onClick"];
|
|
1312
|
+
var _hoisted_11$7 = {
|
|
1313
|
+
key: 0,
|
|
1314
|
+
class: "flex flex-wrap items-center gap-1.5 pt-1",
|
|
1315
|
+
"data-testid": "collection-calendar-no-date"
|
|
1316
|
+
};
|
|
1317
|
+
var _hoisted_12$6 = { class: "text-[10px] font-bold text-slate-400 uppercase tracking-wider mr-1" };
|
|
1318
|
+
var _hoisted_13$5 = ["data-testid", "onClick"];
|
|
1319
|
+
/** Sort key for ordering a day's chips by start time: earliest first, with
|
|
1320
|
+
* clock-less all-day records sinking to the bottom (matching the day view). */
|
|
1321
|
+
var DAY_CHIP_DEFAULT = "bg-indigo-50 text-indigo-700 border-indigo-100 hover:bg-indigo-100";
|
|
1322
|
+
var UNDATED_CHIP_DEFAULT = "bg-slate-50 text-slate-600 border-slate-200 hover:bg-slate-100";
|
|
1323
|
+
//#endregion
|
|
1324
|
+
//#region src/vue/components/CollectionCalendarView.vue
|
|
1325
|
+
var CollectionCalendarView_default = /* @__PURE__ */ (0, vue.defineComponent)({
|
|
1326
|
+
__name: "CollectionCalendarView",
|
|
1327
|
+
props: {
|
|
1328
|
+
schema: {},
|
|
1329
|
+
items: {},
|
|
1330
|
+
anchorField: {},
|
|
1331
|
+
endField: {},
|
|
1332
|
+
timeField: {},
|
|
1333
|
+
colorField: {},
|
|
1334
|
+
selected: {}
|
|
1335
|
+
},
|
|
1336
|
+
emits: ["select", "openDay"],
|
|
1337
|
+
setup(__props, { emit: __emit }) {
|
|
1338
|
+
const props = __props;
|
|
1339
|
+
const emit = __emit;
|
|
1340
|
+
const { t, locale } = useCollectionI18n();
|
|
1341
|
+
const now = /* @__PURE__ */ new Date();
|
|
1342
|
+
const viewYear = (0, vue.ref)(now.getFullYear());
|
|
1343
|
+
const viewMonth = (0, vue.ref)(now.getMonth() + 1);
|
|
1344
|
+
const todayKey = require_promptSafety.ymdKey({
|
|
1345
|
+
year: now.getFullYear(),
|
|
1346
|
+
month: now.getMonth() + 1,
|
|
1347
|
+
day: now.getDate()
|
|
1348
|
+
});
|
|
1349
|
+
const grid = (0, vue.computed)(() => require_promptSafety.buildMonthGrid(viewYear.value, viewMonth.value));
|
|
1350
|
+
const bucketed = (0, vue.computed)(() => require_promptSafety.bucketRecords(props.items, props.anchorField, props.endField, props.timeField));
|
|
1351
|
+
const labelField = (0, vue.computed)(() => require_promptSafety.labelFieldFor(props.schema));
|
|
1352
|
+
function colorOf(item) {
|
|
1353
|
+
return props.colorField ? require_promptSafety.resolveEnumColor(props.schema, props.colorField, item[props.colorField]) : null;
|
|
1354
|
+
}
|
|
1355
|
+
function sliceStartKey(slice) {
|
|
1356
|
+
return slice.kind === "allDay" ? require_promptSafety.MINUTES_PER_DAY + 1 : slice.startMin;
|
|
1357
|
+
}
|
|
1358
|
+
/** Records whose span covers a given day, ordered by start time so the month
|
|
1359
|
+
* grid stacks chips the same way the day (time-allocation) view does. */
|
|
1360
|
+
function recordsOnDay(day) {
|
|
1361
|
+
return bucketed.value.spans.map((span) => ({
|
|
1362
|
+
span,
|
|
1363
|
+
slice: require_promptSafety.daySlice(span, day)
|
|
1364
|
+
})).filter((pair) => pair.slice !== null).sort((left, right) => sliceStartKey(left.slice) - sliceStartKey(right.slice)).map(({ span }) => ({
|
|
1365
|
+
id: require_promptSafety.itemIdOf(span.item, props.schema),
|
|
1366
|
+
label: require_promptSafety.itemLabelOf(span.item, props.schema, labelField.value),
|
|
1367
|
+
color: colorOf(span.item)
|
|
1368
|
+
}));
|
|
1369
|
+
}
|
|
1370
|
+
/** Grid cells paired with the records that land on them, computed once per
|
|
1371
|
+
* render. Clicking any cell opens the day view (create happens there). */
|
|
1372
|
+
const cells = (0, vue.computed)(() => grid.value.map((cell) => ({
|
|
1373
|
+
cell,
|
|
1374
|
+
entries: recordsOnDay(cell.ymd)
|
|
1375
|
+
})));
|
|
1376
|
+
const undatedEntries = (0, vue.computed)(() => bucketed.value.noDate.map((item) => ({
|
|
1377
|
+
id: require_promptSafety.itemIdOf(item, props.schema),
|
|
1378
|
+
label: require_promptSafety.itemLabelOf(item, props.schema, labelField.value),
|
|
1379
|
+
color: colorOf(item)
|
|
1380
|
+
})));
|
|
1381
|
+
function chipClass(entry, uncolored) {
|
|
1382
|
+
if (entry.id === props.selected) return "bg-indigo-600 text-white border-indigo-600";
|
|
1383
|
+
if (!entry.color) return uncolored;
|
|
1384
|
+
return `${entry.color.badge} ${entry.color.border} hover:brightness-95`;
|
|
1385
|
+
}
|
|
1386
|
+
const monthLabel = (0, vue.computed)(() => {
|
|
1387
|
+
try {
|
|
1388
|
+
return new Intl.DateTimeFormat(locale.value, {
|
|
1389
|
+
month: "long",
|
|
1390
|
+
year: "numeric",
|
|
1391
|
+
timeZone: "UTC"
|
|
1392
|
+
}).format(new Date(Date.UTC(viewYear.value, viewMonth.value - 1, 1)));
|
|
1393
|
+
} catch {
|
|
1394
|
+
return `${viewYear.value}-${String(viewMonth.value).padStart(2, "0")}`;
|
|
1395
|
+
}
|
|
1396
|
+
});
|
|
1397
|
+
/** Localized short weekday names, Sunday-first (matches the grid). */
|
|
1398
|
+
const weekdayLabels = (0, vue.computed)(() => {
|
|
1399
|
+
try {
|
|
1400
|
+
const formatter = new Intl.DateTimeFormat(locale.value, {
|
|
1401
|
+
weekday: "short",
|
|
1402
|
+
timeZone: "UTC"
|
|
1403
|
+
});
|
|
1404
|
+
return Array.from({ length: 7 }, (_, idx) => formatter.format(new Date(Date.UTC(2024, 0, 7 + idx))));
|
|
1405
|
+
} catch {
|
|
1406
|
+
return [
|
|
1407
|
+
"Sun",
|
|
1408
|
+
"Mon",
|
|
1409
|
+
"Tue",
|
|
1410
|
+
"Wed",
|
|
1411
|
+
"Thu",
|
|
1412
|
+
"Fri",
|
|
1413
|
+
"Sat"
|
|
1414
|
+
];
|
|
1415
|
+
}
|
|
1416
|
+
});
|
|
1417
|
+
function stepMonth(delta) {
|
|
1418
|
+
const next = viewMonth.value + delta;
|
|
1419
|
+
if (next < 1) {
|
|
1420
|
+
viewMonth.value = 12;
|
|
1421
|
+
viewYear.value -= 1;
|
|
1422
|
+
} else if (next > 12) {
|
|
1423
|
+
viewMonth.value = 1;
|
|
1424
|
+
viewYear.value += 1;
|
|
1425
|
+
} else viewMonth.value = next;
|
|
1426
|
+
}
|
|
1427
|
+
function goToday() {
|
|
1428
|
+
viewYear.value = now.getFullYear();
|
|
1429
|
+
viewMonth.value = now.getMonth() + 1;
|
|
1430
|
+
}
|
|
1431
|
+
return (_ctx, _cache) => {
|
|
1432
|
+
return (0, vue.openBlock)(), (0, vue.createElementBlock)("div", _hoisted_1$8, [
|
|
1433
|
+
(0, vue.createElementVNode)("div", _hoisted_2$8, [
|
|
1434
|
+
(0, vue.createElementVNode)("button", {
|
|
1435
|
+
type: "button",
|
|
1436
|
+
class: "h-8 w-8 flex items-center justify-center rounded text-slate-500 hover:bg-slate-100 transition-colors",
|
|
1437
|
+
"aria-label": (0, vue.unref)(t)("collectionsView.calendarPrevMonth"),
|
|
1438
|
+
"data-testid": "collection-calendar-prev",
|
|
1439
|
+
onClick: _cache[0] || (_cache[0] = ($event) => stepMonth(-1))
|
|
1440
|
+
}, [..._cache[2] || (_cache[2] = [(0, vue.createElementVNode)("span", { class: "material-icons text-lg" }, "chevron_left", -1)])], 8, _hoisted_3$8),
|
|
1441
|
+
(0, vue.createElementVNode)("button", {
|
|
1442
|
+
type: "button",
|
|
1443
|
+
class: "h-8 w-8 flex items-center justify-center rounded text-slate-500 hover:bg-slate-100 transition-colors",
|
|
1444
|
+
"aria-label": (0, vue.unref)(t)("collectionsView.calendarNextMonth"),
|
|
1445
|
+
"data-testid": "collection-calendar-next",
|
|
1446
|
+
onClick: _cache[1] || (_cache[1] = ($event) => stepMonth(1))
|
|
1447
|
+
}, [..._cache[3] || (_cache[3] = [(0, vue.createElementVNode)("span", { class: "material-icons text-lg" }, "chevron_right", -1)])], 8, _hoisted_4$8),
|
|
1448
|
+
(0, vue.createElementVNode)("h3", _hoisted_5$7, (0, vue.toDisplayString)(monthLabel.value), 1),
|
|
1449
|
+
(0, vue.createElementVNode)("button", {
|
|
1450
|
+
type: "button",
|
|
1451
|
+
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",
|
|
1452
|
+
"data-testid": "collection-calendar-today",
|
|
1453
|
+
onClick: goToday
|
|
1454
|
+
}, (0, vue.toDisplayString)((0, vue.unref)(t)("collectionsView.calendarToday")), 1)
|
|
1455
|
+
]),
|
|
1456
|
+
(0, vue.createElementVNode)("div", _hoisted_6$7, [((0, vue.openBlock)(true), (0, vue.createElementBlock)(vue.Fragment, null, (0, vue.renderList)(weekdayLabels.value, (label, idx) => {
|
|
1457
|
+
return (0, vue.openBlock)(), (0, vue.createElementBlock)("div", {
|
|
1458
|
+
key: idx,
|
|
1459
|
+
class: "px-1 py-1 text-center"
|
|
1460
|
+
}, (0, vue.toDisplayString)(label), 1);
|
|
1461
|
+
}), 128))]),
|
|
1462
|
+
(0, vue.createElementVNode)("div", _hoisted_7$7, [((0, vue.openBlock)(true), (0, vue.createElementBlock)(vue.Fragment, null, (0, vue.renderList)(cells.value, ({ cell, entries }) => {
|
|
1463
|
+
return (0, vue.openBlock)(), (0, vue.createElementBlock)("div", {
|
|
1464
|
+
key: cell.key,
|
|
1465
|
+
class: (0, vue.normalizeClass)(["min-h-[5.5rem] rounded-lg border p-1 flex flex-col gap-1 overflow-hidden transition-colors cursor-pointer hover:border-indigo-300 focus:outline-none focus:ring-2 focus:ring-indigo-500/30", cell.inMonth ? "bg-white border-slate-200" : "bg-slate-50/50 border-slate-100"]),
|
|
1466
|
+
role: "button",
|
|
1467
|
+
tabindex: 0,
|
|
1468
|
+
"aria-label": (0, vue.unref)(t)("collectionsView.dayViewOpen", { date: cell.key }),
|
|
1469
|
+
"data-testid": `collection-calendar-day-${cell.key}`,
|
|
1470
|
+
onClick: ($event) => emit("openDay", cell.ymd),
|
|
1471
|
+
onKeydown: [(0, vue.withKeys)((0, vue.withModifiers)(($event) => emit("openDay", cell.ymd), ["self", "prevent"]), ["enter"]), (0, vue.withKeys)((0, vue.withModifiers)(($event) => emit("openDay", cell.ymd), ["self", "prevent"]), ["space"])]
|
|
1472
|
+
}, [(0, vue.createElementVNode)("div", _hoisted_9$7, [(0, vue.createElementVNode)("span", { class: (0, vue.normalizeClass)(["text-[11px] font-bold h-5 min-w-5 px-1 inline-flex items-center justify-center rounded-full", cell.key === (0, vue.unref)(todayKey) ? "bg-indigo-600 text-white" : cell.inMonth ? "text-slate-500" : "text-slate-300"]) }, (0, vue.toDisplayString)(cell.ymd.day), 3)]), ((0, vue.openBlock)(true), (0, vue.createElementBlock)(vue.Fragment, null, (0, vue.renderList)(entries, (entry) => {
|
|
1473
|
+
return (0, vue.openBlock)(), (0, vue.createElementBlock)("button", {
|
|
1474
|
+
key: entry.id,
|
|
1475
|
+
type: "button",
|
|
1476
|
+
class: (0, vue.normalizeClass)(["text-left text-[11px] leading-tight font-semibold truncate rounded px-1.5 py-0.5 border transition-colors", chipClass(entry, DAY_CHIP_DEFAULT)]),
|
|
1477
|
+
"data-testid": `collection-calendar-chip-${entry.id}`,
|
|
1478
|
+
onClick: (0, vue.withModifiers)(($event) => emit("select", entry.id), ["stop"])
|
|
1479
|
+
}, (0, vue.toDisplayString)(entry.label), 11, _hoisted_10$7);
|
|
1480
|
+
}), 128))], 42, _hoisted_8$7);
|
|
1481
|
+
}), 128))]),
|
|
1482
|
+
bucketed.value.noDate.length > 0 ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("div", _hoisted_11$7, [(0, vue.createElementVNode)("span", _hoisted_12$6, (0, vue.toDisplayString)((0, vue.unref)(t)("collectionsView.calendarNoDate")), 1), ((0, vue.openBlock)(true), (0, vue.createElementBlock)(vue.Fragment, null, (0, vue.renderList)(undatedEntries.value, (entry) => {
|
|
1483
|
+
return (0, vue.openBlock)(), (0, vue.createElementBlock)("button", {
|
|
1484
|
+
key: entry.id,
|
|
1485
|
+
type: "button",
|
|
1486
|
+
class: (0, vue.normalizeClass)(["text-[11px] font-semibold truncate rounded px-1.5 py-0.5 border transition-colors", chipClass(entry, UNDATED_CHIP_DEFAULT)]),
|
|
1487
|
+
"data-testid": `collection-calendar-undated-${entry.id}`,
|
|
1488
|
+
onClick: ($event) => emit("select", entry.id)
|
|
1489
|
+
}, (0, vue.toDisplayString)(entry.label), 11, _hoisted_13$5);
|
|
1490
|
+
}), 128))])) : (0, vue.createCommentVNode)("", true)
|
|
1491
|
+
]);
|
|
1492
|
+
};
|
|
1493
|
+
}
|
|
1494
|
+
});
|
|
1495
|
+
//#endregion
|
|
1496
|
+
//#region src/vue/components/CollectionDayView.vue?vue&type=script&setup=true&lang.ts
|
|
1497
|
+
var _hoisted_1$7 = { class: "flex items-center gap-2 border-b border-slate-200 px-4 py-3" };
|
|
1498
|
+
var _hoisted_2$7 = {
|
|
1499
|
+
class: "flex-1 text-sm font-bold text-slate-800",
|
|
1500
|
+
"data-testid": "collection-day-view-title"
|
|
1501
|
+
};
|
|
1502
|
+
var _hoisted_3$7 = ["aria-label"];
|
|
1503
|
+
var _hoisted_4$7 = ["aria-label"];
|
|
1504
|
+
var _hoisted_5$6 = {
|
|
1505
|
+
key: 0,
|
|
1506
|
+
class: "px-4 py-10 text-center text-sm text-slate-400"
|
|
1507
|
+
};
|
|
1508
|
+
var _hoisted_6$6 = { class: "absolute -top-2 left-0 w-10 pr-1 text-right text-[10px] tabular-nums text-slate-400" };
|
|
1509
|
+
var _hoisted_7$6 = {
|
|
1510
|
+
class: "absolute inset-y-0 right-0",
|
|
1511
|
+
style: { "left": "2.75rem" }
|
|
1512
|
+
};
|
|
1513
|
+
var _hoisted_8$6 = ["data-testid", "onClick"];
|
|
1514
|
+
var _hoisted_9$6 = { class: "block truncate text-[11px] font-semibold leading-tight" };
|
|
1515
|
+
var _hoisted_10$6 = {
|
|
1516
|
+
key: 0,
|
|
1517
|
+
"aria-hidden": "true"
|
|
1518
|
+
};
|
|
1519
|
+
var _hoisted_11$6 = {
|
|
1520
|
+
key: 1,
|
|
1521
|
+
"aria-hidden": "true"
|
|
1522
|
+
};
|
|
1523
|
+
var _hoisted_12$5 = {
|
|
1524
|
+
key: 2,
|
|
1525
|
+
class: "flex flex-wrap items-center gap-1.5 border-t border-slate-200 px-4 py-2",
|
|
1526
|
+
"data-testid": "collection-day-view-all-day"
|
|
1527
|
+
};
|
|
1528
|
+
var _hoisted_13$4 = { class: "mr-1 text-[10px] font-bold uppercase tracking-wider text-slate-400" };
|
|
1529
|
+
var _hoisted_14$4 = ["data-testid", "onClick"];
|
|
1530
|
+
var _hoisted_15$4 = {
|
|
1531
|
+
key: 0,
|
|
1532
|
+
class: "min-w-0 flex-1 overflow-y-auto",
|
|
1533
|
+
"data-testid": "collection-day-view-detail"
|
|
1534
|
+
};
|
|
1535
|
+
var HOUR_PX = 48;
|
|
1536
|
+
var MIN_BLOCK_PX = 16;
|
|
1537
|
+
var LINE_PX = 20;
|
|
1538
|
+
var LANE_MIN_MINUTES = 30;
|
|
1539
|
+
var MAX_CHIP_FIELDS = 3;
|
|
1540
|
+
var TIMED_DEFAULT = "bg-indigo-50 text-indigo-700 border-indigo-200 hover:bg-indigo-100";
|
|
1541
|
+
var ALL_DAY_DEFAULT = "bg-slate-50 text-slate-600 border-slate-200 hover:bg-slate-100";
|
|
1542
|
+
//#endregion
|
|
1543
|
+
//#region src/vue/components/CollectionDayView.vue
|
|
1544
|
+
var CollectionDayView_default = /* @__PURE__ */ (0, vue.defineComponent)({
|
|
1545
|
+
__name: "CollectionDayView",
|
|
1546
|
+
props: {
|
|
1547
|
+
schema: {},
|
|
1548
|
+
items: {},
|
|
1549
|
+
day: {},
|
|
1550
|
+
anchorField: {},
|
|
1551
|
+
endField: {},
|
|
1552
|
+
timeField: {},
|
|
1553
|
+
colorField: {},
|
|
1554
|
+
selected: {},
|
|
1555
|
+
canCreate: { type: Boolean },
|
|
1556
|
+
showDetail: { type: Boolean }
|
|
1557
|
+
},
|
|
1558
|
+
emits: [
|
|
1559
|
+
"select",
|
|
1560
|
+
"createOn",
|
|
1561
|
+
"close"
|
|
1562
|
+
],
|
|
1563
|
+
setup(__props, { emit: __emit }) {
|
|
1564
|
+
const props = __props;
|
|
1565
|
+
const emit = __emit;
|
|
1566
|
+
const { t, locale } = useCollectionI18n();
|
|
1567
|
+
const TOTAL_HEIGHT = HOUR_PX * 24;
|
|
1568
|
+
const PX_PER_MIN = HOUR_PX / 60;
|
|
1569
|
+
const scrollEl = (0, vue.ref)(null);
|
|
1570
|
+
const dialogEl = (0, vue.ref)(null);
|
|
1571
|
+
const dayKey = (0, vue.computed)(() => require_promptSafety.ymdKey(props.day));
|
|
1572
|
+
const dayLabel = (0, vue.computed)(() => {
|
|
1573
|
+
try {
|
|
1574
|
+
return new Intl.DateTimeFormat(locale.value, {
|
|
1575
|
+
weekday: "long",
|
|
1576
|
+
year: "numeric",
|
|
1577
|
+
month: "long",
|
|
1578
|
+
day: "numeric",
|
|
1579
|
+
timeZone: "UTC"
|
|
1580
|
+
}).format(new Date(Date.UTC(props.day.year, props.day.month - 1, props.day.day)));
|
|
1581
|
+
} catch {
|
|
1582
|
+
return dayKey.value;
|
|
1583
|
+
}
|
|
1584
|
+
});
|
|
1585
|
+
function hourLabel(hour) {
|
|
1586
|
+
return `${String(hour).padStart(2, "0")}:00`;
|
|
1587
|
+
}
|
|
1588
|
+
const labelField = (0, vue.computed)(() => require_promptSafety.labelFieldFor(props.schema));
|
|
1589
|
+
const CHIP_SKIP_TYPES = new Set([
|
|
1590
|
+
"date",
|
|
1591
|
+
"datetime",
|
|
1592
|
+
"table",
|
|
1593
|
+
"embed",
|
|
1594
|
+
"image",
|
|
1595
|
+
"markdown"
|
|
1596
|
+
]);
|
|
1597
|
+
function secondaryFieldsOf(item) {
|
|
1598
|
+
const out = [];
|
|
1599
|
+
for (const [key, field] of Object.entries(props.schema.fields)) {
|
|
1600
|
+
if (out.length >= MAX_CHIP_FIELDS) break;
|
|
1601
|
+
if (key === props.schema.primaryKey || key === labelField.value) continue;
|
|
1602
|
+
if (key === props.anchorField || key === props.endField || key === props.timeField) continue;
|
|
1603
|
+
if (CHIP_SKIP_TYPES.has(field.type)) continue;
|
|
1604
|
+
const value = item[key];
|
|
1605
|
+
if (value === void 0 || value === null || typeof value === "object") continue;
|
|
1606
|
+
const text = String(value);
|
|
1607
|
+
if (text.length > 0) out.push(text);
|
|
1608
|
+
}
|
|
1609
|
+
return out;
|
|
1610
|
+
}
|
|
1611
|
+
function colorOf(item) {
|
|
1612
|
+
return props.colorField ? require_promptSafety.resolveEnumColor(props.schema, props.colorField, item[props.colorField]) : null;
|
|
1613
|
+
}
|
|
1614
|
+
const dayEntries = (0, vue.computed)(() => {
|
|
1615
|
+
const { spans } = require_promptSafety.bucketRecords(props.items, props.anchorField, props.endField, props.timeField);
|
|
1616
|
+
const entries = [];
|
|
1617
|
+
for (const span of spans) {
|
|
1618
|
+
const slice = require_promptSafety.daySlice(span, props.day);
|
|
1619
|
+
if (!slice) continue;
|
|
1620
|
+
entries.push({
|
|
1621
|
+
id: require_promptSafety.itemIdOf(span.item, props.schema),
|
|
1622
|
+
label: require_promptSafety.itemLabelOf(span.item, props.schema, labelField.value),
|
|
1623
|
+
secondary: secondaryFieldsOf(span.item),
|
|
1624
|
+
color: colorOf(span.item),
|
|
1625
|
+
slice
|
|
1626
|
+
});
|
|
1627
|
+
}
|
|
1628
|
+
return entries;
|
|
1629
|
+
});
|
|
1630
|
+
const allDayEntries = (0, vue.computed)(() => dayEntries.value.filter((entry) => entry.slice.kind === "allDay"));
|
|
1631
|
+
const timedEntries = (0, vue.computed)(() => {
|
|
1632
|
+
const timed = dayEntries.value.filter((entry) => entry.slice.kind !== "allDay");
|
|
1633
|
+
const lanes = require_promptSafety.assignLanes(timed.map((entry) => ({
|
|
1634
|
+
startMin: entry.slice.startMin,
|
|
1635
|
+
endMin: Math.max(entry.slice.endMin, entry.slice.startMin + LANE_MIN_MINUTES)
|
|
1636
|
+
})));
|
|
1637
|
+
return timed.map((entry, index) => {
|
|
1638
|
+
const { lane, lanes: laneCount } = lanes[index];
|
|
1639
|
+
const widthPct = 100 / laneCount;
|
|
1640
|
+
const heightPx = entry.slice.kind === "line" ? LINE_PX : Math.max((entry.slice.endMin - entry.slice.startMin) * PX_PER_MIN, MIN_BLOCK_PX);
|
|
1641
|
+
return {
|
|
1642
|
+
...entry,
|
|
1643
|
+
style: {
|
|
1644
|
+
top: `${entry.slice.startMin * PX_PER_MIN}px`,
|
|
1645
|
+
height: `${heightPx}px`,
|
|
1646
|
+
left: `${lane * widthPct}%`,
|
|
1647
|
+
width: `calc(${widthPct}% - 3px)`
|
|
1648
|
+
}
|
|
1649
|
+
};
|
|
1650
|
+
});
|
|
1651
|
+
});
|
|
1652
|
+
function timedChipClass(entry) {
|
|
1653
|
+
if (entry.id === props.selected) return "bg-indigo-600 text-white border-indigo-600 z-10";
|
|
1654
|
+
if (!entry.color) return TIMED_DEFAULT;
|
|
1655
|
+
return `${entry.color.badge} ${entry.color.border} hover:brightness-95`;
|
|
1656
|
+
}
|
|
1657
|
+
function allDayChipClass(entry) {
|
|
1658
|
+
if (entry.id === props.selected) return "bg-indigo-600 text-white border-indigo-600";
|
|
1659
|
+
if (!entry.color) return ALL_DAY_DEFAULT;
|
|
1660
|
+
return `${entry.color.badge} ${entry.color.border} hover:brightness-95`;
|
|
1661
|
+
}
|
|
1662
|
+
function onSelect(itemId) {
|
|
1663
|
+
emit("select", itemId);
|
|
1664
|
+
}
|
|
1665
|
+
function onCreate() {
|
|
1666
|
+
emit("createOn", dayKey.value);
|
|
1667
|
+
}
|
|
1668
|
+
(0, vue.onMounted)(async () => {
|
|
1669
|
+
await (0, vue.nextTick)();
|
|
1670
|
+
dialogEl.value?.focus();
|
|
1671
|
+
const earliest = timedEntries.value.reduce((min, entry) => Math.min(min, entry.slice.startMin), require_promptSafety.MINUTES_PER_DAY);
|
|
1672
|
+
if (earliest >= 1440) return;
|
|
1673
|
+
if (scrollEl.value) scrollEl.value.scrollTop = Math.max(0, (earliest - 60) * PX_PER_MIN);
|
|
1674
|
+
});
|
|
1675
|
+
return (_ctx, _cache) => {
|
|
1676
|
+
return (0, vue.openBlock)(), (0, vue.createElementBlock)("div", {
|
|
1677
|
+
class: "fixed inset-0 z-40 flex items-center justify-center bg-slate-900/40 p-4",
|
|
1678
|
+
"data-testid": "collection-day-view",
|
|
1679
|
+
onClick: _cache[1] || (_cache[1] = (0, vue.withModifiers)(($event) => emit("close"), ["self"])),
|
|
1680
|
+
onKeydown: _cache[2] || (_cache[2] = (0, vue.withKeys)(($event) => emit("close"), ["esc"]))
|
|
1681
|
+
}, [(0, vue.createElementVNode)("div", {
|
|
1682
|
+
ref_key: "dialogEl",
|
|
1683
|
+
ref: dialogEl,
|
|
1684
|
+
tabindex: "-1",
|
|
1685
|
+
class: (0, vue.normalizeClass)(["flex max-h-[85vh] w-full flex-row rounded-2xl bg-white shadow-xl focus:outline-none", __props.showDetail ? "max-w-4xl" : "max-w-md"]),
|
|
1686
|
+
role: "dialog",
|
|
1687
|
+
"aria-modal": "true"
|
|
1688
|
+
}, [(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"]) }, [
|
|
1689
|
+
(0, vue.createElementVNode)("div", _hoisted_1$7, [
|
|
1690
|
+
(0, vue.createElementVNode)("h3", _hoisted_2$7, (0, vue.toDisplayString)(dayLabel.value), 1),
|
|
1691
|
+
__props.canCreate ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("button", {
|
|
1692
|
+
key: 0,
|
|
1693
|
+
type: "button",
|
|
1694
|
+
class: "h-8 w-8 flex items-center justify-center rounded text-slate-500 hover:bg-slate-100 transition-colors",
|
|
1695
|
+
"aria-label": (0, vue.unref)(t)("collectionsView.calendarCreateOn", { date: dayKey.value }),
|
|
1696
|
+
"data-testid": "collection-day-view-create",
|
|
1697
|
+
onClick: onCreate
|
|
1698
|
+
}, [..._cache[3] || (_cache[3] = [(0, vue.createElementVNode)("span", { class: "material-icons text-lg" }, "add", -1)])], 8, _hoisted_3$7)) : (0, vue.createCommentVNode)("", true),
|
|
1699
|
+
(0, vue.createElementVNode)("button", {
|
|
1700
|
+
type: "button",
|
|
1701
|
+
class: "h-8 w-8 flex items-center justify-center rounded text-slate-500 hover:bg-slate-100 transition-colors",
|
|
1702
|
+
"aria-label": (0, vue.unref)(t)("collectionsView.dayViewClose"),
|
|
1703
|
+
"data-testid": "collection-day-view-close",
|
|
1704
|
+
onClick: _cache[0] || (_cache[0] = ($event) => emit("close"))
|
|
1705
|
+
}, [..._cache[4] || (_cache[4] = [(0, vue.createElementVNode)("span", { class: "material-icons text-lg" }, "close", -1)])], 8, _hoisted_4$7)
|
|
1706
|
+
]),
|
|
1707
|
+
timedEntries.value.length === 0 && allDayEntries.value.length === 0 ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("div", _hoisted_5$6, (0, vue.toDisplayString)((0, vue.unref)(t)("collectionsView.dayViewEmpty")), 1)) : ((0, vue.openBlock)(), (0, vue.createElementBlock)("div", {
|
|
1708
|
+
key: 1,
|
|
1709
|
+
ref_key: "scrollEl",
|
|
1710
|
+
ref: scrollEl,
|
|
1711
|
+
class: "flex-1 overflow-y-auto px-2 py-2"
|
|
1712
|
+
}, [(0, vue.createElementVNode)("div", {
|
|
1713
|
+
class: "relative",
|
|
1714
|
+
style: (0, vue.normalizeStyle)({ height: `${TOTAL_HEIGHT}px` }),
|
|
1715
|
+
"data-testid": "collection-day-view-timeline"
|
|
1716
|
+
}, [((0, vue.openBlock)(), (0, vue.createElementBlock)(vue.Fragment, null, (0, vue.renderList)(24, (hour) => {
|
|
1717
|
+
return (0, vue.createElementVNode)("div", {
|
|
1718
|
+
key: hour,
|
|
1719
|
+
class: "absolute left-0 right-0 border-t border-slate-100",
|
|
1720
|
+
style: (0, vue.normalizeStyle)({ top: `${(hour - 1) * HOUR_PX}px` })
|
|
1721
|
+
}, [(0, vue.createElementVNode)("span", _hoisted_6$6, (0, vue.toDisplayString)(hourLabel(hour - 1)), 1)], 4);
|
|
1722
|
+
}), 64)), (0, vue.createElementVNode)("div", _hoisted_7$6, [((0, vue.openBlock)(true), (0, vue.createElementBlock)(vue.Fragment, null, (0, vue.renderList)(timedEntries.value, (entry) => {
|
|
1723
|
+
return (0, vue.openBlock)(), (0, vue.createElementBlock)("button", {
|
|
1724
|
+
key: entry.id,
|
|
1725
|
+
type: "button",
|
|
1726
|
+
class: (0, vue.normalizeClass)(["absolute overflow-hidden rounded border px-1.5 py-0.5 text-left transition-colors", timedChipClass(entry)]),
|
|
1727
|
+
style: (0, vue.normalizeStyle)(entry.style),
|
|
1728
|
+
"data-testid": `collection-day-view-chip-${entry.id}`,
|
|
1729
|
+
onClick: ($event) => onSelect(entry.id)
|
|
1730
|
+
}, [(0, vue.createElementVNode)("span", _hoisted_9$6, [
|
|
1731
|
+
entry.slice.bleedsBefore ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("span", _hoisted_10$6, "▲ ")) : (0, vue.createCommentVNode)("", true),
|
|
1732
|
+
(0, vue.createTextVNode)((0, vue.toDisplayString)(entry.label), 1),
|
|
1733
|
+
entry.slice.bleedsAfter ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("span", _hoisted_11$6, " ▼")) : (0, vue.createCommentVNode)("", true)
|
|
1734
|
+
]), ((0, vue.openBlock)(true), (0, vue.createElementBlock)(vue.Fragment, null, (0, vue.renderList)(entry.secondary, (text, i) => {
|
|
1735
|
+
return (0, vue.openBlock)(), (0, vue.createElementBlock)("span", {
|
|
1736
|
+
key: i,
|
|
1737
|
+
class: "block truncate text-[10px] leading-tight opacity-70"
|
|
1738
|
+
}, (0, vue.toDisplayString)(text), 1);
|
|
1739
|
+
}), 128))], 14, _hoisted_8$6);
|
|
1740
|
+
}), 128))])], 4)], 512)),
|
|
1741
|
+
allDayEntries.value.length > 0 ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("div", _hoisted_12$5, [(0, vue.createElementVNode)("span", _hoisted_13$4, (0, vue.toDisplayString)((0, vue.unref)(t)("collectionsView.dayViewAllDay")), 1), ((0, vue.openBlock)(true), (0, vue.createElementBlock)(vue.Fragment, null, (0, vue.renderList)(allDayEntries.value, (entry) => {
|
|
1742
|
+
return (0, vue.openBlock)(), (0, vue.createElementBlock)("button", {
|
|
1743
|
+
key: entry.id,
|
|
1744
|
+
type: "button",
|
|
1745
|
+
class: (0, vue.normalizeClass)(["truncate rounded border px-1.5 py-0.5 text-[11px] font-semibold transition-colors", allDayChipClass(entry)]),
|
|
1746
|
+
"data-testid": `collection-day-view-allday-${entry.id}`,
|
|
1747
|
+
onClick: ($event) => onSelect(entry.id)
|
|
1748
|
+
}, (0, vue.toDisplayString)(entry.label), 11, _hoisted_14$4);
|
|
1749
|
+
}), 128))])) : (0, vue.createCommentVNode)("", true)
|
|
1750
|
+
], 2), __props.showDetail ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("div", _hoisted_15$4, [(0, vue.renderSlot)(_ctx.$slots, "detail")])) : (0, vue.createCommentVNode)("", true)], 2)], 32);
|
|
1751
|
+
};
|
|
1752
|
+
}
|
|
1753
|
+
});
|
|
1754
|
+
//#endregion
|
|
1755
|
+
//#region src/vue/components/CollectionKanbanView.vue?vue&type=script&setup=true&lang.ts
|
|
1756
|
+
var _hoisted_1$6 = {
|
|
1757
|
+
class: "h-full overflow-x-auto overflow-y-hidden",
|
|
1758
|
+
"data-testid": "collection-kanban"
|
|
1759
|
+
};
|
|
1760
|
+
var _hoisted_2$6 = { class: "flex gap-3 h-full p-1 min-w-max" };
|
|
1761
|
+
var _hoisted_3$6 = ["data-testid"];
|
|
1762
|
+
var _hoisted_4$6 = { class: "flex items-center justify-between px-3 py-2 border-b border-slate-200" };
|
|
1763
|
+
var _hoisted_5$5 = { class: "flex items-center gap-2 min-w-0" };
|
|
1764
|
+
var _hoisted_6$5 = ["title"];
|
|
1765
|
+
var _hoisted_7$5 = { class: "text-[11px] text-slate-400 shrink-0" };
|
|
1766
|
+
var _hoisted_8$5 = [
|
|
1767
|
+
"data-testid",
|
|
1768
|
+
"aria-label",
|
|
1769
|
+
"onClick",
|
|
1770
|
+
"onKeydown"
|
|
1771
|
+
];
|
|
1772
|
+
var _hoisted_9$5 = { class: "flex items-start gap-2" };
|
|
1773
|
+
var _hoisted_10$5 = [
|
|
1774
|
+
"checked",
|
|
1775
|
+
"aria-label",
|
|
1776
|
+
"data-testid",
|
|
1777
|
+
"onChange"
|
|
1778
|
+
];
|
|
1779
|
+
var _hoisted_11$5 = { class: "text-sm font-medium text-slate-800 truncate" };
|
|
1780
|
+
var UNCATEGORIZED = "";
|
|
1781
|
+
//#endregion
|
|
1782
|
+
//#region src/vue/components/CollectionKanbanView.vue
|
|
1783
|
+
var CollectionKanbanView_default = /* @__PURE__ */ (0, vue.defineComponent)({
|
|
1784
|
+
__name: "CollectionKanbanView",
|
|
1785
|
+
props: {
|
|
1786
|
+
schema: {},
|
|
1787
|
+
groupField: {},
|
|
1788
|
+
items: {},
|
|
1789
|
+
selected: {},
|
|
1790
|
+
notified: {}
|
|
1791
|
+
},
|
|
1792
|
+
emits: ["select", "move"],
|
|
1793
|
+
setup(__props, { emit: __emit }) {
|
|
1794
|
+
const props = __props;
|
|
1795
|
+
const emit = __emit;
|
|
1796
|
+
const { t } = useCollectionI18n();
|
|
1797
|
+
/** The Uncategorized column uses the empty string as its sentinel value. */
|
|
1798
|
+
const groupSpec = (0, vue.computed)(() => props.schema.fields[props.groupField]);
|
|
1799
|
+
/** Declared enum values become columns in order, with a trailing
|
|
1800
|
+
* Uncategorized column for empty/unknown values (also a drop target that
|
|
1801
|
+
* clears the field). The Uncategorized column is omitted when the group
|
|
1802
|
+
* field is `required` — there's no valid "no value" state to drop into,
|
|
1803
|
+
* and clearing via it would only produce a rejected PUT. */
|
|
1804
|
+
const columns = (0, vue.computed)(() => {
|
|
1805
|
+
const values = groupSpec.value?.values ?? [];
|
|
1806
|
+
const declared = values.map((value) => ({
|
|
1807
|
+
value,
|
|
1808
|
+
label: value
|
|
1809
|
+
}));
|
|
1810
|
+
if (groupSpec.value?.required || values.includes(UNCATEGORIZED)) return declared;
|
|
1811
|
+
return [...declared, {
|
|
1812
|
+
value: UNCATEGORIZED,
|
|
1813
|
+
label: t("collectionsView.kanbanUncategorized")
|
|
1814
|
+
}];
|
|
1815
|
+
});
|
|
1816
|
+
function itemId(item) {
|
|
1817
|
+
return String(item[props.schema.primaryKey] ?? "");
|
|
1818
|
+
}
|
|
1819
|
+
const NOTIFY_ACCENT = {
|
|
1820
|
+
urgent: "border-l-4 border-l-red-500",
|
|
1821
|
+
nudge: "border-l-4 border-l-amber-500",
|
|
1822
|
+
info: "border-l-4 border-l-slate-400"
|
|
1823
|
+
};
|
|
1824
|
+
function notifyAccentClass(item) {
|
|
1825
|
+
const severity = props.notified?.get(itemId(item));
|
|
1826
|
+
return severity ? NOTIFY_ACCENT[severity] : "";
|
|
1827
|
+
}
|
|
1828
|
+
/** Card label: the schema's `displayField` value, else the primary key. */
|
|
1829
|
+
function itemLabel(item) {
|
|
1830
|
+
const field = props.schema.displayField;
|
|
1831
|
+
if (field) {
|
|
1832
|
+
const value = item[field];
|
|
1833
|
+
if (typeof value === "string" && value.length > 0) return value;
|
|
1834
|
+
}
|
|
1835
|
+
return itemId(item);
|
|
1836
|
+
}
|
|
1837
|
+
/** Which column a record belongs to: its group value when that value is
|
|
1838
|
+
* one of the declared enum values, else Uncategorized. */
|
|
1839
|
+
function columnOf(item) {
|
|
1840
|
+
const raw = item[props.groupField];
|
|
1841
|
+
if (raw === void 0 || raw === null || raw === "") return UNCATEGORIZED;
|
|
1842
|
+
const value = String(raw);
|
|
1843
|
+
return (groupSpec.value?.values ?? []).includes(value) ? value : UNCATEGORIZED;
|
|
1844
|
+
}
|
|
1845
|
+
const visibleItems = (0, vue.computed)(() => groupSpec.value ? props.items.filter((item) => require_promptSafety.fieldVisible(groupSpec.value, item)) : []);
|
|
1846
|
+
const itemsByColumnMap = (0, vue.computed)(() => {
|
|
1847
|
+
const map = /* @__PURE__ */ new Map();
|
|
1848
|
+
for (const column of columns.value) map.set(column.value, []);
|
|
1849
|
+
for (const item of visibleItems.value) {
|
|
1850
|
+
const value = columnOf(item);
|
|
1851
|
+
(map.get(value) ?? map.get(UNCATEGORIZED))?.push(item);
|
|
1852
|
+
}
|
|
1853
|
+
return map;
|
|
1854
|
+
});
|
|
1855
|
+
function itemsByColumn(value) {
|
|
1856
|
+
return itemsByColumnMap.value.get(value) ?? [];
|
|
1857
|
+
}
|
|
1858
|
+
function onDragChange(columnValue, event) {
|
|
1859
|
+
if (event.added) emit("move", itemId(event.added.element), columnValue);
|
|
1860
|
+
}
|
|
1861
|
+
const cardToggle = (0, vue.computed)(() => {
|
|
1862
|
+
for (const spec of Object.values(props.schema.fields)) if (spec.type === "toggle" && spec.field === props.groupField) return spec;
|
|
1863
|
+
return null;
|
|
1864
|
+
});
|
|
1865
|
+
function cardChecked(item) {
|
|
1866
|
+
const toggle = cardToggle.value;
|
|
1867
|
+
return toggle !== null && String(item[props.groupField] ?? "") === toggle.onValue;
|
|
1868
|
+
}
|
|
1869
|
+
function onCardToggle(item) {
|
|
1870
|
+
const toggle = cardToggle.value;
|
|
1871
|
+
if (!toggle) return;
|
|
1872
|
+
const next = cardChecked(item) ? toggle.offValue : toggle.onValue;
|
|
1873
|
+
if (next === void 0) return;
|
|
1874
|
+
emit("move", itemId(item), next);
|
|
1875
|
+
}
|
|
1876
|
+
return (_ctx, _cache) => {
|
|
1877
|
+
return (0, vue.openBlock)(), (0, vue.createElementBlock)("div", _hoisted_1$6, [(0, vue.createElementVNode)("div", _hoisted_2$6, [((0, vue.openBlock)(true), (0, vue.createElementBlock)(vue.Fragment, null, (0, vue.renderList)(columns.value, (column) => {
|
|
1878
|
+
return (0, vue.openBlock)(), (0, vue.createElementBlock)("div", {
|
|
1879
|
+
key: column.value,
|
|
1880
|
+
"data-testid": `collection-kanban-column-${column.value || "uncategorized"}`,
|
|
1881
|
+
class: "w-72 shrink-0 flex flex-col bg-slate-100 rounded-lg"
|
|
1882
|
+
}, [(0, vue.createElementVNode)("div", _hoisted_4$6, [(0, vue.createElementVNode)("div", _hoisted_5$5, [(0, vue.createElementVNode)("span", { class: (0, vue.normalizeClass)(["w-2 h-2 rounded-full shrink-0", (0, vue.unref)(require_promptSafety.resolveEnumColor)(__props.schema, __props.groupField, column.value).dot]) }, null, 2), (0, vue.createElementVNode)("span", {
|
|
1883
|
+
class: "font-semibold text-xs text-slate-600 truncate",
|
|
1884
|
+
title: column.label
|
|
1885
|
+
}, (0, vue.toDisplayString)(column.label), 9, _hoisted_6$5)]), (0, vue.createElementVNode)("span", _hoisted_7$5, (0, vue.toDisplayString)(itemsByColumn(column.value).length), 1)]), (0, vue.createVNode)((0, vue.unref)(vuedraggable.default), {
|
|
1886
|
+
"model-value": itemsByColumn(column.value),
|
|
1887
|
+
"item-key": __props.schema.primaryKey,
|
|
1888
|
+
group: "collection-kanban-cards",
|
|
1889
|
+
class: "flex-1 overflow-y-auto p-2 space-y-2 min-h-[2rem]",
|
|
1890
|
+
animation: 150,
|
|
1891
|
+
onChange: (e) => onDragChange(column.value, e)
|
|
1892
|
+
}, {
|
|
1893
|
+
item: (0, vue.withCtx)(({ element }) => [(0, vue.createElementVNode)("div", {
|
|
1894
|
+
"data-testid": `collection-kanban-card-${itemId(element)}`,
|
|
1895
|
+
tabindex: "0",
|
|
1896
|
+
role: "button",
|
|
1897
|
+
"aria-label": (0, vue.unref)(t)("collectionsView.kanbanOpenCard", { label: itemLabel(element) }),
|
|
1898
|
+
class: (0, vue.normalizeClass)(["bg-white border border-slate-200 rounded shadow-sm p-2 cursor-grab hover:shadow active:cursor-grabbing focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-400", [itemId(element) === __props.selected ? "ring-2 ring-indigo-500 border-indigo-300" : "", notifyAccentClass(element)]]),
|
|
1899
|
+
onClick: ($event) => emit("select", itemId(element)),
|
|
1900
|
+
onKeydown: [(0, vue.withKeys)((0, vue.withModifiers)((e) => !e.repeat && emit("select", itemId(element)), ["prevent", "self"]), ["enter"]), (0, vue.withKeys)((0, vue.withModifiers)((e) => !e.repeat && emit("select", itemId(element)), ["prevent", "self"]), ["space"])]
|
|
1901
|
+
}, [(0, vue.createElementVNode)("div", _hoisted_9$5, [cardToggle.value ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("input", {
|
|
1902
|
+
key: 0,
|
|
1903
|
+
type: "checkbox",
|
|
1904
|
+
checked: cardChecked(element),
|
|
1905
|
+
class: "mt-0.5 h-4 w-4 rounded border-slate-300 text-indigo-600 focus:ring-indigo-500/20 cursor-pointer shrink-0",
|
|
1906
|
+
"aria-label": cardToggle.value.label,
|
|
1907
|
+
"data-testid": `collection-kanban-toggle-${itemId(element)}`,
|
|
1908
|
+
onClick: _cache[0] || (_cache[0] = (0, vue.withModifiers)(() => {}, ["stop"])),
|
|
1909
|
+
onChange: ($event) => onCardToggle(element)
|
|
1910
|
+
}, null, 40, _hoisted_10$5)) : (0, vue.createCommentVNode)("", true), (0, vue.createElementVNode)("div", _hoisted_11$5, (0, vue.toDisplayString)(itemLabel(element)), 1)])], 42, _hoisted_8$5)]),
|
|
1911
|
+
_: 1
|
|
1912
|
+
}, 8, [
|
|
1913
|
+
"model-value",
|
|
1914
|
+
"item-key",
|
|
1915
|
+
"onChange"
|
|
1916
|
+
])], 8, _hoisted_3$6);
|
|
1917
|
+
}), 128))])]);
|
|
1918
|
+
};
|
|
1919
|
+
}
|
|
1920
|
+
});
|
|
1921
|
+
//#endregion
|
|
1922
|
+
//#region src/vue/components/CollectionRecordPanel.vue?vue&type=script&setup=true&lang.ts
|
|
1923
|
+
var _hoisted_1$5 = { class: "flex items-center gap-2 mb-4" };
|
|
1924
|
+
var _hoisted_2$5 = { class: "flex-1 min-w-0" };
|
|
1925
|
+
var _hoisted_3$5 = { class: "block text-[9px] font-bold text-slate-400 uppercase tracking-wider" };
|
|
1926
|
+
var _hoisted_4$5 = ["data-testid"];
|
|
1927
|
+
var _hoisted_5$4 = ["disabled"];
|
|
1928
|
+
var _hoisted_6$4 = {
|
|
1929
|
+
key: 1,
|
|
1930
|
+
class: "flex items-center gap-2"
|
|
1931
|
+
};
|
|
1932
|
+
var _hoisted_7$4 = [
|
|
1933
|
+
"disabled",
|
|
1934
|
+
"data-testid",
|
|
1935
|
+
"onClick"
|
|
1936
|
+
];
|
|
1937
|
+
var _hoisted_8$4 = {
|
|
1938
|
+
key: 0,
|
|
1939
|
+
class: "material-icons text-sm"
|
|
1940
|
+
};
|
|
1941
|
+
var _hoisted_9$4 = ["aria-label"];
|
|
1942
|
+
var _hoisted_10$4 = {
|
|
1943
|
+
key: 0,
|
|
1944
|
+
class: "mb-3 text-xs font-semibold text-red-600 bg-red-50 border border-red-100 p-2.5 rounded-xl shadow-sm",
|
|
1945
|
+
"data-testid": "collections-detail-action-error"
|
|
1946
|
+
};
|
|
1947
|
+
var _hoisted_11$4 = { class: "grid grid-cols-1 sm:grid-cols-2 gap-x-6 gap-y-4 bg-white rounded-2xl border border-slate-200/60 p-6 shadow-sm" };
|
|
1948
|
+
var _hoisted_12$4 = ["for"];
|
|
1949
|
+
var _hoisted_13$3 = {
|
|
1950
|
+
key: 0,
|
|
1951
|
+
class: "text-rose-500 font-bold"
|
|
1952
|
+
};
|
|
1953
|
+
var _hoisted_14$3 = {
|
|
1954
|
+
key: 0,
|
|
1955
|
+
class: "inline-flex items-center gap-2.5 text-sm text-slate-700 cursor-pointer select-none"
|
|
1956
|
+
};
|
|
1957
|
+
var _hoisted_15$3 = [
|
|
1958
|
+
"id",
|
|
1959
|
+
"onUpdate:modelValue",
|
|
1960
|
+
"data-testid",
|
|
1961
|
+
"onChange"
|
|
1962
|
+
];
|
|
1963
|
+
var _hoisted_16$2 = [
|
|
1964
|
+
"id",
|
|
1965
|
+
"onUpdate:modelValue",
|
|
1966
|
+
"required",
|
|
1967
|
+
"data-testid"
|
|
1968
|
+
];
|
|
1969
|
+
var _hoisted_17$2 = { value: "" };
|
|
1970
|
+
var _hoisted_18$2 = ["value"];
|
|
1971
|
+
var _hoisted_19$2 = [
|
|
1972
|
+
"id",
|
|
1973
|
+
"onUpdate:modelValue",
|
|
1974
|
+
"required",
|
|
1975
|
+
"data-testid"
|
|
1976
|
+
];
|
|
1977
|
+
var _hoisted_20$2 = { value: "" };
|
|
1978
|
+
var _hoisted_21$2 = ["value"];
|
|
1979
|
+
var _hoisted_22$2 = ["data-testid"];
|
|
1980
|
+
var _hoisted_23$2 = {
|
|
1981
|
+
key: 0,
|
|
1982
|
+
class: "overflow-hidden border border-slate-200 rounded-lg shadow-sm"
|
|
1983
|
+
};
|
|
1984
|
+
var _hoisted_24$1 = { class: "w-full text-xs text-slate-600 bg-white" };
|
|
1985
|
+
var _hoisted_25$1 = { class: "bg-slate-50 border-b border-slate-200 text-slate-500 font-bold uppercase tracking-wider" };
|
|
1986
|
+
var _hoisted_26$1 = { class: "divide-y divide-slate-100" };
|
|
1987
|
+
var _hoisted_27$1 = ["onUpdate:modelValue", "onChange"];
|
|
1988
|
+
var _hoisted_28$1 = ["onUpdate:modelValue", "required"];
|
|
1989
|
+
var _hoisted_29$1 = { value: "" };
|
|
1990
|
+
var _hoisted_30$1 = ["value"];
|
|
1991
|
+
var _hoisted_31$1 = ["onUpdate:modelValue", "required"];
|
|
1992
|
+
var _hoisted_32$1 = { value: "" };
|
|
1993
|
+
var _hoisted_33$1 = ["value"];
|
|
1994
|
+
var _hoisted_34$1 = {
|
|
1995
|
+
key: 3,
|
|
1996
|
+
class: "relative flex items-center"
|
|
1997
|
+
};
|
|
1998
|
+
var _hoisted_35$1 = { class: "absolute left-1.5 text-[10px] text-slate-400 font-bold pr-1 border-r border-slate-200" };
|
|
1999
|
+
var _hoisted_36$1 = ["onUpdate:modelValue", "required"];
|
|
2000
|
+
var _hoisted_37$1 = [
|
|
2001
|
+
"onUpdate:modelValue",
|
|
2002
|
+
"type",
|
|
2003
|
+
"required"
|
|
2004
|
+
];
|
|
2005
|
+
var _hoisted_38$1 = { class: "text-center px-1" };
|
|
2006
|
+
var _hoisted_39$1 = [
|
|
2007
|
+
"aria-label",
|
|
2008
|
+
"data-testid",
|
|
2009
|
+
"onClick"
|
|
2010
|
+
];
|
|
2011
|
+
var _hoisted_40$1 = {
|
|
2012
|
+
key: 1,
|
|
2013
|
+
class: "text-xs text-slate-400 italic"
|
|
2014
|
+
};
|
|
2015
|
+
var _hoisted_41$1 = ["data-testid", "onClick"];
|
|
2016
|
+
var _hoisted_42$1 = {
|
|
2017
|
+
key: 4,
|
|
2018
|
+
class: "relative flex items-center"
|
|
2019
|
+
};
|
|
2020
|
+
var _hoisted_43$1 = { class: "absolute left-3 text-slate-400 font-bold text-xs select-none pr-1.5 border-r border-slate-200" };
|
|
2021
|
+
var _hoisted_44$1 = [
|
|
2022
|
+
"id",
|
|
2023
|
+
"onUpdate:modelValue",
|
|
2024
|
+
"required",
|
|
2025
|
+
"data-testid"
|
|
2026
|
+
];
|
|
2027
|
+
var _hoisted_45$1 = [
|
|
2028
|
+
"id",
|
|
2029
|
+
"onUpdate:modelValue",
|
|
2030
|
+
"type",
|
|
2031
|
+
"required",
|
|
2032
|
+
"disabled",
|
|
2033
|
+
"data-testid"
|
|
2034
|
+
];
|
|
2035
|
+
var _hoisted_46$1 = [
|
|
2036
|
+
"id",
|
|
2037
|
+
"onUpdate:modelValue",
|
|
2038
|
+
"rows",
|
|
2039
|
+
"required",
|
|
2040
|
+
"data-testid"
|
|
2041
|
+
];
|
|
2042
|
+
var _hoisted_47$1 = ["data-testid"];
|
|
2043
|
+
var _hoisted_48$1 = {
|
|
2044
|
+
key: 0,
|
|
2045
|
+
class: "inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-[10px] font-semibold bg-emerald-50 text-emerald-700 border border-emerald-200/40"
|
|
2046
|
+
};
|
|
2047
|
+
var _hoisted_49$1 = {
|
|
2048
|
+
key: 1,
|
|
2049
|
+
class: "inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-[10px] font-semibold bg-slate-50 text-slate-400 border border-slate-200/20"
|
|
2050
|
+
};
|
|
2051
|
+
var _hoisted_50$1 = {
|
|
2052
|
+
key: 0,
|
|
2053
|
+
class: "inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-[10px] font-semibold bg-emerald-50 text-emerald-700 border border-emerald-200/40"
|
|
2054
|
+
};
|
|
2055
|
+
var _hoisted_51$1 = {
|
|
2056
|
+
key: 1,
|
|
2057
|
+
class: "inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-[10px] font-semibold bg-slate-50 text-slate-400 border border-slate-200/20"
|
|
2058
|
+
};
|
|
2059
|
+
var _hoisted_52$1 = {
|
|
2060
|
+
key: 2,
|
|
2061
|
+
class: "text-slate-300"
|
|
2062
|
+
};
|
|
2063
|
+
var _hoisted_53$1 = {
|
|
2064
|
+
key: 3,
|
|
2065
|
+
class: "font-semibold text-slate-900 tabular-nums text-sm"
|
|
2066
|
+
};
|
|
2067
|
+
var _hoisted_54$1 = {
|
|
2068
|
+
key: 4,
|
|
2069
|
+
class: "inline-block truncate tabular-nums font-bold text-indigo-900 bg-indigo-50/50 px-2 py-0.5 rounded border border-indigo-100/50"
|
|
2070
|
+
};
|
|
2071
|
+
var _hoisted_55$1 = {
|
|
2072
|
+
key: 5,
|
|
2073
|
+
class: "border border-slate-200/80 rounded-xl overflow-hidden shadow-sm mt-1"
|
|
2074
|
+
};
|
|
2075
|
+
var _hoisted_56$1 = { class: "w-full text-[11px] text-slate-600 bg-white" };
|
|
2076
|
+
var _hoisted_57$1 = { class: "bg-slate-50 border-b border-slate-200 text-slate-500 font-bold uppercase tracking-wider" };
|
|
2077
|
+
var _hoisted_58$1 = { class: "divide-y divide-slate-100" };
|
|
2078
|
+
var _hoisted_59$1 = {
|
|
2079
|
+
key: 0,
|
|
2080
|
+
class: "material-icons text-emerald-600 text-base"
|
|
2081
|
+
};
|
|
2082
|
+
var _hoisted_60$1 = {
|
|
2083
|
+
key: 1,
|
|
2084
|
+
class: "text-slate-300"
|
|
2085
|
+
};
|
|
2086
|
+
var _hoisted_61$1 = {
|
|
2087
|
+
key: 6,
|
|
2088
|
+
class: "text-slate-400 italic"
|
|
2089
|
+
};
|
|
2090
|
+
var _hoisted_62$1 = {
|
|
2091
|
+
key: 7,
|
|
2092
|
+
class: "bg-slate-50 rounded-xl p-4 border border-slate-200/60 text-slate-600 text-xs whitespace-pre-wrap leading-relaxed max-h-[30vh] overflow-y-auto"
|
|
2093
|
+
};
|
|
2094
|
+
var _hoisted_63$1 = [
|
|
2095
|
+
"src",
|
|
2096
|
+
"alt",
|
|
2097
|
+
"data-testid"
|
|
2098
|
+
];
|
|
2099
|
+
var _hoisted_64$1 = ["href", "data-testid"];
|
|
2100
|
+
var _hoisted_65$1 = ["href", "data-testid"];
|
|
2101
|
+
var _hoisted_66$1 = {
|
|
2102
|
+
key: 13,
|
|
2103
|
+
class: "text-slate-800 font-semibold"
|
|
2104
|
+
};
|
|
2105
|
+
var _hoisted_67$1 = {
|
|
2106
|
+
key: 0,
|
|
2107
|
+
class: "col-span-full text-xs font-semibold text-red-600 bg-red-50 border border-red-100 p-2.5 rounded-xl"
|
|
2108
|
+
};
|
|
2109
|
+
//#endregion
|
|
2110
|
+
//#region src/vue/components/CollectionRecordPanel.vue
|
|
2111
|
+
var CollectionRecordPanel_default = /* @__PURE__ */ (0, vue.defineComponent)({
|
|
2112
|
+
__name: "CollectionRecordPanel",
|
|
2113
|
+
props: /* @__PURE__ */ (0, vue.mergeModels)({
|
|
2114
|
+
collection: {},
|
|
2115
|
+
viewing: {},
|
|
2116
|
+
saving: { type: Boolean },
|
|
2117
|
+
saveError: {},
|
|
2118
|
+
actionError: {},
|
|
2119
|
+
actionPending: { type: Boolean },
|
|
2120
|
+
visibleActions: {},
|
|
2121
|
+
liveRecord: {},
|
|
2122
|
+
liveDerived: {},
|
|
2123
|
+
viewTitle: {},
|
|
2124
|
+
isSingleton: { type: Boolean },
|
|
2125
|
+
render: {},
|
|
2126
|
+
locale: {}
|
|
2127
|
+
}, {
|
|
2128
|
+
"editing": { required: true },
|
|
2129
|
+
"editingModifiers": {}
|
|
2130
|
+
}),
|
|
2131
|
+
emits: /* @__PURE__ */ (0, vue.mergeModels)([
|
|
2132
|
+
"submit",
|
|
2133
|
+
"cancel",
|
|
2134
|
+
"edit",
|
|
2135
|
+
"close",
|
|
2136
|
+
"delete",
|
|
2137
|
+
"runAction"
|
|
2138
|
+
], ["update:editing"]),
|
|
2139
|
+
setup(__props, { emit: __emit }) {
|
|
2140
|
+
const resolveImageSrc = collectionUi().imageSrc;
|
|
2141
|
+
const props = __props;
|
|
2142
|
+
const editing = (0, vue.useModel)(__props, "editing");
|
|
2143
|
+
const emit = __emit;
|
|
2144
|
+
const { t } = useCollectionI18n();
|
|
2145
|
+
const embedViews = (0, vue.computed)(() => props.render.embedViews.value);
|
|
2146
|
+
/** The record the read-only displays render from: the live draft while
|
|
2147
|
+
* editing (so non-editable cells like derived/embed preview the in-flight
|
|
2148
|
+
* values), else the open record. Always an object so `[key]` lookups are
|
|
2149
|
+
* safe in the template. */
|
|
2150
|
+
const detailRecord = (0, vue.computed)(() => editing.value ? props.liveDerived ?? props.liveRecord ?? {} : props.viewing ?? {});
|
|
2151
|
+
/** Title for the header: the create label, the edited record's id, or the
|
|
2152
|
+
* open record's title — same h2 slot in every mode. */
|
|
2153
|
+
const headerTitle = (0, vue.computed)(() => {
|
|
2154
|
+
if (editing.value) return editing.value.mode === "create" ? t("collectionsView.createTitle") : editing.value.originalId ?? "";
|
|
2155
|
+
return props.viewTitle;
|
|
2156
|
+
});
|
|
2157
|
+
const rootTestid = (0, vue.computed)(() => {
|
|
2158
|
+
if (!editing.value) return "collections-detail";
|
|
2159
|
+
return editing.value.mode === "create" ? "collections-create" : "collections-edit";
|
|
2160
|
+
});
|
|
2161
|
+
/** Whether a field gets an editable control in edit mode. Toggle (a
|
|
2162
|
+
* projection of an enum that has its own input), derived (computed), and
|
|
2163
|
+
* embed (a foreign record) stay read-only in both modes, so the cell
|
|
2164
|
+
* geometry never changes on the view↔edit toggle. */
|
|
2165
|
+
function isEditableType(type) {
|
|
2166
|
+
return type !== "toggle" && type !== "derived" && type !== "embed";
|
|
2167
|
+
}
|
|
2168
|
+
/** Wide field types span the full grid width in BOTH modes — keeping
|
|
2169
|
+
* `image` full-width here (not just when viewing) is what stops a field
|
|
2170
|
+
* from jumping columns when editing starts. */
|
|
2171
|
+
function colSpanClass(field) {
|
|
2172
|
+
return [
|
|
2173
|
+
"table",
|
|
2174
|
+
"markdown",
|
|
2175
|
+
"embed",
|
|
2176
|
+
"image"
|
|
2177
|
+
].includes(field.type) ? "col-span-full" : "col-span-1";
|
|
2178
|
+
}
|
|
2179
|
+
/** Whether to render a field's cell. Identical rule in both modes so no
|
|
2180
|
+
* cell appears or disappears on toggle: respect the `when` predicate
|
|
2181
|
+
* (against the active record) and hide the primary key except while
|
|
2182
|
+
* creating. */
|
|
2183
|
+
function cellVisible(field) {
|
|
2184
|
+
if (field.primary && editing.value?.mode !== "create") return false;
|
|
2185
|
+
return require_promptSafety.fieldVisible(field, detailRecord.value);
|
|
2186
|
+
}
|
|
2187
|
+
/** Mirror of the create-mode primary-key carve-out: drop the HTML5
|
|
2188
|
+
* `required` flag on the primary field while creating so the browser
|
|
2189
|
+
* doesn't block an intentionally-blank primary (server generates the id). */
|
|
2190
|
+
function isFieldRequiredInUi(field) {
|
|
2191
|
+
if (!field.required) return false;
|
|
2192
|
+
if (editing.value?.mode === "create" && field.primary === true) return false;
|
|
2193
|
+
return true;
|
|
2194
|
+
}
|
|
2195
|
+
/** Tailwind fill/text/border classes tinting an enum `<select>` by its current
|
|
2196
|
+
* value's colour (palette, or notification red/amber/grey when the field is
|
|
2197
|
+
* the schema's notifyWhen target). */
|
|
2198
|
+
function enumControlClass(fieldKey, value) {
|
|
2199
|
+
const cls = require_promptSafety.resolveEnumColor(props.collection.schema, fieldKey, value);
|
|
2200
|
+
return `${cls.badge} ${cls.border}`;
|
|
2201
|
+
}
|
|
2202
|
+
function markBoolTouched(key) {
|
|
2203
|
+
if (editing.value) editing.value.boolTouched[key] = true;
|
|
2204
|
+
}
|
|
2205
|
+
function markRowBoolTouched(row, subKey) {
|
|
2206
|
+
row.boolTouched[subKey] = true;
|
|
2207
|
+
}
|
|
2208
|
+
function addTableRow(key, subFields) {
|
|
2209
|
+
if (!editing.value) return;
|
|
2210
|
+
const rows = editing.value.table[key] ?? [];
|
|
2211
|
+
rows.push(require_promptSafety.emptyRow(subFields));
|
|
2212
|
+
editing.value.table[key] = rows;
|
|
2213
|
+
}
|
|
2214
|
+
function removeTableRow(key, index) {
|
|
2215
|
+
if (!editing.value) return;
|
|
2216
|
+
const rows = editing.value.table[key];
|
|
2217
|
+
if (rows) rows.splice(index, 1);
|
|
2218
|
+
}
|
|
2219
|
+
return (_ctx, _cache) => {
|
|
2220
|
+
const _component_router_link = (0, vue.resolveComponent)("router-link");
|
|
2221
|
+
return (0, vue.openBlock)(), (0, vue.createBlock)((0, vue.resolveDynamicComponent)(editing.value ? "form" : "div"), {
|
|
2222
|
+
class: "px-6 py-5 max-h-[60vh] overflow-y-auto",
|
|
2223
|
+
"data-testid": rootTestid.value,
|
|
2224
|
+
onSubmit: _cache[4] || (_cache[4] = (0, vue.withModifiers)(($event) => emit("submit"), ["prevent"]))
|
|
2225
|
+
}, {
|
|
2226
|
+
default: (0, vue.withCtx)(() => [
|
|
2227
|
+
(0, vue.createElementVNode)("div", _hoisted_1$5, [(0, vue.createElementVNode)("div", _hoisted_2$5, [(0, vue.createElementVNode)("span", _hoisted_3$5, (0, vue.toDisplayString)(__props.collection.title), 1), (0, vue.createElementVNode)("h2", {
|
|
2228
|
+
class: "text-base font-bold text-slate-800 truncate",
|
|
2229
|
+
"data-testid": editing.value ? "collections-edit-title" : "collections-detail-title"
|
|
2230
|
+
}, (0, vue.toDisplayString)(headerTitle.value), 9, _hoisted_4$5)]), editing.value ? ((0, vue.openBlock)(), (0, vue.createElementBlock)(vue.Fragment, { key: 0 }, [(0, vue.createElementVNode)("button", {
|
|
2231
|
+
type: "button",
|
|
2232
|
+
class: "h-8 px-2.5 rounded text-xs font-bold text-slate-500 hover:bg-slate-200/50 transition-colors",
|
|
2233
|
+
"data-testid": "collections-editor-cancel",
|
|
2234
|
+
onClick: _cache[0] || (_cache[0] = ($event) => emit("cancel"))
|
|
2235
|
+
}, (0, vue.toDisplayString)((0, vue.unref)(t)("common.cancel")), 1), (0, vue.createElementVNode)("button", {
|
|
2236
|
+
type: "submit",
|
|
2237
|
+
class: "h-8 px-2.5 rounded bg-indigo-600 text-white font-bold text-xs hover:bg-indigo-700 disabled:opacity-50 transition-all shadow-sm shadow-indigo-600/10",
|
|
2238
|
+
disabled: __props.saving,
|
|
2239
|
+
"data-testid": "collections-editor-save"
|
|
2240
|
+
}, (0, vue.toDisplayString)(__props.saving ? (0, vue.unref)(t)("common.saving") : (0, vue.unref)(t)("common.save")), 9, _hoisted_5$4)], 64)) : ((0, vue.openBlock)(), (0, vue.createElementBlock)("div", _hoisted_6$4, [
|
|
2241
|
+
((0, vue.openBlock)(true), (0, vue.createElementBlock)(vue.Fragment, null, (0, vue.renderList)(__props.visibleActions, (action) => {
|
|
2242
|
+
return (0, vue.openBlock)(), (0, vue.createElementBlock)("button", {
|
|
2243
|
+
key: action.id,
|
|
2244
|
+
type: "button",
|
|
2245
|
+
class: "h-8 px-2.5 rounded border border-indigo-200 bg-indigo-50/50 text-indigo-600 hover:bg-indigo-600 hover:text-white hover:border-indigo-600 font-bold text-xs transition-all flex items-center gap-1 disabled:opacity-50",
|
|
2246
|
+
disabled: __props.actionPending,
|
|
2247
|
+
"data-testid": `collections-detail-action-${action.id}`,
|
|
2248
|
+
onClick: ($event) => emit("runAction", action)
|
|
2249
|
+
}, [action.icon ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("span", _hoisted_8$4, (0, vue.toDisplayString)(action.icon), 1)) : (0, vue.createCommentVNode)("", true), (0, vue.createElementVNode)("span", null, (0, vue.toDisplayString)(action.label), 1)], 8, _hoisted_7$4);
|
|
2250
|
+
}), 128)),
|
|
2251
|
+
(0, vue.createElementVNode)("button", {
|
|
2252
|
+
type: "button",
|
|
2253
|
+
class: "h-8 px-2.5 rounded border border-slate-200 bg-white text-slate-700 hover:bg-slate-50 font-bold text-xs transition-all flex items-center gap-1",
|
|
2254
|
+
"data-testid": "collections-detail-edit",
|
|
2255
|
+
onClick: _cache[1] || (_cache[1] = ($event) => emit("edit"))
|
|
2256
|
+
}, [_cache[5] || (_cache[5] = (0, vue.createElementVNode)("span", { class: "material-icons text-sm" }, "edit", -1)), (0, vue.createElementVNode)("span", null, (0, vue.toDisplayString)((0, vue.unref)(t)("collectionsView.editItem")), 1)]),
|
|
2257
|
+
(0, vue.createElementVNode)("button", {
|
|
2258
|
+
type: "button",
|
|
2259
|
+
class: "h-8 px-2.5 rounded border border-rose-200 bg-white text-rose-600 hover:bg-rose-50 font-bold text-xs transition-all flex items-center gap-1",
|
|
2260
|
+
"data-testid": "collections-detail-remove",
|
|
2261
|
+
onClick: _cache[2] || (_cache[2] = ($event) => emit("delete"))
|
|
2262
|
+
}, [_cache[6] || (_cache[6] = (0, vue.createElementVNode)("span", { class: "material-icons text-sm" }, "delete", -1)), (0, vue.createElementVNode)("span", null, (0, vue.toDisplayString)((0, vue.unref)(t)("common.remove")), 1)]),
|
|
2263
|
+
(0, vue.createElementVNode)("button", {
|
|
2264
|
+
type: "button",
|
|
2265
|
+
class: "h-8 w-8 flex items-center justify-center rounded text-slate-400 hover:bg-slate-100 hover:text-slate-600 transition-colors",
|
|
2266
|
+
"aria-label": (0, vue.unref)(t)("common.close"),
|
|
2267
|
+
"data-testid": "collections-detail-close",
|
|
2268
|
+
onClick: _cache[3] || (_cache[3] = ($event) => emit("close"))
|
|
2269
|
+
}, [..._cache[7] || (_cache[7] = [(0, vue.createElementVNode)("span", { class: "material-icons text-lg" }, "close", -1)])], 8, _hoisted_9$4)
|
|
2270
|
+
]))]),
|
|
2271
|
+
!editing.value && __props.actionError ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("p", _hoisted_10$4, (0, vue.toDisplayString)(__props.actionError), 1)) : (0, vue.createCommentVNode)("", true),
|
|
2272
|
+
(0, vue.createElementVNode)("div", _hoisted_11$4, [((0, vue.openBlock)(true), (0, vue.createElementBlock)(vue.Fragment, null, (0, vue.renderList)(__props.collection.schema.fields, (field, key) => {
|
|
2273
|
+
return (0, vue.openBlock)(), (0, vue.createElementBlock)(vue.Fragment, { key }, [cellVisible(field) ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("div", {
|
|
2274
|
+
key: 0,
|
|
2275
|
+
class: (0, vue.normalizeClass)(["flex flex-col gap-1.5", colSpanClass(field)])
|
|
2276
|
+
}, [(0, vue.createElementVNode)("label", {
|
|
2277
|
+
class: "text-[10px] font-bold text-slate-400 uppercase tracking-wider flex items-center gap-1",
|
|
2278
|
+
for: `collections-field-${key}`
|
|
2279
|
+
}, [(0, vue.createTextVNode)((0, vue.toDisplayString)(field.label) + " ", 1), editing.value && field.required ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("span", _hoisted_13$3, "*")) : (0, vue.createCommentVNode)("", true)], 8, _hoisted_12$4), editing.value && isEditableType(field.type) ? ((0, vue.openBlock)(), (0, vue.createElementBlock)(vue.Fragment, { key: 0 }, [field.type === "boolean" ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("label", _hoisted_14$3, [(0, vue.withDirectives)((0, vue.createElementVNode)("input", {
|
|
2280
|
+
id: `collections-field-${key}`,
|
|
2281
|
+
"onUpdate:modelValue": ($event) => editing.value.bool[key] = $event,
|
|
2282
|
+
type: "checkbox",
|
|
2283
|
+
class: "h-5 w-5 rounded border-slate-300 text-indigo-600 focus:ring-indigo-500/20 cursor-pointer",
|
|
2284
|
+
"data-testid": `collections-input-${key}`,
|
|
2285
|
+
onChange: ($event) => markBoolTouched(String(key))
|
|
2286
|
+
}, null, 40, _hoisted_15$3), [[vue.vModelCheckbox, editing.value.bool[key]]]), (0, vue.createElementVNode)("span", { class: (0, vue.normalizeClass)(["text-xs font-semibold", editing.value.bool[key] ? "text-indigo-600" : "text-slate-500"]) }, (0, vue.toDisplayString)(editing.value.bool[key] ? (0, vue.unref)(t)("common.yes") : (0, vue.unref)(t)("common.no")), 3)])) : field.type === "ref" && field.to && __props.render.refOptions(field.to).length > 0 ? (0, vue.withDirectives)(((0, vue.openBlock)(), (0, vue.createElementBlock)("select", {
|
|
2287
|
+
key: 1,
|
|
2288
|
+
id: `collections-field-${key}`,
|
|
2289
|
+
"onUpdate:modelValue": ($event) => editing.value.text[key] = $event,
|
|
2290
|
+
required: isFieldRequiredInUi(field),
|
|
2291
|
+
class: "w-full rounded-xl border border-slate-200 px-3 py-2 text-xs bg-slate-50 hover:bg-slate-50/50 focus:bg-white focus:border-indigo-500 focus:ring-2 focus:ring-indigo-500/20 focus:outline-none transition-all cursor-pointer font-medium text-slate-700",
|
|
2292
|
+
"data-testid": `collections-input-${key}`
|
|
2293
|
+
}, [(0, vue.createElementVNode)("option", _hoisted_17$2, (0, vue.toDisplayString)((0, vue.unref)(t)("collectionsView.selectPlaceholder")), 1), ((0, vue.openBlock)(true), (0, vue.createElementBlock)(vue.Fragment, null, (0, vue.renderList)(__props.render.refOptions(field.to), (opt) => {
|
|
2294
|
+
return (0, vue.openBlock)(), (0, vue.createElementBlock)("option", {
|
|
2295
|
+
key: opt.slug,
|
|
2296
|
+
value: opt.slug
|
|
2297
|
+
}, (0, vue.toDisplayString)(opt.display), 9, _hoisted_18$2);
|
|
2298
|
+
}), 128))], 8, _hoisted_16$2)), [[vue.vModelSelect, editing.value.text[key]]]) : field.type === "enum" && Array.isArray(field.values) && field.values.length > 0 ? (0, vue.withDirectives)(((0, vue.openBlock)(), (0, vue.createElementBlock)("select", {
|
|
2299
|
+
key: 2,
|
|
2300
|
+
id: `collections-field-${key}`,
|
|
2301
|
+
"onUpdate:modelValue": ($event) => editing.value.text[key] = $event,
|
|
2302
|
+
required: isFieldRequiredInUi(field),
|
|
2303
|
+
class: (0, vue.normalizeClass)(["w-full rounded-xl border px-3 py-2 text-xs focus:bg-white focus:border-indigo-500 focus:ring-2 focus:ring-indigo-500/20 focus:outline-none transition-all cursor-pointer font-medium", enumControlClass(String(key), editing.value.text[key])]),
|
|
2304
|
+
"data-testid": `collections-input-${key}`
|
|
2305
|
+
}, [(0, vue.createElementVNode)("option", _hoisted_20$2, (0, vue.toDisplayString)((0, vue.unref)(t)("collectionsView.selectPlaceholder")), 1), ((0, vue.openBlock)(true), (0, vue.createElementBlock)(vue.Fragment, null, (0, vue.renderList)(field.values, (value) => {
|
|
2306
|
+
return (0, vue.openBlock)(), (0, vue.createElementBlock)("option", {
|
|
2307
|
+
key: value,
|
|
2308
|
+
value
|
|
2309
|
+
}, (0, vue.toDisplayString)(value), 9, _hoisted_21$2);
|
|
2310
|
+
}), 128))], 10, _hoisted_19$2)), [[vue.vModelSelect, editing.value.text[key]]]) : field.type === "table" && field.of ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("div", {
|
|
2311
|
+
key: 3,
|
|
2312
|
+
class: "border border-slate-200 bg-slate-50/30 rounded-xl p-4 space-y-3",
|
|
2313
|
+
"data-testid": `collections-table-${key}`
|
|
2314
|
+
}, [editing.value.table[key] && editing.value.table[key].length > 0 ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("div", _hoisted_23$2, [(0, vue.createElementVNode)("table", _hoisted_24$1, [(0, vue.createElementVNode)("thead", _hoisted_25$1, [(0, vue.createElementVNode)("tr", null, [((0, vue.openBlock)(true), (0, vue.createElementBlock)(vue.Fragment, null, (0, vue.renderList)(field.of, (subField, subKey) => {
|
|
2315
|
+
return (0, vue.openBlock)(), (0, vue.createElementBlock)("th", {
|
|
2316
|
+
key: subKey,
|
|
2317
|
+
class: "text-left px-3 py-2 font-bold"
|
|
2318
|
+
}, (0, vue.toDisplayString)(subField.label), 1);
|
|
2319
|
+
}), 128)), _cache[8] || (_cache[8] = (0, vue.createElementVNode)("th", { class: "w-9" }, null, -1))])]), (0, vue.createElementVNode)("tbody", _hoisted_26$1, [((0, vue.openBlock)(true), (0, vue.createElementBlock)(vue.Fragment, null, (0, vue.renderList)(editing.value.table[key], (row, rowIdx) => {
|
|
2320
|
+
return (0, vue.openBlock)(), (0, vue.createElementBlock)("tr", {
|
|
2321
|
+
key: rowIdx,
|
|
2322
|
+
class: "hover:bg-slate-50/50"
|
|
2323
|
+
}, [((0, vue.openBlock)(true), (0, vue.createElementBlock)(vue.Fragment, null, (0, vue.renderList)(field.of, (subField, subKey) => {
|
|
2324
|
+
return (0, vue.openBlock)(), (0, vue.createElementBlock)("td", {
|
|
2325
|
+
key: subKey,
|
|
2326
|
+
class: "px-2 py-1.5 align-middle"
|
|
2327
|
+
}, [subField.type === "boolean" ? (0, vue.withDirectives)(((0, vue.openBlock)(), (0, vue.createElementBlock)("input", {
|
|
2328
|
+
key: 0,
|
|
2329
|
+
"onUpdate:modelValue": ($event) => row.bool[subKey] = $event,
|
|
2330
|
+
type: "checkbox",
|
|
2331
|
+
class: "h-4 w-4 rounded border-slate-300 text-indigo-600 focus:ring-indigo-500/20 cursor-pointer",
|
|
2332
|
+
onChange: ($event) => markRowBoolTouched(row, String(subKey))
|
|
2333
|
+
}, null, 40, _hoisted_27$1)), [[vue.vModelCheckbox, row.bool[subKey]]]) : subField.type === "enum" && Array.isArray(subField.values) && subField.values.length > 0 ? (0, vue.withDirectives)(((0, vue.openBlock)(), (0, vue.createElementBlock)("select", {
|
|
2334
|
+
key: 1,
|
|
2335
|
+
"onUpdate:modelValue": ($event) => row.text[subKey] = $event,
|
|
2336
|
+
required: subField.required,
|
|
2337
|
+
class: "w-full rounded-lg border border-slate-200 px-2 py-1 text-xs focus:border-indigo-500 focus:outline-none cursor-pointer bg-slate-50 font-medium"
|
|
2338
|
+
}, [(0, vue.createElementVNode)("option", _hoisted_29$1, (0, vue.toDisplayString)((0, vue.unref)(t)("collectionsView.selectPlaceholder")), 1), ((0, vue.openBlock)(true), (0, vue.createElementBlock)(vue.Fragment, null, (0, vue.renderList)(subField.values, (value) => {
|
|
2339
|
+
return (0, vue.openBlock)(), (0, vue.createElementBlock)("option", {
|
|
2340
|
+
key: value,
|
|
2341
|
+
value
|
|
2342
|
+
}, (0, vue.toDisplayString)(value), 9, _hoisted_30$1);
|
|
2343
|
+
}), 128))], 8, _hoisted_28$1)), [[vue.vModelSelect, row.text[subKey]]]) : subField.type === "ref" && subField.to && __props.render.refOptions(subField.to).length > 0 ? (0, vue.withDirectives)(((0, vue.openBlock)(), (0, vue.createElementBlock)("select", {
|
|
2344
|
+
key: 2,
|
|
2345
|
+
"onUpdate:modelValue": ($event) => row.text[subKey] = $event,
|
|
2346
|
+
required: subField.required,
|
|
2347
|
+
class: "w-full rounded-lg border border-slate-200 px-2 py-1 text-xs focus:border-indigo-500 focus:outline-none cursor-pointer bg-slate-50 font-medium"
|
|
2348
|
+
}, [(0, vue.createElementVNode)("option", _hoisted_32$1, (0, vue.toDisplayString)((0, vue.unref)(t)("collectionsView.selectPlaceholder")), 1), ((0, vue.openBlock)(true), (0, vue.createElementBlock)(vue.Fragment, null, (0, vue.renderList)(__props.render.refOptions(subField.to), (opt) => {
|
|
2349
|
+
return (0, vue.openBlock)(), (0, vue.createElementBlock)("option", {
|
|
2350
|
+
key: opt.slug,
|
|
2351
|
+
value: opt.slug
|
|
2352
|
+
}, (0, vue.toDisplayString)(opt.display), 9, _hoisted_33$1);
|
|
2353
|
+
}), 128))], 8, _hoisted_31$1)), [[vue.vModelSelect, row.text[subKey]]]) : subField.type === "money" ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("div", _hoisted_34$1, [(0, vue.createElementVNode)("span", _hoisted_35$1, (0, vue.toDisplayString)(__props.render.currencySymbol(__props.render.resolveCurrency(subField, __props.liveRecord))), 1), (0, vue.withDirectives)((0, vue.createElementVNode)("input", {
|
|
2354
|
+
"onUpdate:modelValue": ($event) => row.text[subKey] = $event,
|
|
2355
|
+
type: "number",
|
|
2356
|
+
step: "0.01",
|
|
2357
|
+
required: subField.required,
|
|
2358
|
+
class: "w-full rounded-lg border border-slate-200 pl-6 pr-1.5 py-1 text-xs focus:border-indigo-500 focus:outline-none font-semibold text-slate-800"
|
|
2359
|
+
}, null, 8, _hoisted_36$1), [[vue.vModelText, row.text[subKey]]])])) : (0, vue.withDirectives)(((0, vue.openBlock)(), (0, vue.createElementBlock)("input", {
|
|
2360
|
+
key: 4,
|
|
2361
|
+
"onUpdate:modelValue": ($event) => row.text[subKey] = $event,
|
|
2362
|
+
type: __props.render.inputTypeFor(subField.type),
|
|
2363
|
+
required: subField.required,
|
|
2364
|
+
class: "w-full rounded-lg border border-slate-200 px-2 py-1 text-xs focus:border-indigo-500 focus:outline-none font-medium text-slate-700"
|
|
2365
|
+
}, null, 8, _hoisted_37$1)), [[vue.vModelDynamic, row.text[subKey]]])]);
|
|
2366
|
+
}), 128)), (0, vue.createElementVNode)("td", _hoisted_38$1, [(0, vue.createElementVNode)("button", {
|
|
2367
|
+
type: "button",
|
|
2368
|
+
class: "h-7 w-7 flex items-center justify-center rounded-lg text-slate-400 hover:text-rose-600 hover:bg-rose-50 transition-colors",
|
|
2369
|
+
"aria-label": (0, vue.unref)(t)("collectionsView.removeRow"),
|
|
2370
|
+
"data-testid": `collections-table-${key}-remove-${rowIdx}`,
|
|
2371
|
+
onClick: ($event) => removeTableRow(String(key), rowIdx)
|
|
2372
|
+
}, [..._cache[9] || (_cache[9] = [(0, vue.createElementVNode)("span", { class: "material-icons text-base" }, "close", -1)])], 8, _hoisted_39$1)])]);
|
|
2373
|
+
}), 128))])])])) : ((0, vue.openBlock)(), (0, vue.createElementBlock)("p", _hoisted_40$1, (0, vue.toDisplayString)((0, vue.unref)(t)("collectionsView.noRows")), 1)), (0, vue.createElementVNode)("button", {
|
|
2374
|
+
type: "button",
|
|
2375
|
+
class: "inline-flex items-center gap-1 text-xs text-indigo-600 hover:text-indigo-800 font-bold hover:underline",
|
|
2376
|
+
"data-testid": `collections-table-${key}-add`,
|
|
2377
|
+
onClick: ($event) => addTableRow(String(key), field.of)
|
|
2378
|
+
}, [_cache[10] || (_cache[10] = (0, vue.createElementVNode)("span", { class: "material-icons text-xs" }, "add", -1)), (0, vue.createElementVNode)("span", null, (0, vue.toDisplayString)((0, vue.unref)(t)("collectionsView.addRow")), 1)], 8, _hoisted_41$1)], 8, _hoisted_22$2)) : field.type === "money" ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("div", _hoisted_42$1, [(0, vue.createElementVNode)("div", _hoisted_43$1, (0, vue.toDisplayString)(__props.render.currencySymbol(__props.render.resolveCurrency(field, __props.liveRecord))), 1), (0, vue.withDirectives)((0, vue.createElementVNode)("input", {
|
|
2379
|
+
id: `collections-field-${key}`,
|
|
2380
|
+
"onUpdate:modelValue": ($event) => editing.value.text[key] = $event,
|
|
2381
|
+
type: "number",
|
|
2382
|
+
step: "0.01",
|
|
2383
|
+
required: isFieldRequiredInUi(field),
|
|
2384
|
+
class: "w-full rounded-xl border border-slate-200 pl-11 pr-3 py-2 text-xs focus:border-indigo-500 focus:ring-2 focus:ring-indigo-500/20 focus:outline-none font-semibold text-slate-800 transition-all",
|
|
2385
|
+
"data-testid": `collections-input-${key}`
|
|
2386
|
+
}, null, 8, _hoisted_44$1), [[vue.vModelText, editing.value.text[key]]])])) : [
|
|
2387
|
+
"string",
|
|
2388
|
+
"email",
|
|
2389
|
+
"number",
|
|
2390
|
+
"date",
|
|
2391
|
+
"datetime",
|
|
2392
|
+
"ref",
|
|
2393
|
+
"image",
|
|
2394
|
+
"file"
|
|
2395
|
+
].includes(field.type) ? (0, vue.withDirectives)(((0, vue.openBlock)(), (0, vue.createElementBlock)("input", {
|
|
2396
|
+
key: 5,
|
|
2397
|
+
id: `collections-field-${key}`,
|
|
2398
|
+
"onUpdate:modelValue": ($event) => editing.value.text[key] = $event,
|
|
2399
|
+
type: __props.render.inputTypeFor(field.type),
|
|
2400
|
+
required: isFieldRequiredInUi(field),
|
|
2401
|
+
disabled: field.primary === true && (editing.value.mode === "edit" || __props.isSingleton),
|
|
2402
|
+
class: "w-full rounded-xl border border-slate-200 px-3 py-2 text-xs focus:border-indigo-500 focus:ring-2 focus:ring-indigo-500/20 focus:outline-none disabled:bg-slate-100 disabled:text-slate-400 font-medium text-slate-700 transition-all",
|
|
2403
|
+
"data-testid": `collections-input-${key}`
|
|
2404
|
+
}, null, 8, _hoisted_45$1)), [[vue.vModelDynamic, editing.value.text[key]]]) : (0, vue.withDirectives)(((0, vue.openBlock)(), (0, vue.createElementBlock)("textarea", {
|
|
2405
|
+
key: 6,
|
|
2406
|
+
id: `collections-field-${key}`,
|
|
2407
|
+
"onUpdate:modelValue": ($event) => editing.value.text[key] = $event,
|
|
2408
|
+
rows: field.type === "markdown" ? 5 : 3,
|
|
2409
|
+
required: isFieldRequiredInUi(field),
|
|
2410
|
+
class: "w-full rounded-xl border border-slate-200 px-3 py-2 text-xs focus:border-indigo-500 focus:ring-2 focus:ring-indigo-500/20 focus:outline-none font-medium text-slate-700 transition-all",
|
|
2411
|
+
"data-testid": `collections-input-${key}`
|
|
2412
|
+
}, null, 8, _hoisted_46$1)), [[vue.vModelText, editing.value.text[key]]])], 64)) : ((0, vue.openBlock)(), (0, vue.createElementBlock)("div", {
|
|
2413
|
+
key: 1,
|
|
2414
|
+
class: "text-xs font-medium text-slate-700 break-words",
|
|
2415
|
+
"data-testid": `collections-detail-value-${key}`
|
|
2416
|
+
}, [field.type === "toggle" ? ((0, vue.openBlock)(), (0, vue.createElementBlock)(vue.Fragment, { key: 0 }, [field.field !== void 0 && String(detailRecord.value[field.field] ?? "") === field.onValue ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("span", _hoisted_48$1, [_cache[11] || (_cache[11] = (0, vue.createElementVNode)("span", { class: "h-1.5 w-1.5 rounded-full bg-emerald-500" }, null, -1)), (0, vue.createTextVNode)(" " + (0, vue.toDisplayString)((0, vue.unref)(t)("common.yes")), 1)])) : ((0, vue.openBlock)(), (0, vue.createElementBlock)("span", _hoisted_49$1, (0, vue.toDisplayString)((0, vue.unref)(t)("common.no")), 1))], 64)) : field.type === "boolean" ? ((0, vue.openBlock)(), (0, vue.createElementBlock)(vue.Fragment, { key: 1 }, [detailRecord.value[key] === true ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("span", _hoisted_50$1, [_cache[12] || (_cache[12] = (0, vue.createElementVNode)("span", { class: "h-1.5 w-1.5 rounded-full bg-emerald-500" }, null, -1)), (0, vue.createTextVNode)(" " + (0, vue.toDisplayString)((0, vue.unref)(t)("common.yes")), 1)])) : detailRecord.value[key] === false ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("span", _hoisted_51$1, (0, vue.toDisplayString)((0, vue.unref)(t)("common.no")), 1)) : ((0, vue.openBlock)(), (0, vue.createElementBlock)("span", _hoisted_52$1, "—"))], 64)) : field.type === "ref" && field.to && typeof detailRecord.value[key] === "string" && detailRecord.value[key] ? ((0, vue.openBlock)(), (0, vue.createBlock)(_component_router_link, {
|
|
2417
|
+
key: 2,
|
|
2418
|
+
to: {
|
|
2419
|
+
path: `/collections/${field.to}`,
|
|
2420
|
+
query: { selected: String(detailRecord.value[key]) }
|
|
2421
|
+
},
|
|
2422
|
+
class: "text-indigo-600 hover:text-indigo-800 font-bold hover:underline",
|
|
2423
|
+
"data-testid": `collections-detail-ref-${key}`
|
|
2424
|
+
}, {
|
|
2425
|
+
default: (0, vue.withCtx)(() => [(0, vue.createTextVNode)((0, vue.toDisplayString)(__props.render.refDisplay(field.to, String(detailRecord.value[key]))), 1)]),
|
|
2426
|
+
_: 2
|
|
2427
|
+
}, 1032, ["to", "data-testid"])) : field.type === "money" ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("span", _hoisted_53$1, (0, vue.toDisplayString)(__props.render.formatMoney(detailRecord.value[key], __props.render.resolveCurrency(field, detailRecord.value), __props.locale)), 1)) : field.type === "derived" ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("span", _hoisted_54$1, (0, vue.toDisplayString)(__props.render.derivedDisplay(field, __props.render.evaluateDerivedAgainstItem(field, String(key), detailRecord.value), detailRecord.value)), 1)) : field.type === "table" && field.of && __props.render.hasTableRows(detailRecord.value[key]) ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("div", _hoisted_55$1, [(0, vue.createElementVNode)("table", _hoisted_56$1, [(0, vue.createElementVNode)("thead", _hoisted_57$1, [(0, vue.createElementVNode)("tr", null, [((0, vue.openBlock)(true), (0, vue.createElementBlock)(vue.Fragment, null, (0, vue.renderList)(field.of, (subField, subKey) => {
|
|
2428
|
+
return (0, vue.openBlock)(), (0, vue.createElementBlock)("th", {
|
|
2429
|
+
key: subKey,
|
|
2430
|
+
class: "text-left px-4 py-2 font-bold"
|
|
2431
|
+
}, (0, vue.toDisplayString)(subField.label), 1);
|
|
2432
|
+
}), 128))])]), (0, vue.createElementVNode)("tbody", _hoisted_58$1, [((0, vue.openBlock)(true), (0, vue.createElementBlock)(vue.Fragment, null, (0, vue.renderList)(__props.render.tableRows(detailRecord.value[key]), (row, rowIdx) => {
|
|
2433
|
+
return (0, vue.openBlock)(), (0, vue.createElementBlock)("tr", {
|
|
2434
|
+
key: rowIdx,
|
|
2435
|
+
class: "hover:bg-slate-50/50"
|
|
2436
|
+
}, [((0, vue.openBlock)(true), (0, vue.createElementBlock)(vue.Fragment, null, (0, vue.renderList)(field.of, (subField, subKey) => {
|
|
2437
|
+
return (0, vue.openBlock)(), (0, vue.createElementBlock)("td", {
|
|
2438
|
+
key: subKey,
|
|
2439
|
+
class: "px-4 py-2 align-middle font-medium"
|
|
2440
|
+
}, [subField.type === "boolean" ? ((0, vue.openBlock)(), (0, vue.createElementBlock)(vue.Fragment, { key: 0 }, [row[subKey] === true ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("span", _hoisted_59$1, "check_circle")) : ((0, vue.openBlock)(), (0, vue.createElementBlock)("span", _hoisted_60$1, "—"))], 64)) : ((0, vue.openBlock)(), (0, vue.createElementBlock)("span", {
|
|
2441
|
+
key: 1,
|
|
2442
|
+
class: (0, vue.normalizeClass)([subField.type === "money" ? "font-bold text-slate-800 tabular-nums" : ""])
|
|
2443
|
+
}, (0, vue.toDisplayString)(__props.render.formatSubCell(subField, row[subKey], detailRecord.value)), 3))]);
|
|
2444
|
+
}), 128))]);
|
|
2445
|
+
}), 128))])])])) : field.type === "table" ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("span", _hoisted_61$1, (0, vue.toDisplayString)((0, vue.unref)(t)("collectionsView.noRows")), 1)) : field.type === "markdown" ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("div", _hoisted_62$1, (0, vue.toDisplayString)(__props.render.detailText(detailRecord.value[key])), 1)) : field.type === "embed" && embedViews.value[key] ? ((0, vue.openBlock)(), (0, vue.createBlock)(CollectionEmbedView_default, {
|
|
2446
|
+
key: 8,
|
|
2447
|
+
view: embedViews.value[key],
|
|
2448
|
+
"field-key": String(key)
|
|
2449
|
+
}, null, 8, ["view", "field-key"])) : field.type === "image" && typeof detailRecord.value[key] === "string" && detailRecord.value[key] ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("img", {
|
|
2450
|
+
key: 9,
|
|
2451
|
+
src: (0, vue.unref)(resolveImageSrc)(String(detailRecord.value[key])),
|
|
2452
|
+
alt: field.label,
|
|
2453
|
+
class: "max-h-64 max-w-full object-contain rounded-lg border border-slate-200 bg-slate-50",
|
|
2454
|
+
"data-testid": `collections-detail-image-${key}`
|
|
2455
|
+
}, null, 8, _hoisted_63$1)) : field.type !== "file" && __props.render.isExternalUrl(detailRecord.value[key]) ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("a", {
|
|
2456
|
+
key: 10,
|
|
2457
|
+
href: String(detailRecord.value[key]),
|
|
2458
|
+
target: "_blank",
|
|
2459
|
+
rel: "noopener noreferrer",
|
|
2460
|
+
class: "text-blue-600 hover:text-blue-800 font-semibold hover:underline break-all",
|
|
2461
|
+
"data-testid": `collections-detail-url-${key}`
|
|
2462
|
+
}, (0, vue.toDisplayString)(String(detailRecord.value[key])), 9, _hoisted_64$1)) : field.type === "file" && __props.render.artifactUrl(detailRecord.value[key]) ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("a", {
|
|
2463
|
+
key: 11,
|
|
2464
|
+
href: __props.render.artifactUrl(detailRecord.value[key]) ?? void 0,
|
|
2465
|
+
target: "_blank",
|
|
2466
|
+
rel: "noopener noreferrer",
|
|
2467
|
+
class: "text-blue-600 hover:text-blue-800 font-semibold hover:underline break-all",
|
|
2468
|
+
"data-testid": `collections-detail-file-${key}`
|
|
2469
|
+
}, (0, vue.toDisplayString)(String(detailRecord.value[key])), 9, _hoisted_65$1)) : field.type === "file" && __props.render.fileRoutePath(detailRecord.value[key]) ? ((0, vue.openBlock)(), (0, vue.createBlock)(_component_router_link, {
|
|
2470
|
+
key: 12,
|
|
2471
|
+
to: __props.render.fileRoutePath(detailRecord.value[key]) ?? "",
|
|
2472
|
+
class: "text-blue-600 hover:text-blue-800 font-semibold hover:underline break-all",
|
|
2473
|
+
"data-testid": `collections-detail-file-${key}`
|
|
2474
|
+
}, {
|
|
2475
|
+
default: (0, vue.withCtx)(() => [(0, vue.createTextVNode)((0, vue.toDisplayString)(String(detailRecord.value[key])), 1)]),
|
|
2476
|
+
_: 2
|
|
2477
|
+
}, 1032, ["to", "data-testid"])) : ((0, vue.openBlock)(), (0, vue.createElementBlock)("span", _hoisted_66$1, (0, vue.toDisplayString)(__props.render.formatCell(detailRecord.value[key], field.type)), 1))], 8, _hoisted_47$1))], 2)) : (0, vue.createCommentVNode)("", true)], 64);
|
|
2478
|
+
}), 128)), editing.value && __props.saveError ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("p", _hoisted_67$1, (0, vue.toDisplayString)(__props.saveError), 1)) : (0, vue.createCommentVNode)("", true)])
|
|
2479
|
+
]),
|
|
2480
|
+
_: 1
|
|
2481
|
+
}, 40, ["data-testid"]);
|
|
2482
|
+
};
|
|
2483
|
+
}
|
|
2484
|
+
});
|
|
2485
|
+
//#endregion
|
|
2486
|
+
//#region src/vue/components/CollectionViewConfigModal.vue?vue&type=script&setup=true&lang.ts
|
|
2487
|
+
var _hoisted_1$4 = {
|
|
2488
|
+
"data-testid": "collection-config-modal",
|
|
2489
|
+
class: "flex flex-col overflow-hidden"
|
|
2490
|
+
};
|
|
2491
|
+
var _hoisted_2$4 = { class: "flex items-center justify-between gap-2 border-b border-slate-100 px-5 py-3" };
|
|
2492
|
+
var _hoisted_3$4 = { class: "text-sm font-bold text-slate-700" };
|
|
2493
|
+
var _hoisted_4$4 = ["title", "aria-label"];
|
|
2494
|
+
var _hoisted_5$3 = { class: "overflow-y-auto px-5 py-4" };
|
|
2495
|
+
var _hoisted_6$3 = { class: "mb-2 text-[10px] font-bold uppercase tracking-wider text-slate-400" };
|
|
2496
|
+
var _hoisted_7$3 = {
|
|
2497
|
+
key: 0,
|
|
2498
|
+
class: "mb-3 rounded border border-rose-200 bg-rose-50 px-3 py-2 text-xs font-medium text-rose-600",
|
|
2499
|
+
"data-testid": "collection-config-error"
|
|
2500
|
+
};
|
|
2501
|
+
var _hoisted_8$3 = {
|
|
2502
|
+
key: 1,
|
|
2503
|
+
class: "flex flex-col gap-1"
|
|
2504
|
+
};
|
|
2505
|
+
var _hoisted_9$3 = { class: "material-icons text-base text-slate-400" };
|
|
2506
|
+
var _hoisted_10$3 = { class: "flex-1 truncate text-sm font-semibold text-slate-700" };
|
|
2507
|
+
var _hoisted_11$3 = [
|
|
2508
|
+
"title",
|
|
2509
|
+
"aria-label",
|
|
2510
|
+
"data-testid",
|
|
2511
|
+
"disabled",
|
|
2512
|
+
"onClick"
|
|
2513
|
+
];
|
|
2514
|
+
var _hoisted_12$3 = {
|
|
2515
|
+
key: 2,
|
|
2516
|
+
class: "text-xs text-slate-400",
|
|
2517
|
+
"data-testid": "collection-config-empty"
|
|
2518
|
+
};
|
|
2519
|
+
//#endregion
|
|
2520
|
+
//#region src/vue/components/CollectionViewConfigModal.vue
|
|
2521
|
+
var CollectionViewConfigModal_default = /* @__PURE__ */ (0, vue.defineComponent)({
|
|
2522
|
+
__name: "CollectionViewConfigModal",
|
|
2523
|
+
props: {
|
|
2524
|
+
slug: {},
|
|
2525
|
+
title: {},
|
|
2526
|
+
views: {}
|
|
2527
|
+
},
|
|
2528
|
+
emits: ["close", "changed"],
|
|
2529
|
+
setup(__props, { emit: __emit }) {
|
|
2530
|
+
const props = __props;
|
|
2531
|
+
const emit = __emit;
|
|
2532
|
+
const { t } = useCollectionI18n();
|
|
2533
|
+
const deleting = (0, vue.ref)(null);
|
|
2534
|
+
const error = (0, vue.ref)(null);
|
|
2535
|
+
async function onDelete(view) {
|
|
2536
|
+
if (!await collectionUi().confirm({
|
|
2537
|
+
message: t("collectionsView.config.confirmDelete", { label: view.label }),
|
|
2538
|
+
confirmText: t("common.remove"),
|
|
2539
|
+
cancelText: t("common.cancel"),
|
|
2540
|
+
variant: "danger"
|
|
2541
|
+
})) return;
|
|
2542
|
+
error.value = null;
|
|
2543
|
+
deleting.value = view.id;
|
|
2544
|
+
try {
|
|
2545
|
+
const result = await collectionUi().deleteView(props.slug, view.id);
|
|
2546
|
+
if (!result.ok) {
|
|
2547
|
+
error.value = result.error;
|
|
2548
|
+
return;
|
|
2549
|
+
}
|
|
2550
|
+
emit("changed");
|
|
2551
|
+
} catch (err) {
|
|
2552
|
+
error.value = require_promptSafety.errorMessage(err);
|
|
2553
|
+
} finally {
|
|
2554
|
+
deleting.value = null;
|
|
2555
|
+
}
|
|
2556
|
+
}
|
|
2557
|
+
return (_ctx, _cache) => {
|
|
2558
|
+
return (0, vue.openBlock)(), (0, vue.createBlock)(CollectionRecordModal_default, { onClose: _cache[1] || (_cache[1] = ($event) => emit("close")) }, {
|
|
2559
|
+
default: (0, vue.withCtx)(() => [(0, vue.createElementVNode)("div", _hoisted_1$4, [(0, vue.createElementVNode)("header", _hoisted_2$4, [(0, vue.createElementVNode)("h2", _hoisted_3$4, (0, vue.toDisplayString)((0, vue.unref)(t)("collectionsView.config.title", { title: __props.title })), 1), (0, vue.createElementVNode)("button", {
|
|
2560
|
+
type: "button",
|
|
2561
|
+
class: "h-8 w-8 flex items-center justify-center rounded text-slate-400 hover:bg-slate-50 hover:text-slate-600",
|
|
2562
|
+
title: (0, vue.unref)(t)("common.close"),
|
|
2563
|
+
"aria-label": (0, vue.unref)(t)("common.close"),
|
|
2564
|
+
"data-testid": "collection-config-close",
|
|
2565
|
+
onClick: _cache[0] || (_cache[0] = ($event) => emit("close"))
|
|
2566
|
+
}, [..._cache[2] || (_cache[2] = [(0, vue.createElementVNode)("span", { class: "material-icons text-sm" }, "close", -1)])], 8, _hoisted_4$4)]), (0, vue.createElementVNode)("div", _hoisted_5$3, [
|
|
2567
|
+
(0, vue.createElementVNode)("h3", _hoisted_6$3, (0, vue.toDisplayString)((0, vue.unref)(t)("collectionsView.config.viewsHeading")), 1),
|
|
2568
|
+
error.value ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("p", _hoisted_7$3, (0, vue.toDisplayString)(error.value), 1)) : (0, vue.createCommentVNode)("", true),
|
|
2569
|
+
__props.views.length > 0 ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("ul", _hoisted_8$3, [((0, vue.openBlock)(true), (0, vue.createElementBlock)(vue.Fragment, null, (0, vue.renderList)(__props.views, (view) => {
|
|
2570
|
+
return (0, vue.openBlock)(), (0, vue.createElementBlock)("li", {
|
|
2571
|
+
key: view.id,
|
|
2572
|
+
class: "flex items-center gap-2 rounded border border-slate-200 bg-white px-3 py-2"
|
|
2573
|
+
}, [
|
|
2574
|
+
(0, vue.createElementVNode)("span", _hoisted_9$3, (0, vue.toDisplayString)(view.icon || "dashboard_customize"), 1),
|
|
2575
|
+
(0, vue.createElementVNode)("span", _hoisted_10$3, (0, vue.toDisplayString)(view.label), 1),
|
|
2576
|
+
(0, vue.createElementVNode)("button", {
|
|
2577
|
+
type: "button",
|
|
2578
|
+
class: "h-8 w-8 flex items-center justify-center rounded border border-rose-200 bg-white text-rose-600 hover:bg-rose-50 disabled:opacity-50",
|
|
2579
|
+
title: (0, vue.unref)(t)("collectionsView.config.deleteView", { label: view.label }),
|
|
2580
|
+
"aria-label": (0, vue.unref)(t)("collectionsView.config.deleteView", { label: view.label }),
|
|
2581
|
+
"data-testid": `collection-view-delete-${view.id}`,
|
|
2582
|
+
disabled: deleting.value !== null,
|
|
2583
|
+
onClick: ($event) => onDelete(view)
|
|
2584
|
+
}, [..._cache[3] || (_cache[3] = [(0, vue.createElementVNode)("span", { class: "material-icons text-sm" }, "delete_forever", -1)])], 8, _hoisted_11$3)
|
|
2585
|
+
]);
|
|
2586
|
+
}), 128))])) : ((0, vue.openBlock)(), (0, vue.createElementBlock)("p", _hoisted_12$3, (0, vue.toDisplayString)((0, vue.unref)(t)("collectionsView.config.empty")), 1))
|
|
2587
|
+
])])]),
|
|
2588
|
+
_: 1
|
|
2589
|
+
});
|
|
2590
|
+
};
|
|
2591
|
+
}
|
|
2592
|
+
});
|
|
2593
|
+
//#endregion
|
|
2594
|
+
//#region src/vue/components/CollectionCustomView.vue?vue&type=script&setup=true&lang.ts
|
|
2595
|
+
var _hoisted_1$3 = { class: "custom-view-container" };
|
|
2596
|
+
var _hoisted_2$3 = {
|
|
2597
|
+
key: 0,
|
|
2598
|
+
class: "custom-view-message",
|
|
2599
|
+
role: "alert",
|
|
2600
|
+
"data-testid": "collection-custom-view-error"
|
|
2601
|
+
};
|
|
2602
|
+
var _hoisted_3$3 = {
|
|
2603
|
+
key: 1,
|
|
2604
|
+
class: "custom-view-message",
|
|
2605
|
+
"data-testid": "collection-custom-view-loading"
|
|
2606
|
+
};
|
|
2607
|
+
var _hoisted_4$3 = ["title", "srcdoc"];
|
|
2608
|
+
var REMINT_LEAD_MS = 6e4;
|
|
2609
|
+
var MIN_REMINT_DELAY_MS = 1e4;
|
|
2610
|
+
var CollectionCustomView_vue_vue_type_script_setup_true_lang_default = /* @__PURE__ */ (0, vue.defineComponent)({
|
|
2611
|
+
__name: "CollectionCustomView",
|
|
2612
|
+
props: {
|
|
2613
|
+
slug: {},
|
|
2614
|
+
view: {}
|
|
2615
|
+
},
|
|
2616
|
+
setup(__props) {
|
|
2617
|
+
const { t } = useCollectionI18n();
|
|
2618
|
+
const props = __props;
|
|
2619
|
+
const loading = (0, vue.ref)(true);
|
|
2620
|
+
const error = (0, vue.ref)(null);
|
|
2621
|
+
const srcdoc = (0, vue.ref)(null);
|
|
2622
|
+
let refreshTimer;
|
|
2623
|
+
function clearRefresh() {
|
|
2624
|
+
if (refreshTimer !== void 0) {
|
|
2625
|
+
clearTimeout(refreshTimer);
|
|
2626
|
+
refreshTimer = void 0;
|
|
2627
|
+
}
|
|
2628
|
+
}
|
|
2629
|
+
function scheduleRefresh(expMs) {
|
|
2630
|
+
clearRefresh();
|
|
2631
|
+
const delay = Math.max(expMs - Date.now() - REMINT_LEAD_MS, MIN_REMINT_DELAY_MS);
|
|
2632
|
+
refreshTimer = setTimeout(() => void load(), delay);
|
|
2633
|
+
}
|
|
2634
|
+
let loadSeq = 0;
|
|
2635
|
+
async function load() {
|
|
2636
|
+
clearRefresh();
|
|
2637
|
+
const seq = ++loadSeq;
|
|
2638
|
+
const stale = () => seq !== loadSeq;
|
|
2639
|
+
loading.value = true;
|
|
2640
|
+
error.value = null;
|
|
2641
|
+
srcdoc.value = null;
|
|
2642
|
+
const binding = collectionUi();
|
|
2643
|
+
try {
|
|
2644
|
+
const mint = await binding.mintViewToken(props.slug, props.view.id);
|
|
2645
|
+
if (stale()) return;
|
|
2646
|
+
if (!mint.ok) {
|
|
2647
|
+
error.value = mint.error;
|
|
2648
|
+
return;
|
|
2649
|
+
}
|
|
2650
|
+
scheduleRefresh(mint.data.exp);
|
|
2651
|
+
const resp = await binding.fetchViewHtml(props.slug, props.view.id);
|
|
2652
|
+
if (stale()) return;
|
|
2653
|
+
if (!resp.ok) {
|
|
2654
|
+
error.value = `HTTP ${resp.status}`;
|
|
2655
|
+
return;
|
|
2656
|
+
}
|
|
2657
|
+
srcdoc.value = binding.buildViewSrcdoc(resp.html, {
|
|
2658
|
+
slug: props.slug,
|
|
2659
|
+
token: mint.data.token,
|
|
2660
|
+
dataUrl: mint.data.dataUrl,
|
|
2661
|
+
origin: window.location.origin
|
|
2662
|
+
});
|
|
2663
|
+
} catch (err) {
|
|
2664
|
+
if (!stale()) error.value = require_promptSafety.errorMessage(err);
|
|
2665
|
+
} finally {
|
|
2666
|
+
if (!stale()) loading.value = false;
|
|
2667
|
+
}
|
|
2668
|
+
}
|
|
2669
|
+
(0, vue.watch)([() => props.slug, () => props.view.id], () => void load(), { immediate: true });
|
|
2670
|
+
(0, vue.onBeforeUnmount)(clearRefresh);
|
|
2671
|
+
return (_ctx, _cache) => {
|
|
2672
|
+
return (0, vue.openBlock)(), (0, vue.createElementBlock)("div", _hoisted_1$3, [error.value ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("div", _hoisted_2$3, (0, vue.toDisplayString)((0, vue.unref)(t)("collectionsView.customViewError", { error: error.value })), 1)) : loading.value ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("div", _hoisted_3$3, (0, vue.toDisplayString)((0, vue.unref)(t)("collectionsView.customViewLoading")), 1)) : srcdoc.value ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("iframe", {
|
|
2673
|
+
key: __props.view.id,
|
|
2674
|
+
"data-testid": "collection-custom-view-iframe",
|
|
2675
|
+
title: __props.view.label,
|
|
2676
|
+
srcdoc: srcdoc.value,
|
|
2677
|
+
sandbox: "allow-scripts allow-popups allow-popups-to-escape-sandbox",
|
|
2678
|
+
class: "w-full h-full border-0"
|
|
2679
|
+
}, null, 8, _hoisted_4$3)) : (0, vue.createCommentVNode)("", true)]);
|
|
2680
|
+
};
|
|
2681
|
+
}
|
|
2682
|
+
});
|
|
2683
|
+
//#endregion
|
|
2684
|
+
//#region \0plugin-vue:export-helper
|
|
2685
|
+
var _plugin_vue_export_helper_default = (sfc, props) => {
|
|
2686
|
+
const target = sfc.__vccOpts || sfc;
|
|
2687
|
+
for (const [key, val] of props) target[key] = val;
|
|
2688
|
+
return target;
|
|
2689
|
+
};
|
|
2690
|
+
//#endregion
|
|
2691
|
+
//#region src/vue/components/CollectionCustomView.vue
|
|
2692
|
+
var CollectionCustomView_default = /* @__PURE__ */ _plugin_vue_export_helper_default(CollectionCustomView_vue_vue_type_script_setup_true_lang_default, [["__scopeId", "data-v-3ab71997"]]);
|
|
2693
|
+
//#endregion
|
|
2694
|
+
//#region src/vue/components/CollectionView.vue?vue&type=script&setup=true&lang.ts
|
|
2695
|
+
var _hoisted_1$2 = { class: "h-full flex flex-col bg-slate-50/30" };
|
|
2696
|
+
var _hoisted_2$2 = { class: "flex items-center gap-3 px-6 py-2 border-b border-slate-200 bg-white" };
|
|
2697
|
+
var _hoisted_3$2 = ["title", "aria-label"];
|
|
2698
|
+
var _hoisted_4$2 = {
|
|
2699
|
+
key: 1,
|
|
2700
|
+
class: "h-9 w-9 flex items-center justify-center rounded-xl bg-indigo-50 text-indigo-600 border border-indigo-100"
|
|
2701
|
+
};
|
|
2702
|
+
var _hoisted_5$2 = { class: "material-symbols-outlined text-xl" };
|
|
2703
|
+
var _hoisted_6$2 = { class: "flex-1 min-w-0" };
|
|
2704
|
+
var _hoisted_7$2 = { class: "text-base font-bold text-slate-800 truncate" };
|
|
2705
|
+
var _hoisted_8$2 = {
|
|
2706
|
+
key: 0,
|
|
2707
|
+
class: "block text-[10px] text-slate-400 font-bold uppercase tracking-wider"
|
|
2708
|
+
};
|
|
2709
|
+
var _hoisted_9$2 = ["disabled"];
|
|
2710
|
+
var _hoisted_10$2 = { class: "material-icons text-sm" };
|
|
2711
|
+
var _hoisted_11$2 = [
|
|
2712
|
+
"disabled",
|
|
2713
|
+
"data-testid",
|
|
2714
|
+
"onClick"
|
|
2715
|
+
];
|
|
2716
|
+
var _hoisted_12$2 = {
|
|
2717
|
+
key: 0,
|
|
2718
|
+
class: "material-icons text-sm"
|
|
2719
|
+
};
|
|
2720
|
+
var _hoisted_13$2 = ["title", "aria-label"];
|
|
2721
|
+
var _hoisted_14$2 = ["title", "aria-label"];
|
|
2722
|
+
var _hoisted_15$2 = {
|
|
2723
|
+
key: 0,
|
|
2724
|
+
class: "px-6 py-3 bg-white border-b border-slate-100 flex items-center justify-between gap-4"
|
|
2725
|
+
};
|
|
2726
|
+
var _hoisted_16$1 = {
|
|
2727
|
+
key: 0,
|
|
2728
|
+
class: "relative flex-1 max-w-md"
|
|
2729
|
+
};
|
|
2730
|
+
var _hoisted_17$1 = ["placeholder", "aria-label"];
|
|
2731
|
+
var _hoisted_18$1 = ["aria-label"];
|
|
2732
|
+
var _hoisted_19$1 = { class: "flex items-center gap-2" };
|
|
2733
|
+
var _hoisted_20$1 = ["aria-label"];
|
|
2734
|
+
var _hoisted_21$1 = ["aria-pressed"];
|
|
2735
|
+
var _hoisted_22$1 = ["aria-pressed"];
|
|
2736
|
+
var _hoisted_23$1 = ["aria-pressed"];
|
|
2737
|
+
var _hoisted_24 = [
|
|
2738
|
+
"aria-pressed",
|
|
2739
|
+
"data-testid",
|
|
2740
|
+
"onClick"
|
|
2741
|
+
];
|
|
2742
|
+
var _hoisted_25 = { class: "material-icons text-sm" };
|
|
2743
|
+
var _hoisted_26 = ["title", "aria-label"];
|
|
2744
|
+
var _hoisted_27 = ["title", "aria-label"];
|
|
2745
|
+
var _hoisted_28 = ["value", "aria-label"];
|
|
2746
|
+
var _hoisted_29 = ["value"];
|
|
2747
|
+
var _hoisted_30 = ["value", "aria-label"];
|
|
2748
|
+
var _hoisted_31 = ["value"];
|
|
2749
|
+
var _hoisted_32 = {
|
|
2750
|
+
key: 3,
|
|
2751
|
+
class: "text-[10px] text-slate-400 font-bold uppercase tracking-wider select-none"
|
|
2752
|
+
};
|
|
2753
|
+
var _hoisted_33 = {
|
|
2754
|
+
key: 1,
|
|
2755
|
+
class: "mx-6 mt-4 rounded-xl border border-amber-200 bg-amber-50/60 p-4 text-sm text-amber-900 shadow-sm flex items-center gap-3",
|
|
2756
|
+
"data-testid": "collections-data-issues"
|
|
2757
|
+
};
|
|
2758
|
+
var _hoisted_34 = { class: "flex-1" };
|
|
2759
|
+
var _hoisted_35 = { class: "flex-1 overflow-auto" };
|
|
2760
|
+
var _hoisted_36 = {
|
|
2761
|
+
key: 0,
|
|
2762
|
+
class: "flex flex-col items-center justify-center py-20 text-sm text-slate-500 gap-3"
|
|
2763
|
+
};
|
|
2764
|
+
var _hoisted_37 = {
|
|
2765
|
+
key: 1,
|
|
2766
|
+
class: "m-6 rounded-xl border border-red-200 bg-red-50/50 p-4 text-sm text-red-800 shadow-sm flex items-center gap-3"
|
|
2767
|
+
};
|
|
2768
|
+
var _hoisted_38 = { key: 2 };
|
|
2769
|
+
var _hoisted_39 = {
|
|
2770
|
+
key: 3,
|
|
2771
|
+
class: "p-4"
|
|
2772
|
+
};
|
|
2773
|
+
var _hoisted_40 = {
|
|
2774
|
+
key: 4,
|
|
2775
|
+
class: "h-full flex flex-col"
|
|
2776
|
+
};
|
|
2777
|
+
var _hoisted_41 = {
|
|
2778
|
+
key: 0,
|
|
2779
|
+
class: "m-3 mb-0 rounded-xl border border-red-200 bg-red-50/50 p-4 text-sm text-red-800 shadow-sm flex items-center gap-3",
|
|
2780
|
+
"data-testid": "collections-inline-error"
|
|
2781
|
+
};
|
|
2782
|
+
var _hoisted_42 = { class: "flex-1" };
|
|
2783
|
+
var _hoisted_43 = ["aria-label"];
|
|
2784
|
+
var _hoisted_44 = { class: "flex-1 min-h-0 px-3 py-2" };
|
|
2785
|
+
var _hoisted_45 = {
|
|
2786
|
+
key: 5,
|
|
2787
|
+
class: "h-full",
|
|
2788
|
+
"data-testid": "collection-custom-view-body"
|
|
2789
|
+
};
|
|
2790
|
+
var _hoisted_46 = {
|
|
2791
|
+
key: 6,
|
|
2792
|
+
class: "flex flex-col items-center justify-center py-20 text-sm text-slate-400 gap-2"
|
|
2793
|
+
};
|
|
2794
|
+
var _hoisted_47 = { class: "font-semibold text-slate-600" };
|
|
2795
|
+
var _hoisted_48 = {
|
|
2796
|
+
key: 7,
|
|
2797
|
+
class: "flex flex-col items-center justify-center py-20 text-sm text-slate-400 gap-2"
|
|
2798
|
+
};
|
|
2799
|
+
var _hoisted_49 = { class: "font-semibold text-slate-600" };
|
|
2800
|
+
var _hoisted_50 = {
|
|
2801
|
+
key: 8,
|
|
2802
|
+
class: "overflow-x-auto [container-type:inline-size]"
|
|
2803
|
+
};
|
|
2804
|
+
var _hoisted_51 = {
|
|
2805
|
+
key: 0,
|
|
2806
|
+
class: "m-4 rounded-xl border border-red-200 bg-red-50/50 p-4 text-sm text-red-800 shadow-sm flex items-center gap-3",
|
|
2807
|
+
"data-testid": "collections-inline-error"
|
|
2808
|
+
};
|
|
2809
|
+
var _hoisted_52 = { class: "flex-1" };
|
|
2810
|
+
var _hoisted_53 = ["aria-label"];
|
|
2811
|
+
var _hoisted_54 = { class: "min-w-full text-xs" };
|
|
2812
|
+
var _hoisted_55 = { class: "bg-slate-50 border-b border-slate-200" };
|
|
2813
|
+
var _hoisted_56 = ["aria-sort"];
|
|
2814
|
+
var _hoisted_57 = { class: "flex items-center gap-1" };
|
|
2815
|
+
var _hoisted_58 = ["title"];
|
|
2816
|
+
var _hoisted_59 = [
|
|
2817
|
+
"data-testid",
|
|
2818
|
+
"aria-label",
|
|
2819
|
+
"onClick",
|
|
2820
|
+
"onPointerenter"
|
|
2821
|
+
];
|
|
2822
|
+
var _hoisted_60 = { class: "material-icons text-base align-middle" };
|
|
2823
|
+
var _hoisted_61 = { class: "divide-y divide-slate-100 bg-white" };
|
|
2824
|
+
var _hoisted_62 = [
|
|
2825
|
+
"aria-label",
|
|
2826
|
+
"data-testid",
|
|
2827
|
+
"onClick",
|
|
2828
|
+
"onKeydown"
|
|
2829
|
+
];
|
|
2830
|
+
var _hoisted_63 = [
|
|
2831
|
+
"checked",
|
|
2832
|
+
"disabled",
|
|
2833
|
+
"data-testid",
|
|
2834
|
+
"aria-label",
|
|
2835
|
+
"onChange"
|
|
2836
|
+
];
|
|
2837
|
+
var _hoisted_64 = [
|
|
2838
|
+
"checked",
|
|
2839
|
+
"disabled",
|
|
2840
|
+
"data-testid",
|
|
2841
|
+
"aria-label",
|
|
2842
|
+
"onChange"
|
|
2843
|
+
];
|
|
2844
|
+
var _hoisted_65 = {
|
|
2845
|
+
key: 2,
|
|
2846
|
+
class: "block truncate"
|
|
2847
|
+
};
|
|
2848
|
+
var _hoisted_66 = [
|
|
2849
|
+
"value",
|
|
2850
|
+
"disabled",
|
|
2851
|
+
"data-testid",
|
|
2852
|
+
"aria-label",
|
|
2853
|
+
"onChange"
|
|
2854
|
+
];
|
|
2855
|
+
var _hoisted_67 = {
|
|
2856
|
+
key: 0,
|
|
2857
|
+
value: ""
|
|
2858
|
+
};
|
|
2859
|
+
var _hoisted_68 = ["value"];
|
|
2860
|
+
var _hoisted_69 = {
|
|
2861
|
+
key: 4,
|
|
2862
|
+
class: "block truncate tabular-nums font-semibold text-slate-900"
|
|
2863
|
+
};
|
|
2864
|
+
var _hoisted_70 = {
|
|
2865
|
+
key: 5,
|
|
2866
|
+
class: "inline-flex items-center gap-1 px-2 py-0.5 rounded-lg text-[10px] font-bold bg-slate-100 text-slate-600 border border-slate-200/40"
|
|
2867
|
+
};
|
|
2868
|
+
var _hoisted_71 = {
|
|
2869
|
+
key: 6,
|
|
2870
|
+
class: "inline-block truncate tabular-nums font-bold text-indigo-900 bg-indigo-50/50 px-1.5 py-0.5 rounded border border-indigo-100/50"
|
|
2871
|
+
};
|
|
2872
|
+
var _hoisted_72 = ["href", "data-testid"];
|
|
2873
|
+
var _hoisted_73 = ["href", "data-testid"];
|
|
2874
|
+
var _hoisted_74 = {
|
|
2875
|
+
key: 10,
|
|
2876
|
+
class: "block truncate text-slate-600"
|
|
2877
|
+
};
|
|
2878
|
+
var _hoisted_75 = { class: "bg-white rounded-2xl shadow-2xl w-full max-w-xl flex flex-col border border-slate-200 overflow-hidden" };
|
|
2879
|
+
var _hoisted_76 = { class: "px-6 py-4 border-b border-slate-100 flex items-center gap-3 bg-slate-50/50" };
|
|
2880
|
+
var _hoisted_77 = { class: "flex-1" };
|
|
2881
|
+
var _hoisted_78 = {
|
|
2882
|
+
id: "collections-chat-title",
|
|
2883
|
+
class: "text-sm font-bold text-slate-800 uppercase tracking-wide"
|
|
2884
|
+
};
|
|
2885
|
+
var _hoisted_79 = { class: "text-xs text-slate-400 font-semibold" };
|
|
2886
|
+
var _hoisted_80 = ["aria-label"];
|
|
2887
|
+
var _hoisted_81 = { class: "px-6 py-5" };
|
|
2888
|
+
var _hoisted_82 = ["placeholder", "onKeydown"];
|
|
2889
|
+
var _hoisted_83 = { class: "px-6 py-3.5 border-t border-slate-100 flex items-center justify-end gap-2 bg-slate-50/50" };
|
|
2890
|
+
var _hoisted_84 = ["disabled"];
|
|
2891
|
+
//#endregion
|
|
2892
|
+
//#region src/vue/components/CollectionView.vue
|
|
2893
|
+
var CollectionView_default = /* @__PURE__ */ (0, vue.defineComponent)({
|
|
2894
|
+
__name: "CollectionView",
|
|
2895
|
+
props: {
|
|
2896
|
+
slug: {},
|
|
2897
|
+
selected: {},
|
|
2898
|
+
sendTextMessage: { type: Function },
|
|
2899
|
+
initialView: {},
|
|
2900
|
+
initialAnchorField: {},
|
|
2901
|
+
initialGroupField: {}
|
|
2902
|
+
},
|
|
2903
|
+
emits: ["select", "viewStateChange"],
|
|
2904
|
+
setup(__props, { emit: __emit }) {
|
|
2905
|
+
const props = __props;
|
|
2906
|
+
const emit = __emit;
|
|
2907
|
+
const { t, locale } = useCollectionI18n();
|
|
2908
|
+
const cui = collectionUi();
|
|
2909
|
+
const { confirm: openConfirm, unpin, pinToggle, startChat } = cui;
|
|
2910
|
+
const appApi = { startNewChat: startChat };
|
|
2911
|
+
/** Embedded when a `slug` prop is supplied; standalone (route-driven)
|
|
2912
|
+
* otherwise. Switches the slug/selected source and the open/close
|
|
2913
|
+
* navigation behaviour. */
|
|
2914
|
+
const embedded = (0, vue.computed)(() => props.slug !== void 0);
|
|
2915
|
+
/** Active collection slug: the prop in embedded mode, else the route
|
|
2916
|
+
* param. */
|
|
2917
|
+
const activeSlug = (0, vue.computed)(() => {
|
|
2918
|
+
if (props.slug !== void 0) return props.slug;
|
|
2919
|
+
const slug = cui.routeSlug();
|
|
2920
|
+
return slug !== void 0 && slug.length > 0 ? slug : void 0;
|
|
2921
|
+
});
|
|
2922
|
+
/** Active open-record id: the prop in embedded mode (may be undefined),
|
|
2923
|
+
* else the `?selected=` query. */
|
|
2924
|
+
const activeSelected = (0, vue.computed)(() => {
|
|
2925
|
+
if (embedded.value) return props.selected;
|
|
2926
|
+
return cui.routeSelectedId();
|
|
2927
|
+
});
|
|
2928
|
+
const collection = (0, vue.ref)(null);
|
|
2929
|
+
const items = (0, vue.ref)([]);
|
|
2930
|
+
const loading = (0, vue.ref)(true);
|
|
2931
|
+
const loadError = (0, vue.ref)(null);
|
|
2932
|
+
const dataIssues = (0, vue.ref)([]);
|
|
2933
|
+
const notifiedSeverities = (0, vue.computed)(() => {
|
|
2934
|
+
const slug = collection.value?.slug;
|
|
2935
|
+
return slug ? cui.notifiedSeverities(slug) : /* @__PURE__ */ new Map();
|
|
2936
|
+
});
|
|
2937
|
+
/** True while a feed collection's manual refresh is in flight. */
|
|
2938
|
+
const refreshing = (0, vue.ref)(false);
|
|
2939
|
+
/** Slug already auto-refreshed on first open — prevents a reload loop
|
|
2940
|
+
* (the auto-refresh reloads the view, which would re-trigger otherwise). */
|
|
2941
|
+
const autoRefreshedSlug = (0, vue.ref)(null);
|
|
2942
|
+
const editing = (0, vue.ref)(null);
|
|
2943
|
+
/** The record currently shown in read-only "open" mode. Distinct
|
|
2944
|
+
* from `editing`: open mode renders formatted values (no inputs)
|
|
2945
|
+
* and is what a `/collections/<slug>?selected=<id>` deep link
|
|
2946
|
+
* lands on. Mutually exclusive with `editing` in practice —
|
|
2947
|
+
* `editFromView` hands off from one to the other. */
|
|
2948
|
+
const viewing = (0, vue.ref)(null);
|
|
2949
|
+
/** The calendar day whose time-allocation popup is open, or null. The
|
|
2950
|
+
* selected record (`viewing`) renders in that popup's right pane; a record
|
|
2951
|
+
* with no resolvable day falls back to the panel below the grid. */
|
|
2952
|
+
const openDay = (0, vue.ref)(null);
|
|
2953
|
+
const saving = (0, vue.ref)(false);
|
|
2954
|
+
const saveError = (0, vue.ref)(null);
|
|
2955
|
+
/** Error from an inline table-cell edit (checkbox/dropdown). Distinct
|
|
2956
|
+
* from `saveError` (rendered only inside the detail panel, which is
|
|
2957
|
+
* closed during inline editing) — shown as a banner above the table. */
|
|
2958
|
+
const inlineError = (0, vue.ref)(null);
|
|
2959
|
+
/** Per-load snapshot of enum cells that had NO value when fetched
|
|
2960
|
+
* (keyed `<rowId>:<fieldKey>`). Only these cells offer the empty
|
|
2961
|
+
* placeholder option in their inline dropdown — a cell that already
|
|
2962
|
+
* has a value can't be blanked inline (use the edit form for that). */
|
|
2963
|
+
const enumOriginallyEmpty = (0, vue.ref)(/* @__PURE__ */ new Set());
|
|
2964
|
+
/** Rows with an inline cell save in flight (by `rowId`). While a row is
|
|
2965
|
+
* here its inline controls are disabled, so two quick edits to the same
|
|
2966
|
+
* row can't race two full-record PUTs — an older PUT landing last would
|
|
2967
|
+
* otherwise clobber the newer field on disk while the UI shows the
|
|
2968
|
+
* newer optimistic value (Codex PR #1599 P2). */
|
|
2969
|
+
const inlineSavingRows = (0, vue.ref)(/* @__PURE__ */ new Set());
|
|
2970
|
+
const actionPending = (0, vue.ref)(false);
|
|
2971
|
+
const actionError = (0, vue.ref)(null);
|
|
2972
|
+
const collectionActionPending = (0, vue.ref)(false);
|
|
2973
|
+
const chatOpen = (0, vue.ref)(false);
|
|
2974
|
+
const chatMessage = (0, vue.ref)("");
|
|
2975
|
+
const chatInputEl = (0, vue.ref)(null);
|
|
2976
|
+
const render = useCollectionRendering(collection, locale);
|
|
2977
|
+
const { refRecordCache, refDisplay, formatMoney, resolveCurrency, derivedDisplay, evaluateDerivedAgainstItem, formatCell, isExternalUrl, artifactUrl, fileRoutePath } = render;
|
|
2978
|
+
const searchQuery = (0, vue.ref)("");
|
|
2979
|
+
/** Case-insensitive substring match across an item's scalar fields.
|
|
2980
|
+
* Object-valued fields (table rows, nested records) are skipped —
|
|
2981
|
+
* they don't render as searchable text in the list table. */
|
|
2982
|
+
function itemMatchesQuery(item, query) {
|
|
2983
|
+
return Object.values(item).some((val) => {
|
|
2984
|
+
if (val === void 0 || val === null || typeof val === "object") return false;
|
|
2985
|
+
return String(val).toLowerCase().includes(query);
|
|
2986
|
+
});
|
|
2987
|
+
}
|
|
2988
|
+
const filteredItems = (0, vue.computed)(() => {
|
|
2989
|
+
const query = searchQuery.value.trim().toLowerCase();
|
|
2990
|
+
if (!query) return items.value;
|
|
2991
|
+
return items.value.filter((item) => itemMatchesQuery(item, query));
|
|
2992
|
+
});
|
|
2993
|
+
function storedSortFor(slug) {
|
|
2994
|
+
return slug && readCollectionSort(slug) || null;
|
|
2995
|
+
}
|
|
2996
|
+
const sortState = (0, vue.ref)(storedSortFor(activeSlug.value));
|
|
2997
|
+
const hoveredSortKey = (0, vue.ref)(null);
|
|
2998
|
+
function sortDirectionFor(key) {
|
|
2999
|
+
return sortState.value?.field === key ? sortState.value.direction : null;
|
|
3000
|
+
}
|
|
3001
|
+
/** The direction whose visuals to render: on hover, preview the next
|
|
3002
|
+
* click's state; otherwise show the column's actual state. */
|
|
3003
|
+
function effectiveSortDir(key) {
|
|
3004
|
+
const current = sortDirectionFor(key);
|
|
3005
|
+
return hoveredSortKey.value === key ? require_promptSafety.nextSortDirection(current) : current;
|
|
3006
|
+
}
|
|
3007
|
+
/** Cycle a column none → asc → desc → none; activating one clears the rest. */
|
|
3008
|
+
function cycleSort(key) {
|
|
3009
|
+
const next = require_promptSafety.nextSortDirection(sortDirectionFor(key));
|
|
3010
|
+
sortState.value = next ? {
|
|
3011
|
+
field: key,
|
|
3012
|
+
direction: next
|
|
3013
|
+
} : null;
|
|
3014
|
+
}
|
|
3015
|
+
function sortIconName(key) {
|
|
3016
|
+
return effectiveSortDir(key) === "desc" ? "arrow_downward" : "arrow_upward";
|
|
3017
|
+
}
|
|
3018
|
+
function sortButtonClass(key) {
|
|
3019
|
+
return effectiveSortDir(key) ? "text-slate-600" : "text-slate-300";
|
|
3020
|
+
}
|
|
3021
|
+
/** ARIA `aria-sort` token for a column's header cell. */
|
|
3022
|
+
function sortAriaValue(key) {
|
|
3023
|
+
const dir = sortDirectionFor(key);
|
|
3024
|
+
return dir === "asc" ? "ascending" : dir === "desc" ? "descending" : "none";
|
|
3025
|
+
}
|
|
3026
|
+
/** Comparable value for scalar fields that key off the raw cell value. */
|
|
3027
|
+
function scalarSortValue(field, raw) {
|
|
3028
|
+
switch (field.type) {
|
|
3029
|
+
case "number":
|
|
3030
|
+
case "money": return require_promptSafety.numericSortValue(raw);
|
|
3031
|
+
case "date":
|
|
3032
|
+
case "datetime": return require_promptSafety.dateSortValue(raw);
|
|
3033
|
+
case "enum": return require_promptSafety.enumSortValue(field.values, raw);
|
|
3034
|
+
case "boolean": return require_promptSafety.boolSortValue(raw === true);
|
|
3035
|
+
case "ref": return field.to && typeof raw === "string" && raw ? require_promptSafety.stringSortValue(refDisplay(field.to, raw)) : require_promptSafety.stringSortValue(raw);
|
|
3036
|
+
default: return require_promptSafety.stringSortValue(raw);
|
|
3037
|
+
}
|
|
3038
|
+
}
|
|
3039
|
+
/** Comparable value for one row under the active field. Toggle and derived
|
|
3040
|
+
* need the whole record; every other type keys off the raw cell. */
|
|
3041
|
+
function sortValueOf(field, key, item) {
|
|
3042
|
+
if (field.type === "toggle") return require_promptSafety.boolSortValue(toggleChecked(item, field));
|
|
3043
|
+
if (field.type === "derived") return derivedSortValue(field, key, item);
|
|
3044
|
+
return scalarSortValue(field, item[key]);
|
|
3045
|
+
}
|
|
3046
|
+
/** Derived rows sort by their display type: money/number → numeric,
|
|
3047
|
+
* date/datetime → epoch, anything else → the enriched value as a string. */
|
|
3048
|
+
function derivedSortValue(field, key, item) {
|
|
3049
|
+
const { display } = field;
|
|
3050
|
+
if (display === void 0 || display === "number" || display === "money") return require_promptSafety.numericSortValue(evaluateDerivedAgainstItem(field, key, item));
|
|
3051
|
+
const enriched = collection.value ? render.deriveAll(collection.value.schema, item, render.refRecordCache.value) : item;
|
|
3052
|
+
if (display === "date" || display === "datetime") return require_promptSafety.dateSortValue(enriched[key]);
|
|
3053
|
+
return require_promptSafety.stringSortValue(enriched[key]);
|
|
3054
|
+
}
|
|
3055
|
+
const sortedItems = (0, vue.computed)(() => {
|
|
3056
|
+
const state = sortState.value;
|
|
3057
|
+
const field = state ? collection.value?.schema.fields[state.field] : void 0;
|
|
3058
|
+
if (!state || !field) return filteredItems.value;
|
|
3059
|
+
return require_promptSafety.sortItems(filteredItems.value, state.direction, (item) => sortValueOf(field, state.field, item));
|
|
3060
|
+
});
|
|
3061
|
+
/** Stringified primary-key value for a row (the row's stable identity). */
|
|
3062
|
+
function rowId(item) {
|
|
3063
|
+
const primaryKey = collection.value?.schema.primaryKey;
|
|
3064
|
+
return primaryKey ? String(item[primaryKey] ?? "") : "";
|
|
3065
|
+
}
|
|
3066
|
+
/** Stable key for one cell in the `enumOriginallyEmpty` snapshot. */
|
|
3067
|
+
function cellKey(rowIdValue, fieldKey) {
|
|
3068
|
+
return `${rowIdValue}:${fieldKey}`;
|
|
3069
|
+
}
|
|
3070
|
+
/** Build the set of enum cells that were empty in the freshly-fetched
|
|
3071
|
+
* records — the only cells whose inline dropdown offers an empty option. */
|
|
3072
|
+
function snapshotEmptyEnums(schema, records) {
|
|
3073
|
+
const empty = /* @__PURE__ */ new Set();
|
|
3074
|
+
const enumKeys = Object.entries(schema.fields).filter(([, field]) => field.type === "enum").map(([fieldKey]) => fieldKey);
|
|
3075
|
+
if (enumKeys.length === 0) return empty;
|
|
3076
|
+
for (const record of records) {
|
|
3077
|
+
const recordId = String(record[schema.primaryKey] ?? "");
|
|
3078
|
+
for (const fieldKey of enumKeys) if (record[fieldKey] == null || record[fieldKey] === "") empty.add(cellKey(recordId, fieldKey));
|
|
3079
|
+
}
|
|
3080
|
+
return empty;
|
|
3081
|
+
}
|
|
3082
|
+
/** Whether an inline enum dropdown should render its empty placeholder
|
|
3083
|
+
* option: only for cells with no value at load time. */
|
|
3084
|
+
function showEnumPlaceholder(item, fieldKey) {
|
|
3085
|
+
return enumOriginallyEmpty.value.has(cellKey(rowId(item), fieldKey));
|
|
3086
|
+
}
|
|
3087
|
+
/** Tailwind fill/text/border classes tinting an inline enum `<select>` by its
|
|
3088
|
+
* current value's colour (palette, or notification red/amber/grey when the
|
|
3089
|
+
* field is the schema's notifyWhen target). */
|
|
3090
|
+
function enumControlClass(fieldKey, value) {
|
|
3091
|
+
const schema = collection.value?.schema;
|
|
3092
|
+
if (!schema) return "";
|
|
3093
|
+
const cls = require_promptSafety.resolveEnumColor(schema, fieldKey, value);
|
|
3094
|
+
return `${cls.badge} ${cls.border}`;
|
|
3095
|
+
}
|
|
3096
|
+
/** This row is the one open in read-only detail. */
|
|
3097
|
+
function isRowOpen(item) {
|
|
3098
|
+
return viewing.value !== null && rowId(viewing.value) === rowId(item);
|
|
3099
|
+
}
|
|
3100
|
+
/** This row is the one being edited (highlights it in the list while the
|
|
3101
|
+
* edit modal is open). Create mode has no backing row, so nothing matches. */
|
|
3102
|
+
function isEditingRow(item) {
|
|
3103
|
+
const draft = editing.value;
|
|
3104
|
+
if (!draft || draft.mode === "create") return false;
|
|
3105
|
+
return draft.originalId === rowId(item);
|
|
3106
|
+
}
|
|
3107
|
+
/** Re-run a feed collection's retrieval now, then reload its records.
|
|
3108
|
+
* Only reachable when `schema.ingest` is present (button is gated). */
|
|
3109
|
+
async function refreshFeed() {
|
|
3110
|
+
const current = collection.value;
|
|
3111
|
+
if (!current?.schema.ingest || refreshing.value) return;
|
|
3112
|
+
refreshing.value = true;
|
|
3113
|
+
inlineError.value = null;
|
|
3114
|
+
const result = await cui.refreshCollection(current.slug);
|
|
3115
|
+
refreshing.value = false;
|
|
3116
|
+
if (!result.ok) {
|
|
3117
|
+
loadError.value = result.error;
|
|
3118
|
+
return;
|
|
3119
|
+
}
|
|
3120
|
+
await loadCollection(current.slug);
|
|
3121
|
+
if (result.data.errors.length > 0) inlineError.value = t("collectionsView.refreshFailed", { error: result.data.errors.join("; ") });
|
|
3122
|
+
}
|
|
3123
|
+
/** Collection-level header actions. No `when` predicate (no record). */
|
|
3124
|
+
const collectionActions = (0, vue.computed)(() => collection.value?.schema.collectionActions ?? []);
|
|
3125
|
+
/** Run a collection-level action: ask the server to assemble the seed
|
|
3126
|
+
* prompt (a progress summary of all records + the template), then start
|
|
3127
|
+
* a new chat in the action's role with it. Generic — no domain knowledge. */
|
|
3128
|
+
async function runCollectionAction(action) {
|
|
3129
|
+
const current = collection.value;
|
|
3130
|
+
if (!current || collectionActionPending.value) return;
|
|
3131
|
+
collectionActionPending.value = true;
|
|
3132
|
+
inlineError.value = null;
|
|
3133
|
+
const result = await cui.runCollectionAction(current.slug, action.id);
|
|
3134
|
+
collectionActionPending.value = false;
|
|
3135
|
+
if (!result.ok) {
|
|
3136
|
+
inlineError.value = result.error;
|
|
3137
|
+
return;
|
|
3138
|
+
}
|
|
3139
|
+
if (props.sendTextMessage) {
|
|
3140
|
+
props.sendTextMessage(result.data.prompt);
|
|
3141
|
+
return;
|
|
3142
|
+
}
|
|
3143
|
+
appApi.startNewChat(result.data.prompt, result.data.role);
|
|
3144
|
+
}
|
|
3145
|
+
/** Report the server-detected record data problems back to the LLM so it
|
|
3146
|
+
* fixes the offending files. Mirrors the `presentCollection` validation
|
|
3147
|
+
* path (`dispatchPresentCollection`), but user-initiated via the Repair
|
|
3148
|
+
* button instead of fired automatically after a write. Dispatches into
|
|
3149
|
+
* the current chat when embedded, else seeds a new General chat. */
|
|
3150
|
+
function repairCollection() {
|
|
3151
|
+
const current = collection.value;
|
|
3152
|
+
if (!current || dataIssues.value.length === 0) return;
|
|
3153
|
+
const lines = dataIssues.value.map((issue) => `- ${require_promptSafety.defangForPrompt(issue.file)}: ${require_promptSafety.defangForPrompt(issue.problem)}`).join("\n");
|
|
3154
|
+
const prompt = t("collectionsView.repairPrompt", {
|
|
3155
|
+
title: current.title,
|
|
3156
|
+
count: dataIssues.value.length,
|
|
3157
|
+
issues: lines
|
|
3158
|
+
});
|
|
3159
|
+
if (props.sendTextMessage) {
|
|
3160
|
+
props.sendTextMessage(prompt);
|
|
3161
|
+
return;
|
|
3162
|
+
}
|
|
3163
|
+
appApi.startNewChat(prompt, cui.generalRoleId);
|
|
3164
|
+
}
|
|
3165
|
+
/** Actions whose optional `when` predicate matches the open record.
|
|
3166
|
+
* Status-driven buttons (e.g. invoice "Record payment") stay hidden
|
|
3167
|
+
* until the record reaches the matching state. */
|
|
3168
|
+
const visibleActions = (0, vue.computed)(() => {
|
|
3169
|
+
const record = viewing.value;
|
|
3170
|
+
if (!record) return [];
|
|
3171
|
+
return (collection.value?.schema.actions ?? []).filter((action) => require_promptSafety.actionVisible(action, record));
|
|
3172
|
+
});
|
|
3173
|
+
/** Run a schema-declared action on the open record: ask the server to
|
|
3174
|
+
* assemble the seed prompt, then start a new chat in the action's
|
|
3175
|
+
* role with it. Generic — no knowledge of what the action does. */
|
|
3176
|
+
async function runAction(action) {
|
|
3177
|
+
if (!collection.value || !viewing.value) return;
|
|
3178
|
+
const itemId = String(viewing.value[collection.value.schema.primaryKey] ?? "");
|
|
3179
|
+
if (!itemId) return;
|
|
3180
|
+
actionPending.value = true;
|
|
3181
|
+
actionError.value = null;
|
|
3182
|
+
const result = await cui.runItemAction(collection.value.slug, itemId, action.id);
|
|
3183
|
+
actionPending.value = false;
|
|
3184
|
+
if (!result.ok) {
|
|
3185
|
+
actionError.value = result.error;
|
|
3186
|
+
return;
|
|
3187
|
+
}
|
|
3188
|
+
if (props.sendTextMessage) {
|
|
3189
|
+
props.sendTextMessage(result.data.prompt);
|
|
3190
|
+
return;
|
|
3191
|
+
}
|
|
3192
|
+
appApi.startNewChat(result.data.prompt, result.data.role);
|
|
3193
|
+
}
|
|
3194
|
+
/** Open the chat modal, blanking any prior draft and focusing the input. */
|
|
3195
|
+
function openChat() {
|
|
3196
|
+
chatMessage.value = "";
|
|
3197
|
+
chatOpen.value = true;
|
|
3198
|
+
(0, vue.nextTick)(() => chatInputEl.value?.focus());
|
|
3199
|
+
}
|
|
3200
|
+
function closeChat() {
|
|
3201
|
+
chatOpen.value = false;
|
|
3202
|
+
}
|
|
3203
|
+
/** Build the chat seed text for the current view.
|
|
3204
|
+
*
|
|
3205
|
+
* A collection IS a skill, so its slug doubles as a slash command:
|
|
3206
|
+
* "I want to create an item" on `mc_worklog` becomes
|
|
3207
|
+
* `/mc_worklog I want to create an item`.
|
|
3208
|
+
*
|
|
3209
|
+
* A feed is data-only — it has NO skill, so `/<slug>` would resolve to
|
|
3210
|
+
* nothing. Instead, point the agent at the feed's schema + records
|
|
3211
|
+
* (`feeds/<slug>/schema.json` and `<dataPath>/`) and let it act on the
|
|
3212
|
+
* request directly. */
|
|
3213
|
+
function buildChatSeed(slug, message) {
|
|
3214
|
+
const schema = collection.value?.schema;
|
|
3215
|
+
if (!schema?.ingest) return `/${slug} ${message}`;
|
|
3216
|
+
return t("collectionsView.feedChatSeed", {
|
|
3217
|
+
slug,
|
|
3218
|
+
dataPath: schema.dataPath ?? `data/feeds/${slug}`,
|
|
3219
|
+
message
|
|
3220
|
+
});
|
|
3221
|
+
}
|
|
3222
|
+
/** Start a new general-role chat seeded from the current view. */
|
|
3223
|
+
function submitChat() {
|
|
3224
|
+
if (!collection.value) return;
|
|
3225
|
+
const message = chatMessage.value.trim();
|
|
3226
|
+
if (!message) return;
|
|
3227
|
+
closeChat();
|
|
3228
|
+
const text = buildChatSeed(collection.value.slug, message);
|
|
3229
|
+
if (props.sendTextMessage) {
|
|
3230
|
+
props.sendTextMessage(text);
|
|
3231
|
+
return;
|
|
3232
|
+
}
|
|
3233
|
+
appApi.startNewChat(text, cui.generalRoleId);
|
|
3234
|
+
}
|
|
3235
|
+
async function loadCollection(slug) {
|
|
3236
|
+
const requestedKind = !embedded.value && cui.isFeedRoute() ? "feed" : "collection";
|
|
3237
|
+
loading.value = true;
|
|
3238
|
+
loadError.value = null;
|
|
3239
|
+
collection.value = null;
|
|
3240
|
+
items.value = [];
|
|
3241
|
+
dataIssues.value = [];
|
|
3242
|
+
searchQuery.value = "";
|
|
3243
|
+
render.resetLinkedCaches();
|
|
3244
|
+
viewing.value = null;
|
|
3245
|
+
openDay.value = null;
|
|
3246
|
+
const result = await cui.fetchCollectionDetail(slug);
|
|
3247
|
+
loading.value = false;
|
|
3248
|
+
if (!result.ok) {
|
|
3249
|
+
loadError.value = result.status === 404 ? "not-found" : result.error;
|
|
3250
|
+
if (result.status === 404 && !embedded.value && activeSlug.value === slug) unpin(requestedKind, slug);
|
|
3251
|
+
return;
|
|
3252
|
+
}
|
|
3253
|
+
collection.value = result.data.collection;
|
|
3254
|
+
items.value = result.data.items;
|
|
3255
|
+
dataIssues.value = result.data.issues ?? [];
|
|
3256
|
+
enumOriginallyEmpty.value = snapshotEmptyEnums(result.data.collection.schema, result.data.items);
|
|
3257
|
+
await render.loadLinkedCollections(result.data.collection.schema, slug);
|
|
3258
|
+
if (collection.value?.slug === slug) {
|
|
3259
|
+
syncViewToSelected();
|
|
3260
|
+
maybeOpenCalendarForSelected();
|
|
3261
|
+
}
|
|
3262
|
+
maybeAutoRefreshFeed(slug);
|
|
3263
|
+
}
|
|
3264
|
+
function maybeAutoRefreshFeed(slug) {
|
|
3265
|
+
if (embedded.value) return;
|
|
3266
|
+
const current = collection.value;
|
|
3267
|
+
if (current?.slug !== slug || !current.schema.ingest) return;
|
|
3268
|
+
if (items.value.length > 0 || autoRefreshedSlug.value === slug) return;
|
|
3269
|
+
autoRefreshedSlug.value = slug;
|
|
3270
|
+
refreshFeed();
|
|
3271
|
+
}
|
|
3272
|
+
/** Schema fields excluding display-only `embed` fields — used by the
|
|
3273
|
+
* list table only (a whole embedded record doesn't fit a table cell,
|
|
3274
|
+
* and it'd be identical in every row). The detail modal and the edit
|
|
3275
|
+
* form iterate the full `schema.fields` so embeds render there too. */
|
|
3276
|
+
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) : []);
|
|
3277
|
+
/** True when the current collection declares `schema.singleton` —
|
|
3278
|
+
* exactly one record, its primary key fixed to the declared value. */
|
|
3279
|
+
const isSingleton = (0, vue.computed)(() => Boolean(collection.value?.schema.singleton));
|
|
3280
|
+
/** Whether the Add button should show. Always for a normal collection;
|
|
3281
|
+
* for a singleton only until its one record exists. */
|
|
3282
|
+
const canCreate = (0, vue.computed)(() => {
|
|
3283
|
+
if (!collection.value) return false;
|
|
3284
|
+
return !(isSingleton.value && items.value.length > 0);
|
|
3285
|
+
});
|
|
3286
|
+
const canDeleteCollection = (0, vue.computed)(() => {
|
|
3287
|
+
const current = collection.value;
|
|
3288
|
+
if (!current) return false;
|
|
3289
|
+
return current.source === "project" && !current.slug.startsWith("mc-");
|
|
3290
|
+
});
|
|
3291
|
+
const isFeed = (0, vue.computed)(() => Boolean(collection.value?.schema.ingest));
|
|
3292
|
+
const canDeleteFeed = (0, vue.computed)(() => isFeed.value && !embedded.value);
|
|
3293
|
+
const isFeedRoute = (0, vue.computed)(() => !embedded.value && cui.isFeedRoute());
|
|
3294
|
+
/** The view to open with: the embedded card's restored `initialView` if
|
|
3295
|
+
* present (its own persisted state wins), else the slug's stored
|
|
3296
|
+
* preference, else "table". Embedded cards READ the store but never WRITE
|
|
3297
|
+
* it (the persist watch only emits `viewStateChange` for them), so a stale
|
|
3298
|
+
* card re-rendering can't clobber the shared preference. */
|
|
3299
|
+
function initialViewMode() {
|
|
3300
|
+
if (props.initialView) return props.initialView;
|
|
3301
|
+
const slug = activeSlug.value;
|
|
3302
|
+
return slug && readCollectionViewMode(slug) || "table";
|
|
3303
|
+
}
|
|
3304
|
+
const view = (0, vue.ref)(initialViewMode());
|
|
3305
|
+
/** `date` / `datetime` fields in declaration order — the calendar can anchor
|
|
3306
|
+
* on any (a `datetime` anchor also carries the clock for the day view). */
|
|
3307
|
+
const dateFields = (0, vue.computed)(() => collection.value ? Object.entries(collection.value.schema.fields).filter(([, field]) => field.type === "date" || field.type === "datetime").map(([key]) => key) : []);
|
|
3308
|
+
/** Whether the table ↔ calendar toggle is offered. */
|
|
3309
|
+
const hasCalendar = (0, vue.computed)(() => dateFields.value.length > 0);
|
|
3310
|
+
/** `enum` fields in declaration order — the kanban can group on any. */
|
|
3311
|
+
const enumFields = (0, vue.computed)(() => collection.value ? Object.entries(collection.value.schema.fields).filter(([, field]) => field.type === "enum").map(([key]) => key) : []);
|
|
3312
|
+
/** Whether the kanban toggle is offered (needs an `enum` field to group on). */
|
|
3313
|
+
const hasKanban = (0, vue.computed)(() => enumFields.value.length > 0);
|
|
3314
|
+
/** The effective view, collapsing any stale mode whose enabling field
|
|
3315
|
+
* vanished (e.g. `view = "kanban"` after switching to an enum-less
|
|
3316
|
+
* collection) back to "table". Single source of truth for the toggle and
|
|
3317
|
+
* the body branches. */
|
|
3318
|
+
/** Custom (LLM-authored) HTML views declared on the schema. */
|
|
3319
|
+
const customViews = (0, vue.computed)(() => collection.value?.schema.views ?? []);
|
|
3320
|
+
const hasCustomViews = (0, vue.computed)(() => customViews.value.length > 0);
|
|
3321
|
+
const activeView = (0, vue.computed)(() => {
|
|
3322
|
+
if (view.value === "calendar" && hasCalendar.value) return "calendar";
|
|
3323
|
+
if (view.value === "kanban" && hasKanban.value) return "kanban";
|
|
3324
|
+
if (view.value.startsWith("custom:")) {
|
|
3325
|
+
const viewId = view.value.slice(7);
|
|
3326
|
+
if (customViews.value.some((entry) => entry.id === viewId)) return view.value;
|
|
3327
|
+
}
|
|
3328
|
+
return "table";
|
|
3329
|
+
});
|
|
3330
|
+
/** The selected custom view's spec, or null when a built-in view is active. */
|
|
3331
|
+
const activeCustomView = (0, vue.computed)(() => {
|
|
3332
|
+
const mode = activeView.value;
|
|
3333
|
+
if (!mode.startsWith("custom:")) return null;
|
|
3334
|
+
const viewId = mode.slice(7);
|
|
3335
|
+
return customViews.value.find((entry) => entry.id === viewId) ?? null;
|
|
3336
|
+
});
|
|
3337
|
+
/** Narrow a (possibly custom) mode to a built-in one, used where only the
|
|
3338
|
+
* built-in views are representable (the embedded card's viewState). */
|
|
3339
|
+
function builtInViewOrTable(mode) {
|
|
3340
|
+
return mode === "calendar" || mode === "kanban" ? mode : "table";
|
|
3341
|
+
}
|
|
3342
|
+
/** Whether to offer the "+" (author a new custom view) button. Standalone
|
|
3343
|
+
* page only (the seed starts a chat). Feeds qualify too — their views are
|
|
3344
|
+
* authored under feeds/<slug>/ and the seed prompt points there. */
|
|
3345
|
+
const canAddCustomView = (0, vue.computed)(() => Boolean(collection.value) && !embedded.value);
|
|
3346
|
+
/** Seed a chat asking Claude to author a new custom view for this collection.
|
|
3347
|
+
* Reuses the same chat-seed path as collection actions — the host injects a
|
|
3348
|
+
* templated prompt; Claude asks, authors the HTML, and registers it. The
|
|
3349
|
+
* authoring base is source-aware: a feed lives under `feeds/<slug>/`, every
|
|
3350
|
+
* other collection under the `data/skills/<slug>/` staging dir. */
|
|
3351
|
+
function addCustomView() {
|
|
3352
|
+
const current = collection.value;
|
|
3353
|
+
if (!current) return;
|
|
3354
|
+
const base = current.schema.ingest ? `feeds/${current.slug}` : `data/skills/${current.slug}`;
|
|
3355
|
+
const prompt = t("collectionsView.addViewPrompt", {
|
|
3356
|
+
title: current.title,
|
|
3357
|
+
base
|
|
3358
|
+
});
|
|
3359
|
+
if (props.sendTextMessage) {
|
|
3360
|
+
props.sendTextMessage(prompt);
|
|
3361
|
+
return;
|
|
3362
|
+
}
|
|
3363
|
+
appApi.startNewChat(prompt, cui.generalRoleId);
|
|
3364
|
+
}
|
|
3365
|
+
const configOpen = (0, vue.ref)(false);
|
|
3366
|
+
/** Whether to offer the config gear. Standalone page only, and only when
|
|
3367
|
+
* there's a deletable custom view to manage — i.e. the collection is one
|
|
3368
|
+
* whose views the server will delete (project non-preset, or a feed; never a
|
|
3369
|
+
* read-only user-scope skill). Mirrors the server's refusal rules. */
|
|
3370
|
+
const canConfigureViews = (0, vue.computed)(() => !embedded.value && hasCustomViews.value && (canDeleteCollection.value || isFeed.value));
|
|
3371
|
+
/** Reload the collection after the config modal deletes a view so the toggle
|
|
3372
|
+
* row + the modal's own list reflect the removal. */
|
|
3373
|
+
async function onViewsChanged() {
|
|
3374
|
+
const current = collection.value;
|
|
3375
|
+
if (current) await loadCollection(current.slug);
|
|
3376
|
+
}
|
|
3377
|
+
/** True when the calendar is the active body. */
|
|
3378
|
+
const calendarActive = (0, vue.computed)(() => activeView.value === "calendar");
|
|
3379
|
+
/** True when the kanban is the active body. */
|
|
3380
|
+
const kanbanActive = (0, vue.computed)(() => activeView.value === "kanban");
|
|
3381
|
+
const kanbanOverride = (0, vue.ref)(props.initialGroupField ?? null);
|
|
3382
|
+
const kanbanGroupField = (0, vue.computed)(() => {
|
|
3383
|
+
if (kanbanOverride.value && enumFields.value.includes(kanbanOverride.value)) return kanbanOverride.value;
|
|
3384
|
+
const hint = collection.value?.schema.kanbanField;
|
|
3385
|
+
if (hint && enumFields.value.includes(hint)) return hint;
|
|
3386
|
+
return enumFields.value[0] ?? "";
|
|
3387
|
+
});
|
|
3388
|
+
const anchorOverride = (0, vue.ref)(props.initialAnchorField ?? null);
|
|
3389
|
+
const calendarAnchorField = (0, vue.computed)(() => {
|
|
3390
|
+
if (anchorOverride.value && dateFields.value.includes(anchorOverride.value)) return anchorOverride.value;
|
|
3391
|
+
const hint = collection.value?.schema.calendarField;
|
|
3392
|
+
if (hint && dateFields.value.includes(hint)) return hint;
|
|
3393
|
+
return dateFields.value[0] ?? "";
|
|
3394
|
+
});
|
|
3395
|
+
const calendarEndField = (0, vue.computed)(() => {
|
|
3396
|
+
const schema = collection.value?.schema;
|
|
3397
|
+
if (!schema?.calendarEndField) return void 0;
|
|
3398
|
+
return calendarAnchorField.value === schema.calendarField ? schema.calendarEndField : void 0;
|
|
3399
|
+
});
|
|
3400
|
+
const calendarTimeField = (0, vue.computed)(() => {
|
|
3401
|
+
const schema = collection.value?.schema;
|
|
3402
|
+
if (!schema?.calendarTimeField) return void 0;
|
|
3403
|
+
return calendarAnchorField.value === schema.calendarField ? schema.calendarTimeField : void 0;
|
|
3404
|
+
});
|
|
3405
|
+
function setView(next) {
|
|
3406
|
+
view.value = next;
|
|
3407
|
+
}
|
|
3408
|
+
/** Select a custom view by id (builds the `custom:<id>` mode key). */
|
|
3409
|
+
function setCustomView(viewId) {
|
|
3410
|
+
view.value = `custom:${viewId}`;
|
|
3411
|
+
}
|
|
3412
|
+
/** Selector-key for a custom view, for active-state comparison in the template. */
|
|
3413
|
+
function customViewKey(viewId) {
|
|
3414
|
+
return `custom:${viewId}`;
|
|
3415
|
+
}
|
|
3416
|
+
/** A short, slug-safe id not already used by a loaded record. Collisions
|
|
3417
|
+
* are astronomically unlikely (32 bits), but we still re-roll a few
|
|
3418
|
+
* times against the in-memory set before giving up and using the last
|
|
3419
|
+
* candidate (the server's overwrite guard is the final backstop). */
|
|
3420
|
+
function generateUniqueItemId(primaryKey) {
|
|
3421
|
+
const existing = new Set(items.value.map((item) => String(item[primaryKey] ?? "")));
|
|
3422
|
+
let candidate = require_promptSafety.shortHexId();
|
|
3423
|
+
for (let attempt = 0; attempt < 8 && existing.has(candidate); attempt++) candidate = require_promptSafety.shortHexId();
|
|
3424
|
+
return candidate;
|
|
3425
|
+
}
|
|
3426
|
+
function openCreate() {
|
|
3427
|
+
if (!collection.value) return;
|
|
3428
|
+
const text = {};
|
|
3429
|
+
const bool = {};
|
|
3430
|
+
const boolOriginallyPresent = {};
|
|
3431
|
+
const boolTouched = {};
|
|
3432
|
+
const table = {};
|
|
3433
|
+
for (const [key, field] of Object.entries(collection.value.schema.fields)) if (field.type === "boolean") {
|
|
3434
|
+
bool[key] = false;
|
|
3435
|
+
boolOriginallyPresent[key] = false;
|
|
3436
|
+
boolTouched[key] = false;
|
|
3437
|
+
} else if (field.type === "table") table[key] = [];
|
|
3438
|
+
else if (field.type !== "derived" && field.type !== "embed" && field.type !== "toggle") text[key] = "";
|
|
3439
|
+
const { singleton, primaryKey } = collection.value.schema;
|
|
3440
|
+
if (singleton) text[primaryKey] = singleton;
|
|
3441
|
+
else if (primaryKey in text) text[primaryKey] = generateUniqueItemId(primaryKey);
|
|
3442
|
+
viewing.value = null;
|
|
3443
|
+
editing.value = {
|
|
3444
|
+
mode: "create",
|
|
3445
|
+
text,
|
|
3446
|
+
bool,
|
|
3447
|
+
boolOriginallyPresent,
|
|
3448
|
+
boolTouched,
|
|
3449
|
+
table,
|
|
3450
|
+
originalId: null
|
|
3451
|
+
};
|
|
3452
|
+
saveError.value = null;
|
|
3453
|
+
}
|
|
3454
|
+
function openEdit(item) {
|
|
3455
|
+
if (!collection.value) return;
|
|
3456
|
+
const text = {};
|
|
3457
|
+
const bool = {};
|
|
3458
|
+
const boolOriginallyPresent = {};
|
|
3459
|
+
const boolTouched = {};
|
|
3460
|
+
const table = {};
|
|
3461
|
+
for (const [key, field] of Object.entries(collection.value.schema.fields)) {
|
|
3462
|
+
const raw = item[key];
|
|
3463
|
+
if (field.type === "boolean") {
|
|
3464
|
+
bool[key] = raw === true;
|
|
3465
|
+
boolOriginallyPresent[key] = typeof raw === "boolean";
|
|
3466
|
+
boolTouched[key] = false;
|
|
3467
|
+
} else if (field.type === "table" && field.of) {
|
|
3468
|
+
const sub = field.of;
|
|
3469
|
+
table[key] = (Array.isArray(raw) ? raw : []).filter((row) => Boolean(row) && typeof row === "object" && !Array.isArray(row)).map((row) => require_promptSafety.rowFromItem(row, sub));
|
|
3470
|
+
} else if (field.type !== "derived" && field.type !== "embed" && field.type !== "toggle") text[key] = raw === void 0 || raw === null ? "" : String(raw);
|
|
3471
|
+
}
|
|
3472
|
+
const primaryRaw = item[collection.value.schema.primaryKey];
|
|
3473
|
+
const originalId = typeof primaryRaw === "string" ? primaryRaw : String(primaryRaw ?? "");
|
|
3474
|
+
viewing.value = null;
|
|
3475
|
+
editing.value = {
|
|
3476
|
+
mode: "edit",
|
|
3477
|
+
text,
|
|
3478
|
+
bool,
|
|
3479
|
+
boolOriginallyPresent,
|
|
3480
|
+
boolTouched,
|
|
3481
|
+
table,
|
|
3482
|
+
originalId
|
|
3483
|
+
};
|
|
3484
|
+
saveError.value = null;
|
|
3485
|
+
}
|
|
3486
|
+
function closeEditor() {
|
|
3487
|
+
editing.value = null;
|
|
3488
|
+
saving.value = false;
|
|
3489
|
+
saveError.value = null;
|
|
3490
|
+
}
|
|
3491
|
+
/** Cancel the editor. Edit → reopen the record's read-only detail (don't
|
|
3492
|
+
* collapse the panel); create → just close (no prior detail to show). */
|
|
3493
|
+
function cancelEditor() {
|
|
3494
|
+
const draft = editing.value;
|
|
3495
|
+
const returnTo = draft && draft.mode === "edit" ? draft.originalId : null;
|
|
3496
|
+
closeEditor();
|
|
3497
|
+
if (returnTo) {
|
|
3498
|
+
const item = findItemById(returnTo);
|
|
3499
|
+
if (item) showDetail(item);
|
|
3500
|
+
}
|
|
3501
|
+
}
|
|
3502
|
+
/** Open mode (read-only detail). Toggles: clicking the already-open row
|
|
3503
|
+
* collapses it. Opening a row cancels any in-progress edit (one panel
|
|
3504
|
+
* open at a time). In embedded mode, report the open id so the host
|
|
3505
|
+
* card can persist it in `viewState`. */
|
|
3506
|
+
function openView(item) {
|
|
3507
|
+
if (isRowOpen(item) && !editing.value) {
|
|
3508
|
+
closeView();
|
|
3509
|
+
return;
|
|
3510
|
+
}
|
|
3511
|
+
if (editing.value) closeEditor();
|
|
3512
|
+
showDetail(item);
|
|
3513
|
+
}
|
|
3514
|
+
/** Open the read-only detail for a record WITHOUT the click-toggle. Used
|
|
3515
|
+
* when reopening detail programmatically (after save / cancel), where
|
|
3516
|
+
* `openView`'s "click the open row to collapse" guard would otherwise
|
|
3517
|
+
* immediately close a row the embedded `viewState` sync just reopened. */
|
|
3518
|
+
function showDetail(item) {
|
|
3519
|
+
viewing.value = item;
|
|
3520
|
+
actionError.value = null;
|
|
3521
|
+
if (embedded.value && collection.value) emit("select", String(item[collection.value.schema.primaryKey] ?? ""));
|
|
3522
|
+
}
|
|
3523
|
+
/** Close open mode. Embedded mode reports the close via `select(null)`
|
|
3524
|
+
* (the card clears its `viewState`); standalone mode drops the
|
|
3525
|
+
* `?selected=` query param so a refresh / back-button doesn't reopen
|
|
3526
|
+
* the record and the URL reflects the closed state. */
|
|
3527
|
+
function closeView() {
|
|
3528
|
+
viewing.value = null;
|
|
3529
|
+
actionError.value = null;
|
|
3530
|
+
if (embedded.value) {
|
|
3531
|
+
emit("select", null);
|
|
3532
|
+
return;
|
|
3533
|
+
}
|
|
3534
|
+
if (cui.routeSelectedId() !== void 0) cui.setSelectedId(null);
|
|
3535
|
+
}
|
|
3536
|
+
/** Backdrop click / Escape on the shared record modal. While editing this
|
|
3537
|
+
* cancels the draft (reopening the detail, matching the in-panel Cancel
|
|
3538
|
+
* button — so a stray click never silently discards edits); while viewing
|
|
3539
|
+
* it closes the detail. */
|
|
3540
|
+
function closeRecordModal() {
|
|
3541
|
+
if (editing.value) {
|
|
3542
|
+
cancelEditor();
|
|
3543
|
+
return;
|
|
3544
|
+
}
|
|
3545
|
+
closeView();
|
|
3546
|
+
}
|
|
3547
|
+
/** Hand off from open mode to the editor for the same record. */
|
|
3548
|
+
function editFromView() {
|
|
3549
|
+
const item = viewing.value;
|
|
3550
|
+
if (!item) return;
|
|
3551
|
+
viewing.value = null;
|
|
3552
|
+
openEdit(item);
|
|
3553
|
+
}
|
|
3554
|
+
function findItemById(itemId) {
|
|
3555
|
+
if (!collection.value) return void 0;
|
|
3556
|
+
const { primaryKey } = collection.value.schema;
|
|
3557
|
+
return items.value.find((item) => String(item[primaryKey] ?? "") === itemId);
|
|
3558
|
+
}
|
|
3559
|
+
/** Reconcile the open-mode view with the `?selected=<id>` query —
|
|
3560
|
+
* the single source of truth for which record is open. Opens the
|
|
3561
|
+
* matching record, or closes the modal when the param is absent /
|
|
3562
|
+
* empty / points at an id that isn't loaded (deleted record, stale
|
|
3563
|
+
* link). Keeping `viewing` in lockstep with the URL means browser
|
|
3564
|
+
* back / forward and a removed param both close the modal instead
|
|
3565
|
+
* of leaving stale UI on screen (Codex P2 + CodeRabbit on #1502). */
|
|
3566
|
+
function syncViewToSelected() {
|
|
3567
|
+
const selected = activeSelected.value;
|
|
3568
|
+
if (typeof selected !== "string" || selected.length === 0) {
|
|
3569
|
+
viewing.value = null;
|
|
3570
|
+
return;
|
|
3571
|
+
}
|
|
3572
|
+
viewing.value = findItemById(selected) ?? null;
|
|
3573
|
+
}
|
|
3574
|
+
/** Title for the open-mode header: the record's primary-key value
|
|
3575
|
+
* (e.g. `INV-2026-0001`), falling back to the collection title.
|
|
3576
|
+
* Non-string primary keys (numeric ids) are stringified rather
|
|
3577
|
+
* than discarded (CodeRabbit on #1502). */
|
|
3578
|
+
const viewTitle = (0, vue.computed)(() => {
|
|
3579
|
+
if (!viewing.value || !collection.value) return "";
|
|
3580
|
+
const pkValue = viewing.value[collection.value.schema.primaryKey];
|
|
3581
|
+
if (pkValue === void 0 || pkValue === null || pkValue === "") return collection.value.title ?? "";
|
|
3582
|
+
return String(pkValue);
|
|
3583
|
+
});
|
|
3584
|
+
/** Live computed record from the current draft. Drives derived
|
|
3585
|
+
* field displays in the form so subtotal/tax/total update as
|
|
3586
|
+
* the user edits line items. */
|
|
3587
|
+
const liveRecord = (0, vue.computed)(() => {
|
|
3588
|
+
if (!collection.value || !editing.value) return null;
|
|
3589
|
+
return require_promptSafety.draftToRecord(editing.value, collection.value.schema);
|
|
3590
|
+
});
|
|
3591
|
+
/** Live record with derived fields resolved (drives the form's
|
|
3592
|
+
* read-only derived inputs). Derivation lives in the shared
|
|
3593
|
+
* rendering composable; this binds it to the current draft. */
|
|
3594
|
+
const liveDerived = (0, vue.computed)(() => {
|
|
3595
|
+
if (!collection.value || !liveRecord.value) return null;
|
|
3596
|
+
return render.deriveAll(collection.value.schema, liveRecord.value, refRecordCache.value);
|
|
3597
|
+
});
|
|
3598
|
+
/** Short summary for a `table`-typed cell in the main collection
|
|
3599
|
+
* table. Counts rows; nothing fancier yet (per-row preview is
|
|
3600
|
+
* hard to fit in a single cell). */
|
|
3601
|
+
function tableSummary(value) {
|
|
3602
|
+
if (!Array.isArray(value)) return "—";
|
|
3603
|
+
if (value.length === 0) return "—";
|
|
3604
|
+
return t("collectionsView.tableSummary", { count: value.length });
|
|
3605
|
+
}
|
|
3606
|
+
async function saveEditor() {
|
|
3607
|
+
if (!collection.value || !editing.value) return;
|
|
3608
|
+
const { slug, schema } = collection.value;
|
|
3609
|
+
const draft = editing.value;
|
|
3610
|
+
saveError.value = null;
|
|
3611
|
+
const missing = require_promptSafety.firstMissingRequiredField(draft, schema);
|
|
3612
|
+
if (missing) {
|
|
3613
|
+
saveError.value = `${missing}: ${t("collectionsView.requiredField")}`;
|
|
3614
|
+
return;
|
|
3615
|
+
}
|
|
3616
|
+
saving.value = true;
|
|
3617
|
+
const record = require_promptSafety.draftToRecord(draft, schema);
|
|
3618
|
+
const result = draft.mode === "create" ? await cui.createItem(slug, record) : await cui.updateItem(slug, draft.originalId ?? "", record);
|
|
3619
|
+
saving.value = false;
|
|
3620
|
+
if (!result.ok) {
|
|
3621
|
+
saveError.value = result.error;
|
|
3622
|
+
return;
|
|
3623
|
+
}
|
|
3624
|
+
const savedId = result.data.itemId;
|
|
3625
|
+
closeEditor();
|
|
3626
|
+
await loadCollection(slug);
|
|
3627
|
+
const saved = findItemById(savedId);
|
|
3628
|
+
if (saved) showDetail(saved);
|
|
3629
|
+
}
|
|
3630
|
+
/** Write a single cell's value directly onto the live `items` entry.
|
|
3631
|
+
* Reactive in Vue 3 (proxy), so the bound checkbox/select re-renders.
|
|
3632
|
+
* `undefined` (enum cleared to the placeholder) renders as the empty
|
|
3633
|
+
* option; the PUT body omits the key via `buildUpdatedRecord`. */
|
|
3634
|
+
function applyInlineValue(item, key, value) {
|
|
3635
|
+
item[key] = value;
|
|
3636
|
+
}
|
|
3637
|
+
/** True while this row has an inline cell save in flight — its inline
|
|
3638
|
+
* controls render disabled to serialize edits (one PUT per row). */
|
|
3639
|
+
function isRowInlineSaving(item) {
|
|
3640
|
+
return inlineSavingRows.value.has(rowId(item));
|
|
3641
|
+
}
|
|
3642
|
+
/** Inline table-cell edit (boolean checkbox / enum dropdown): optimistic
|
|
3643
|
+
* update, then PUT the full record. Gated per row so a second edit can't
|
|
3644
|
+
* race the in-flight one. On failure, roll the cell back and surface the
|
|
3645
|
+
* error. Bypasses the detail/edit panel entirely. */
|
|
3646
|
+
async function commitInlineEdit(item, key, field, raw) {
|
|
3647
|
+
if (!collection.value) return;
|
|
3648
|
+
const { slug } = collection.value;
|
|
3649
|
+
const itemId = rowId(item);
|
|
3650
|
+
if (!itemId || inlineSavingRows.value.has(itemId)) return;
|
|
3651
|
+
const previous = item[key];
|
|
3652
|
+
const coerced = require_promptSafety.coerceInlineValue(field, raw);
|
|
3653
|
+
applyInlineValue(item, key, coerced);
|
|
3654
|
+
inlineError.value = null;
|
|
3655
|
+
inlineSavingRows.value.add(itemId);
|
|
3656
|
+
const result = await cui.updateItem(slug, itemId, require_promptSafety.buildUpdatedRecord(item, key, coerced));
|
|
3657
|
+
inlineSavingRows.value.delete(itemId);
|
|
3658
|
+
if (!result.ok) {
|
|
3659
|
+
applyInlineValue(item, key, previous);
|
|
3660
|
+
inlineError.value = result.error;
|
|
3661
|
+
}
|
|
3662
|
+
}
|
|
3663
|
+
/** Whether a `toggle` field reads as checked: its projected enum field
|
|
3664
|
+
* currently equals `onValue`. The toggle stores nothing itself. */
|
|
3665
|
+
function toggleChecked(item, field) {
|
|
3666
|
+
return field.field !== void 0 && String(item[field.field] ?? "") === field.onValue;
|
|
3667
|
+
}
|
|
3668
|
+
/** Flip a `toggle`: write the projected enum field to `offValue` when
|
|
3669
|
+
* currently checked, else `onValue`. Reuses the inline-edit PUT path
|
|
3670
|
+
* (optimistic + rollback) — the toggle has no value of its own. */
|
|
3671
|
+
function commitToggle(item, field) {
|
|
3672
|
+
const targetKey = field.field;
|
|
3673
|
+
if (!targetKey || !collection.value) return;
|
|
3674
|
+
const enumField = collection.value.schema.fields[targetKey];
|
|
3675
|
+
if (!enumField) return;
|
|
3676
|
+
const next = toggleChecked(item, field) ? field.offValue : field.onValue;
|
|
3677
|
+
if (next === void 0) return;
|
|
3678
|
+
commitInlineEdit(item, targetKey, enumField, next);
|
|
3679
|
+
}
|
|
3680
|
+
async function confirmDelete(item) {
|
|
3681
|
+
if (!collection.value) return;
|
|
3682
|
+
const { slug } = collection.value;
|
|
3683
|
+
const { primaryKey } = collection.value.schema;
|
|
3684
|
+
const idRaw = item[primaryKey];
|
|
3685
|
+
const itemId = typeof idRaw === "string" ? idRaw : String(idRaw ?? "");
|
|
3686
|
+
if (!itemId) return;
|
|
3687
|
+
if (!await openConfirm({
|
|
3688
|
+
message: t("collectionsView.confirmDelete"),
|
|
3689
|
+
confirmText: t("common.remove"),
|
|
3690
|
+
cancelText: t("common.cancel"),
|
|
3691
|
+
variant: "danger"
|
|
3692
|
+
})) return;
|
|
3693
|
+
const result = await cui.deleteItem(slug, itemId);
|
|
3694
|
+
if (!result.ok) {
|
|
3695
|
+
loadError.value = result.error;
|
|
3696
|
+
return;
|
|
3697
|
+
}
|
|
3698
|
+
await loadCollection(slug);
|
|
3699
|
+
}
|
|
3700
|
+
async function confirmCollectionDelete() {
|
|
3701
|
+
const current = collection.value;
|
|
3702
|
+
if (!current) return;
|
|
3703
|
+
const { slug, title } = current;
|
|
3704
|
+
if (!await openConfirm({
|
|
3705
|
+
message: t("collectionsView.confirmDeleteCollection", { title }),
|
|
3706
|
+
confirmText: t("common.remove"),
|
|
3707
|
+
cancelText: t("common.cancel"),
|
|
3708
|
+
variant: "danger"
|
|
3709
|
+
})) return;
|
|
3710
|
+
const result = await cui.deleteCollection(slug);
|
|
3711
|
+
if (!result.ok) {
|
|
3712
|
+
loadError.value = result.error;
|
|
3713
|
+
return;
|
|
3714
|
+
}
|
|
3715
|
+
cui.gotoIndex("collection");
|
|
3716
|
+
}
|
|
3717
|
+
function goBack() {
|
|
3718
|
+
cui.gotoIndex(isFeedRoute.value ? "feed" : "collection");
|
|
3719
|
+
}
|
|
3720
|
+
async function confirmFeedDelete() {
|
|
3721
|
+
const current = collection.value;
|
|
3722
|
+
if (!current) return;
|
|
3723
|
+
const { slug, title } = current;
|
|
3724
|
+
if (!await openConfirm({
|
|
3725
|
+
message: t("collectionsView.confirmDeleteFeed", { title }),
|
|
3726
|
+
confirmText: t("common.remove"),
|
|
3727
|
+
cancelText: t("common.cancel"),
|
|
3728
|
+
variant: "danger"
|
|
3729
|
+
})) return;
|
|
3730
|
+
const result = await cui.deleteFeed(slug);
|
|
3731
|
+
if (!result.ok) {
|
|
3732
|
+
loadError.value = result.error;
|
|
3733
|
+
return;
|
|
3734
|
+
}
|
|
3735
|
+
cui.gotoIndex("feed");
|
|
3736
|
+
}
|
|
3737
|
+
/** Open the create form with the clicked calendar day prefilled into the
|
|
3738
|
+
* anchor field. The calendar day view's + affordance; the create flow itself
|
|
3739
|
+
* is the same one the Add button uses. A `datetime` anchor renders as a
|
|
3740
|
+
* `datetime-local` input, which rejects a bare `YYYY-MM-DD` — seed midnight
|
|
3741
|
+
* so the chosen day actually survives the prefill. */
|
|
3742
|
+
function createOnDate(iso) {
|
|
3743
|
+
if (!canCreate.value) return;
|
|
3744
|
+
openCreate();
|
|
3745
|
+
const anchor = calendarAnchorField.value;
|
|
3746
|
+
if (!editing.value || !anchor) return;
|
|
3747
|
+
const anchorType = collection.value?.schema.fields[anchor]?.type;
|
|
3748
|
+
editing.value.text[anchor] = anchorType === "datetime" ? `${iso}T00:00` : iso;
|
|
3749
|
+
}
|
|
3750
|
+
/** The civil day a record sits on, from its calendar anchor field (handles
|
|
3751
|
+
* both `date` and `datetime`). Null for undated records. */
|
|
3752
|
+
function dayOfItem(item) {
|
|
3753
|
+
return require_promptSafety.dateOf(item[calendarAnchorField.value]);
|
|
3754
|
+
}
|
|
3755
|
+
/** Mirror the open record into the `?selected=<id>` query (standalone mode)
|
|
3756
|
+
* so the calendar's day-view + selection is a copy-pasteable link. In-app
|
|
3757
|
+
* selection didn't previously touch the URL; the calendar now does. */
|
|
3758
|
+
function writeSelectedToUrl(itemId) {
|
|
3759
|
+
if (embedded.value || cui.routeSelectedId() === itemId) return;
|
|
3760
|
+
cui.setSelectedId(itemId);
|
|
3761
|
+
}
|
|
3762
|
+
/** Calendar chip / kanban card click → open that record's detail. In the
|
|
3763
|
+
* calendar it opens the day (time-allocation) popup on the record's day with
|
|
3764
|
+
* the detail in the right pane; an undated record falls back to the panel
|
|
3765
|
+
* below the grid. Unlike `openView`, this never toggles — a second click on
|
|
3766
|
+
* the same record keeps it open. */
|
|
3767
|
+
function onCalendarSelect(itemId) {
|
|
3768
|
+
if (!itemId) {
|
|
3769
|
+
closeView();
|
|
3770
|
+
return;
|
|
3771
|
+
}
|
|
3772
|
+
const item = findItemById(itemId);
|
|
3773
|
+
if (!item) return;
|
|
3774
|
+
if (editing.value) closeEditor();
|
|
3775
|
+
if (calendarActive.value) openDay.value = dayOfItem(item);
|
|
3776
|
+
showDetail(item);
|
|
3777
|
+
writeSelectedToUrl(itemId);
|
|
3778
|
+
}
|
|
3779
|
+
/** A calendar day cell was activated → open its popup on a clean slate
|
|
3780
|
+
* (clear any prior selection so the popup opens timeline-only). */
|
|
3781
|
+
function onOpenDay(day) {
|
|
3782
|
+
if (editing.value) closeEditor();
|
|
3783
|
+
closeView();
|
|
3784
|
+
openDay.value = day;
|
|
3785
|
+
}
|
|
3786
|
+
/** Close the day popup: drop the open day, the selection, AND any in-progress
|
|
3787
|
+
* draft together. Clearing `editing` matters because the shared record modal
|
|
3788
|
+
* shows whenever `editing` is set and no day is open — so without this, an
|
|
3789
|
+
* edit/create started inside the day popup would re-appear in the centred
|
|
3790
|
+
* modal the instant the popup closed (Codex P2 on #1656). */
|
|
3791
|
+
function onDayClose() {
|
|
3792
|
+
openDay.value = null;
|
|
3793
|
+
if (editing.value) closeEditor();
|
|
3794
|
+
closeView();
|
|
3795
|
+
}
|
|
3796
|
+
/** Deep-link entry: a `?selected=<id>` link to a calendar-capable collection
|
|
3797
|
+
* opens in calendar view with the popup focused on the record's day. Runs
|
|
3798
|
+
* on load / slug change only (not on in-app selection), so table users
|
|
3799
|
+
* aren't forced into the calendar. */
|
|
3800
|
+
function maybeOpenCalendarForSelected() {
|
|
3801
|
+
if (embedded.value || !hasCalendar.value || !viewing.value) return;
|
|
3802
|
+
const day = dayOfItem(viewing.value);
|
|
3803
|
+
if (!day) return;
|
|
3804
|
+
view.value = "calendar";
|
|
3805
|
+
openDay.value = day;
|
|
3806
|
+
}
|
|
3807
|
+
/** Kanban card dropped in a column → set the record's group field to the
|
|
3808
|
+
* column value (the empty string clears it for the Uncategorized column).
|
|
3809
|
+
* Reuses the inline-edit path (optimistic write + PUT + rollback). */
|
|
3810
|
+
function onKanbanMove(itemId, value) {
|
|
3811
|
+
const item = findItemById(itemId);
|
|
3812
|
+
const key = kanbanGroupField.value;
|
|
3813
|
+
const field = collection.value?.schema.fields[key];
|
|
3814
|
+
if (!item || !field) return;
|
|
3815
|
+
commitInlineEdit(item, key, field, value);
|
|
3816
|
+
}
|
|
3817
|
+
(0, vue.watch)(activeSlug, (slug, prevSlug) => {
|
|
3818
|
+
if (prevSlug !== void 0 && slug !== prevSlug) {
|
|
3819
|
+
view.value = slug && readCollectionViewMode(slug) || "table";
|
|
3820
|
+
anchorOverride.value = null;
|
|
3821
|
+
kanbanOverride.value = null;
|
|
3822
|
+
sortState.value = storedSortFor(slug);
|
|
3823
|
+
}
|
|
3824
|
+
if (slug) loadCollection(slug);
|
|
3825
|
+
else {
|
|
3826
|
+
collection.value = null;
|
|
3827
|
+
items.value = [];
|
|
3828
|
+
enumOriginallyEmpty.value = /* @__PURE__ */ new Set();
|
|
3829
|
+
inlineSavingRows.value = /* @__PURE__ */ new Set();
|
|
3830
|
+
searchQuery.value = "";
|
|
3831
|
+
loading.value = false;
|
|
3832
|
+
}
|
|
3833
|
+
}, { immediate: true });
|
|
3834
|
+
(0, vue.watch)([
|
|
3835
|
+
activeView,
|
|
3836
|
+
calendarAnchorField,
|
|
3837
|
+
kanbanGroupField,
|
|
3838
|
+
sortState,
|
|
3839
|
+
loading
|
|
3840
|
+
], () => {
|
|
3841
|
+
if (embedded.value) emit("viewStateChange", {
|
|
3842
|
+
view: builtInViewOrTable(activeView.value),
|
|
3843
|
+
anchorField: calendarAnchorField.value,
|
|
3844
|
+
groupField: kanbanGroupField.value
|
|
3845
|
+
});
|
|
3846
|
+
if (activeSlug.value && !loading.value && collection.value) {
|
|
3847
|
+
if (!embedded.value) writeCollectionViewMode(activeSlug.value, activeView.value);
|
|
3848
|
+
writeCollectionSort(activeSlug.value, sortState.value);
|
|
3849
|
+
}
|
|
3850
|
+
});
|
|
3851
|
+
(0, vue.watch)(activeSelected, () => {
|
|
3852
|
+
if (loading.value || !collection.value) return;
|
|
3853
|
+
syncViewToSelected();
|
|
3854
|
+
openDay.value = viewing.value ? dayOfItem(viewing.value) : null;
|
|
3855
|
+
});
|
|
3856
|
+
return (_ctx, _cache) => {
|
|
3857
|
+
const _component_router_link = (0, vue.resolveComponent)("router-link");
|
|
3858
|
+
return (0, vue.openBlock)(), (0, vue.createElementBlock)("div", _hoisted_1$2, [
|
|
3859
|
+
(0, vue.createElementVNode)("header", _hoisted_2$2, [
|
|
3860
|
+
!embedded.value ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("button", {
|
|
3861
|
+
key: 0,
|
|
3862
|
+
type: "button",
|
|
3863
|
+
class: "h-8 w-8 flex items-center justify-center rounded text-slate-500 hover:bg-slate-50 hover:text-slate-800 transition-colors",
|
|
3864
|
+
title: (0, vue.unref)(t)("collectionsView.backToIndex"),
|
|
3865
|
+
"aria-label": (0, vue.unref)(t)("collectionsView.backToIndex"),
|
|
3866
|
+
"data-testid": "collections-back",
|
|
3867
|
+
onClick: goBack
|
|
3868
|
+
}, [..._cache[25] || (_cache[25] = [(0, vue.createElementVNode)("span", { class: "material-icons text-lg" }, "arrow_back", -1)])], 8, _hoisted_3$2)) : (0, vue.createCommentVNode)("", true),
|
|
3869
|
+
collection.value ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("div", _hoisted_4$2, [(0, vue.createElementVNode)("span", _hoisted_5$2, (0, vue.toDisplayString)(collection.value.icon), 1)])) : (0, vue.createCommentVNode)("", true),
|
|
3870
|
+
(0, vue.createElementVNode)("div", _hoisted_6$2, [(0, vue.createElementVNode)("h1", _hoisted_7$2, (0, vue.toDisplayString)(collection.value?.title ?? (0, vue.unref)(t)("collectionsView.title")), 1), collection.value ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("span", _hoisted_8$2, (0, vue.toDisplayString)(collection.value.slug), 1)) : (0, vue.createCommentVNode)("", true)]),
|
|
3871
|
+
collection.value && !embedded.value ? ((0, vue.openBlock)(), (0, vue.createBlock)((0, vue.resolveDynamicComponent)((0, vue.unref)(pinToggle)), {
|
|
3872
|
+
key: 2,
|
|
3873
|
+
kind: isFeedRoute.value ? "feed" : "collection",
|
|
3874
|
+
slug: collection.value.slug,
|
|
3875
|
+
title: collection.value.title,
|
|
3876
|
+
icon: collection.value.icon
|
|
3877
|
+
}, null, 8, [
|
|
3878
|
+
"kind",
|
|
3879
|
+
"slug",
|
|
3880
|
+
"title",
|
|
3881
|
+
"icon"
|
|
3882
|
+
])) : (0, vue.createCommentVNode)("", true),
|
|
3883
|
+
collection.value?.schema.ingest ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("button", {
|
|
3884
|
+
key: 3,
|
|
3885
|
+
type: "button",
|
|
3886
|
+
class: "h-8 px-2.5 flex items-center gap-1 rounded border border-indigo-200 bg-white hover:bg-indigo-50 text-indigo-600 font-bold text-xs transition-colors disabled:opacity-50",
|
|
3887
|
+
disabled: refreshing.value,
|
|
3888
|
+
"data-testid": "collections-refresh-feed",
|
|
3889
|
+
onClick: refreshFeed
|
|
3890
|
+
}, [(0, vue.createElementVNode)("span", _hoisted_10$2, (0, vue.toDisplayString)(refreshing.value ? "hourglass_empty" : "refresh"), 1), (0, vue.createElementVNode)("span", null, (0, vue.toDisplayString)((0, vue.unref)(t)("collectionsView.refreshFeed")), 1)], 8, _hoisted_9$2)) : (0, vue.createCommentVNode)("", true),
|
|
3891
|
+
collection.value ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("button", {
|
|
3892
|
+
key: 4,
|
|
3893
|
+
type: "button",
|
|
3894
|
+
class: "h-8 px-2.5 flex items-center gap-1 rounded border border-indigo-200 bg-white hover:bg-indigo-50 text-indigo-600 font-bold text-xs transition-colors",
|
|
3895
|
+
"data-testid": "collections-chat",
|
|
3896
|
+
onClick: openChat
|
|
3897
|
+
}, [_cache[26] || (_cache[26] = (0, vue.createElementVNode)("span", { class: "material-icons text-sm" }, "forum", -1)), (0, vue.createElementVNode)("span", null, (0, vue.toDisplayString)((0, vue.unref)(t)("collectionsView.chat")), 1)])) : (0, vue.createCommentVNode)("", true),
|
|
3898
|
+
((0, vue.openBlock)(true), (0, vue.createElementBlock)(vue.Fragment, null, (0, vue.renderList)(collectionActions.value, (action) => {
|
|
3899
|
+
return (0, vue.openBlock)(), (0, vue.createElementBlock)("button", {
|
|
3900
|
+
key: action.id,
|
|
3901
|
+
type: "button",
|
|
3902
|
+
class: "h-8 px-2.5 flex items-center gap-1 rounded border border-indigo-200 bg-white hover:bg-indigo-50 text-indigo-600 font-bold text-xs transition-colors disabled:opacity-50",
|
|
3903
|
+
disabled: collectionActionPending.value,
|
|
3904
|
+
"data-testid": `collections-action-${action.id}`,
|
|
3905
|
+
onClick: ($event) => runCollectionAction(action)
|
|
3906
|
+
}, [action.icon ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("span", _hoisted_12$2, (0, vue.toDisplayString)(action.icon), 1)) : (0, vue.createCommentVNode)("", true), (0, vue.createElementVNode)("span", null, (0, vue.toDisplayString)(action.label), 1)], 8, _hoisted_11$2);
|
|
3907
|
+
}), 128)),
|
|
3908
|
+
canCreate.value && !calendarActive.value ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("button", {
|
|
3909
|
+
key: 5,
|
|
3910
|
+
type: "button",
|
|
3911
|
+
class: "h-8 px-2.5 flex items-center gap-1 rounded bg-indigo-600 hover:bg-indigo-700 text-white font-bold text-xs transition-colors shadow-sm",
|
|
3912
|
+
"data-testid": "collections-add-item",
|
|
3913
|
+
onClick: openCreate
|
|
3914
|
+
}, [_cache[27] || (_cache[27] = (0, vue.createElementVNode)("span", { class: "material-icons text-sm" }, "add", -1)), (0, vue.createElementVNode)("span", null, (0, vue.toDisplayString)((0, vue.unref)(t)("common.add")), 1)])) : (0, vue.createCommentVNode)("", true),
|
|
3915
|
+
canDeleteCollection.value && !embedded.value ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("button", {
|
|
3916
|
+
key: 6,
|
|
3917
|
+
type: "button",
|
|
3918
|
+
class: "h-8 w-8 flex items-center justify-center rounded border border-rose-200 bg-white text-rose-600 hover:bg-rose-50 transition-colors",
|
|
3919
|
+
title: (0, vue.unref)(t)("collectionsView.deleteCollection"),
|
|
3920
|
+
"aria-label": (0, vue.unref)(t)("collectionsView.deleteCollection"),
|
|
3921
|
+
"data-testid": "collections-delete",
|
|
3922
|
+
onClick: confirmCollectionDelete
|
|
3923
|
+
}, [..._cache[28] || (_cache[28] = [(0, vue.createElementVNode)("span", { class: "material-icons text-sm" }, "delete_forever", -1)])], 8, _hoisted_13$2)) : (0, vue.createCommentVNode)("", true),
|
|
3924
|
+
canDeleteFeed.value && !embedded.value ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("button", {
|
|
3925
|
+
key: 7,
|
|
3926
|
+
type: "button",
|
|
3927
|
+
class: "h-8 w-8 flex items-center justify-center rounded border border-rose-200 bg-white text-rose-600 hover:bg-rose-50 transition-colors",
|
|
3928
|
+
title: (0, vue.unref)(t)("collectionsView.deleteFeed"),
|
|
3929
|
+
"aria-label": (0, vue.unref)(t)("collectionsView.deleteFeed"),
|
|
3930
|
+
"data-testid": "feeds-delete",
|
|
3931
|
+
onClick: confirmFeedDelete
|
|
3932
|
+
}, [..._cache[29] || (_cache[29] = [(0, vue.createElementVNode)("span", { class: "material-icons text-sm" }, "delete_forever", -1)])], 8, _hoisted_14$2)) : (0, vue.createCommentVNode)("", true)
|
|
3933
|
+
]),
|
|
3934
|
+
collection.value && (items.value.length > 0 || hasCalendar.value || hasKanban.value || hasCustomViews.value || canAddCustomView.value) ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("div", _hoisted_15$2, [items.value.length > 0 ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("div", _hoisted_16$1, [
|
|
3935
|
+
_cache[31] || (_cache[31] = (0, vue.createElementVNode)("span", { class: "absolute inset-y-0 left-0 flex items-center pl-3 text-slate-400 pointer-events-none" }, [(0, vue.createElementVNode)("span", { class: "material-icons text-lg" }, "search")], -1)),
|
|
3936
|
+
(0, vue.withDirectives)((0, vue.createElementVNode)("input", {
|
|
3937
|
+
"onUpdate:modelValue": _cache[0] || (_cache[0] = ($event) => searchQuery.value = $event),
|
|
3938
|
+
type: "text",
|
|
3939
|
+
placeholder: (0, vue.unref)(t)("collectionsView.searchPlaceholder"),
|
|
3940
|
+
"aria-label": (0, vue.unref)(t)("collectionsView.searchPlaceholder"),
|
|
3941
|
+
class: "w-full bg-slate-50 border border-slate-200/80 rounded-xl pl-9 pr-8 py-1.5 text-xs placeholder-slate-400 focus:outline-none focus:ring-2 focus:ring-indigo-500/20 focus:border-indigo-500 focus:bg-white transition-all font-medium"
|
|
3942
|
+
}, null, 8, _hoisted_17$1), [[vue.vModelText, searchQuery.value]]),
|
|
3943
|
+
searchQuery.value ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("button", {
|
|
3944
|
+
key: 0,
|
|
3945
|
+
type: "button",
|
|
3946
|
+
"aria-label": (0, vue.unref)(t)("collectionsView.clearSearch"),
|
|
3947
|
+
class: "absolute inset-y-0 right-0 flex items-center pr-2.5 text-slate-400 hover:text-slate-600",
|
|
3948
|
+
onClick: _cache[1] || (_cache[1] = ($event) => searchQuery.value = "")
|
|
3949
|
+
}, [..._cache[30] || (_cache[30] = [(0, vue.createElementVNode)("span", { class: "material-icons text-sm" }, "close", -1)])], 8, _hoisted_18$1)) : (0, vue.createCommentVNode)("", true)
|
|
3950
|
+
])) : (0, vue.createCommentVNode)("", true), (0, vue.createElementVNode)("div", _hoisted_19$1, [
|
|
3951
|
+
hasCalendar.value || hasKanban.value || hasCustomViews.value || canAddCustomView.value ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("div", {
|
|
3952
|
+
key: 0,
|
|
3953
|
+
class: "flex gap-0.5",
|
|
3954
|
+
role: "group",
|
|
3955
|
+
"aria-label": (0, vue.unref)(t)("collectionsView.viewToggle")
|
|
3956
|
+
}, [
|
|
3957
|
+
(0, vue.createElementVNode)("button", {
|
|
3958
|
+
type: "button",
|
|
3959
|
+
class: (0, vue.normalizeClass)(["h-8 px-2.5 flex items-center gap-1 rounded text-xs font-bold transition-colors", activeView.value === "table" ? "bg-indigo-600 text-white" : "bg-white text-slate-500 border border-slate-200 hover:bg-slate-50"]),
|
|
3960
|
+
"aria-pressed": activeView.value === "table",
|
|
3961
|
+
"data-testid": "collection-view-toggle-table",
|
|
3962
|
+
onClick: _cache[2] || (_cache[2] = ($event) => setView("table"))
|
|
3963
|
+
}, [_cache[32] || (_cache[32] = (0, vue.createElementVNode)("span", { class: "material-icons text-sm" }, "table_rows", -1)), (0, vue.createElementVNode)("span", null, (0, vue.toDisplayString)((0, vue.unref)(t)("collectionsView.viewTable")), 1)], 10, _hoisted_21$1),
|
|
3964
|
+
hasCalendar.value ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("button", {
|
|
3965
|
+
key: 0,
|
|
3966
|
+
type: "button",
|
|
3967
|
+
class: (0, vue.normalizeClass)(["h-8 px-2.5 flex items-center gap-1 rounded text-xs font-bold transition-colors", activeView.value === "calendar" ? "bg-indigo-600 text-white" : "bg-white text-slate-500 border border-slate-200 hover:bg-slate-50"]),
|
|
3968
|
+
"aria-pressed": activeView.value === "calendar",
|
|
3969
|
+
"data-testid": "collection-view-toggle-calendar",
|
|
3970
|
+
onClick: _cache[3] || (_cache[3] = ($event) => setView("calendar"))
|
|
3971
|
+
}, [_cache[33] || (_cache[33] = (0, vue.createElementVNode)("span", { class: "material-icons text-sm" }, "calendar_month", -1)), (0, vue.createElementVNode)("span", null, (0, vue.toDisplayString)((0, vue.unref)(t)("collectionsView.viewCalendar")), 1)], 10, _hoisted_22$1)) : (0, vue.createCommentVNode)("", true),
|
|
3972
|
+
hasKanban.value ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("button", {
|
|
3973
|
+
key: 1,
|
|
3974
|
+
type: "button",
|
|
3975
|
+
class: (0, vue.normalizeClass)(["h-8 px-2.5 flex items-center gap-1 rounded text-xs font-bold transition-colors", activeView.value === "kanban" ? "bg-indigo-600 text-white" : "bg-white text-slate-500 border border-slate-200 hover:bg-slate-50"]),
|
|
3976
|
+
"aria-pressed": activeView.value === "kanban",
|
|
3977
|
+
"data-testid": "collection-view-toggle-kanban",
|
|
3978
|
+
onClick: _cache[4] || (_cache[4] = ($event) => setView("kanban"))
|
|
3979
|
+
}, [_cache[34] || (_cache[34] = (0, vue.createElementVNode)("span", { class: "material-icons text-sm" }, "view_kanban", -1)), (0, vue.createElementVNode)("span", null, (0, vue.toDisplayString)((0, vue.unref)(t)("collectionsView.viewKanban")), 1)], 10, _hoisted_23$1)) : (0, vue.createCommentVNode)("", true),
|
|
3980
|
+
((0, vue.openBlock)(true), (0, vue.createElementBlock)(vue.Fragment, null, (0, vue.renderList)(customViews.value, (cv) => {
|
|
3981
|
+
return (0, vue.openBlock)(), (0, vue.createElementBlock)("button", {
|
|
3982
|
+
key: cv.id,
|
|
3983
|
+
type: "button",
|
|
3984
|
+
class: (0, vue.normalizeClass)(["h-8 px-2.5 flex items-center gap-1 rounded text-xs font-bold transition-colors", activeView.value === customViewKey(cv.id) ? "bg-indigo-600 text-white" : "bg-white text-slate-500 border border-slate-200 hover:bg-slate-50"]),
|
|
3985
|
+
"aria-pressed": activeView.value === customViewKey(cv.id),
|
|
3986
|
+
"data-testid": `collection-view-custom-${cv.id}`,
|
|
3987
|
+
onClick: ($event) => setCustomView(cv.id)
|
|
3988
|
+
}, [(0, vue.createElementVNode)("span", _hoisted_25, (0, vue.toDisplayString)(cv.icon || "dashboard_customize"), 1), (0, vue.createElementVNode)("span", null, (0, vue.toDisplayString)(cv.label), 1)], 10, _hoisted_24);
|
|
3989
|
+
}), 128)),
|
|
3990
|
+
canAddCustomView.value ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("button", {
|
|
3991
|
+
key: 2,
|
|
3992
|
+
type: "button",
|
|
3993
|
+
class: "h-8 w-8 flex items-center justify-center rounded bg-white text-slate-500 border border-slate-200 hover:bg-slate-50",
|
|
3994
|
+
title: (0, vue.unref)(t)("collectionsView.addView"),
|
|
3995
|
+
"aria-label": (0, vue.unref)(t)("collectionsView.addView"),
|
|
3996
|
+
"data-testid": "collection-view-add",
|
|
3997
|
+
onClick: addCustomView
|
|
3998
|
+
}, [..._cache[35] || (_cache[35] = [(0, vue.createElementVNode)("span", { class: "material-icons text-sm" }, "add", -1)])], 8, _hoisted_26)) : (0, vue.createCommentVNode)("", true),
|
|
3999
|
+
canConfigureViews.value ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("button", {
|
|
4000
|
+
key: 3,
|
|
4001
|
+
type: "button",
|
|
4002
|
+
class: "h-8 w-8 flex items-center justify-center rounded bg-white text-slate-500 border border-slate-200 hover:bg-slate-50",
|
|
4003
|
+
title: (0, vue.unref)(t)("collectionsView.config.open"),
|
|
4004
|
+
"aria-label": (0, vue.unref)(t)("collectionsView.config.open"),
|
|
4005
|
+
"data-testid": "collection-config-open",
|
|
4006
|
+
onClick: _cache[5] || (_cache[5] = ($event) => configOpen.value = true)
|
|
4007
|
+
}, [..._cache[36] || (_cache[36] = [(0, vue.createElementVNode)("span", { class: "material-icons text-sm" }, "settings", -1)])], 8, _hoisted_27)) : (0, vue.createCommentVNode)("", true)
|
|
4008
|
+
], 8, _hoisted_20$1)) : (0, vue.createCommentVNode)("", true),
|
|
4009
|
+
calendarActive.value && dateFields.value.length > 1 ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("select", {
|
|
4010
|
+
key: 1,
|
|
4011
|
+
value: calendarAnchorField.value,
|
|
4012
|
+
class: "h-8 px-2 rounded border border-slate-200 bg-white text-xs font-semibold text-slate-600 focus:outline-none focus:border-indigo-500 cursor-pointer",
|
|
4013
|
+
"aria-label": (0, vue.unref)(t)("collectionsView.calendarFieldLabel"),
|
|
4014
|
+
"data-testid": "collection-calendar-field",
|
|
4015
|
+
onChange: _cache[6] || (_cache[6] = ($event) => anchorOverride.value = $event.target.value)
|
|
4016
|
+
}, [((0, vue.openBlock)(true), (0, vue.createElementBlock)(vue.Fragment, null, (0, vue.renderList)(dateFields.value, (key) => {
|
|
4017
|
+
return (0, vue.openBlock)(), (0, vue.createElementBlock)("option", {
|
|
4018
|
+
key,
|
|
4019
|
+
value: key
|
|
4020
|
+
}, (0, vue.toDisplayString)(collection.value?.schema.fields[key]?.label ?? key), 9, _hoisted_29);
|
|
4021
|
+
}), 128))], 40, _hoisted_28)) : (0, vue.createCommentVNode)("", true),
|
|
4022
|
+
kanbanActive.value && enumFields.value.length > 1 ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("select", {
|
|
4023
|
+
key: 2,
|
|
4024
|
+
value: kanbanGroupField.value,
|
|
4025
|
+
class: "h-8 px-2 rounded border border-slate-200 bg-white text-xs font-semibold text-slate-600 focus:outline-none focus:border-indigo-500 cursor-pointer",
|
|
4026
|
+
"aria-label": (0, vue.unref)(t)("collectionsView.kanbanFieldLabel"),
|
|
4027
|
+
"data-testid": "collection-kanban-field",
|
|
4028
|
+
onChange: _cache[7] || (_cache[7] = ($event) => kanbanOverride.value = $event.target.value)
|
|
4029
|
+
}, [((0, vue.openBlock)(true), (0, vue.createElementBlock)(vue.Fragment, null, (0, vue.renderList)(enumFields.value, (key) => {
|
|
4030
|
+
return (0, vue.openBlock)(), (0, vue.createElementBlock)("option", {
|
|
4031
|
+
key,
|
|
4032
|
+
value: key
|
|
4033
|
+
}, (0, vue.toDisplayString)(collection.value?.schema.fields[key]?.label ?? key), 9, _hoisted_31);
|
|
4034
|
+
}), 128))], 40, _hoisted_30)) : (0, vue.createCommentVNode)("", true),
|
|
4035
|
+
items.value.length > 0 ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("div", _hoisted_32, (0, vue.toDisplayString)((0, vue.unref)(t)("collectionsView.searchSummary", {
|
|
4036
|
+
shown: filteredItems.value.length,
|
|
4037
|
+
total: items.value.length
|
|
4038
|
+
})), 1)) : (0, vue.createCommentVNode)("", true)
|
|
4039
|
+
])])) : (0, vue.createCommentVNode)("", true),
|
|
4040
|
+
collection.value && dataIssues.value.length > 0 ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("div", _hoisted_33, [
|
|
4041
|
+
_cache[38] || (_cache[38] = (0, vue.createElementVNode)("span", { class: "material-icons text-amber-600" }, "warning", -1)),
|
|
4042
|
+
(0, vue.createElementVNode)("span", _hoisted_34, (0, vue.toDisplayString)((0, vue.unref)(t)("collectionsView.dataIssuesDetected", { count: dataIssues.value.length })), 1),
|
|
4043
|
+
(0, vue.createElementVNode)("button", {
|
|
4044
|
+
type: "button",
|
|
4045
|
+
class: "h-8 px-2.5 flex items-center gap-1 rounded border border-amber-300 bg-white hover:bg-amber-100 text-amber-700 font-bold text-xs transition-colors",
|
|
4046
|
+
"data-testid": "collections-repair",
|
|
4047
|
+
onClick: repairCollection
|
|
4048
|
+
}, [_cache[37] || (_cache[37] = (0, vue.createElementVNode)("span", { class: "material-icons text-sm" }, "build", -1)), (0, vue.createElementVNode)("span", null, (0, vue.toDisplayString)((0, vue.unref)(t)("collectionsView.repair")), 1)])
|
|
4049
|
+
])) : (0, vue.createCommentVNode)("", true),
|
|
4050
|
+
(0, vue.createElementVNode)("div", _hoisted_35, [loading.value ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("div", _hoisted_36, [_cache[39] || (_cache[39] = (0, vue.createElementVNode)("div", { class: "h-8 w-8 border-2 border-indigo-600/20 border-t-indigo-600 rounded-full animate-spin" }, null, -1)), (0, vue.createElementVNode)("span", null, (0, vue.toDisplayString)((0, vue.unref)(t)("common.loading")), 1)])) : loadError.value ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("div", _hoisted_37, [_cache[40] || (_cache[40] = (0, vue.createElementVNode)("span", { class: "material-icons text-red-600" }, "error", -1)), (0, vue.createElementVNode)("span", null, (0, vue.toDisplayString)(loadError.value === "not-found" ? (0, vue.unref)(t)("collectionsView.notFound") : `${(0, vue.unref)(t)("collectionsView.loadFailed")}: ${loadError.value}`), 1)])) : !collection.value ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("div", _hoisted_38)) : calendarActive.value ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("div", _hoisted_39, [(0, vue.createVNode)(CollectionCalendarView_default, {
|
|
4051
|
+
schema: collection.value.schema,
|
|
4052
|
+
items: filteredItems.value,
|
|
4053
|
+
"anchor-field": calendarAnchorField.value,
|
|
4054
|
+
"end-field": calendarEndField.value,
|
|
4055
|
+
"time-field": calendarTimeField.value,
|
|
4056
|
+
"color-field": hasKanban.value ? kanbanGroupField.value : "",
|
|
4057
|
+
selected: viewing.value ? String(viewing.value[collection.value.schema.primaryKey] ?? "") : void 0,
|
|
4058
|
+
onSelect: onCalendarSelect,
|
|
4059
|
+
onOpenDay
|
|
4060
|
+
}, null, 8, [
|
|
4061
|
+
"schema",
|
|
4062
|
+
"items",
|
|
4063
|
+
"anchor-field",
|
|
4064
|
+
"end-field",
|
|
4065
|
+
"time-field",
|
|
4066
|
+
"color-field",
|
|
4067
|
+
"selected"
|
|
4068
|
+
]), openDay.value ? ((0, vue.openBlock)(), (0, vue.createBlock)(CollectionDayView_default, {
|
|
4069
|
+
key: 0,
|
|
4070
|
+
schema: collection.value.schema,
|
|
4071
|
+
items: filteredItems.value,
|
|
4072
|
+
day: openDay.value,
|
|
4073
|
+
"anchor-field": calendarAnchorField.value,
|
|
4074
|
+
"end-field": calendarEndField.value,
|
|
4075
|
+
"time-field": calendarTimeField.value,
|
|
4076
|
+
"color-field": hasKanban.value ? kanbanGroupField.value : "",
|
|
4077
|
+
selected: viewing.value ? String(viewing.value[collection.value.schema.primaryKey] ?? "") : void 0,
|
|
4078
|
+
"can-create": canCreate.value,
|
|
4079
|
+
"show-detail": Boolean(viewing.value || editing.value),
|
|
4080
|
+
onSelect: onCalendarSelect,
|
|
4081
|
+
onCreateOn: createOnDate,
|
|
4082
|
+
onClose: onDayClose
|
|
4083
|
+
}, {
|
|
4084
|
+
detail: (0, vue.withCtx)(() => [(0, vue.createVNode)(CollectionRecordPanel_default, {
|
|
4085
|
+
editing: editing.value,
|
|
4086
|
+
"onUpdate:editing": _cache[8] || (_cache[8] = ($event) => editing.value = $event),
|
|
4087
|
+
collection: collection.value,
|
|
4088
|
+
viewing: viewing.value,
|
|
4089
|
+
saving: saving.value,
|
|
4090
|
+
"save-error": saveError.value,
|
|
4091
|
+
"action-error": actionError.value,
|
|
4092
|
+
"action-pending": actionPending.value,
|
|
4093
|
+
"visible-actions": visibleActions.value,
|
|
4094
|
+
"live-record": liveRecord.value,
|
|
4095
|
+
"live-derived": liveDerived.value,
|
|
4096
|
+
"view-title": viewTitle.value,
|
|
4097
|
+
"is-singleton": isSingleton.value,
|
|
4098
|
+
render: (0, vue.unref)(render),
|
|
4099
|
+
locale: (0, vue.unref)(locale),
|
|
4100
|
+
onSubmit: saveEditor,
|
|
4101
|
+
onCancel: cancelEditor,
|
|
4102
|
+
onEdit: editFromView,
|
|
4103
|
+
onClose: onDayClose,
|
|
4104
|
+
onDelete: _cache[9] || (_cache[9] = ($event) => viewing.value && confirmDelete(viewing.value)),
|
|
4105
|
+
onRunAction: runAction
|
|
4106
|
+
}, null, 8, [
|
|
4107
|
+
"editing",
|
|
4108
|
+
"collection",
|
|
4109
|
+
"viewing",
|
|
4110
|
+
"saving",
|
|
4111
|
+
"save-error",
|
|
4112
|
+
"action-error",
|
|
4113
|
+
"action-pending",
|
|
4114
|
+
"visible-actions",
|
|
4115
|
+
"live-record",
|
|
4116
|
+
"live-derived",
|
|
4117
|
+
"view-title",
|
|
4118
|
+
"is-singleton",
|
|
4119
|
+
"render",
|
|
4120
|
+
"locale"
|
|
4121
|
+
])]),
|
|
4122
|
+
_: 1
|
|
4123
|
+
}, 8, [
|
|
4124
|
+
"schema",
|
|
4125
|
+
"items",
|
|
4126
|
+
"day",
|
|
4127
|
+
"anchor-field",
|
|
4128
|
+
"end-field",
|
|
4129
|
+
"time-field",
|
|
4130
|
+
"color-field",
|
|
4131
|
+
"selected",
|
|
4132
|
+
"can-create",
|
|
4133
|
+
"show-detail"
|
|
4134
|
+
])) : (0, vue.createCommentVNode)("", true)])) : kanbanActive.value ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("div", _hoisted_40, [inlineError.value ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("div", _hoisted_41, [
|
|
4135
|
+
_cache[42] || (_cache[42] = (0, vue.createElementVNode)("span", { class: "material-icons text-red-600" }, "error", -1)),
|
|
4136
|
+
(0, vue.createElementVNode)("span", _hoisted_42, (0, vue.toDisplayString)((0, vue.unref)(t)("collectionsView.inlineSaveFailed", { error: inlineError.value })), 1),
|
|
4137
|
+
(0, vue.createElementVNode)("button", {
|
|
4138
|
+
type: "button",
|
|
4139
|
+
class: "h-8 w-8 flex items-center justify-center rounded text-red-600 hover:bg-red-100",
|
|
4140
|
+
"aria-label": (0, vue.unref)(t)("common.close"),
|
|
4141
|
+
onClick: _cache[10] || (_cache[10] = ($event) => inlineError.value = null)
|
|
4142
|
+
}, [..._cache[41] || (_cache[41] = [(0, vue.createElementVNode)("span", { class: "material-icons text-base" }, "close", -1)])], 8, _hoisted_43)
|
|
4143
|
+
])) : (0, vue.createCommentVNode)("", true), (0, vue.createElementVNode)("div", _hoisted_44, [(0, vue.createVNode)(CollectionKanbanView_default, {
|
|
4144
|
+
schema: collection.value.schema,
|
|
4145
|
+
items: filteredItems.value,
|
|
4146
|
+
"group-field": kanbanGroupField.value,
|
|
4147
|
+
selected: viewing.value ? String(viewing.value[collection.value.schema.primaryKey] ?? "") : void 0,
|
|
4148
|
+
notified: notifiedSeverities.value,
|
|
4149
|
+
onSelect: onCalendarSelect,
|
|
4150
|
+
onMove: onKanbanMove
|
|
4151
|
+
}, null, 8, [
|
|
4152
|
+
"schema",
|
|
4153
|
+
"items",
|
|
4154
|
+
"group-field",
|
|
4155
|
+
"selected",
|
|
4156
|
+
"notified"
|
|
4157
|
+
])])])) : activeCustomView.value ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("div", _hoisted_45, [(0, vue.createVNode)(CollectionCustomView_default, {
|
|
4158
|
+
slug: collection.value.slug,
|
|
4159
|
+
view: activeCustomView.value
|
|
4160
|
+
}, null, 8, ["slug", "view"])])) : items.value.length === 0 && editing.value?.mode !== "create" ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("div", _hoisted_46, [_cache[43] || (_cache[43] = (0, vue.createElementVNode)("span", { class: "material-icons text-4xl text-slate-300" }, "folder_open", -1)), (0, vue.createElementVNode)("p", _hoisted_47, (0, vue.toDisplayString)((0, vue.unref)(t)("collectionsView.itemsEmpty")), 1)])) : filteredItems.value.length === 0 && editing.value?.mode !== "create" ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("div", _hoisted_48, [
|
|
4161
|
+
_cache[44] || (_cache[44] = (0, vue.createElementVNode)("span", { class: "material-icons text-4xl text-slate-300" }, "search_off", -1)),
|
|
4162
|
+
(0, vue.createElementVNode)("p", _hoisted_49, (0, vue.toDisplayString)((0, vue.unref)(t)("collectionsView.noMatchingItems")), 1),
|
|
4163
|
+
(0, vue.createElementVNode)("button", {
|
|
4164
|
+
type: "button",
|
|
4165
|
+
class: "text-xs text-indigo-600 font-semibold hover:underline",
|
|
4166
|
+
onClick: _cache[11] || (_cache[11] = ($event) => searchQuery.value = "")
|
|
4167
|
+
}, (0, vue.toDisplayString)((0, vue.unref)(t)("collectionsView.clearSearch")), 1)
|
|
4168
|
+
])) : ((0, vue.openBlock)(), (0, vue.createElementBlock)("div", _hoisted_50, [inlineError.value ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("div", _hoisted_51, [
|
|
4169
|
+
_cache[46] || (_cache[46] = (0, vue.createElementVNode)("span", { class: "material-icons text-red-600" }, "error", -1)),
|
|
4170
|
+
(0, vue.createElementVNode)("span", _hoisted_52, (0, vue.toDisplayString)((0, vue.unref)(t)("collectionsView.inlineSaveFailed", { error: inlineError.value })), 1),
|
|
4171
|
+
(0, vue.createElementVNode)("button", {
|
|
4172
|
+
type: "button",
|
|
4173
|
+
class: "h-8 w-8 flex items-center justify-center rounded text-red-600 hover:bg-red-100",
|
|
4174
|
+
"aria-label": (0, vue.unref)(t)("common.close"),
|
|
4175
|
+
onClick: _cache[12] || (_cache[12] = ($event) => inlineError.value = null)
|
|
4176
|
+
}, [..._cache[45] || (_cache[45] = [(0, vue.createElementVNode)("span", { class: "material-icons text-base" }, "close", -1)])], 8, _hoisted_53)
|
|
4177
|
+
])) : (0, vue.createCommentVNode)("", true), (0, vue.createElementVNode)("table", _hoisted_54, [(0, vue.createElementVNode)("thead", null, [(0, vue.createElementVNode)("tr", _hoisted_55, [((0, vue.openBlock)(true), (0, vue.createElementBlock)(vue.Fragment, null, (0, vue.renderList)(listColumnFields.value, ([key, field]) => {
|
|
4178
|
+
return (0, vue.openBlock)(), (0, vue.createElementBlock)("th", {
|
|
4179
|
+
key,
|
|
4180
|
+
"aria-sort": (0, vue.unref)(require_promptSafety.isSortableField)(field) ? sortAriaValue(key) : void 0,
|
|
4181
|
+
class: "px-5 py-3 font-bold text-slate-500 text-left uppercase tracking-wider whitespace-nowrap"
|
|
4182
|
+
}, [(0, vue.createElementVNode)("div", _hoisted_57, [(0, vue.createElementVNode)("span", {
|
|
4183
|
+
class: "truncate max-w-[14rem]",
|
|
4184
|
+
title: field.label
|
|
4185
|
+
}, (0, vue.toDisplayString)(field.label), 9, _hoisted_58), (0, vue.unref)(require_promptSafety.isSortableField)(field) ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("button", {
|
|
4186
|
+
key: 0,
|
|
4187
|
+
type: "button",
|
|
4188
|
+
class: (0, vue.normalizeClass)(["inline-flex items-center justify-center rounded p-0.5 -my-1 leading-none transition-colors", sortButtonClass(key)]),
|
|
4189
|
+
"data-testid": `collections-sort-${key}`,
|
|
4190
|
+
"aria-label": (0, vue.unref)(t)("collectionsView.sortBy", { field: field.label }),
|
|
4191
|
+
onClick: (0, vue.withModifiers)(($event) => cycleSort(key), ["stop"]),
|
|
4192
|
+
onPointerenter: ($event) => hoveredSortKey.value = key,
|
|
4193
|
+
onPointerleave: _cache[13] || (_cache[13] = ($event) => hoveredSortKey.value = null)
|
|
4194
|
+
}, [(0, vue.createElementVNode)("span", _hoisted_60, (0, vue.toDisplayString)(sortIconName(key)), 1)], 42, _hoisted_59)) : (0, vue.createCommentVNode)("", true)])], 8, _hoisted_56);
|
|
4195
|
+
}), 128))])]), (0, vue.createElementVNode)("tbody", _hoisted_61, [((0, vue.openBlock)(true), (0, vue.createElementBlock)(vue.Fragment, null, (0, vue.renderList)(sortedItems.value, (item) => {
|
|
4196
|
+
return (0, vue.openBlock)(), (0, vue.createElementBlock)("tr", {
|
|
4197
|
+
key: String(item[collection.value.schema.primaryKey] ?? ""),
|
|
4198
|
+
class: (0, vue.normalizeClass)(["hover:bg-slate-50/70 cursor-pointer transition-colors focus:outline-none focus:bg-indigo-50/30", isRowOpen(item) || isEditingRow(item) ? "bg-indigo-50/40" : ""]),
|
|
4199
|
+
role: "button",
|
|
4200
|
+
tabindex: "0",
|
|
4201
|
+
"aria-label": (0, vue.unref)(t)("collectionsView.openItem", { id: String(item[collection.value.schema.primaryKey] ?? "") }),
|
|
4202
|
+
"data-testid": `collections-row-${item[collection.value.schema.primaryKey]}`,
|
|
4203
|
+
onClick: ($event) => openView(item),
|
|
4204
|
+
onKeydown: [(0, vue.withKeys)((0, vue.withModifiers)(($event) => openView(item), ["self"]), ["enter"]), (0, vue.withKeys)((0, vue.withModifiers)(($event) => openView(item), ["self", "prevent"]), ["space"])]
|
|
4205
|
+
}, [((0, vue.openBlock)(true), (0, vue.createElementBlock)(vue.Fragment, null, (0, vue.renderList)(listColumnFields.value, ([key, field]) => {
|
|
4206
|
+
return (0, vue.openBlock)(), (0, vue.createElementBlock)("td", {
|
|
4207
|
+
key,
|
|
4208
|
+
class: "px-5 py-2 text-slate-700 align-middle max-w-xs font-medium"
|
|
4209
|
+
}, [(0, vue.unref)(require_promptSafety.fieldVisible)(field, item) ? ((0, vue.openBlock)(), (0, vue.createElementBlock)(vue.Fragment, { key: 0 }, [field.type === "toggle" ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("input", {
|
|
4210
|
+
key: 0,
|
|
4211
|
+
type: "checkbox",
|
|
4212
|
+
checked: toggleChecked(item, field),
|
|
4213
|
+
disabled: isRowInlineSaving(item),
|
|
4214
|
+
class: "h-5 w-5 rounded border-slate-300 text-indigo-600 focus:ring-indigo-500/20 cursor-pointer align-middle disabled:opacity-50 disabled:cursor-not-allowed",
|
|
4215
|
+
"data-testid": `collections-inline-toggle-${key}-${item[collection.value.schema.primaryKey]}`,
|
|
4216
|
+
"aria-label": field.label,
|
|
4217
|
+
onClick: _cache[14] || (_cache[14] = (0, vue.withModifiers)(() => {}, ["stop"])),
|
|
4218
|
+
onChange: ($event) => commitToggle(item, field)
|
|
4219
|
+
}, null, 40, _hoisted_63)) : field.type === "boolean" ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("input", {
|
|
4220
|
+
key: 1,
|
|
4221
|
+
type: "checkbox",
|
|
4222
|
+
checked: item[key] === true,
|
|
4223
|
+
disabled: isRowInlineSaving(item),
|
|
4224
|
+
class: "h-5 w-5 rounded border-slate-300 text-indigo-600 focus:ring-indigo-500/20 cursor-pointer align-middle disabled:opacity-50 disabled:cursor-not-allowed",
|
|
4225
|
+
"data-testid": `collections-inline-bool-${key}-${item[collection.value.schema.primaryKey]}`,
|
|
4226
|
+
"aria-label": field.label,
|
|
4227
|
+
onClick: _cache[15] || (_cache[15] = (0, vue.withModifiers)(() => {}, ["stop"])),
|
|
4228
|
+
onChange: ($event) => commitInlineEdit(item, String(key), field, $event.target.checked)
|
|
4229
|
+
}, null, 40, _hoisted_64)) : field.type === "ref" && field.to && typeof item[key] === "string" && item[key] ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("span", _hoisted_65, [(0, vue.createVNode)(_component_router_link, {
|
|
4230
|
+
to: {
|
|
4231
|
+
path: `/collections/${field.to}`,
|
|
4232
|
+
query: { selected: String(item[key]) }
|
|
4233
|
+
},
|
|
4234
|
+
class: "text-indigo-600 hover:text-indigo-800 hover:underline font-semibold",
|
|
4235
|
+
"data-testid": `collections-ref-link-${key}-${item[key]}`,
|
|
4236
|
+
onClick: _cache[16] || (_cache[16] = (0, vue.withModifiers)(() => {}, ["stop"]))
|
|
4237
|
+
}, {
|
|
4238
|
+
default: (0, vue.withCtx)(() => [(0, vue.createTextVNode)((0, vue.toDisplayString)((0, vue.unref)(refDisplay)(field.to, String(item[key]))), 1)]),
|
|
4239
|
+
_: 2
|
|
4240
|
+
}, 1032, ["to", "data-testid"])])) : field.type === "enum" && Array.isArray(field.values) && field.values.length > 0 ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("select", {
|
|
4241
|
+
key: 3,
|
|
4242
|
+
value: item[key] == null ? "" : String(item[key]),
|
|
4243
|
+
disabled: isRowInlineSaving(item),
|
|
4244
|
+
class: (0, vue.normalizeClass)(["rounded-lg border px-2 py-0.5 text-[11px] font-semibold focus:border-indigo-500 focus:ring-2 focus:ring-indigo-500/20 focus:outline-none cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed", enumControlClass(String(key), item[key])]),
|
|
4245
|
+
"data-testid": `collections-inline-enum-${key}-${item[collection.value.schema.primaryKey]}`,
|
|
4246
|
+
"aria-label": field.label,
|
|
4247
|
+
onClick: _cache[17] || (_cache[17] = (0, vue.withModifiers)(() => {}, ["stop"])),
|
|
4248
|
+
onChange: ($event) => commitInlineEdit(item, String(key), field, $event.target.value)
|
|
4249
|
+
}, [showEnumPlaceholder(item, String(key)) ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("option", _hoisted_67, (0, vue.toDisplayString)((0, vue.unref)(t)("collectionsView.selectPlaceholder")), 1)) : (0, vue.createCommentVNode)("", true), ((0, vue.openBlock)(true), (0, vue.createElementBlock)(vue.Fragment, null, (0, vue.renderList)(field.values, (value) => {
|
|
4250
|
+
return (0, vue.openBlock)(), (0, vue.createElementBlock)("option", {
|
|
4251
|
+
key: value,
|
|
4252
|
+
value
|
|
4253
|
+
}, (0, vue.toDisplayString)(value), 9, _hoisted_68);
|
|
4254
|
+
}), 128))], 42, _hoisted_66)) : field.type === "money" ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("span", _hoisted_69, (0, vue.toDisplayString)((0, vue.unref)(formatMoney)(item[key], (0, vue.unref)(resolveCurrency)(field, item), (0, vue.unref)(locale))), 1)) : field.type === "table" ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("span", _hoisted_70, [_cache[47] || (_cache[47] = (0, vue.createElementVNode)("span", { class: "material-icons text-[11px]" }, "list", -1)), (0, vue.createElementVNode)("span", null, (0, vue.toDisplayString)(tableSummary(item[key])), 1)])) : field.type === "derived" ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("span", _hoisted_71, (0, vue.toDisplayString)((0, vue.unref)(derivedDisplay)(field, (0, vue.unref)(evaluateDerivedAgainstItem)(field, String(key), item), item)), 1)) : field.type !== "file" && (0, vue.unref)(isExternalUrl)(item[key]) ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("a", {
|
|
4255
|
+
key: 7,
|
|
4256
|
+
href: String(item[key]),
|
|
4257
|
+
target: "_blank",
|
|
4258
|
+
rel: "noopener noreferrer",
|
|
4259
|
+
class: "block truncate text-blue-600 hover:text-blue-800 hover:underline font-semibold",
|
|
4260
|
+
"data-testid": `collections-url-link-${key}-${item[collection.value.schema.primaryKey]}`,
|
|
4261
|
+
onClick: _cache[18] || (_cache[18] = (0, vue.withModifiers)(() => {}, ["stop"]))
|
|
4262
|
+
}, (0, vue.toDisplayString)(String(item[key])), 9, _hoisted_72)) : field.type === "file" && (0, vue.unref)(artifactUrl)(item[key]) ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("a", {
|
|
4263
|
+
key: 8,
|
|
4264
|
+
href: (0, vue.unref)(artifactUrl)(item[key]) ?? void 0,
|
|
4265
|
+
target: "_blank",
|
|
4266
|
+
rel: "noopener noreferrer",
|
|
4267
|
+
class: "block truncate text-blue-600 hover:text-blue-800 hover:underline font-semibold",
|
|
4268
|
+
"data-testid": `collections-file-link-${key}-${item[collection.value.schema.primaryKey]}`,
|
|
4269
|
+
onClick: _cache[19] || (_cache[19] = (0, vue.withModifiers)(() => {}, ["stop"]))
|
|
4270
|
+
}, (0, vue.toDisplayString)(String(item[key])), 9, _hoisted_73)) : field.type === "file" && (0, vue.unref)(fileRoutePath)(item[key]) ? ((0, vue.openBlock)(), (0, vue.createBlock)(_component_router_link, {
|
|
4271
|
+
key: 9,
|
|
4272
|
+
to: (0, vue.unref)(fileRoutePath)(item[key]) ?? "",
|
|
4273
|
+
class: "block truncate text-blue-600 hover:text-blue-800 hover:underline font-semibold",
|
|
4274
|
+
"data-testid": `collections-file-link-${key}-${item[collection.value.schema.primaryKey]}`,
|
|
4275
|
+
onClick: _cache[20] || (_cache[20] = (0, vue.withModifiers)(() => {}, ["stop"]))
|
|
4276
|
+
}, {
|
|
4277
|
+
default: (0, vue.withCtx)(() => [(0, vue.createTextVNode)((0, vue.toDisplayString)(String(item[key])), 1)]),
|
|
4278
|
+
_: 2
|
|
4279
|
+
}, 1032, ["to", "data-testid"])) : ((0, vue.openBlock)(), (0, vue.createElementBlock)("span", _hoisted_74, (0, vue.toDisplayString)((0, vue.unref)(formatCell)(item[key], field.type)), 1))], 64)) : (0, vue.createCommentVNode)("", true)]);
|
|
4280
|
+
}), 128))], 42, _hoisted_62);
|
|
4281
|
+
}), 128))])])]))]),
|
|
4282
|
+
collection.value && (viewing.value || editing.value) && !(calendarActive.value && openDay.value) ? ((0, vue.openBlock)(), (0, vue.createBlock)(CollectionRecordModal_default, {
|
|
4283
|
+
key: 2,
|
|
4284
|
+
onClose: closeRecordModal
|
|
4285
|
+
}, {
|
|
4286
|
+
default: (0, vue.withCtx)(() => [(0, vue.createVNode)(CollectionRecordPanel_default, {
|
|
4287
|
+
editing: editing.value,
|
|
4288
|
+
"onUpdate:editing": _cache[21] || (_cache[21] = ($event) => editing.value = $event),
|
|
4289
|
+
collection: collection.value,
|
|
4290
|
+
viewing: viewing.value,
|
|
4291
|
+
saving: saving.value,
|
|
4292
|
+
"save-error": saveError.value,
|
|
4293
|
+
"action-error": actionError.value,
|
|
4294
|
+
"action-pending": actionPending.value,
|
|
4295
|
+
"visible-actions": visibleActions.value,
|
|
4296
|
+
"live-record": liveRecord.value,
|
|
4297
|
+
"live-derived": liveDerived.value,
|
|
4298
|
+
"view-title": viewTitle.value,
|
|
4299
|
+
"is-singleton": isSingleton.value,
|
|
4300
|
+
render: (0, vue.unref)(render),
|
|
4301
|
+
locale: (0, vue.unref)(locale),
|
|
4302
|
+
onSubmit: saveEditor,
|
|
4303
|
+
onCancel: cancelEditor,
|
|
4304
|
+
onEdit: editFromView,
|
|
4305
|
+
onClose: closeView,
|
|
4306
|
+
onDelete: _cache[22] || (_cache[22] = ($event) => viewing.value && confirmDelete(viewing.value)),
|
|
4307
|
+
onRunAction: runAction
|
|
4308
|
+
}, null, 8, [
|
|
4309
|
+
"editing",
|
|
4310
|
+
"collection",
|
|
4311
|
+
"viewing",
|
|
4312
|
+
"saving",
|
|
4313
|
+
"save-error",
|
|
4314
|
+
"action-error",
|
|
4315
|
+
"action-pending",
|
|
4316
|
+
"visible-actions",
|
|
4317
|
+
"live-record",
|
|
4318
|
+
"live-derived",
|
|
4319
|
+
"view-title",
|
|
4320
|
+
"is-singleton",
|
|
4321
|
+
"render",
|
|
4322
|
+
"locale"
|
|
4323
|
+
])]),
|
|
4324
|
+
_: 1
|
|
4325
|
+
})) : (0, vue.createCommentVNode)("", true),
|
|
4326
|
+
configOpen.value && collection.value ? ((0, vue.openBlock)(), (0, vue.createBlock)(CollectionViewConfigModal_default, {
|
|
4327
|
+
key: 3,
|
|
4328
|
+
slug: collection.value.slug,
|
|
4329
|
+
title: collection.value.title,
|
|
4330
|
+
views: customViews.value,
|
|
4331
|
+
onChanged: onViewsChanged,
|
|
4332
|
+
onClose: _cache[23] || (_cache[23] = ($event) => configOpen.value = false)
|
|
4333
|
+
}, null, 8, [
|
|
4334
|
+
"slug",
|
|
4335
|
+
"title",
|
|
4336
|
+
"views"
|
|
4337
|
+
])) : (0, vue.createCommentVNode)("", true),
|
|
4338
|
+
chatOpen.value && collection.value ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("div", {
|
|
4339
|
+
key: 4,
|
|
4340
|
+
class: "fixed inset-0 z-30 flex items-center justify-center bg-slate-900/60 backdrop-blur-sm p-4 transition-all duration-300",
|
|
4341
|
+
role: "dialog",
|
|
4342
|
+
"aria-modal": "true",
|
|
4343
|
+
"aria-labelledby": "collections-chat-title",
|
|
4344
|
+
"data-testid": "collections-chat-modal",
|
|
4345
|
+
onClick: (0, vue.withModifiers)(closeChat, ["self"]),
|
|
4346
|
+
onKeydown: (0, vue.withKeys)(closeChat, ["esc"])
|
|
4347
|
+
}, [(0, vue.createElementVNode)("div", _hoisted_75, [
|
|
4348
|
+
(0, vue.createElementVNode)("header", _hoisted_76, [
|
|
4349
|
+
_cache[49] || (_cache[49] = (0, vue.createElementVNode)("div", { class: "h-9 w-9 flex items-center justify-center rounded-xl bg-indigo-50 text-indigo-600 border border-indigo-100/50" }, [(0, vue.createElementVNode)("span", { class: "material-icons text-lg" }, "forum")], -1)),
|
|
4350
|
+
(0, vue.createElementVNode)("div", _hoisted_77, [(0, vue.createElementVNode)("h2", _hoisted_78, (0, vue.toDisplayString)((0, vue.unref)(t)("collectionsView.chatTitle")), 1), (0, vue.createElementVNode)("span", _hoisted_79, (0, vue.toDisplayString)(collection.value.title), 1)]),
|
|
4351
|
+
(0, vue.createElementVNode)("button", {
|
|
4352
|
+
type: "button",
|
|
4353
|
+
class: "h-8 w-8 flex items-center justify-center rounded text-slate-400 hover:bg-slate-200/50 hover:text-slate-600 transition-colors",
|
|
4354
|
+
"aria-label": (0, vue.unref)(t)("common.close"),
|
|
4355
|
+
"data-testid": "collections-chat-close",
|
|
4356
|
+
onClick: closeChat
|
|
4357
|
+
}, [..._cache[48] || (_cache[48] = [(0, vue.createElementVNode)("span", { class: "material-icons text-lg" }, "close", -1)])], 8, _hoisted_80)
|
|
4358
|
+
]),
|
|
4359
|
+
(0, vue.createElementVNode)("div", _hoisted_81, [(0, vue.withDirectives)((0, vue.createElementVNode)("textarea", {
|
|
4360
|
+
ref_key: "chatInputEl",
|
|
4361
|
+
ref: chatInputEl,
|
|
4362
|
+
"onUpdate:modelValue": _cache[24] || (_cache[24] = ($event) => chatMessage.value = $event),
|
|
4363
|
+
rows: "4",
|
|
4364
|
+
placeholder: (0, vue.unref)(t)("collectionsView.chatPlaceholder"),
|
|
4365
|
+
class: "w-full bg-slate-50 border border-slate-200/80 rounded-xl px-3 py-2.5 text-sm placeholder-slate-400 focus:outline-none focus:ring-2 focus:ring-indigo-500/20 focus:border-indigo-500 focus:bg-white transition-all resize-none",
|
|
4366
|
+
"data-testid": "collections-chat-input",
|
|
4367
|
+
onKeydown: [(0, vue.withKeys)((0, vue.withModifiers)(submitChat, ["meta"]), ["enter"]), (0, vue.withKeys)((0, vue.withModifiers)(submitChat, ["ctrl"]), ["enter"])]
|
|
4368
|
+
}, null, 40, _hoisted_82), [[vue.vModelText, chatMessage.value]])]),
|
|
4369
|
+
(0, vue.createElementVNode)("footer", _hoisted_83, [(0, vue.createElementVNode)("button", {
|
|
4370
|
+
type: "button",
|
|
4371
|
+
class: "h-8 px-2.5 rounded text-xs font-bold text-slate-500 hover:bg-slate-200/50 transition-colors",
|
|
4372
|
+
"data-testid": "collections-chat-cancel",
|
|
4373
|
+
onClick: closeChat
|
|
4374
|
+
}, (0, vue.toDisplayString)((0, vue.unref)(t)("common.cancel")), 1), (0, vue.createElementVNode)("button", {
|
|
4375
|
+
type: "button",
|
|
4376
|
+
class: "h-8 px-2.5 rounded bg-indigo-600 text-white font-bold text-xs hover:bg-indigo-700 disabled:opacity-50 transition-all shadow-sm shadow-indigo-600/10",
|
|
4377
|
+
disabled: !chatMessage.value.trim(),
|
|
4378
|
+
"data-testid": "collections-chat-send",
|
|
4379
|
+
onClick: submitChat
|
|
4380
|
+
}, (0, vue.toDisplayString)((0, vue.unref)(t)("collectionsView.chatStart")), 9, _hoisted_84)])
|
|
4381
|
+
])], 32)) : (0, vue.createCommentVNode)("", true)
|
|
4382
|
+
]);
|
|
4383
|
+
};
|
|
4384
|
+
}
|
|
4385
|
+
});
|
|
4386
|
+
//#endregion
|
|
4387
|
+
//#region src/vue/components/CollectionsIndexView.vue?vue&type=script&setup=true&lang.ts
|
|
4388
|
+
var _hoisted_1$1 = {
|
|
4389
|
+
class: "h-full overflow-y-auto bg-slate-50/50 px-6 py-6",
|
|
4390
|
+
"data-testid": "collections-view-root"
|
|
4391
|
+
};
|
|
4392
|
+
var _hoisted_2$1 = { class: "max-w-4xl mx-auto" };
|
|
4393
|
+
var _hoisted_3$1 = { class: "flex items-center justify-between mb-6" };
|
|
4394
|
+
var _hoisted_4$1 = { class: "text-xl font-semibold text-slate-800" };
|
|
4395
|
+
var _hoisted_5$1 = {
|
|
4396
|
+
key: 0,
|
|
4397
|
+
class: "flex flex-col items-center justify-center py-20 text-sm text-slate-500 gap-3"
|
|
4398
|
+
};
|
|
4399
|
+
var _hoisted_6$1 = {
|
|
4400
|
+
key: 1,
|
|
4401
|
+
class: "rounded-xl border border-red-200 bg-red-50/50 p-4 text-sm text-red-800 shadow-sm flex items-center gap-3"
|
|
4402
|
+
};
|
|
4403
|
+
var _hoisted_7$1 = {
|
|
4404
|
+
key: 2,
|
|
4405
|
+
class: "rounded-xl border border-slate-200 bg-white px-6 py-12 text-center text-sm text-slate-500 shadow-sm"
|
|
4406
|
+
};
|
|
4407
|
+
var _hoisted_8$1 = { class: "font-medium text-slate-700" };
|
|
4408
|
+
var _hoisted_9$1 = {
|
|
4409
|
+
key: 3,
|
|
4410
|
+
class: "grid gap-4 sm:grid-cols-2"
|
|
4411
|
+
};
|
|
4412
|
+
var _hoisted_10$1 = [
|
|
4413
|
+
"aria-label",
|
|
4414
|
+
"data-testid",
|
|
4415
|
+
"onClick",
|
|
4416
|
+
"onKeydown"
|
|
4417
|
+
];
|
|
4418
|
+
var _hoisted_11$1 = { class: "material-symbols-outlined text-2xl" };
|
|
4419
|
+
var _hoisted_12$1 = { class: "flex-1 min-w-0" };
|
|
4420
|
+
var _hoisted_13$1 = { class: "block font-semibold text-slate-800 text-[15px] group-hover:text-indigo-950 transition-colors truncate" };
|
|
4421
|
+
var _hoisted_14$1 = { class: "block text-[10px] text-slate-400 mt-1 tracking-wider font-semibold uppercase flex items-center gap-1.5" };
|
|
4422
|
+
var _hoisted_15$1 = { class: "text-[10px] bg-slate-100 px-1 rounded lowercase text-slate-500 font-mono font-normal" };
|
|
4423
|
+
//#endregion
|
|
4424
|
+
//#region src/vue/components/CollectionsIndexView.vue
|
|
4425
|
+
var CollectionsIndexView_default = /* @__PURE__ */ (0, vue.defineComponent)({
|
|
4426
|
+
__name: "CollectionsIndexView",
|
|
4427
|
+
setup(__props) {
|
|
4428
|
+
const { t } = useCollectionI18n();
|
|
4429
|
+
const cui = collectionUi();
|
|
4430
|
+
const { pinToggle, reconcileShortcuts } = cui;
|
|
4431
|
+
const collections = (0, vue.ref)([]);
|
|
4432
|
+
const loading = (0, vue.ref)(true);
|
|
4433
|
+
const loadError = (0, vue.ref)(null);
|
|
4434
|
+
async function loadCollections() {
|
|
4435
|
+
loading.value = true;
|
|
4436
|
+
loadError.value = null;
|
|
4437
|
+
const result = await cui.listCollections();
|
|
4438
|
+
loading.value = false;
|
|
4439
|
+
if (!result.ok) {
|
|
4440
|
+
loadError.value = result.error;
|
|
4441
|
+
return;
|
|
4442
|
+
}
|
|
4443
|
+
collections.value = result.data.collections.filter((collection) => collection.source !== "feed");
|
|
4444
|
+
reconcileShortcuts("collection", collections.value.map((collection) => ({
|
|
4445
|
+
slug: collection.slug,
|
|
4446
|
+
title: collection.title,
|
|
4447
|
+
icon: collection.icon
|
|
4448
|
+
})));
|
|
4449
|
+
}
|
|
4450
|
+
function openCollection(slug) {
|
|
4451
|
+
cui.gotoDetail("collection", slug);
|
|
4452
|
+
}
|
|
4453
|
+
function startCreateCollectionChat() {
|
|
4454
|
+
cui.startChat(t("collectionsView.addCollectionPrompt"), cui.generalRoleId);
|
|
4455
|
+
}
|
|
4456
|
+
(0, vue.onMounted)(loadCollections);
|
|
4457
|
+
return (_ctx, _cache) => {
|
|
4458
|
+
return (0, vue.openBlock)(), (0, vue.createElementBlock)("div", _hoisted_1$1, [(0, vue.createElementVNode)("div", _hoisted_2$1, [(0, vue.createElementVNode)("div", _hoisted_3$1, [(0, vue.createElementVNode)("h1", _hoisted_4$1, (0, vue.toDisplayString)((0, vue.unref)(t)("collectionsView.title")), 1), (0, vue.createElementVNode)("button", {
|
|
4459
|
+
type: "button",
|
|
4460
|
+
class: "h-8 px-2.5 flex items-center gap-1 rounded bg-indigo-600 hover:bg-indigo-700 text-white font-bold text-xs transition-colors shadow-sm",
|
|
4461
|
+
"data-testid": "collections-add-collection",
|
|
4462
|
+
onClick: startCreateCollectionChat
|
|
4463
|
+
}, [_cache[0] || (_cache[0] = (0, vue.createElementVNode)("span", { class: "material-icons text-sm" }, "add", -1)), (0, vue.createElementVNode)("span", null, (0, vue.toDisplayString)((0, vue.unref)(t)("collectionsView.addCollectionLabel")), 1)])]), loading.value ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("div", _hoisted_5$1, [_cache[1] || (_cache[1] = (0, vue.createElementVNode)("div", { class: "h-8 w-8 border-2 border-indigo-600/20 border-t-indigo-600 rounded-full animate-spin" }, null, -1)), (0, vue.createElementVNode)("span", null, (0, vue.toDisplayString)((0, vue.unref)(t)("common.loading")), 1)])) : loadError.value ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("div", _hoisted_6$1, [_cache[2] || (_cache[2] = (0, vue.createElementVNode)("span", { class: "material-icons text-red-600" }, "error", -1)), (0, vue.createElementVNode)("span", null, (0, vue.toDisplayString)((0, vue.unref)(t)("collectionsView.loadFailed")) + ": " + (0, vue.toDisplayString)(loadError.value), 1)])) : collections.value.length === 0 ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("div", _hoisted_7$1, [_cache[3] || (_cache[3] = (0, vue.createElementVNode)("span", { class: "material-icons text-4xl text-slate-300 mb-2" }, "dashboard_customize", -1)), (0, vue.createElementVNode)("p", _hoisted_8$1, (0, vue.toDisplayString)((0, vue.unref)(t)("collectionsView.indexEmpty")), 1)])) : ((0, vue.openBlock)(), (0, vue.createElementBlock)("div", _hoisted_9$1, [((0, vue.openBlock)(true), (0, vue.createElementBlock)(vue.Fragment, null, (0, vue.renderList)(collections.value, (collection) => {
|
|
4464
|
+
return (0, vue.openBlock)(), (0, vue.createElementBlock)("div", {
|
|
4465
|
+
key: collection.slug,
|
|
4466
|
+
class: "group relative rounded-xl border border-slate-200 bg-white p-5 shadow-sm hover:shadow-md hover:border-indigo-300 transition-all duration-300 cursor-pointer flex items-center gap-4 focus:outline-none focus:ring-2 focus:ring-indigo-500/20",
|
|
4467
|
+
role: "button",
|
|
4468
|
+
tabindex: "0",
|
|
4469
|
+
"aria-label": (0, vue.unref)(t)("collectionsView.openCollection", { title: collection.title }),
|
|
4470
|
+
"data-testid": `collections-index-card-${collection.slug}`,
|
|
4471
|
+
onClick: ($event) => openCollection(collection.slug),
|
|
4472
|
+
onKeydown: [(0, vue.withKeys)((0, vue.withModifiers)(($event) => openCollection(collection.slug), ["self"]), ["enter"]), (0, vue.withKeys)((0, vue.withModifiers)(($event) => openCollection(collection.slug), ["self", "prevent"]), ["space"])]
|
|
4473
|
+
}, [
|
|
4474
|
+
(0, vue.createElementVNode)("div", { class: (0, vue.normalizeClass)(["absolute left-0 top-0 bottom-0 w-1 rounded-l-xl transition-all duration-300 group-hover:w-1.5", collection.source === "project" ? "bg-indigo-600" : "bg-violet-600"]) }, null, 2),
|
|
4475
|
+
(0, vue.createElementVNode)("div", { class: (0, vue.normalizeClass)(["h-12 w-12 flex items-center justify-center rounded-xl transition-all duration-300 group-hover:scale-105 shadow-sm", collection.source === "project" ? "bg-indigo-50 text-indigo-600 group-hover:bg-indigo-100/80 border border-indigo-100/50" : "bg-violet-50 text-violet-600 group-hover:bg-violet-100/80 border border-violet-100/50"]) }, [(0, vue.createElementVNode)("span", _hoisted_11$1, (0, vue.toDisplayString)(collection.icon), 1)], 2),
|
|
4476
|
+
(0, vue.createElementVNode)("div", _hoisted_12$1, [(0, vue.createElementVNode)("span", _hoisted_13$1, (0, vue.toDisplayString)(collection.title), 1), (0, vue.createElementVNode)("span", _hoisted_14$1, [
|
|
4477
|
+
(0, vue.createElementVNode)("span", { class: (0, vue.normalizeClass)(["h-1.5 w-1.5 rounded-full", collection.source === "project" ? "bg-indigo-500" : "bg-violet-500"]) }, null, 2),
|
|
4478
|
+
(0, vue.createTextVNode)(" " + (0, vue.toDisplayString)((0, vue.unref)(t)(`collectionsView.source.${collection.source}`)) + " · ", 1),
|
|
4479
|
+
(0, vue.createElementVNode)("code", _hoisted_15$1, (0, vue.toDisplayString)(collection.slug), 1)
|
|
4480
|
+
])]),
|
|
4481
|
+
((0, vue.openBlock)(), (0, vue.createBlock)((0, vue.resolveDynamicComponent)((0, vue.unref)(pinToggle)), {
|
|
4482
|
+
kind: "collection",
|
|
4483
|
+
slug: collection.slug,
|
|
4484
|
+
title: collection.title,
|
|
4485
|
+
icon: collection.icon
|
|
4486
|
+
}, null, 8, [
|
|
4487
|
+
"slug",
|
|
4488
|
+
"title",
|
|
4489
|
+
"icon"
|
|
4490
|
+
])),
|
|
4491
|
+
_cache[4] || (_cache[4] = (0, vue.createElementVNode)("div", { class: "h-8 w-8 flex items-center justify-center rounded-lg bg-slate-50 group-hover:bg-indigo-50 text-slate-400 group-hover:text-indigo-600 transition-all duration-300" }, [(0, vue.createElementVNode)("span", { class: "material-icons text-lg transition-transform duration-300 group-hover:translate-x-0.5" }, "chevron_right")], -1))
|
|
4492
|
+
], 40, _hoisted_10$1);
|
|
4493
|
+
}), 128))]))])]);
|
|
4494
|
+
};
|
|
4495
|
+
}
|
|
4496
|
+
});
|
|
4497
|
+
//#endregion
|
|
4498
|
+
//#region src/vue/components/FeedsView.vue?vue&type=script&setup=true&lang.ts
|
|
4499
|
+
var _hoisted_1 = {
|
|
4500
|
+
class: "h-full overflow-y-auto bg-slate-50/50 px-6 py-6",
|
|
4501
|
+
"data-testid": "feeds-view-root"
|
|
4502
|
+
};
|
|
4503
|
+
var _hoisted_2 = { class: "max-w-4xl mx-auto" };
|
|
4504
|
+
var _hoisted_3 = { class: "flex items-center justify-between mb-6" };
|
|
4505
|
+
var _hoisted_4 = { class: "text-xl font-semibold text-slate-800" };
|
|
4506
|
+
var _hoisted_5 = {
|
|
4507
|
+
key: 0,
|
|
4508
|
+
class: "mb-4 flex items-center gap-2 rounded-xl border border-red-200 bg-red-50/50 p-3 text-sm text-red-800 shadow-sm",
|
|
4509
|
+
"data-testid": "feeds-refresh-error"
|
|
4510
|
+
};
|
|
4511
|
+
var _hoisted_6 = {
|
|
4512
|
+
key: 1,
|
|
4513
|
+
class: "flex flex-col items-center justify-center py-20 text-sm text-slate-500 gap-3"
|
|
4514
|
+
};
|
|
4515
|
+
var _hoisted_7 = {
|
|
4516
|
+
key: 2,
|
|
4517
|
+
class: "rounded-xl border border-red-200 bg-red-50/50 p-4 text-sm text-red-800 shadow-sm flex items-center gap-3"
|
|
4518
|
+
};
|
|
4519
|
+
var _hoisted_8 = {
|
|
4520
|
+
key: 3,
|
|
4521
|
+
class: "rounded-xl border border-slate-200 bg-white px-6 py-12 text-center text-sm text-slate-500 shadow-sm"
|
|
4522
|
+
};
|
|
4523
|
+
var _hoisted_9 = { class: "font-medium text-slate-700" };
|
|
4524
|
+
var _hoisted_10 = {
|
|
4525
|
+
key: 4,
|
|
4526
|
+
class: "grid gap-4 sm:grid-cols-2"
|
|
4527
|
+
};
|
|
4528
|
+
var _hoisted_11 = [
|
|
4529
|
+
"data-testid",
|
|
4530
|
+
"onClick",
|
|
4531
|
+
"onKeydown"
|
|
4532
|
+
];
|
|
4533
|
+
var _hoisted_12 = { class: "h-12 w-12 flex items-center justify-center rounded-xl bg-indigo-50 text-indigo-600 border border-indigo-100/50" };
|
|
4534
|
+
var _hoisted_13 = { class: "material-symbols-outlined text-2xl" };
|
|
4535
|
+
var _hoisted_14 = { class: "flex-1 min-w-0" };
|
|
4536
|
+
var _hoisted_15 = { class: "block font-semibold text-slate-800 text-[15px] truncate" };
|
|
4537
|
+
var _hoisted_16 = { class: "block text-[10px] text-slate-400 mt-1 tracking-wider font-semibold uppercase" };
|
|
4538
|
+
var _hoisted_17 = [
|
|
4539
|
+
"disabled",
|
|
4540
|
+
"title",
|
|
4541
|
+
"aria-label",
|
|
4542
|
+
"data-testid",
|
|
4543
|
+
"onClick"
|
|
4544
|
+
];
|
|
4545
|
+
var _hoisted_18 = { class: "material-icons text-lg" };
|
|
4546
|
+
var _hoisted_19 = { class: "w-full max-w-md rounded-xl bg-white p-5 shadow-xl" };
|
|
4547
|
+
var _hoisted_20 = { class: "text-sm font-semibold text-slate-800 mb-1" };
|
|
4548
|
+
var _hoisted_21 = { class: "text-xs text-slate-500 mb-3" };
|
|
4549
|
+
var _hoisted_22 = { class: "mt-4 flex justify-end gap-2" };
|
|
4550
|
+
var _hoisted_23 = ["disabled"];
|
|
4551
|
+
//#endregion
|
|
4552
|
+
//#region src/vue/components/FeedsView.vue
|
|
4553
|
+
var FeedsView_default = /* @__PURE__ */ (0, vue.defineComponent)({
|
|
4554
|
+
__name: "FeedsView",
|
|
4555
|
+
setup(__props) {
|
|
4556
|
+
const { t } = useCollectionI18n();
|
|
4557
|
+
const cui = collectionUi();
|
|
4558
|
+
const { pinToggle, reconcileShortcuts } = cui;
|
|
4559
|
+
const feeds = (0, vue.ref)([]);
|
|
4560
|
+
const loading = (0, vue.ref)(true);
|
|
4561
|
+
const loadError = (0, vue.ref)(null);
|
|
4562
|
+
const refreshingSlug = (0, vue.ref)(null);
|
|
4563
|
+
/** Non-destructive banner for a per-feed Refresh that failed (the
|
|
4564
|
+
* endpoint reports retriever errors via `errors` even on HTTP 200). */
|
|
4565
|
+
const refreshError = (0, vue.ref)(null);
|
|
4566
|
+
const addOpen = (0, vue.ref)(false);
|
|
4567
|
+
const addUrl = (0, vue.ref)("");
|
|
4568
|
+
const addInputEl = (0, vue.ref)(null);
|
|
4569
|
+
async function load() {
|
|
4570
|
+
loading.value = true;
|
|
4571
|
+
loadError.value = null;
|
|
4572
|
+
const result = await cui.listFeeds();
|
|
4573
|
+
loading.value = false;
|
|
4574
|
+
if (!result.ok) {
|
|
4575
|
+
loadError.value = result.error;
|
|
4576
|
+
return;
|
|
4577
|
+
}
|
|
4578
|
+
feeds.value = result.data.feeds;
|
|
4579
|
+
reconcileShortcuts("feed", feeds.value.map((feed) => ({
|
|
4580
|
+
slug: feed.slug,
|
|
4581
|
+
title: feed.title,
|
|
4582
|
+
icon: feed.icon || "dynamic_feed"
|
|
4583
|
+
})));
|
|
4584
|
+
}
|
|
4585
|
+
function open(slug) {
|
|
4586
|
+
cui.gotoDetail("feed", slug);
|
|
4587
|
+
}
|
|
4588
|
+
async function refresh(slug) {
|
|
4589
|
+
refreshingSlug.value = slug;
|
|
4590
|
+
refreshError.value = null;
|
|
4591
|
+
const result = await cui.refreshCollection(slug);
|
|
4592
|
+
refreshingSlug.value = null;
|
|
4593
|
+
if (!result.ok) {
|
|
4594
|
+
refreshError.value = t("collectionsView.refreshFailed", { error: result.error });
|
|
4595
|
+
return;
|
|
4596
|
+
}
|
|
4597
|
+
await load();
|
|
4598
|
+
if (result.data.errors.length > 0) refreshError.value = t("collectionsView.refreshFailed", { error: result.data.errors.join("; ") });
|
|
4599
|
+
}
|
|
4600
|
+
function startAddFeedChat() {
|
|
4601
|
+
addUrl.value = "";
|
|
4602
|
+
addOpen.value = true;
|
|
4603
|
+
(0, vue.nextTick)(() => addInputEl.value?.focus());
|
|
4604
|
+
}
|
|
4605
|
+
function closeAdd() {
|
|
4606
|
+
addOpen.value = false;
|
|
4607
|
+
}
|
|
4608
|
+
function submitAdd() {
|
|
4609
|
+
const url = addUrl.value.trim();
|
|
4610
|
+
if (!url) return;
|
|
4611
|
+
addOpen.value = false;
|
|
4612
|
+
cui.startChat(t("collectionsView.addFeedPrompt", { url }), cui.personalRoleId);
|
|
4613
|
+
}
|
|
4614
|
+
function formatTime(iso) {
|
|
4615
|
+
const date = new Date(iso);
|
|
4616
|
+
return Number.isNaN(date.getTime()) ? iso : date.toLocaleString();
|
|
4617
|
+
}
|
|
4618
|
+
(0, vue.onMounted)(load);
|
|
4619
|
+
return (_ctx, _cache) => {
|
|
4620
|
+
return (0, vue.openBlock)(), (0, vue.createElementBlock)("div", _hoisted_1, [(0, vue.createElementVNode)("div", _hoisted_2, [
|
|
4621
|
+
(0, vue.createElementVNode)("div", _hoisted_3, [(0, vue.createElementVNode)("h1", _hoisted_4, (0, vue.toDisplayString)((0, vue.unref)(t)("collectionsView.feedsTitle")), 1), (0, vue.createElementVNode)("button", {
|
|
4622
|
+
type: "button",
|
|
4623
|
+
class: "h-8 px-2.5 flex items-center gap-1 rounded bg-indigo-600 hover:bg-indigo-700 text-white font-bold text-xs transition-colors shadow-sm",
|
|
4624
|
+
"data-testid": "feeds-add",
|
|
4625
|
+
onClick: startAddFeedChat
|
|
4626
|
+
}, [_cache[1] || (_cache[1] = (0, vue.createElementVNode)("span", { class: "material-icons text-sm" }, "add", -1)), (0, vue.createElementVNode)("span", null, (0, vue.toDisplayString)((0, vue.unref)(t)("common.add")), 1)])]),
|
|
4627
|
+
refreshError.value ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("div", _hoisted_5, [_cache[2] || (_cache[2] = (0, vue.createElementVNode)("span", { class: "material-icons text-base text-red-600" }, "error", -1)), (0, vue.createElementVNode)("span", null, (0, vue.toDisplayString)(refreshError.value), 1)])) : (0, vue.createCommentVNode)("", true),
|
|
4628
|
+
loading.value ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("div", _hoisted_6, [_cache[3] || (_cache[3] = (0, vue.createElementVNode)("div", { class: "h-8 w-8 border-2 border-indigo-600/20 border-t-indigo-600 rounded-full animate-spin" }, null, -1)), (0, vue.createElementVNode)("span", null, (0, vue.toDisplayString)((0, vue.unref)(t)("common.loading")), 1)])) : loadError.value ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("div", _hoisted_7, [_cache[4] || (_cache[4] = (0, vue.createElementVNode)("span", { class: "material-icons text-red-600" }, "error", -1)), (0, vue.createElementVNode)("span", null, (0, vue.toDisplayString)((0, vue.unref)(t)("collectionsView.loadFailed")) + ": " + (0, vue.toDisplayString)(loadError.value), 1)])) : feeds.value.length === 0 ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("div", _hoisted_8, [_cache[5] || (_cache[5] = (0, vue.createElementVNode)("span", { class: "material-icons text-4xl text-slate-300 mb-2" }, "dynamic_feed", -1)), (0, vue.createElementVNode)("p", _hoisted_9, (0, vue.toDisplayString)((0, vue.unref)(t)("collectionsView.feedsEmpty")), 1)])) : ((0, vue.openBlock)(), (0, vue.createElementBlock)("div", _hoisted_10, [((0, vue.openBlock)(true), (0, vue.createElementBlock)(vue.Fragment, null, (0, vue.renderList)(feeds.value, (feed) => {
|
|
4629
|
+
return (0, vue.openBlock)(), (0, vue.createElementBlock)("div", {
|
|
4630
|
+
key: feed.slug,
|
|
4631
|
+
class: "group relative rounded-xl border border-slate-200 bg-white p-5 shadow-sm hover:shadow-md hover:border-indigo-300 transition-all duration-300 cursor-pointer flex items-center gap-4 focus:outline-none focus:ring-2 focus:ring-indigo-500/20",
|
|
4632
|
+
role: "button",
|
|
4633
|
+
tabindex: "0",
|
|
4634
|
+
"data-testid": `feeds-card-${feed.slug}`,
|
|
4635
|
+
onClick: ($event) => open(feed.slug),
|
|
4636
|
+
onKeydown: [(0, vue.withKeys)((0, vue.withModifiers)(($event) => open(feed.slug), ["self"]), ["enter"]), (0, vue.withKeys)((0, vue.withModifiers)(($event) => open(feed.slug), ["self", "prevent"]), ["space"])]
|
|
4637
|
+
}, [
|
|
4638
|
+
(0, vue.createElementVNode)("div", _hoisted_12, [(0, vue.createElementVNode)("span", _hoisted_13, (0, vue.toDisplayString)(feed.icon || "dynamic_feed"), 1)]),
|
|
4639
|
+
(0, vue.createElementVNode)("div", _hoisted_14, [(0, vue.createElementVNode)("span", _hoisted_15, (0, vue.toDisplayString)(feed.title), 1), (0, vue.createElementVNode)("span", _hoisted_16, [(0, vue.createTextVNode)((0, vue.toDisplayString)(feed.kind) + " · " + (0, vue.toDisplayString)(feed.schedule) + " ", 1), feed.lastFetchedAt ? ((0, vue.openBlock)(), (0, vue.createElementBlock)(vue.Fragment, { key: 0 }, [(0, vue.createTextVNode)(" · " + (0, vue.toDisplayString)(formatTime(feed.lastFetchedAt)), 1)], 64)) : (0, vue.createCommentVNode)("", true)])]),
|
|
4640
|
+
((0, vue.openBlock)(), (0, vue.createBlock)((0, vue.resolveDynamicComponent)((0, vue.unref)(pinToggle)), {
|
|
4641
|
+
kind: "feed",
|
|
4642
|
+
slug: feed.slug,
|
|
4643
|
+
title: feed.title,
|
|
4644
|
+
icon: feed.icon || "dynamic_feed"
|
|
4645
|
+
}, null, 8, [
|
|
4646
|
+
"slug",
|
|
4647
|
+
"title",
|
|
4648
|
+
"icon"
|
|
4649
|
+
])),
|
|
4650
|
+
(0, vue.createElementVNode)("button", {
|
|
4651
|
+
type: "button",
|
|
4652
|
+
class: "h-8 w-8 flex items-center justify-center rounded-lg bg-slate-50 hover:bg-indigo-50 text-slate-400 hover:text-indigo-600 transition-all duration-300 disabled:opacity-50",
|
|
4653
|
+
disabled: refreshingSlug.value === feed.slug,
|
|
4654
|
+
title: (0, vue.unref)(t)("collectionsView.refreshFeed"),
|
|
4655
|
+
"aria-label": (0, vue.unref)(t)("collectionsView.refreshFeed"),
|
|
4656
|
+
"data-testid": `feeds-refresh-${feed.slug}`,
|
|
4657
|
+
onClick: (0, vue.withModifiers)(($event) => refresh(feed.slug), ["stop"])
|
|
4658
|
+
}, [(0, vue.createElementVNode)("span", _hoisted_18, (0, vue.toDisplayString)(refreshingSlug.value === feed.slug ? "hourglass_empty" : "refresh"), 1)], 8, _hoisted_17)
|
|
4659
|
+
], 40, _hoisted_11);
|
|
4660
|
+
}), 128))]))
|
|
4661
|
+
]), addOpen.value ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("div", {
|
|
4662
|
+
key: 0,
|
|
4663
|
+
class: "fixed inset-0 z-40 flex items-center justify-center bg-black/30 p-4",
|
|
4664
|
+
onClick: (0, vue.withModifiers)(closeAdd, ["self"])
|
|
4665
|
+
}, [(0, vue.createElementVNode)("div", _hoisted_19, [
|
|
4666
|
+
(0, vue.createElementVNode)("h2", _hoisted_20, (0, vue.toDisplayString)((0, vue.unref)(t)("collectionsView.addFeedTitle")), 1),
|
|
4667
|
+
(0, vue.createElementVNode)("p", _hoisted_21, (0, vue.toDisplayString)((0, vue.unref)(t)("collectionsView.addFeedHint")), 1),
|
|
4668
|
+
(0, vue.withDirectives)((0, vue.createElementVNode)("input", {
|
|
4669
|
+
ref_key: "addInputEl",
|
|
4670
|
+
ref: addInputEl,
|
|
4671
|
+
"onUpdate:modelValue": _cache[0] || (_cache[0] = ($event) => addUrl.value = $event),
|
|
4672
|
+
type: "url",
|
|
4673
|
+
placeholder: "https://example.com/feed.xml",
|
|
4674
|
+
class: "w-full rounded border border-slate-200 px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-indigo-500/20 focus:border-indigo-500",
|
|
4675
|
+
"data-testid": "feeds-add-url",
|
|
4676
|
+
onKeydown: [(0, vue.withKeys)(submitAdd, ["enter"]), (0, vue.withKeys)(closeAdd, ["esc"])]
|
|
4677
|
+
}, null, 544), [[vue.vModelText, addUrl.value]]),
|
|
4678
|
+
(0, vue.createElementVNode)("div", _hoisted_22, [(0, vue.createElementVNode)("button", {
|
|
4679
|
+
type: "button",
|
|
4680
|
+
class: "h-8 px-3 rounded text-xs font-medium text-slate-600 hover:bg-slate-100",
|
|
4681
|
+
onClick: closeAdd
|
|
4682
|
+
}, (0, vue.toDisplayString)((0, vue.unref)(t)("common.cancel")), 1), (0, vue.createElementVNode)("button", {
|
|
4683
|
+
type: "button",
|
|
4684
|
+
class: "h-8 px-3 rounded bg-indigo-600 hover:bg-indigo-700 text-white text-xs font-bold disabled:opacity-50",
|
|
4685
|
+
disabled: !addUrl.value.trim(),
|
|
4686
|
+
"data-testid": "feeds-add-submit",
|
|
4687
|
+
onClick: submitAdd
|
|
4688
|
+
}, (0, vue.toDisplayString)((0, vue.unref)(t)("common.add")), 9, _hoisted_23)])
|
|
4689
|
+
])])) : (0, vue.createCommentVNode)("", true)]);
|
|
4690
|
+
};
|
|
4691
|
+
}
|
|
4692
|
+
});
|
|
4693
|
+
//#endregion
|
|
4694
|
+
exports.CollectionCalendarView = CollectionCalendarView_default;
|
|
4695
|
+
exports.CollectionCustomView = CollectionCustomView_default;
|
|
4696
|
+
exports.CollectionDayView = CollectionDayView_default;
|
|
4697
|
+
exports.CollectionEmbedView = CollectionEmbedView_default;
|
|
4698
|
+
exports.CollectionKanbanView = CollectionKanbanView_default;
|
|
4699
|
+
exports.CollectionRecordModal = CollectionRecordModal_default;
|
|
4700
|
+
exports.CollectionRecordPanel = CollectionRecordPanel_default;
|
|
4701
|
+
exports.CollectionView = CollectionView_default;
|
|
4702
|
+
exports.CollectionViewConfigModal = CollectionViewConfigModal_default;
|
|
4703
|
+
exports.CollectionsIndexView = CollectionsIndexView_default;
|
|
4704
|
+
exports.FeedsView = FeedsView_default;
|
|
4705
|
+
exports.collectionUi = collectionUi;
|
|
4706
|
+
exports.configureCollectionUi = configureCollectionUi;
|
|
4707
|
+
exports.readCollectionSort = readCollectionSort;
|
|
4708
|
+
exports.readCollectionViewMode = readCollectionViewMode;
|
|
4709
|
+
exports.useCollectionRendering = useCollectionRendering;
|
|
4710
|
+
exports.writeCollectionSort = writeCollectionSort;
|
|
4711
|
+
exports.writeCollectionViewMode = writeCollectionViewMode;
|
|
4712
|
+
|
|
4713
|
+
//# sourceMappingURL=vue.cjs.map
|