@questpie/admin 3.2.7 → 3.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.
Files changed (135) hide show
  1. package/README.md +4 -6
  2. package/dist/client/blocks/block-renderer.d.mts +2 -2
  3. package/dist/client/builder/admin-types.d.mts +3 -3
  4. package/dist/client/builder/types/action-types.d.mts +1 -1
  5. package/dist/client/builder/types/collection-types.d.mts +59 -2
  6. package/dist/client/components/blocks/block-editor-provider.mjs +13 -0
  7. package/dist/client/components/fields/array-field.mjs +105 -122
  8. package/dist/client/components/fields/asset-preview-field.mjs +1 -1
  9. package/dist/client/components/fields/blocks-field/blocks-field.mjs +1 -1
  10. package/dist/client/components/fields/boolean-field.mjs +1 -1
  11. package/dist/client/components/fields/date-field.mjs +1 -1
  12. package/dist/client/components/fields/datetime-field.mjs +1 -1
  13. package/dist/client/components/fields/email-field.mjs +1 -1
  14. package/dist/client/components/fields/field-wrapper.mjs +44 -15
  15. package/dist/client/components/fields/number-field.mjs +1 -1
  16. package/dist/client/components/fields/object-array-field.mjs +179 -149
  17. package/dist/client/components/fields/object-field.mjs +96 -87
  18. package/dist/client/components/fields/relation-picker.mjs +1 -1
  19. package/dist/client/components/fields/relation-select.mjs +1 -1
  20. package/dist/client/components/fields/rich-text-editor/index.mjs +1 -1
  21. package/dist/client/components/fields/select-field.mjs +1 -1
  22. package/dist/client/components/fields/text-field.mjs +1 -1
  23. package/dist/client/components/fields/textarea-field.mjs +1 -1
  24. package/dist/client/components/fields/time-field.mjs +1 -1
  25. package/dist/client/components/fields/upload-field.mjs +1 -1
  26. package/dist/client/components/history-sidebar.mjs +10 -4
  27. package/dist/client/components/structured-diff.mjs +367 -0
  28. package/dist/client/components/ui/sidebar.mjs +1 -1
  29. package/dist/client/hooks/use-field-options.mjs +34 -15
  30. package/dist/client/hooks/use-transition-stage.mjs +2 -2
  31. package/dist/client/modules/admin.d.mts +3 -0
  32. package/dist/client/modules/admin.mjs +3 -0
  33. package/dist/client/preview/block-scope-context.d.mts +2 -2
  34. package/dist/client/preview/preview-banner.d.mts +2 -2
  35. package/dist/client/preview/preview-field.d.mts +4 -4
  36. package/dist/client/utils/auto-expand-fields.mjs +1 -1
  37. package/dist/client/views/collection/auto-form-fields.mjs +23 -19
  38. package/dist/client/views/collection/cells/complex-cells.mjs +1 -1
  39. package/dist/client/views/collection/columns/build-columns.mjs +1 -1
  40. package/dist/client/views/collection/columns/column-defaults.mjs +17 -4
  41. package/dist/client/views/collection/field-renderer.mjs +19 -7
  42. package/dist/client/views/collection/form-view.mjs +10 -6
  43. package/dist/client/views/collection/list-view.mjs +830 -0
  44. package/dist/client/views/collection/outline.mjs +363 -0
  45. package/dist/client/views/collection/table-view.mjs +25 -16
  46. package/dist/client/views/globals/global-form-view.mjs +47 -27
  47. package/dist/client/views/layout/admin-layout.d.mts +15 -1
  48. package/dist/client/views/layout/admin-layout.mjs +95 -31
  49. package/dist/client/views/layout/admin-sidebar.mjs +2 -2
  50. package/dist/client.d.mts +6 -6
  51. package/dist/client.mjs +1 -1
  52. package/dist/components/rich-text/rich-text-renderer.d.mts +2 -2
  53. package/dist/factories.d.mts +19 -0
  54. package/dist/factories.mjs +11 -0
  55. package/dist/fields.d.mts +4 -0
  56. package/dist/fields.mjs +5 -0
  57. package/dist/index.d.mts +6 -6
  58. package/dist/index.mjs +1 -1
  59. package/dist/modules/admin.d.mts +10 -0
  60. package/dist/modules/admin.mjs +9 -0
  61. package/dist/modules/audit.d.mts +5 -0
  62. package/dist/modules/audit.mjs +5 -0
  63. package/dist/server/augmentation/form-layout.d.mts +57 -2
  64. package/dist/server/augmentation/index.d.mts +3 -1
  65. package/dist/server/augmentation/shell.d.mts +48 -0
  66. package/dist/server/augmentation.d.mts +2 -1
  67. package/dist/server/codegen/admin-client-template.mjs +11 -4
  68. package/dist/server/fields/blocks.d.mts +9 -2
  69. package/dist/server/fields/blocks.mjs +1 -1
  70. package/dist/server/fields/index.d.mts +2 -2
  71. package/dist/server/fields/index.mjs +2 -2
  72. package/dist/server/fields/rich-text.d.mts +9 -2
  73. package/dist/server/fields/rich-text.mjs +1 -1
  74. package/dist/server/i18n/messages/cs.mjs +8 -0
  75. package/dist/server/i18n/messages/de.mjs +8 -0
  76. package/dist/server/i18n/messages/en.mjs +8 -0
  77. package/dist/server/i18n/messages/es.mjs +8 -0
  78. package/dist/server/i18n/messages/fr.mjs +8 -0
  79. package/dist/server/i18n/messages/pl.mjs +8 -0
  80. package/dist/server/i18n/messages/pt.mjs +8 -0
  81. package/dist/server/i18n/messages/sk.mjs +8 -0
  82. package/dist/server/modules/admin/.generated/module.d.mts +24 -19
  83. package/dist/server/modules/admin/.generated/module.mjs +5 -1
  84. package/dist/server/modules/admin/.generated/registries.d.mts +6 -4
  85. package/dist/server/modules/admin/client/.generated/module.d.mts +70 -70
  86. package/dist/server/modules/admin/client/.generated/module.mjs +3 -1
  87. package/dist/server/modules/admin/client/views/collection-form.d.mts +6 -0
  88. package/dist/server/modules/admin/client/views/collection-table.d.mts +6 -0
  89. package/dist/server/modules/admin/client/views/global-form.d.mts +6 -0
  90. package/dist/server/modules/admin/client/views/list-view.d.mts +6 -0
  91. package/dist/server/modules/admin/client/views/list-view.mjs +10 -0
  92. package/dist/server/modules/admin/collections/account.d.mts +50 -50
  93. package/dist/server/modules/admin/collections/admin-locks.d.mts +54 -54
  94. package/dist/server/modules/admin/collections/admin-preferences.d.mts +39 -39
  95. package/dist/server/modules/admin/collections/admin-saved-views.d.mts +47 -47
  96. package/dist/server/modules/admin/collections/apikey.d.mts +39 -39
  97. package/dist/server/modules/admin/collections/assets.d.mts +39 -39
  98. package/dist/server/modules/admin/collections/session.d.mts +42 -42
  99. package/dist/server/modules/admin/collections/user.d.mts +63 -63
  100. package/dist/server/modules/admin/collections/verification.d.mts +36 -36
  101. package/dist/server/modules/admin/dto/admin-config.dto.mjs +17 -0
  102. package/dist/server/modules/admin/index.d.mts +30 -31
  103. package/dist/server/modules/admin/routes/admin-config.d.mts +2 -17
  104. package/dist/server/modules/admin/routes/admin-config.mjs +21 -5
  105. package/dist/server/modules/admin/routes/execute-action.d.mts +9 -9
  106. package/dist/server/modules/admin/routes/execute-action.mjs +18 -12
  107. package/dist/server/modules/admin/routes/i18n-helpers.d.mts +4 -0
  108. package/dist/server/modules/admin/routes/locales.d.mts +2 -2
  109. package/dist/server/modules/admin/routes/preview.d.mts +24 -19
  110. package/dist/server/modules/admin/routes/preview.mjs +83 -62
  111. package/dist/server/modules/admin/routes/reactive.d.mts +9 -9
  112. package/dist/server/modules/admin/routes/route-helpers.mjs +36 -1
  113. package/dist/server/modules/admin/routes/setup.d.mts +7 -14
  114. package/dist/server/modules/admin/routes/setup.mjs +16 -3
  115. package/dist/server/modules/admin/routes/translations.d.mts +4 -4
  116. package/dist/server/modules/admin/routes/widget-data.d.mts +5 -5
  117. package/dist/server/modules/admin/views/list-view.d.mts +8 -0
  118. package/dist/server/modules/admin/views/list-view.mjs +7 -0
  119. package/dist/server/modules/admin-preferences/collections/saved-views.d.mts +41 -41
  120. package/dist/server/modules/audit/.generated/module.d.mts +6 -6
  121. package/dist/server/modules/audit/collections/audit-log.d.mts +87 -80
  122. package/dist/server/modules/audit/collections/audit-log.mjs +7 -2
  123. package/dist/server/modules/audit/config/localize-title.mjs +67 -0
  124. package/dist/server/modules/audit/index.d.mts +3 -2
  125. package/dist/server/modules/audit/jobs/audit-cleanup.d.mts +2 -2
  126. package/dist/server/modules/audit/log-audit-entry.d.mts +85 -0
  127. package/dist/server/modules/audit/log-audit-entry.mjs +125 -0
  128. package/dist/server/plugin.d.mts +1 -1
  129. package/dist/server/plugin.mjs +31 -31
  130. package/dist/server.d.mts +6 -4
  131. package/dist/server.mjs +9 -8
  132. package/dist/shared/preview-utils.d.mts +4 -4
  133. package/dist/shared/preview-utils.mjs +5 -7
  134. package/package.json +13 -3
  135. package/dist/client/hooks/use-audit-history.mjs +0 -38
@@ -0,0 +1,363 @@
1
+ //#region src/client/views/collection/outline.ts
2
+ const DEFAULT_MAX_DEPTH = 12;
3
+ const NO_VALUE = "No value";
4
+ function getId(value) {
5
+ if (typeof value === "string" || typeof value === "number") return String(value);
6
+ if (value && typeof value === "object") {
7
+ const id = value.id;
8
+ if (typeof id === "string" || typeof id === "number") return String(id);
9
+ }
10
+ return null;
11
+ }
12
+ function getPathValue(source, path) {
13
+ if (!path) return source;
14
+ const parts = path.split(".").filter(Boolean);
15
+ let current = source;
16
+ for (const part of parts) {
17
+ if (!current || typeof current !== "object") return void 0;
18
+ current = current[part];
19
+ }
20
+ return current;
21
+ }
22
+ function defaultLabelForValue(value) {
23
+ if (value === null || value === void 0 || value === "") return NO_VALUE;
24
+ if (Array.isArray(value)) return value.length ? value.map((item) => defaultLabelForValue(item)).join(", ") : NO_VALUE;
25
+ if (typeof value === "object") {
26
+ const record = value;
27
+ return String(record.title ?? record.name ?? record.label ?? record.id ?? NO_VALUE);
28
+ }
29
+ return String(value);
30
+ }
31
+ function stableGroupKey(value) {
32
+ if (value === null || value === void 0 || value === "") return "__empty__";
33
+ if (Array.isArray(value)) return value.map(stableGroupKey).join(",");
34
+ if (typeof value === "object") return getId(value) ?? JSON.stringify(value);
35
+ return String(value);
36
+ }
37
+ function shouldExpand(key, depth, outline, collapsedKeys) {
38
+ if (collapsedKeys.has(key)) return false;
39
+ const defaultExpanded = outline?.defaultExpanded ?? true;
40
+ if (defaultExpanded === true) return true;
41
+ if (defaultExpanded === false) return false;
42
+ return depth === 0;
43
+ }
44
+ function compareByOrder(a, b, order) {
45
+ if (Array.isArray(order)) {
46
+ const ai = order.indexOf(a.key);
47
+ const bi = order.indexOf(b.key);
48
+ if (ai !== -1 || bi !== -1) {
49
+ if (ai === -1) return 1;
50
+ if (bi === -1) return -1;
51
+ return ai - bi;
52
+ }
53
+ }
54
+ const result = a.label.localeCompare(b.label);
55
+ return order === "desc" ? -result : result;
56
+ }
57
+ function matchesWhere(edge, where) {
58
+ if (!where) return true;
59
+ return Object.entries(where).every(([key, expected]) => {
60
+ const actual = getPathValue(edge, key);
61
+ if (expected && typeof expected === "object" && !Array.isArray(expected)) {
62
+ if ("in" in expected) return (expected.in ?? []).map(String).includes(String(actual));
63
+ }
64
+ return String(actual) === String(expected);
65
+ });
66
+ }
67
+ function isRecord(value) {
68
+ return !!value && typeof value === "object" && !Array.isArray(value);
69
+ }
70
+ function buildFieldRows(level, docs, depth, levelIndex, ctx, scopeKey) {
71
+ const groups = /* @__PURE__ */ new Map();
72
+ for (const doc of docs) {
73
+ const value = getPathValue(doc, level.field);
74
+ const key = stableGroupKey(value);
75
+ const group = groups.get(key);
76
+ if (group) {
77
+ group.docs.push(doc);
78
+ continue;
79
+ }
80
+ groups.set(key, {
81
+ key,
82
+ label: ctx.labelForValue(value, level.labelField ?? level.field),
83
+ value,
84
+ docs: [doc]
85
+ });
86
+ }
87
+ return Array.from(groups.values()).sort((a, b) => compareByOrder(a, b, level.order)).flatMap((group) => {
88
+ const rowKey = `${scopeKey}/field:${level.field}:${group.key}`;
89
+ const expanded = shouldExpand(rowKey, depth, ctx.outline, ctx.collapsedKeys);
90
+ return [{
91
+ kind: "group",
92
+ key: rowKey,
93
+ label: group.label,
94
+ depth,
95
+ count: group.docs.length,
96
+ expandable: true,
97
+ collapsed: !expanded
98
+ }, ...expanded ? buildLevelRows(group.docs, depth + 1, levelIndex + 1, ctx, rowKey) : []];
99
+ });
100
+ }
101
+ function buildRelationFieldRows(level, docs, depth, levelIndex, ctx, scopeKey) {
102
+ const fieldPath = level.field ? `${level.relation}.${level.field}` : level.relation;
103
+ const groups = /* @__PURE__ */ new Map();
104
+ for (const doc of docs) {
105
+ const relationValue = getPathValue(doc, level.relation);
106
+ const value = level.field ? getPathValue(relationValue, level.field) : relationValue;
107
+ const key = stableGroupKey(value);
108
+ const group = groups.get(key);
109
+ if (group) {
110
+ group.docs.push(doc);
111
+ continue;
112
+ }
113
+ groups.set(key, {
114
+ key,
115
+ label: ctx.labelForValue(value, level.labelField ?? fieldPath),
116
+ docs: [doc]
117
+ });
118
+ }
119
+ return Array.from(groups.values()).sort((a, b) => compareByOrder(a, b, level.order)).flatMap((group) => {
120
+ const rowKey = `${scopeKey}/relation-field:${fieldPath}:${group.key}`;
121
+ const expanded = shouldExpand(rowKey, depth, ctx.outline, ctx.collapsedKeys);
122
+ return [{
123
+ kind: "group",
124
+ key: rowKey,
125
+ label: group.label,
126
+ depth,
127
+ count: group.docs.length,
128
+ expandable: true,
129
+ collapsed: !expanded
130
+ }, ...expanded ? buildLevelRows(group.docs, depth + 1, levelIndex + 1, ctx, rowKey) : []];
131
+ });
132
+ }
133
+ function getRepeatMaxDepth(repeat, fallback) {
134
+ if (!repeat) return 1;
135
+ if (typeof repeat === "object") return repeat.maxDepth ?? fallback;
136
+ return fallback;
137
+ }
138
+ function buildEdgeRows(level, docs, depth, levelIndex, ctx, scopeKey) {
139
+ const docsById = /* @__PURE__ */ new Map();
140
+ for (const doc of docs) {
141
+ const id = getId(doc);
142
+ if (id) docsById.set(id, doc);
143
+ }
144
+ const edgeDocs = (ctx.edgesByCollection[level.collection] ?? []).filter((edge) => matchesWhere(edge, level.where));
145
+ if (ctx.outline?.preserveMatchingBranches !== false) for (let pass = 0; pass < ctx.maxDepth; pass++) {
146
+ let changed = false;
147
+ for (const edge of edgeDocs) {
148
+ const parentValue = getPathValue(edge, level.parentField);
149
+ const childValue = getPathValue(edge, level.childField);
150
+ const parentId = getId(parentValue);
151
+ const childId = getId(childValue);
152
+ if (!parentId || !childId || !docsById.has(childId)) continue;
153
+ if (docsById.has(parentId) || !isRecord(parentValue)) continue;
154
+ docsById.set(parentId, parentValue);
155
+ changed = true;
156
+ }
157
+ if (!changed) break;
158
+ }
159
+ const outlineDocs = Array.from(docsById.values());
160
+ const childrenByParent = /* @__PURE__ */ new Map();
161
+ const childIds = /* @__PURE__ */ new Set();
162
+ const edgeSeen = /* @__PURE__ */ new Set();
163
+ for (const edge of edgeDocs) {
164
+ const parentId = getId(getPathValue(edge, level.parentField));
165
+ const childId = getId(getPathValue(edge, level.childField));
166
+ if (!parentId || !childId || parentId === childId) continue;
167
+ if (!docsById.has(childId)) continue;
168
+ const dedupeKey = `${parentId}:${childId}:${JSON.stringify(level.where ?? {})}`;
169
+ if (edgeSeen.has(dedupeKey)) continue;
170
+ edgeSeen.add(dedupeKey);
171
+ childIds.add(childId);
172
+ const children = childrenByParent.get(parentId) ?? [];
173
+ children.push({
174
+ childId,
175
+ edge
176
+ });
177
+ childrenByParent.set(parentId, children);
178
+ }
179
+ const roots = outlineDocs.filter((doc) => {
180
+ const id = getId(doc);
181
+ return !id || !childIds.has(id) || !docsById.has(id);
182
+ });
183
+ const maxRepeatDepth = getRepeatMaxDepth(level.repeat, ctx.maxDepth);
184
+ const rows = [];
185
+ const visited = /* @__PURE__ */ new Set();
186
+ function pushChildRows(parentId, nextDepth, branchDepth, ancestorIds) {
187
+ const children = childrenByParent.get(parentId) ?? [];
188
+ if (children.length === 0) return;
189
+ if (branchDepth >= maxRepeatDepth) return;
190
+ if (level.groupByEdgeField) {
191
+ const groups = /* @__PURE__ */ new Map();
192
+ for (const child of children) {
193
+ const value = getPathValue(child.edge, level.groupByEdgeField);
194
+ const key = stableGroupKey(value);
195
+ const group = groups.get(key);
196
+ if (group) group.children.push(child);
197
+ else groups.set(key, {
198
+ label: ctx.labelForValue(value, level.groupByEdgeField),
199
+ children: [child]
200
+ });
201
+ }
202
+ for (const [key, group] of groups) {
203
+ const rowKey = `${scopeKey}/edge-group:${level.collection}:${parentId}:${key}`;
204
+ const expanded = shouldExpand(rowKey, nextDepth, ctx.outline, ctx.collapsedKeys);
205
+ rows.push({
206
+ kind: "group",
207
+ key: rowKey,
208
+ label: group.label,
209
+ depth: nextDepth,
210
+ count: group.children.length,
211
+ expandable: true,
212
+ collapsed: !expanded
213
+ });
214
+ if (!expanded) continue;
215
+ for (const child of group.children) pushEdgeRecord(child.childId, nextDepth + 1, branchDepth + 1, ancestorIds);
216
+ }
217
+ return;
218
+ }
219
+ for (const child of children) pushEdgeRecord(child.childId, nextDepth, branchDepth + 1, ancestorIds);
220
+ }
221
+ function pushEdgeRecord(id, rowDepth, branchDepth, ancestorIds) {
222
+ const doc = docsById.get(id);
223
+ if (!doc || ancestorIds.has(id)) return;
224
+ const rowKey = `${scopeKey}/record:${id}`;
225
+ const hasChildren = (childrenByParent.get(id) ?? []).some((child) => docsById.has(child.childId));
226
+ const expanded = hasChildren && shouldExpand(rowKey, rowDepth, ctx.outline, ctx.collapsedKeys);
227
+ rows.push({
228
+ kind: "record",
229
+ key: rowKey,
230
+ id,
231
+ doc,
232
+ depth: rowDepth,
233
+ expandable: hasChildren,
234
+ collapsed: hasChildren ? !expanded : void 0
235
+ });
236
+ visited.add(id);
237
+ if (hasChildren && expanded) {
238
+ const nextAncestors = new Set(ancestorIds);
239
+ nextAncestors.add(id);
240
+ pushChildRows(id, rowDepth + 1, branchDepth, nextAncestors);
241
+ }
242
+ }
243
+ for (const doc of roots) {
244
+ const id = getId(doc);
245
+ if (!id) {
246
+ rows.push({
247
+ kind: "record",
248
+ key: `${scopeKey}/record:${rows.length}`,
249
+ id: String(rows.length),
250
+ doc,
251
+ depth
252
+ });
253
+ continue;
254
+ }
255
+ pushEdgeRecord(id, depth, 0, /* @__PURE__ */ new Set());
256
+ }
257
+ for (const doc of outlineDocs) {
258
+ const id = getId(doc);
259
+ if (id && !visited.has(id)) pushEdgeRecord(id, depth, 0, /* @__PURE__ */ new Set());
260
+ }
261
+ if (levelIndex + 1 >= ctx.levels.length) return rows;
262
+ return rows.flatMap((row) => row.kind === "record" ? [row, ...buildLevelRows([row.doc], row.depth + 1, levelIndex + 1, ctx, row.key)] : [row]);
263
+ }
264
+ function createPathNode(key, label) {
265
+ return {
266
+ key,
267
+ label,
268
+ children: /* @__PURE__ */ new Map(),
269
+ docs: []
270
+ };
271
+ }
272
+ function countPathNode(node) {
273
+ let count = node.docs.length;
274
+ for (const child of node.children.values()) count += countPathNode(child);
275
+ return count;
276
+ }
277
+ function buildPathRows(level, docs, depth, levelIndex, ctx, scopeKey) {
278
+ const root = createPathNode("path:root", "Root");
279
+ const separator = level.separator ?? "/";
280
+ const maxPathDepth = getRepeatMaxDepth(level.repeat, ctx.maxDepth);
281
+ for (const doc of docs) {
282
+ const segments = String(getPathValue(doc, level.field) ?? "").trim().split(separator).filter(Boolean).slice(0, maxPathDepth);
283
+ const folderSegments = level.syntheticFolders && segments.length > 0 ? segments.slice(0, Math.max(segments.length - 1, 0)) : segments;
284
+ let node = root;
285
+ let currentPath = "";
286
+ for (const segment of folderSegments) {
287
+ currentPath = currentPath ? `${currentPath}${separator}${segment}` : segment;
288
+ let child = node.children.get(segment);
289
+ if (!child) {
290
+ child = createPathNode(`${scopeKey}/path:${level.field}:${currentPath}`, segment);
291
+ node.children.set(segment, child);
292
+ }
293
+ node = child;
294
+ }
295
+ node.docs.push(doc);
296
+ }
297
+ function walk(node, rowDepth) {
298
+ const rows = [];
299
+ for (const child of Array.from(node.children.values()).sort((a, b) => a.label.localeCompare(b.label))) {
300
+ const expanded = shouldExpand(child.key, rowDepth, ctx.outline, ctx.collapsedKeys);
301
+ rows.push({
302
+ kind: "synthetic",
303
+ key: child.key,
304
+ label: child.label,
305
+ depth: rowDepth,
306
+ count: countPathNode(child),
307
+ expandable: true,
308
+ collapsed: !expanded
309
+ });
310
+ if (expanded) rows.push(...walk(child, rowDepth + 1));
311
+ }
312
+ for (const doc of node.docs) {
313
+ const id = getId(doc) ?? `${node.key}:${rows.length}`;
314
+ rows.push({
315
+ kind: "record",
316
+ key: `${scopeKey}/record:${id}`,
317
+ id,
318
+ doc,
319
+ depth: rowDepth
320
+ });
321
+ if (levelIndex + 1 < ctx.levels.length) rows.push(...buildLevelRows([doc], rowDepth + 1, levelIndex + 1, ctx, `${scopeKey}/record:${id}`));
322
+ }
323
+ return rows;
324
+ }
325
+ return walk(root, depth);
326
+ }
327
+ function buildLevelRows(docs, depth, levelIndex, ctx, scopeKey = "root") {
328
+ if (docs.length === 0) return [];
329
+ if (depth > ctx.maxDepth || levelIndex >= ctx.levels.length) return docs.map((doc, index) => {
330
+ const id = getId(doc) ?? `${depth}:${index}`;
331
+ return {
332
+ kind: "record",
333
+ key: `${scopeKey}/record:${id}`,
334
+ id,
335
+ doc,
336
+ depth
337
+ };
338
+ });
339
+ const level = ctx.levels[levelIndex];
340
+ if (level.kind === "field") return buildFieldRows(level, docs, depth, levelIndex, ctx, scopeKey);
341
+ if (level.kind === "relation-field") return buildRelationFieldRows(level, docs, depth, levelIndex, ctx, scopeKey);
342
+ if (level.kind === "edge") return buildEdgeRows(level, docs, depth, levelIndex, ctx, scopeKey);
343
+ return buildPathRows(level, docs, depth, levelIndex, ctx, scopeKey);
344
+ }
345
+ function buildOutlineRows({ docs, outline, edgesByCollection = {}, collapsedKeys, labelForValue = defaultLabelForValue, maxDepth }) {
346
+ const levels = outline?.levels?.filter(Boolean) ?? [];
347
+ const ctx = {
348
+ levels,
349
+ outline,
350
+ edgesByCollection,
351
+ collapsedKeys: new Set(collapsedKeys ?? []),
352
+ labelForValue,
353
+ maxDepth: maxDepth ?? outline?.maxDepth ?? DEFAULT_MAX_DEPTH
354
+ };
355
+ if (levels.length === 0) return buildLevelRows(docs, 0, 0, {
356
+ ...ctx,
357
+ levels: []
358
+ });
359
+ return buildLevelRows(docs, 0, 0, ctx);
360
+ }
361
+
362
+ //#endregion
363
+ export { buildOutlineRows };
@@ -7,10 +7,10 @@ import { flattenOptions } from "../../components/primitives/types.mjs";
7
7
  import { resolveOptionLabelForValue } from "../../components/primitives/option-label.mjs";
8
8
  import { Button } from "../../components/ui/button.mjs";
9
9
  import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "../../components/ui/select.mjs";
10
+ import { LocaleSwitcher } from "../../components/locale-switcher.mjs";
10
11
  import { Sheet, SheetContent, SheetDescription, SheetFooter, SheetHeader, SheetTitle } from "../../components/ui/sheet.mjs";
11
12
  import { sanitizeFilename } from "../../components/fields/field-utils.mjs";
12
13
  import { Tooltip, TooltipContent, TooltipTrigger } from "../../components/ui/tooltip.mjs";
13
- import { LocaleSwitcher } from "../../components/locale-switcher.mjs";
14
14
  import { Checkbox } from "../../components/ui/checkbox.mjs";
15
15
  import { createActionRegistryProxy } from "../../builder/types/action-registry.mjs";
16
16
  import { ActionButton } from "../../components/actions/action-button.mjs";
@@ -52,7 +52,7 @@ import { DndContext, DragOverlay, KeyboardSensor, PointerSensor, closestCenter,
52
52
  import { SortableContext, arrayMove, sortableKeyboardCoordinates, useSortable, verticalListSortingStrategy } from "@dnd-kit/sortable";
53
53
  import { CSS } from "@dnd-kit/utilities";
54
54
  import { toast } from "sonner";
55
- import { flexRender, getCoreRowModel, getSortedRowModel, useReactTable } from "@tanstack/react-table";
55
+ import { flexRender, getCoreRowModel, useReactTable } from "@tanstack/react-table";
56
56
 
57
57
  //#region src/client/views/collection/table-view.tsx
58
58
  /**
@@ -252,11 +252,14 @@ function mapListSchemaToConfig(list) {
252
252
  if (list.columns?.length) config.columns = list.columns;
253
253
  if (list.defaultSort) config.defaultSort = list.defaultSort;
254
254
  if (list.orderable) config.orderable = list.orderable;
255
- if (list.searchable?.length) {
255
+ if (Array.isArray(list.searchable) && list.searchable.length) {
256
256
  config.searchFields = list.searchable;
257
257
  config.searchable = true;
258
- }
258
+ } else if (typeof list.searchable === "boolean") config.searchable = list.searchable;
259
+ if (list.filterable?.length) config.filterable = list.filterable;
259
260
  if (list.grouping?.fields?.length) config.grouping = list.grouping;
261
+ if (list.layout) config.layout = list.layout;
262
+ if (list.outline?.levels?.length) config.outline = list.outline;
260
263
  config.actions = mapListActionsToDefinitions(list.actions);
261
264
  return config;
262
265
  }
@@ -422,15 +425,6 @@ function TableViewInner({ collection, config, viewConfig, navigate, basePath = "
422
425
  resolvedListConfig,
423
426
  collectionMeta
424
427
  ]);
425
- const expandedFields = useMemo(() => autoExpandFields({
426
- fields: resolvedFields,
427
- list: resolvedListConfig,
428
- relations: collectionMeta?.relations
429
- }), [
430
- resolvedFields,
431
- resolvedListConfig,
432
- collectionMeta?.relations
433
- ]);
434
428
  const [isSheetOpen, setIsSheetOpen] = useSidebarSearchParam("view-options", { legacyKey: "viewOptions" });
435
429
  const [searchTerm, setSearchTerm] = useState("");
436
430
  const [isSearchPanelOpen, setIsSearchPanelOpen] = useState(false);
@@ -479,6 +473,18 @@ function TableViewInner({ collection, config, viewConfig, navigate, basePath = "
479
473
  groupBy: defaultGroupBy
480
474
  }, collection, user?.id);
481
475
  const effectiveRealtime = viewState.config.realtime ?? resolvedRealtime;
476
+ const visibleColumnsForExpansion = useMemo(() => viewState.config.visibleColumns.length > 0 ? viewState.config.visibleColumns : defaultColumns, [viewState.config.visibleColumns, defaultColumns]);
477
+ const expandedFields = useMemo(() => autoExpandFields({
478
+ fields: resolvedFields,
479
+ list: resolvedListConfig,
480
+ visibleColumns: visibleColumnsForExpansion,
481
+ relations: collectionMeta?.relations
482
+ }), [
483
+ resolvedFields,
484
+ resolvedListConfig,
485
+ visibleColumnsForExpansion,
486
+ collectionMeta?.relations
487
+ ]);
482
488
  const isKnownSortField = React.useCallback((field) => !!field && (field === "_title" || !!resolvedFields?.[field]), [resolvedFields]);
483
489
  const hasOrderField = isKnownSortField(orderField);
484
490
  const canUseOrderableSort = isOrderableEnabled && hasOrderField;
@@ -489,7 +495,10 @@ function TableViewInner({ collection, config, viewConfig, navigate, basePath = "
489
495
  field: orderField,
490
496
  direction: orderDirection
491
497
  };
492
- return null;
498
+ return {
499
+ field: "createdAt",
500
+ direction: "desc"
501
+ };
493
502
  }, [
494
503
  viewState.config.sortConfig,
495
504
  resolvedListConfig?.defaultSort,
@@ -875,7 +884,7 @@ function TableViewInner({ collection, config, viewConfig, navigate, basePath = "
875
884
  data: filteredItems,
876
885
  columns: visibleColumnDefs,
877
886
  getCoreRowModel: getCoreRowModel(),
878
- getSortedRowModel: getSortedRowModel(),
887
+ manualSorting: true,
879
888
  onSortingChange: handleSortingChange,
880
889
  enableRowSelection: true,
881
890
  onRowSelectionChange: setRowSelection,
@@ -1537,4 +1546,4 @@ function TableViewInner({ collection, config, viewConfig, navigate, basePath = "
1537
1546
  }
1538
1547
 
1539
1548
  //#endregion
1540
- export { TableView as default };
1549
+ export { UploadCollectionButton, TableView as default, mapListSchemaToConfig, stringifyGroupValue };
@@ -2,11 +2,11 @@ import { useResolveText, useTranslation } from "../../i18n/hooks.mjs";
2
2
  import { useSafeContentLocales } from "../../runtime/content-locales-provider.mjs";
3
3
  import { useScopedLocale } from "../../runtime/locale-scope.mjs";
4
4
  import { Button } from "../../components/ui/button.mjs";
5
- import { Badge } from "../../components/ui/badge.mjs";
6
- import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "../../components/ui/dialog.mjs";
7
5
  import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "../../components/ui/dropdown-menu.mjs";
8
6
  import { LocaleSwitcher } from "../../components/locale-switcher.mjs";
9
7
  import { Label } from "../../components/ui/label.mjs";
8
+ import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "../../components/ui/dialog.mjs";
9
+ import { Badge } from "../../components/ui/badge.mjs";
10
10
  import { Skeleton } from "../../components/ui/skeleton.mjs";
11
11
  import { Checkbox } from "../../components/ui/checkbox.mjs";
12
12
  import { DateTimeInput } from "../../components/primitives/date-input.mjs";
@@ -23,11 +23,10 @@ import { useTransitionStage } from "../../hooks/use-transition-stage.mjs";
23
23
  import { detectManyToManyRelations, hasManyToManyRelations } from "../../utils/detect-relations.mjs";
24
24
  import { shouldHandleAdminShortcut } from "../../utils/keyboard-shortcuts.mjs";
25
25
  import { AdminViewHeader } from "../layout/admin-view-layout.mjs";
26
- import { useGlobalAuditHistory } from "../../hooks/use-audit-history.mjs";
27
26
  import { Icon } from "@iconify/react";
28
27
  import * as React from "react";
29
28
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
30
- import { FormProvider, useForm } from "react-hook-form";
29
+ import { FormProvider, useForm, useFormState } from "react-hook-form";
31
30
  import { toast } from "sonner";
32
31
  import { QuestpieClientError } from "questpie/client";
33
32
 
@@ -97,6 +96,31 @@ function GlobalFormViewSkeleton() {
97
96
  ]
98
97
  });
99
98
  }
99
+ const GlobalFormDirtyRefBridge = React.memo(function GlobalFormDirtyRefBridge$1({ control, onDirtyChange }) {
100
+ const { isDirty } = useFormState({ control });
101
+ React.useEffect(() => {
102
+ onDirtyChange(isDirty);
103
+ }, [isDirty, onDirtyChange]);
104
+ return null;
105
+ });
106
+ const GlobalSaveButton = React.memo(function GlobalSaveButton$1({ control, isMutationPending, t }) {
107
+ const { isSubmitting } = useFormState({ control });
108
+ const isSubmittingNow = isMutationPending || isSubmitting;
109
+ return /* @__PURE__ */ jsx(Button, {
110
+ type: "submit",
111
+ size: "sm",
112
+ disabled: isSubmittingNow,
113
+ className: "gap-2",
114
+ children: isSubmittingNow ? /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(Icon, {
115
+ icon: "ph:spinner-gap",
116
+ className: "size-4 animate-spin"
117
+ }), t("common.loading")] }) : /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(Icon, {
118
+ icon: "ph:check",
119
+ width: 16,
120
+ height: 16
121
+ }), t("common.save")] })
122
+ });
123
+ });
100
124
  /**
101
125
  * GlobalFormView - Default form-based edit view for globals
102
126
  */
@@ -136,7 +160,6 @@ function GlobalFormView({ global: globalName, config, viewConfig, registry, show
136
160
  id: globalData?.id,
137
161
  limit: 50
138
162
  }, { enabled: isHistoryOpen && !!globalSchema?.options?.versioning });
139
- const { data: auditData, isLoading: auditLoading } = useGlobalAuditHistory(globalName, { limit: 50 }, { enabled: isHistoryOpen });
140
163
  const workflowConfig = globalSchema?.options?.workflow;
141
164
  const workflowEnabled = !!workflowConfig?.enabled;
142
165
  /** Lightweight versions query (limit: 1) to read the current stage. */
@@ -167,6 +190,10 @@ function GlobalFormView({ global: globalName, config, viewConfig, registry, show
167
190
  defaultValues: globalData ?? {},
168
191
  resolver
169
192
  });
193
+ const formIsDirtyRef = React.useRef(false);
194
+ const handleFormDirtyChange = React.useCallback((isDirty) => {
195
+ formIsDirtyRef.current = isDirty;
196
+ }, []);
170
197
  /** Execute the confirmed workflow transition (immediate or scheduled). */
171
198
  const confirmTransition = React.useCallback(() => {
172
199
  if (!transitionTarget) return;
@@ -215,7 +242,7 @@ function GlobalFormView({ global: globalName, config, viewConfig, registry, show
215
242
  });
216
243
  const handleContentLocaleChange = React.useCallback((nextLocale) => {
217
244
  if (nextLocale === prevLocaleRef.current) return;
218
- if (form.formState.isDirty && !localeChangeDialog.open) {
245
+ if (formIsDirtyRef.current && !localeChangeDialog.open) {
219
246
  skipResetRef.current = true;
220
247
  localeSnapshotRef.current = form.getValues();
221
248
  setLocaleChangeDialog({
@@ -229,7 +256,6 @@ function GlobalFormView({ global: globalName, config, viewConfig, registry, show
229
256
  setContentLocale(nextLocale);
230
257
  }, [
231
258
  form,
232
- form.formState.isDirty,
233
259
  localeChangeDialog.open,
234
260
  setContentLocale
235
261
  ]);
@@ -316,7 +342,6 @@ function GlobalFormView({ global: globalName, config, viewConfig, registry, show
316
342
  document.addEventListener("keydown", handleKeyDown);
317
343
  return () => document.removeEventListener("keydown", handleKeyDown);
318
344
  }, [form, onSubmit]);
319
- const isSubmitting = updateMutation.isPending || form.formState.isSubmitting;
320
345
  const confirmRevertVersion = React.useCallback(async () => {
321
346
  if (!pendingRevertVersion) return;
322
347
  const payload = {};
@@ -361,10 +386,14 @@ function GlobalFormView({ global: globalName, config, viewConfig, registry, show
361
386
  })
362
387
  });
363
388
  if (dataLoading) return /* @__PURE__ */ jsx(GlobalFormViewSkeleton, {});
364
- const globalLabel = resolveText(resolvedConfig?.label ?? schemaFields?._globalLabel, globalName);
389
+ const globalLabel = resolveText(resolvedConfig?.label ?? schemaFields["_globalLabel"], globalName);
365
390
  return /* @__PURE__ */ jsxs(FormProvider, {
366
391
  ...form,
367
392
  children: [
393
+ /* @__PURE__ */ jsx(GlobalFormDirtyRefBridge, {
394
+ control: form.control,
395
+ onDirtyChange: handleFormDirtyChange
396
+ }),
368
397
  /* @__PURE__ */ jsxs("form", {
369
398
  onSubmit: form.handleSubmit(onSubmit),
370
399
  className: "qa-global-form w-full space-y-4",
@@ -414,7 +443,7 @@ function GlobalFormView({ global: globalName, config, viewConfig, registry, show
414
443
  }), stage.label || stage.name]
415
444
  }, stage.name))
416
445
  })] }),
417
- /* @__PURE__ */ jsxs(Button, {
446
+ globalSchema?.options?.versioning && /* @__PURE__ */ jsxs(Button, {
418
447
  type: "button",
419
448
  variant: "outline",
420
449
  size: "icon-sm",
@@ -428,33 +457,24 @@ function GlobalFormView({ global: globalName, config, viewConfig, registry, show
428
457
  children: t("history.title")
429
458
  })]
430
459
  }),
431
- /* @__PURE__ */ jsx(Button, {
432
- type: "submit",
433
- size: "sm",
434
- disabled: isSubmitting,
435
- className: "gap-2",
436
- children: isSubmitting ? /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(Icon, {
437
- icon: "ph:spinner-gap",
438
- className: "size-4 animate-spin"
439
- }), t("common.loading")] }) : /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(Icon, {
440
- icon: "ph:check",
441
- width: 16,
442
- height: 16
443
- }), t("common.save")] })
460
+ /* @__PURE__ */ jsx(GlobalSaveButton, {
461
+ control: form.control,
462
+ isMutationPending: updateMutation.isPending,
463
+ t
444
464
  })
445
465
  ] })
446
466
  }), /* @__PURE__ */ jsx(AutoFormFields, {
447
467
  collection: globalName,
448
468
  mode: "global",
449
469
  config: resolvedConfig,
450
- registry
470
+ registry,
471
+ resolvedFields: schemaFields,
472
+ schema: globalSchema
451
473
  })]
452
474
  }),
453
- /* @__PURE__ */ jsx(HistorySidebar, {
475
+ isHistoryOpen && /* @__PURE__ */ jsx(HistorySidebar, {
454
476
  open: isHistoryOpen,
455
477
  onOpenChange: setIsHistoryOpen,
456
- auditEntries: auditData ?? [],
457
- isLoadingAudit: auditLoading,
458
478
  versions: versionsData ?? [],
459
479
  fields: globalSchema?.fields,
460
480
  isLoadingVersions: versionsLoading,
@@ -1,3 +1,5 @@
1
+ import { ServerAdminShellRailConfig } from "../../../server/augmentation/shell.mjs";
2
+ import "../../../server/augmentation.mjs";
1
3
  import { AdminToasterProps } from "../../components/ui/sonner.mjs";
2
4
  import { AdminSidebarProps } from "./admin-sidebar.mjs";
3
5
  import { AdminTheme } from "./admin-theme.mjs";
@@ -73,6 +75,18 @@ interface AdminLayoutSharedProps {
73
75
  */
74
76
  layoutMode?: LayoutMode;
75
77
  }
78
+ interface AdminShellRailProps {
79
+ /** Current route path, usually the browser pathname. */
80
+ activeRoute?: string;
81
+ /** Admin base path, usually "/admin". */
82
+ basePath: string;
83
+ /** Resolved rail placement. */
84
+ placement: "left" | "right";
85
+ /** Raw rail config from server admin config. */
86
+ config: ServerAdminShellRailConfig;
87
+ /** Navigate function from the admin runtime. */
88
+ navigate: (path: string) => void;
89
+ }
76
90
  interface AdminLayoutProps extends AdminLayoutSharedProps {
77
91
  /**
78
92
  * Brand name for sidebar.
@@ -127,4 +141,4 @@ declare function AdminLayout({
127
141
  layoutMode
128
142
  }: AdminLayoutProps): React.ReactElement;
129
143
  //#endregion
130
- export { AdminLayout, AdminLayoutSharedProps };
144
+ export { AdminLayout, AdminLayoutSharedProps, AdminShellRailProps };