@stigmer/react 3.11.1-dev.20260812192248 → 3.12.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 (168) hide show
  1. package/activity/useRecentActivity.d.ts +3 -2
  2. package/activity/useRecentActivity.d.ts.map +1 -1
  3. package/activity/useRecentActivity.js +3 -2
  4. package/activity/useRecentActivity.js.map +1 -1
  5. package/agent/AgentDetailView.d.ts +2 -12
  6. package/agent/AgentDetailView.d.ts.map +1 -1
  7. package/agent/AgentDetailView.js +10 -42
  8. package/agent/AgentDetailView.js.map +1 -1
  9. package/agent/internal/agentToInput.d.ts.map +1 -1
  10. package/agent/internal/agentToInput.js +0 -8
  11. package/agent/internal/agentToInput.js.map +1 -1
  12. package/dependency-graph/DependencyTreeNode.d.ts.map +1 -1
  13. package/dependency-graph/DependencyTreeNode.js +1 -7
  14. package/dependency-graph/DependencyTreeNode.js.map +1 -1
  15. package/dependency-graph/types.d.ts +8 -20
  16. package/dependency-graph/types.d.ts.map +1 -1
  17. package/dependency-graph/useDependencyGraph.d.ts.map +1 -1
  18. package/dependency-graph/useDependencyGraph.js +2 -20
  19. package/dependency-graph/useDependencyGraph.js.map +1 -1
  20. package/index.d.ts +0 -2
  21. package/index.d.ts.map +1 -1
  22. package/index.js +0 -6
  23. package/index.js.map +1 -1
  24. package/library/useExportResource.d.ts +3 -4
  25. package/library/useExportResource.d.ts.map +1 -1
  26. package/library/useExportResource.js +1 -1
  27. package/library/useExportResource.js.map +1 -1
  28. package/manifest/EditResourceYamlDialog.d.ts.map +1 -1
  29. package/manifest/EditResourceYamlDialog.js.map +1 -1
  30. package/package.json +4 -4
  31. package/src/activity/useRecentActivity.ts +3 -2
  32. package/src/agent/AgentDetailView.tsx +6 -130
  33. package/src/agent/internal/__tests__/agentToInput.test.ts +2 -11
  34. package/src/agent/internal/agentToInput.ts +0 -11
  35. package/src/dependency-graph/DependencyTreeNode.tsx +1 -7
  36. package/src/dependency-graph/__tests__/useDependencyGraph.test.ts +18 -34
  37. package/src/dependency-graph/types.ts +8 -20
  38. package/src/dependency-graph/useDependencyGraph.ts +2 -22
  39. package/src/index.ts +0 -82
  40. package/src/library/useExportResource.ts +3 -4
  41. package/src/manifest/EditResourceYamlDialog.tsx +1 -2
  42. package/src/manifest/__tests__/ApplyManifestDialog.test.tsx +2 -2
  43. package/src/manifest/__tests__/EditResourceYamlDialog.test.tsx +9 -9
  44. package/styles.css +1 -1
  45. package/workflow/CanvasTransitionEdge.d.ts +1 -1
  46. package/datastore/CollectionRecordsBrowser.d.ts +0 -34
  47. package/datastore/CollectionRecordsBrowser.d.ts.map +0 -1
  48. package/datastore/CollectionRecordsBrowser.js +0 -226
  49. package/datastore/CollectionRecordsBrowser.js.map +0 -1
  50. package/datastore/CollectionSchemaView.d.ts +0 -29
  51. package/datastore/CollectionSchemaView.d.ts.map +0 -1
  52. package/datastore/CollectionSchemaView.js +0 -93
  53. package/datastore/CollectionSchemaView.js.map +0 -1
  54. package/datastore/DatastoreDetailView.d.ts +0 -44
  55. package/datastore/DatastoreDetailView.d.ts.map +0 -1
  56. package/datastore/DatastoreDetailView.js +0 -76
  57. package/datastore/DatastoreDetailView.js.map +0 -1
  58. package/datastore/DatastoreSyncReport.d.ts +0 -19
  59. package/datastore/DatastoreSyncReport.d.ts.map +0 -1
  60. package/datastore/DatastoreSyncReport.js +0 -45
  61. package/datastore/DatastoreSyncReport.js.map +0 -1
  62. package/datastore/DeleteDatastoreDialog.d.ts +0 -35
  63. package/datastore/DeleteDatastoreDialog.d.ts.map +0 -1
  64. package/datastore/DeleteDatastoreDialog.js +0 -90
  65. package/datastore/DeleteDatastoreDialog.js.map +0 -1
  66. package/datastore/FieldValueControl.d.ts +0 -55
  67. package/datastore/FieldValueControl.d.ts.map +0 -1
  68. package/datastore/FieldValueControl.js +0 -138
  69. package/datastore/FieldValueControl.js.map +0 -1
  70. package/datastore/RecordFilterBuilder.d.ts +0 -37
  71. package/datastore/RecordFilterBuilder.d.ts.map +0 -1
  72. package/datastore/RecordFilterBuilder.js +0 -122
  73. package/datastore/RecordFilterBuilder.js.map +0 -1
  74. package/datastore/RecordFormPanel.d.ts +0 -45
  75. package/datastore/RecordFormPanel.d.ts.map +0 -1
  76. package/datastore/RecordFormPanel.js +0 -215
  77. package/datastore/RecordFormPanel.js.map +0 -1
  78. package/datastore/index.d.ts +0 -43
  79. package/datastore/index.d.ts.map +0 -1
  80. package/datastore/index.js +0 -22
  81. package/datastore/index.js.map +0 -1
  82. package/datastore/recordFilter.d.ts +0 -62
  83. package/datastore/recordFilter.d.ts.map +0 -1
  84. package/datastore/recordFilter.js +0 -104
  85. package/datastore/recordFilter.js.map +0 -1
  86. package/datastore/recordValues.d.ts +0 -85
  87. package/datastore/recordValues.d.ts.map +0 -1
  88. package/datastore/recordValues.js +0 -282
  89. package/datastore/recordValues.js.map +0 -1
  90. package/datastore/useDatastore.d.ts +0 -39
  91. package/datastore/useDatastore.d.ts.map +0 -1
  92. package/datastore/useDatastore.js +0 -44
  93. package/datastore/useDatastore.js.map +0 -1
  94. package/datastore/useDatastoreCount.d.ts +0 -50
  95. package/datastore/useDatastoreCount.d.ts.map +0 -1
  96. package/datastore/useDatastoreCount.js +0 -27
  97. package/datastore/useDatastoreCount.js.map +0 -1
  98. package/datastore/useDatastoreDescription.d.ts +0 -54
  99. package/datastore/useDatastoreDescription.d.ts.map +0 -1
  100. package/datastore/useDatastoreDescription.js +0 -50
  101. package/datastore/useDatastoreDescription.js.map +0 -1
  102. package/datastore/useDatastoreList.d.ts +0 -57
  103. package/datastore/useDatastoreList.d.ts.map +0 -1
  104. package/datastore/useDatastoreList.js +0 -29
  105. package/datastore/useDatastoreList.js.map +0 -1
  106. package/datastore/useDatastoreSearch.d.ts +0 -19
  107. package/datastore/useDatastoreSearch.d.ts.map +0 -1
  108. package/datastore/useDatastoreSearch.js +0 -22
  109. package/datastore/useDatastoreSearch.js.map +0 -1
  110. package/datastore/useDeleteDatastore.d.ts +0 -53
  111. package/datastore/useDeleteDatastore.d.ts.map +0 -1
  112. package/datastore/useDeleteDatastore.js +0 -40
  113. package/datastore/useDeleteDatastore.js.map +0 -1
  114. package/datastore/useDeleteRecord.d.ts +0 -34
  115. package/datastore/useDeleteRecord.d.ts.map +0 -1
  116. package/datastore/useDeleteRecord.js +0 -43
  117. package/datastore/useDeleteRecord.js.map +0 -1
  118. package/datastore/useInsertRecord.d.ts +0 -50
  119. package/datastore/useInsertRecord.d.ts.map +0 -1
  120. package/datastore/useInsertRecord.js +0 -55
  121. package/datastore/useInsertRecord.js.map +0 -1
  122. package/datastore/useRecordCollection.d.ts +0 -104
  123. package/datastore/useRecordCollection.d.ts.map +0 -1
  124. package/datastore/useRecordCollection.js +0 -136
  125. package/datastore/useRecordCollection.js.map +0 -1
  126. package/datastore/useRecordList.d.ts +0 -66
  127. package/datastore/useRecordList.d.ts.map +0 -1
  128. package/datastore/useRecordList.js +0 -58
  129. package/datastore/useRecordList.js.map +0 -1
  130. package/datastore/useUpdateRecord.d.ts +0 -52
  131. package/datastore/useUpdateRecord.d.ts.map +0 -1
  132. package/datastore/useUpdateRecord.js +0 -55
  133. package/datastore/useUpdateRecord.js.map +0 -1
  134. package/src/agent/__tests__/AgentDetailView.datastores.test.tsx +0 -166
  135. package/src/datastore/CollectionRecordsBrowser.tsx +0 -571
  136. package/src/datastore/CollectionSchemaView.tsx +0 -247
  137. package/src/datastore/DatastoreDetailView.tsx +0 -201
  138. package/src/datastore/DatastoreSyncReport.tsx +0 -105
  139. package/src/datastore/DeleteDatastoreDialog.tsx +0 -202
  140. package/src/datastore/FieldValueControl.tsx +0 -289
  141. package/src/datastore/RecordFilterBuilder.tsx +0 -422
  142. package/src/datastore/RecordFormPanel.tsx +0 -432
  143. package/src/datastore/__tests__/CollectionRecordsBrowser.test.tsx +0 -245
  144. package/src/datastore/__tests__/DeleteDatastoreDialog.test.tsx +0 -133
  145. package/src/datastore/__tests__/FieldValueControl.test.tsx +0 -160
  146. package/src/datastore/__tests__/RecordFilterBuilder.test.tsx +0 -155
  147. package/src/datastore/__tests__/RecordFormPanel.test.tsx +0 -227
  148. package/src/datastore/__tests__/recordFilter.test.ts +0 -169
  149. package/src/datastore/__tests__/recordMutations.test.tsx +0 -195
  150. package/src/datastore/__tests__/recordValues.test.ts +0 -359
  151. package/src/datastore/__tests__/schemaAndSyncViews.test.tsx +0 -142
  152. package/src/datastore/__tests__/useDatastoreDescription.test.tsx +0 -143
  153. package/src/datastore/__tests__/useRecordCollection.test.tsx +0 -250
  154. package/src/datastore/__tests__/useRecordList.test.tsx +0 -85
  155. package/src/datastore/index.ts +0 -111
  156. package/src/datastore/recordFilter.ts +0 -154
  157. package/src/datastore/recordValues.ts +0 -327
  158. package/src/datastore/useDatastore.ts +0 -67
  159. package/src/datastore/useDatastoreCount.ts +0 -69
  160. package/src/datastore/useDatastoreDescription.ts +0 -90
  161. package/src/datastore/useDatastoreList.ts +0 -79
  162. package/src/datastore/useDatastoreSearch.ts +0 -39
  163. package/src/datastore/useDeleteDatastore.ts +0 -87
  164. package/src/datastore/useDeleteRecord.ts +0 -77
  165. package/src/datastore/useInsertRecord.ts +0 -92
  166. package/src/datastore/useRecordCollection.ts +0 -256
  167. package/src/datastore/useRecordList.ts +0 -131
  168. package/src/datastore/useUpdateRecord.ts +0 -95
@@ -1,571 +0,0 @@
1
- "use client";
2
-
3
- import { useCallback, useMemo, useState } from "react";
4
- import { cn } from "@stigmer/theme";
5
- import { timestampDate } from "@bufbuild/protobuf/wkt";
6
- import { getUserMessage, isPermissionDenied } from "@stigmer/sdk";
7
- import {
8
- DatastoreVerb,
9
- FieldType,
10
- type CollectionDeclaration,
11
- type DatastoreSpec,
12
- } from "@stigmer/protos/ai/stigmer/agentic/datastore/v1/spec_pb";
13
- import type {
14
- CollectionDescription,
15
- RecordEnvelope,
16
- } from "@stigmer/protos/ai/stigmer/agentic/datastore/v1/record_io_pb";
17
- import { TruncatedText } from "../internal/truncated-text.js";
18
- import { ResourceTable } from "../resource-workbench/components/ResourceTable.js";
19
- import { ConfirmDialog } from "../resource-detail/ConfirmDialog.js";
20
- import { useConfirmAction } from "../resource-detail/useConfirmAction.js";
21
- import { toast } from "../feedback/toast.js";
22
- import { FIELD_INPUT_CLASSES } from "./FieldValueControl.js";
23
- import { RecordFilterBuilder } from "./RecordFilterBuilder.js";
24
- import { RecordFormPanel } from "./RecordFormPanel.js";
25
- import { buildRecordFilter, type RecordConditionDraft } from "./recordFilter.js";
26
- import {
27
- formatFieldValue,
28
- formatSystemTimestamp,
29
- isSortableField,
30
- } from "./recordValues.js";
31
- import { useDatastoreDescription } from "./useDatastoreDescription.js";
32
- import { useRecordCollection, type RecordColumnDef } from "./useRecordCollection.js";
33
- import { useDeleteRecord } from "./useDeleteRecord.js";
34
- import type { SortValue } from "../resource-workbench/types.js";
35
-
36
- /** Props for {@link CollectionRecordsBrowser}. */
37
- export interface CollectionRecordsBrowserProps {
38
- /** Organization slug. */
39
- readonly org: string;
40
- /** Datastore slug. */
41
- readonly datastoreSlug: string;
42
- /** The datastore spec — collections and fields drive the grid columns. */
43
- readonly spec: DatastoreSpec;
44
- /** Additional CSS classes for the root container. */
45
- readonly className?: string;
46
- }
47
-
48
- const PAGE_SIZE = 25;
49
-
50
- /**
51
- * The records browser (DD-008 SD-3): collection selector, partition
52
- * picker, schema-aware filter builder, typed-cell grid, RPC-paged
53
- * navigation, and verb-gated write affordances — the console surface
54
- * that absorbs the Supabase-dashboard capability the datastore
55
- * primitive retires.
56
- *
57
- * Authorization is rendered as the two layers it is:
58
- * - Write affordances gate on `describeDatastore`'s per-collection
59
- * effective verbs (projections, never authority — the server's
60
- * record layer stays the enforcer).
61
- * - **The denied state is a first-class render**, driven primarily by
62
- * empty access lists (deny-by-default renders as empty access, never
63
- * an error) with a caught reach-level `PERMISSION_DENIED` as the
64
- * defensive branch. Both show the relayable message plus operator
65
- * guidance — never a silently empty grid.
66
- *
67
- * All visual properties flow through `--stgm-*` design tokens.
68
- * Zero Console dependencies — safe for platform builder embedding.
69
- */
70
- export function CollectionRecordsBrowser({
71
- org,
72
- datastoreSlug,
73
- spec,
74
- className,
75
- }: CollectionRecordsBrowserProps) {
76
- const collections = spec.collections;
77
- const [collectionName, setCollectionName] = useState(collections[0]?.name ?? "");
78
- const collection = collections.find((c) => c.name === collectionName);
79
-
80
- const description = useDatastoreDescription(org, datastoreSlug);
81
- const [partition, setPartition] = useState("default");
82
-
83
- // Filter, sort, and page state — per collection selection.
84
- const [conditions, setConditions] = useState<readonly RecordConditionDraft[]>([]);
85
- const [sort, setSort] = useState<SortValue | null>(null);
86
- const [page, setPage] = useState(1);
87
-
88
- const selectCollection = useCallback((name: string) => {
89
- setCollectionName(name);
90
- setConditions([]);
91
- setSort(null);
92
- setPage(1);
93
- }, []);
94
-
95
- const selectPartition = useCallback((name: string) => {
96
- setPartition(name);
97
- setPage(1);
98
- }, []);
99
-
100
- const applyConditions = useCallback((next: readonly RecordConditionDraft[]) => {
101
- setConditions(next);
102
- setPage(1);
103
- }, []);
104
-
105
- if (collections.length === 0) {
106
- return (
107
- <p className={cn("stg:px-3 stg:py-6 stg:text-center stg:text-sm stg:text-muted-foreground", className)}>
108
- This datastore declares no collections.
109
- </p>
110
- );
111
- }
112
- if (!collection) return null;
113
-
114
- const collectionAccess = description.description?.collections.find(
115
- (c) => c.name === collection.name,
116
- );
117
-
118
- return (
119
- <div className={cn("stg:flex stg:flex-col stg:gap-3", className)}>
120
- <div className="stg:flex stg:flex-wrap stg:items-center stg:gap-2">
121
- <label className="stg:flex stg:items-center stg:gap-1.5 stg:text-xs stg:font-medium stg:text-muted-foreground">
122
- Collection
123
- <select
124
- className={cn(FIELD_INPUT_CLASSES, "stg:w-auto")}
125
- value={collectionName}
126
- onChange={(e) => selectCollection(e.target.value)}
127
- >
128
- {collections.map((c) => (
129
- <option key={c.name} value={c.name}>
130
- {c.name}
131
- </option>
132
- ))}
133
- </select>
134
- </label>
135
- <label className="stg:flex stg:items-center stg:gap-1.5 stg:text-xs stg:font-medium stg:text-muted-foreground">
136
- Partition
137
- <select
138
- className={cn(FIELD_INPUT_CLASSES, "stg:w-auto")}
139
- value={partition}
140
- onChange={(e) => selectPartition(e.target.value)}
141
- >
142
- {description.partitions.map((p) => (
143
- <option key={p} value={p}>
144
- {p}
145
- </option>
146
- ))}
147
- </select>
148
- </label>
149
- </div>
150
-
151
- <RecordsPane
152
- org={org}
153
- datastoreSlug={datastoreSlug}
154
- collection={collection}
155
- partition={partition}
156
- conditions={conditions}
157
- onConditionsChange={applyConditions}
158
- sort={sort}
159
- onSortChange={setSort}
160
- page={page}
161
- onPageChange={setPage}
162
- access={collectionAccess}
163
- describeError={description.error}
164
- describeLoaded={description.description !== null}
165
- />
166
- </div>
167
- );
168
- }
169
-
170
- // ---------------------------------------------------------------------------
171
- // The per-collection pane: denied panel or grid
172
- // ---------------------------------------------------------------------------
173
-
174
- function RecordsPane({
175
- org,
176
- datastoreSlug,
177
- collection,
178
- partition,
179
- conditions,
180
- onConditionsChange,
181
- sort,
182
- onSortChange,
183
- page,
184
- onPageChange,
185
- access,
186
- describeError,
187
- describeLoaded,
188
- }: {
189
- readonly org: string;
190
- readonly datastoreSlug: string;
191
- readonly collection: CollectionDeclaration;
192
- readonly partition: string;
193
- readonly conditions: readonly RecordConditionDraft[];
194
- readonly onConditionsChange: (c: readonly RecordConditionDraft[]) => void;
195
- readonly sort: SortValue | null;
196
- readonly onSortChange: (s: SortValue | null) => void;
197
- readonly page: number;
198
- readonly onPageChange: (p: number) => void;
199
- readonly access: CollectionDescription | undefined;
200
- readonly describeError: Error | null;
201
- readonly describeLoaded: boolean;
202
- }) {
203
- const verbs = useMemo(() => {
204
- const set = new Set<DatastoreVerb>();
205
- for (const grant of access?.access ?? []) set.add(grant.verb);
206
- return set;
207
- }, [access]);
208
- const canRead = verbs.has(DatastoreVerb.read);
209
- const canInsert = verbs.has(DatastoreVerb.insert);
210
- const canUpdate = verbs.has(DatastoreVerb.update);
211
- const canDelete = verbs.has(DatastoreVerb.delete);
212
-
213
- // The caller's column-level read access (empty means every field): a
214
- // field-restricted read grant returns only these fields, so the grid
215
- // and the filter builder narrow to them — no permanently-empty
216
- // columns, no filter the server would refuse (DD-008 invariant 2).
217
- const readableFields = useMemo(
218
- () => access?.access.find((a) => a.verb === DatastoreVerb.read)?.readableFields ?? [],
219
- [access],
220
- );
221
-
222
- const scope = { org, datastore: datastoreSlug, collection: collection.name, partition };
223
-
224
- const filter = useMemo(() => buildRecordFilter(conditions), [conditions]);
225
- const columns = useMemo(
226
- () => buildRecordColumns(collection, readableFields),
227
- [collection, readableFields],
228
- );
229
-
230
- // Fetch only once read access is confirmed — the denied panel is the
231
- // primary render for deny-by-default, not a failed query.
232
- const records = useRecordCollection({
233
- org: describeLoaded && canRead ? org : null,
234
- datastore: datastoreSlug,
235
- collection: collection.name,
236
- partition,
237
- filter,
238
- page,
239
- pageSize: PAGE_SIZE,
240
- sort,
241
- onSortChange,
242
- columns,
243
- });
244
-
245
- // Row actions and write panel state.
246
- const [formOpen, setFormOpen] = useState(false);
247
- const [editing, setEditing] = useState<RecordEnvelope | null>(null);
248
- const { confirmState, confirm, handleConfirm, handleCancel } = useConfirmAction();
249
- const deleteRecord = useDeleteRecord();
250
-
251
- const openInsert = useCallback(() => {
252
- setEditing(null);
253
- setFormOpen(true);
254
- }, []);
255
-
256
- const openEdit = useCallback((record: RecordEnvelope) => {
257
- setEditing(record);
258
- setFormOpen(true);
259
- }, []);
260
-
261
- // Deps narrowed to the specific stable callbacks used (DD-010).
262
- const deleteRecordFn = deleteRecord.deleteRecord;
263
- const refetchRecords = records.refetch;
264
- const collectionName = collection.name;
265
- const handleDelete = useCallback(
266
- async (record: RecordEnvelope) => {
267
- const ok = await confirm({
268
- title: "Delete record?",
269
- description: `Record ${record.id} will be permanently deleted.`,
270
- confirmLabel: "Delete",
271
- variant: "destructive",
272
- });
273
- if (!ok) return;
274
- try {
275
- await deleteRecordFn({
276
- org,
277
- datastore: datastoreSlug,
278
- collection: collectionName,
279
- partition,
280
- id: record.id,
281
- });
282
- toast.success("Record deleted");
283
- refetchRecords();
284
- } catch (err) {
285
- toast.error(getUserMessage(err));
286
- }
287
- },
288
- [confirm, deleteRecordFn, refetchRecords, org, datastoreSlug, collectionName, partition],
289
- );
290
-
291
- // --- Denied states (DD-008 SD-3): first-class renders ---------------------
292
- if (describeError && isPermissionDenied(describeError)) {
293
- return <DeniedPanel message={getUserMessage(describeError)} />;
294
- }
295
- if (describeLoaded && !canRead) {
296
- return (
297
- <DeniedPanel
298
- message={`You do not have record access to “${collection.name}”.`}
299
- />
300
- );
301
- }
302
- if (records.error && isPermissionDenied(records.error)) {
303
- return <DeniedPanel message={getUserMessage(records.error)} />;
304
- }
305
-
306
- const totalPages = Math.max(records.totalPages, 1);
307
-
308
- return (
309
- <div className="stg:flex stg:flex-col stg:gap-3">
310
- <div className="stg:flex stg:flex-wrap stg:items-center stg:gap-2">
311
- <RecordFilterBuilder
312
- collection={collection}
313
- readableFields={readableFields}
314
- conditions={conditions}
315
- onChange={onConditionsChange}
316
- className="stg:min-w-0 stg:flex-1"
317
- />
318
- {canInsert && (
319
- <button
320
- type="button"
321
- onClick={openInsert}
322
- className={cn(
323
- "stg:shrink-0 stg:rounded-md stg:bg-primary stg:px-3 stg:py-1.5 stg:text-xs stg:font-medium stg:text-primary-foreground",
324
- "stg:hover:bg-primary-hover",
325
- "stg:focus-visible:outline-none stg:focus-visible:ring-2 stg:focus-visible:ring-ring",
326
- )}
327
- >
328
- Insert record
329
- </button>
330
- )}
331
- </div>
332
-
333
- {records.error ? (
334
- <div
335
- role="alert"
336
- className="stg:rounded-md stg:border stg:border-destructive stg:bg-card stg:px-3 stg:py-2 stg:text-sm stg:text-destructive"
337
- >
338
- {getUserMessage(records.error)}
339
- </div>
340
- ) : records.isLoading ? (
341
- <div className="stg:rounded-lg stg:border stg:border-border stg:px-3 stg:py-6 stg:text-center stg:text-sm stg:text-muted-foreground">
342
- Loading records…
343
- </div>
344
- ) : records.records.length === 0 ? (
345
- <div className="stg:rounded-lg stg:border stg:border-border stg:px-3 stg:py-6 stg:text-center stg:text-sm stg:text-muted-foreground">
346
- {conditions.length > 0
347
- ? "No records match the current filters."
348
- : `No records in “${collection.name}” yet.`}
349
- </div>
350
- ) : (
351
- records.table && (
352
- <div className="stg:overflow-x-auto stg:rounded-lg stg:border stg:border-border">
353
- <ResourceTable
354
- table={records.table}
355
- aria-label={`Records in ${collection.name}`}
356
- renderRowAction={
357
- canUpdate || canDelete
358
- ? (record) => (
359
- <RowActions
360
- record={record}
361
- canUpdate={canUpdate}
362
- canDelete={canDelete}
363
- onEdit={openEdit}
364
- onDelete={handleDelete}
365
- />
366
- )
367
- : undefined
368
- }
369
- />
370
- </div>
371
- )
372
- )}
373
-
374
- <div className="stg:flex stg:items-center stg:justify-between stg:text-xs stg:text-muted-foreground">
375
- <span>
376
- {records.total} record{records.total === 1 ? "" : "s"}
377
- </span>
378
- <div className="stg:flex stg:items-center stg:gap-2">
379
- <button
380
- type="button"
381
- onClick={() => onPageChange(page - 1)}
382
- disabled={page <= 1}
383
- className={paginationButtonClass}
384
- >
385
- Previous
386
- </button>
387
- <span>
388
- Page {page} of {totalPages}
389
- </span>
390
- <button
391
- type="button"
392
- onClick={() => onPageChange(page + 1)}
393
- disabled={page >= totalPages}
394
- className={paginationButtonClass}
395
- >
396
- Next
397
- </button>
398
- </div>
399
- </div>
400
-
401
- <RecordFormPanel
402
- open={formOpen}
403
- onOpenChange={setFormOpen}
404
- scope={scope}
405
- collection={collection}
406
- record={editing}
407
- onSaved={() => records.refetch()}
408
- />
409
- <ConfirmDialog state={confirmState} onConfirm={handleConfirm} onCancel={handleCancel} />
410
- </div>
411
- );
412
- }
413
-
414
- const paginationButtonClass = cn(
415
- "stg:rounded-md stg:border stg:border-input stg:bg-background stg:px-2 stg:py-1 stg:text-xs stg:font-medium stg:text-foreground",
416
- "stg:hover:bg-accent stg:hover:text-accent-foreground",
417
- "stg:focus-visible:outline-none stg:focus-visible:ring-1 stg:focus-visible:ring-ring",
418
- "stg:disabled:pointer-events-none stg:disabled:opacity-50",
419
- );
420
-
421
- // ---------------------------------------------------------------------------
422
- // Denied panel — an explanation, never an empty grid
423
- // ---------------------------------------------------------------------------
424
-
425
- function DeniedPanel({ message }: { readonly message: string }) {
426
- return (
427
- <div
428
- role="status"
429
- className="stg:flex stg:flex-col stg:items-center stg:gap-2 stg:rounded-lg stg:border stg:border-border stg:bg-muted stg:px-4 stg:py-8 stg:text-center"
430
- >
431
- <p className="stg:text-sm stg:font-medium stg:text-foreground">{message}</p>
432
- <p className="stg:max-w-md stg:text-xs stg:text-muted-foreground">
433
- Record access is granted by the datastore's authorization block. To
434
- gain access, add a role binding for your principal or set a{" "}
435
- <code className="stg:font-mono">default_role</code> in the datastore YAML,
436
- with the verbs you need granted on this collection.
437
- </p>
438
- </div>
439
- );
440
- }
441
-
442
- // ---------------------------------------------------------------------------
443
- // Columns: typed cells from FieldDeclaration + system columns
444
- // ---------------------------------------------------------------------------
445
-
446
- /**
447
- * Grid columns for a collection: declared fields with typed cells, then
448
- * `created_at` (visible by default). The remaining system columns
449
- * (`id`, `updated_at`, `created_by`) stay out of the default grid —
450
- * they are shown in the record form's read-only summary; a column
451
- * toggle is recorded growth.
452
- *
453
- * `readableFields` narrows the declared columns to the caller's
454
- * column-level read access (empty means every field): a hidden field's
455
- * cells would be permanently empty, which reads as missing data rather
456
- * than restricted access.
457
- */
458
- function buildRecordColumns(
459
- collection: CollectionDeclaration,
460
- readableFields: readonly string[],
461
- ): RecordColumnDef[] {
462
- const readable = readableFields.length > 0 ? new Set(readableFields) : null;
463
- const declaredColumns =
464
- readable === null
465
- ? collection.fields
466
- : collection.fields.filter((field) => readable.has(field.name));
467
- const fieldColumns: RecordColumnDef[] = declaredColumns.map((field) => ({
468
- id: field.name,
469
- header: field.name,
470
- sortable: isSortableField(field),
471
- cell: (record) => (
472
- <TypedCell
473
- value={formatFieldValue(field.type, record.fields?.[field.name])}
474
- numeric={field.type === FieldType.integer || field.type === FieldType.number}
475
- mono={field.type === FieldType.json}
476
- />
477
- ),
478
- }));
479
-
480
- return [
481
- ...fieldColumns,
482
- {
483
- id: "created_at",
484
- header: "created_at",
485
- sortable: true,
486
- cell: (record) => (
487
- <TypedCell
488
- value={
489
- record.createdAt
490
- ? formatSystemTimestamp(timestampDate(record.createdAt).toISOString())
491
- : ""
492
- }
493
- mono
494
- />
495
- ),
496
- },
497
- ];
498
- }
499
-
500
- function TypedCell({
501
- value,
502
- numeric,
503
- mono,
504
- }: {
505
- readonly value: string;
506
- readonly numeric?: boolean;
507
- readonly mono?: boolean;
508
- }) {
509
- // TruncatedText's overflow gating supersedes the old hand-rolled
510
- // 80-character slice + conditional title: CSS truncates the cell and the
511
- // tooltip opens only when the value is actually clipped.
512
- return (
513
- <TruncatedText
514
- text={value}
515
- className={cn(
516
- "stg:block stg:max-w-xs",
517
- numeric && "stg:text-right stg:tabular-nums",
518
- mono && "stg:font-mono",
519
- )}
520
- />
521
- );
522
- }
523
-
524
- // ---------------------------------------------------------------------------
525
- // Row actions
526
- // ---------------------------------------------------------------------------
527
-
528
- function RowActions({
529
- record,
530
- canUpdate,
531
- canDelete,
532
- onEdit,
533
- onDelete,
534
- }: {
535
- readonly record: RecordEnvelope;
536
- readonly canUpdate: boolean;
537
- readonly canDelete: boolean;
538
- readonly onEdit: (record: RecordEnvelope) => void;
539
- readonly onDelete: (record: RecordEnvelope) => void;
540
- }) {
541
- return (
542
- <div className="stg:flex stg:items-center stg:gap-1">
543
- {canUpdate && (
544
- <button
545
- type="button"
546
- onClick={() => onEdit(record)}
547
- aria-label={`Edit record ${record.id}`}
548
- className={rowActionClass}
549
- >
550
- Edit
551
- </button>
552
- )}
553
- {canDelete && (
554
- <button
555
- type="button"
556
- onClick={() => onDelete(record)}
557
- aria-label={`Delete record ${record.id}`}
558
- className={cn(rowActionClass, "stg:text-destructive stg:hover:text-destructive")}
559
- >
560
- Delete
561
- </button>
562
- )}
563
- </div>
564
- );
565
- }
566
-
567
- const rowActionClass = cn(
568
- "stg:rounded-md stg:px-1.5 stg:py-0.5 stg:text-xs stg:font-medium stg:text-muted-foreground",
569
- "stg:hover:bg-accent stg:hover:text-foreground",
570
- "stg:focus-visible:outline-none stg:focus-visible:ring-1 stg:focus-visible:ring-ring",
571
- );