@xnetjs/react 0.0.2 → 0.1.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/README.md +206 -2
- package/dist/chunk-6VOICQZ3.js +760 -0
- package/dist/chunk-7OQXRHEQ.js +4649 -0
- package/dist/chunk-EJ5RW5GI.js +93 -0
- package/dist/chunk-IHTMVTTE.js +1108 -0
- package/dist/chunk-JCOFKBOB.js +11 -0
- package/dist/chunk-KSHTDZ2V.js +893 -0
- package/dist/chunk-QHNYQVUM.js +989 -0
- package/dist/context-CFu9i136.d.ts +392 -0
- package/dist/core.d.ts +372 -0
- package/dist/core.js +29 -0
- package/dist/database.d.ts +383 -0
- package/dist/database.js +19 -0
- package/dist/experimental-B2FrBnkV.d.ts +1584 -0
- package/dist/experimental.d.ts +15 -0
- package/dist/experimental.js +248 -0
- package/dist/index.d.ts +601 -2700
- package/dist/index.js +4782 -4427
- package/dist/{instrumentation-Cn94kn8-.d.ts → instrumentation-CpIuG2y5.d.ts} +32 -1
- package/dist/internal.d.ts +32 -22
- package/dist/internal.js +16 -4
- package/dist/telemetry-context-B7r6H1KW.d.ts +35 -0
- package/dist/useNodeStore-DTCSBF51.d.ts +28 -0
- package/dist/useQuery-D7ajycrc.d.ts +302 -0
- package/package.json +28 -10
- package/dist/chunk-CWHCGYDW.js +0 -2238
|
@@ -0,0 +1,893 @@
|
|
|
1
|
+
import {
|
|
2
|
+
useNodeStore
|
|
3
|
+
} from "./chunk-IHTMVTTE.js";
|
|
4
|
+
|
|
5
|
+
// src/hooks/useDatabaseDoc.ts
|
|
6
|
+
import {
|
|
7
|
+
getColumns,
|
|
8
|
+
getColumn,
|
|
9
|
+
createColumn as createColumnOp,
|
|
10
|
+
updateColumn as updateColumnOp,
|
|
11
|
+
deleteColumn as deleteColumnOp,
|
|
12
|
+
reorderColumn as reorderColumnOp,
|
|
13
|
+
duplicateColumn as duplicateColumnOp,
|
|
14
|
+
getViews,
|
|
15
|
+
getView,
|
|
16
|
+
createView as createViewOp,
|
|
17
|
+
updateView as updateViewOp,
|
|
18
|
+
deleteView as deleteViewOp,
|
|
19
|
+
duplicateView as duplicateViewOp,
|
|
20
|
+
initializeDatabaseDoc,
|
|
21
|
+
isDatabaseDocInitialized,
|
|
22
|
+
getDatabaseDocumentModel
|
|
23
|
+
} from "@xnetjs/data";
|
|
24
|
+
import { useState, useEffect, useCallback, useRef } from "react";
|
|
25
|
+
import * as Y from "yjs";
|
|
26
|
+
function useDatabaseDoc(databaseId) {
|
|
27
|
+
const { store, isReady } = useNodeStore();
|
|
28
|
+
const [doc, setDoc] = useState(null);
|
|
29
|
+
const [columns, setColumns] = useState([]);
|
|
30
|
+
const [views, setViews] = useState([]);
|
|
31
|
+
const [storageMode, setStorageMode] = useState("empty");
|
|
32
|
+
const [loading, setLoading] = useState(true);
|
|
33
|
+
const [error, setError] = useState(null);
|
|
34
|
+
const storeRef = useRef(store);
|
|
35
|
+
storeRef.current = store;
|
|
36
|
+
const docRef = useRef(null);
|
|
37
|
+
docRef.current = doc;
|
|
38
|
+
const storageModeRef = useRef("empty");
|
|
39
|
+
storageModeRef.current = storageMode;
|
|
40
|
+
const refreshStateFromDoc = useCallback((currentDoc) => {
|
|
41
|
+
setStorageMode(getDatabaseDocumentModel(currentDoc));
|
|
42
|
+
setColumns(getColumns(currentDoc));
|
|
43
|
+
setViews(getViews(currentDoc));
|
|
44
|
+
}, []);
|
|
45
|
+
useEffect(() => {
|
|
46
|
+
if (!store || !isReady || !databaseId) {
|
|
47
|
+
setDoc(null);
|
|
48
|
+
setColumns([]);
|
|
49
|
+
setViews([]);
|
|
50
|
+
setStorageMode("empty");
|
|
51
|
+
setLoading(false);
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
let mounted = true;
|
|
55
|
+
const loadDoc = async () => {
|
|
56
|
+
try {
|
|
57
|
+
setLoading(true);
|
|
58
|
+
setError(null);
|
|
59
|
+
const ydoc = new Y.Doc({ guid: databaseId, gc: false });
|
|
60
|
+
const storedContent = await store.getDocumentContent(databaseId);
|
|
61
|
+
if (storedContent && storedContent.length > 0) {
|
|
62
|
+
Y.applyUpdate(ydoc, storedContent);
|
|
63
|
+
}
|
|
64
|
+
if (!mounted) {
|
|
65
|
+
ydoc.destroy();
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
if (!isDatabaseDocInitialized(ydoc)) {
|
|
69
|
+
initializeDatabaseDoc(ydoc);
|
|
70
|
+
}
|
|
71
|
+
docRef.current = ydoc;
|
|
72
|
+
setDoc(ydoc);
|
|
73
|
+
refreshStateFromDoc(ydoc);
|
|
74
|
+
} catch (err) {
|
|
75
|
+
if (!mounted) return;
|
|
76
|
+
setError(err instanceof Error ? err : new Error(String(err)));
|
|
77
|
+
} finally {
|
|
78
|
+
if (mounted) setLoading(false);
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
loadDoc();
|
|
82
|
+
return () => {
|
|
83
|
+
mounted = false;
|
|
84
|
+
if (docRef.current && store) {
|
|
85
|
+
const content = Y.encodeStateAsUpdate(docRef.current);
|
|
86
|
+
store.setDocumentContent(databaseId, content).catch(() => {
|
|
87
|
+
});
|
|
88
|
+
docRef.current.destroy();
|
|
89
|
+
docRef.current = null;
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
}, [store, isReady, databaseId, refreshStateFromDoc]);
|
|
93
|
+
useEffect(() => {
|
|
94
|
+
if (!doc) return;
|
|
95
|
+
const handleUpdate = () => {
|
|
96
|
+
refreshStateFromDoc(doc);
|
|
97
|
+
};
|
|
98
|
+
doc.on("update", handleUpdate);
|
|
99
|
+
handleUpdate();
|
|
100
|
+
return () => {
|
|
101
|
+
doc.off("update", handleUpdate);
|
|
102
|
+
};
|
|
103
|
+
}, [doc, refreshStateFromDoc]);
|
|
104
|
+
useEffect(() => {
|
|
105
|
+
if (!doc || !store) return;
|
|
106
|
+
let saveTimeout = null;
|
|
107
|
+
const handleUpdate = () => {
|
|
108
|
+
if (saveTimeout) {
|
|
109
|
+
clearTimeout(saveTimeout);
|
|
110
|
+
}
|
|
111
|
+
saveTimeout = setTimeout(() => {
|
|
112
|
+
if (docRef.current && store) {
|
|
113
|
+
const content = Y.encodeStateAsUpdate(docRef.current);
|
|
114
|
+
store.setDocumentContent(databaseId, content).catch(() => {
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
}, 500);
|
|
118
|
+
};
|
|
119
|
+
doc.on("update", handleUpdate);
|
|
120
|
+
return () => {
|
|
121
|
+
doc.off("update", handleUpdate);
|
|
122
|
+
if (saveTimeout) {
|
|
123
|
+
clearTimeout(saveTimeout);
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
}, [doc, store, databaseId]);
|
|
127
|
+
const handleCreateColumn = useCallback(
|
|
128
|
+
(definition) => {
|
|
129
|
+
if (!docRef.current) return null;
|
|
130
|
+
return createColumnOp(docRef.current, definition);
|
|
131
|
+
},
|
|
132
|
+
[]
|
|
133
|
+
);
|
|
134
|
+
const handleUpdateColumn = useCallback(
|
|
135
|
+
(columnId, updates) => {
|
|
136
|
+
if (!docRef.current) return;
|
|
137
|
+
updateColumnOp(docRef.current, columnId, updates);
|
|
138
|
+
},
|
|
139
|
+
[]
|
|
140
|
+
);
|
|
141
|
+
const handleDeleteColumn = useCallback((columnId) => {
|
|
142
|
+
if (!docRef.current) return;
|
|
143
|
+
deleteColumnOp(docRef.current, columnId);
|
|
144
|
+
}, []);
|
|
145
|
+
const handleReorderColumn = useCallback((columnId, newIndex) => {
|
|
146
|
+
if (!docRef.current) return;
|
|
147
|
+
reorderColumnOp(docRef.current, columnId, newIndex);
|
|
148
|
+
}, []);
|
|
149
|
+
const handleDuplicateColumn = useCallback((columnId, newName) => {
|
|
150
|
+
if (!docRef.current) return null;
|
|
151
|
+
return duplicateColumnOp(docRef.current, columnId, newName);
|
|
152
|
+
}, []);
|
|
153
|
+
const handleGetColumn = useCallback((columnId) => {
|
|
154
|
+
if (!docRef.current) return null;
|
|
155
|
+
return getColumn(docRef.current, columnId);
|
|
156
|
+
}, []);
|
|
157
|
+
const handleCreateView = useCallback((config) => {
|
|
158
|
+
if (!docRef.current) return null;
|
|
159
|
+
return createViewOp(docRef.current, config);
|
|
160
|
+
}, []);
|
|
161
|
+
const handleUpdateView = useCallback(
|
|
162
|
+
(viewId, updates) => {
|
|
163
|
+
if (!docRef.current) return;
|
|
164
|
+
updateViewOp(docRef.current, viewId, updates);
|
|
165
|
+
},
|
|
166
|
+
[]
|
|
167
|
+
);
|
|
168
|
+
const handleDeleteView = useCallback((viewId) => {
|
|
169
|
+
if (!docRef.current) return;
|
|
170
|
+
deleteViewOp(docRef.current, viewId);
|
|
171
|
+
}, []);
|
|
172
|
+
const handleDuplicateView = useCallback((viewId, newName) => {
|
|
173
|
+
if (!docRef.current) return null;
|
|
174
|
+
return duplicateViewOp(docRef.current, viewId, newName);
|
|
175
|
+
}, []);
|
|
176
|
+
const handleGetView = useCallback((viewId) => {
|
|
177
|
+
if (!docRef.current) return null;
|
|
178
|
+
return getView(docRef.current, viewId);
|
|
179
|
+
}, []);
|
|
180
|
+
return {
|
|
181
|
+
columns,
|
|
182
|
+
views,
|
|
183
|
+
doc,
|
|
184
|
+
storageMode,
|
|
185
|
+
loading,
|
|
186
|
+
error,
|
|
187
|
+
// Column operations
|
|
188
|
+
createColumn: handleCreateColumn,
|
|
189
|
+
updateColumn: handleUpdateColumn,
|
|
190
|
+
deleteColumn: handleDeleteColumn,
|
|
191
|
+
reorderColumn: handleReorderColumn,
|
|
192
|
+
duplicateColumn: handleDuplicateColumn,
|
|
193
|
+
getColumn: handleGetColumn,
|
|
194
|
+
// View operations
|
|
195
|
+
createView: handleCreateView,
|
|
196
|
+
updateView: handleUpdateView,
|
|
197
|
+
deleteView: handleDeleteView,
|
|
198
|
+
duplicateView: handleDuplicateView,
|
|
199
|
+
getView: handleGetView
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// src/hooks/useDatabase.ts
|
|
204
|
+
import {
|
|
205
|
+
queryRows,
|
|
206
|
+
createRow as createRowOp,
|
|
207
|
+
updateCells,
|
|
208
|
+
deleteRow as deleteRowOp,
|
|
209
|
+
moveRow,
|
|
210
|
+
fromCellProperties,
|
|
211
|
+
filterRows,
|
|
212
|
+
sortRows
|
|
213
|
+
} from "@xnetjs/data";
|
|
214
|
+
import { useState as useState2, useEffect as useEffect2, useCallback as useCallback2, useMemo, useRef as useRef2 } from "react";
|
|
215
|
+
function useDatabase(databaseId, options = {}) {
|
|
216
|
+
const { store, isReady } = useNodeStore();
|
|
217
|
+
const {
|
|
218
|
+
columns,
|
|
219
|
+
views,
|
|
220
|
+
doc,
|
|
221
|
+
storageMode,
|
|
222
|
+
loading: databaseDocLoading
|
|
223
|
+
} = useDatabaseDoc(databaseId);
|
|
224
|
+
const [rows, setRows] = useState2([]);
|
|
225
|
+
const [total, setTotal] = useState2(0);
|
|
226
|
+
const [cursor, setCursor] = useState2();
|
|
227
|
+
const [offset, setOffset] = useState2(0);
|
|
228
|
+
const [hasMore, setHasMore] = useState2(false);
|
|
229
|
+
const [loading, setLoading] = useState2(true);
|
|
230
|
+
const [loadingMore, setLoadingMore] = useState2(false);
|
|
231
|
+
const [error, setError] = useState2(null);
|
|
232
|
+
const [activeViewId, setActiveViewId] = useState2(options.view);
|
|
233
|
+
const { pageSize = 50, filters, sorts, search } = options;
|
|
234
|
+
const storeRef = useRef2(store);
|
|
235
|
+
storeRef.current = store;
|
|
236
|
+
const docRef = useRef2(doc);
|
|
237
|
+
docRef.current = doc;
|
|
238
|
+
const storageModeRef = useRef2(storageMode);
|
|
239
|
+
storageModeRef.current = storageMode;
|
|
240
|
+
const rowsRef = useRef2([]);
|
|
241
|
+
rowsRef.current = rows;
|
|
242
|
+
const resolvedActiveViewId = options.view ?? activeViewId;
|
|
243
|
+
const activeView = useMemo(() => {
|
|
244
|
+
if (resolvedActiveViewId) {
|
|
245
|
+
return views.find((v) => v.id === resolvedActiveViewId) ?? null;
|
|
246
|
+
}
|
|
247
|
+
return views[0] ?? null;
|
|
248
|
+
}, [views, resolvedActiveViewId]);
|
|
249
|
+
const effectiveFilters = useMemo(
|
|
250
|
+
() => filters ?? activeView?.filters ?? null,
|
|
251
|
+
[filters, activeView]
|
|
252
|
+
);
|
|
253
|
+
const effectiveSorts = useMemo(() => sorts ?? activeView?.sorts ?? [], [sorts, activeView]);
|
|
254
|
+
const materializedView = useMemo(
|
|
255
|
+
() => resolveDatabaseMaterializedView(databaseId, activeView, options.materializedView),
|
|
256
|
+
[activeView, databaseId, options.materializedView]
|
|
257
|
+
);
|
|
258
|
+
useEffect2(() => {
|
|
259
|
+
setActiveViewId(options.view);
|
|
260
|
+
}, [options.view]);
|
|
261
|
+
const fetchRows = useCallback2(
|
|
262
|
+
async (reset = true) => {
|
|
263
|
+
if (!store || !isReady) return;
|
|
264
|
+
try {
|
|
265
|
+
if (reset) {
|
|
266
|
+
setLoading(true);
|
|
267
|
+
setCursor(void 0);
|
|
268
|
+
setOffset(0);
|
|
269
|
+
} else {
|
|
270
|
+
setLoadingMore(true);
|
|
271
|
+
}
|
|
272
|
+
const queryOffset = reset ? 0 : offset;
|
|
273
|
+
const canonicalResult = await queryRows(store, databaseId, {
|
|
274
|
+
limit: pageSize * 10,
|
|
275
|
+
offset: queryOffset,
|
|
276
|
+
materializedView
|
|
277
|
+
});
|
|
278
|
+
let parsedRows = canonicalResult.rows.map((node) => nodeToRow(node, columns));
|
|
279
|
+
if (effectiveFilters && effectiveFilters.conditions.length > 0) {
|
|
280
|
+
parsedRows = filterRows(parsedRows, columns, effectiveFilters);
|
|
281
|
+
}
|
|
282
|
+
if (effectiveSorts.length > 0) {
|
|
283
|
+
parsedRows = sortRows(parsedRows, columns, effectiveSorts);
|
|
284
|
+
}
|
|
285
|
+
parsedRows = parsedRows.slice(0, pageSize);
|
|
286
|
+
if (reset) {
|
|
287
|
+
setRows(parsedRows);
|
|
288
|
+
} else {
|
|
289
|
+
setRows((prev) => [...prev, ...parsedRows]);
|
|
290
|
+
}
|
|
291
|
+
setTotal(parsedRows.length);
|
|
292
|
+
setCursor(void 0);
|
|
293
|
+
setOffset(queryOffset + canonicalResult.rows.length);
|
|
294
|
+
setHasMore(canonicalResult.hasMore);
|
|
295
|
+
setError(null);
|
|
296
|
+
} catch (err) {
|
|
297
|
+
setError(err instanceof Error ? err : new Error(String(err)));
|
|
298
|
+
} finally {
|
|
299
|
+
setLoading(false);
|
|
300
|
+
setLoadingMore(false);
|
|
301
|
+
}
|
|
302
|
+
},
|
|
303
|
+
[
|
|
304
|
+
store,
|
|
305
|
+
isReady,
|
|
306
|
+
databaseId,
|
|
307
|
+
pageSize,
|
|
308
|
+
cursor,
|
|
309
|
+
offset,
|
|
310
|
+
columns,
|
|
311
|
+
effectiveFilters,
|
|
312
|
+
effectiveSorts,
|
|
313
|
+
materializedView
|
|
314
|
+
]
|
|
315
|
+
);
|
|
316
|
+
useEffect2(() => {
|
|
317
|
+
if (databaseDocLoading) {
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
if (columns.length > 0) {
|
|
321
|
+
void fetchRows(true);
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
setRows([]);
|
|
325
|
+
setTotal(0);
|
|
326
|
+
setCursor(void 0);
|
|
327
|
+
setOffset(0);
|
|
328
|
+
setHasMore(false);
|
|
329
|
+
setError(null);
|
|
330
|
+
setLoading(false);
|
|
331
|
+
setLoadingMore(false);
|
|
332
|
+
}, [databaseId, columns.length, databaseDocLoading, doc, storageMode]);
|
|
333
|
+
useEffect2(() => {
|
|
334
|
+
if (!databaseDocLoading && columns.length > 0) {
|
|
335
|
+
void fetchRows(true);
|
|
336
|
+
}
|
|
337
|
+
}, [
|
|
338
|
+
databaseDocLoading,
|
|
339
|
+
effectiveFilters,
|
|
340
|
+
effectiveSorts,
|
|
341
|
+
search,
|
|
342
|
+
doc,
|
|
343
|
+
storageMode,
|
|
344
|
+
materializedView
|
|
345
|
+
]);
|
|
346
|
+
useEffect2(() => {
|
|
347
|
+
if (!store) return;
|
|
348
|
+
const unsubscribe = store.subscribe((event) => {
|
|
349
|
+
const node = event.node;
|
|
350
|
+
if (node?.schemaId === "xnet://xnet.fyi/DatabaseRow") {
|
|
351
|
+
const dbId = node.properties.database;
|
|
352
|
+
if (dbId === databaseId) {
|
|
353
|
+
fetchRows(true);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
});
|
|
357
|
+
return () => {
|
|
358
|
+
unsubscribe();
|
|
359
|
+
};
|
|
360
|
+
}, [store, databaseId, fetchRows, doc]);
|
|
361
|
+
const loadMore = useCallback2(async () => {
|
|
362
|
+
if (!hasMore || loadingMore) return;
|
|
363
|
+
await fetchRows(false);
|
|
364
|
+
}, [hasMore, loadingMore, fetchRows]);
|
|
365
|
+
const handleCreateRow = useCallback2(
|
|
366
|
+
async (values) => {
|
|
367
|
+
if (!storeRef.current) throw new Error("Store not ready");
|
|
368
|
+
const lastRow = rows[rows.length - 1];
|
|
369
|
+
const rowId = await createRowOp(storeRef.current, {
|
|
370
|
+
databaseId,
|
|
371
|
+
cells: values ?? {},
|
|
372
|
+
after: lastRow?.sortKey
|
|
373
|
+
});
|
|
374
|
+
return rowId;
|
|
375
|
+
},
|
|
376
|
+
[databaseId, rows]
|
|
377
|
+
);
|
|
378
|
+
const handleUpdateRow = useCallback2(
|
|
379
|
+
async (rowId, values) => {
|
|
380
|
+
if (!storeRef.current) throw new Error("Store not ready");
|
|
381
|
+
await updateCells(storeRef.current, rowId, values);
|
|
382
|
+
},
|
|
383
|
+
[]
|
|
384
|
+
);
|
|
385
|
+
const handleDeleteRow = useCallback2(async (rowId) => {
|
|
386
|
+
if (!storeRef.current) throw new Error("Store not ready");
|
|
387
|
+
await deleteRowOp(storeRef.current, rowId);
|
|
388
|
+
}, []);
|
|
389
|
+
const handleReorderRow = useCallback2(
|
|
390
|
+
async (rowId, before, after) => {
|
|
391
|
+
if (!storeRef.current) throw new Error("Store not ready");
|
|
392
|
+
const resolveBoundary = (value) => {
|
|
393
|
+
if (!value) return void 0;
|
|
394
|
+
const matchingRow = rowsRef.current.find((row) => row.id === value || row.sortKey === value);
|
|
395
|
+
return matchingRow?.sortKey ?? value;
|
|
396
|
+
};
|
|
397
|
+
await moveRow(storeRef.current, rowId, {
|
|
398
|
+
before: resolveBoundary(before),
|
|
399
|
+
after: resolveBoundary(after)
|
|
400
|
+
});
|
|
401
|
+
},
|
|
402
|
+
[]
|
|
403
|
+
);
|
|
404
|
+
const handleDeleteRows = useCallback2(async (rowIds) => {
|
|
405
|
+
if (!storeRef.current) throw new Error("Store not ready");
|
|
406
|
+
await Promise.all(rowIds.map((id) => deleteRowOp(storeRef.current, id)));
|
|
407
|
+
}, []);
|
|
408
|
+
return {
|
|
409
|
+
columns,
|
|
410
|
+
views,
|
|
411
|
+
rows,
|
|
412
|
+
total,
|
|
413
|
+
hasMore,
|
|
414
|
+
loadMore,
|
|
415
|
+
activeView,
|
|
416
|
+
setActiveView: setActiveViewId,
|
|
417
|
+
createRow: handleCreateRow,
|
|
418
|
+
updateRow: handleUpdateRow,
|
|
419
|
+
deleteRow: handleDeleteRow,
|
|
420
|
+
reorderRow: handleReorderRow,
|
|
421
|
+
deleteRows: handleDeleteRows,
|
|
422
|
+
loading,
|
|
423
|
+
loadingMore,
|
|
424
|
+
error,
|
|
425
|
+
refetch: () => fetchRows(true)
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
function resolveDatabaseMaterializedView(databaseId, activeView, option) {
|
|
429
|
+
if (!option) {
|
|
430
|
+
return void 0;
|
|
431
|
+
}
|
|
432
|
+
if (option === true) {
|
|
433
|
+
return activeView ? { viewId: `database:${databaseId}:view:${activeView.id}` } : void 0;
|
|
434
|
+
}
|
|
435
|
+
return option;
|
|
436
|
+
}
|
|
437
|
+
function nodeToRow(node, _columns) {
|
|
438
|
+
const cells = fromCellProperties(node.properties);
|
|
439
|
+
return {
|
|
440
|
+
id: node.id,
|
|
441
|
+
sortKey: node.properties.sortKey,
|
|
442
|
+
cells,
|
|
443
|
+
createdAt: node.createdAt,
|
|
444
|
+
createdBy: node.createdBy
|
|
445
|
+
};
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// src/hooks/useDatabaseRow.ts
|
|
449
|
+
import { getRow, updateCells as updateCells2, deleteRow as deleteRowOp2 } from "@xnetjs/data";
|
|
450
|
+
import { useState as useState3, useEffect as useEffect3, useCallback as useCallback3, useRef as useRef3 } from "react";
|
|
451
|
+
import * as Y2 from "yjs";
|
|
452
|
+
function useDatabaseRow(rowId) {
|
|
453
|
+
const { store, isReady } = useNodeStore();
|
|
454
|
+
const [row, setRow] = useState3(null);
|
|
455
|
+
const [doc, setDoc] = useState3(null);
|
|
456
|
+
const [loading, setLoading] = useState3(true);
|
|
457
|
+
const [error, setError] = useState3(null);
|
|
458
|
+
const optimisticRef = useRef3({});
|
|
459
|
+
const storeRef = useRef3(store);
|
|
460
|
+
storeRef.current = store;
|
|
461
|
+
useEffect3(() => {
|
|
462
|
+
if (!store || !isReady || !rowId) {
|
|
463
|
+
setRow(null);
|
|
464
|
+
setDoc(null);
|
|
465
|
+
setLoading(false);
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
468
|
+
let mounted = true;
|
|
469
|
+
const load = async () => {
|
|
470
|
+
try {
|
|
471
|
+
setLoading(true);
|
|
472
|
+
const node = await getRow(store, rowId);
|
|
473
|
+
if (!mounted) return;
|
|
474
|
+
if (node) {
|
|
475
|
+
setRow({
|
|
476
|
+
id: node.id,
|
|
477
|
+
databaseId: node.properties.database,
|
|
478
|
+
sortKey: node.properties.sortKey,
|
|
479
|
+
cells: node.cells,
|
|
480
|
+
createdAt: node.createdAt,
|
|
481
|
+
createdBy: node.createdBy
|
|
482
|
+
});
|
|
483
|
+
const storedContent = await store.getDocumentContent(rowId);
|
|
484
|
+
if (storedContent && storedContent.length > 0) {
|
|
485
|
+
const ydoc = new Y2.Doc({ guid: rowId, gc: false });
|
|
486
|
+
Y2.applyUpdate(ydoc, storedContent);
|
|
487
|
+
setDoc(ydoc);
|
|
488
|
+
}
|
|
489
|
+
} else {
|
|
490
|
+
setRow(null);
|
|
491
|
+
setDoc(null);
|
|
492
|
+
}
|
|
493
|
+
setError(null);
|
|
494
|
+
} catch (err) {
|
|
495
|
+
if (!mounted) return;
|
|
496
|
+
setError(err instanceof Error ? err : new Error(String(err)));
|
|
497
|
+
} finally {
|
|
498
|
+
if (mounted) setLoading(false);
|
|
499
|
+
}
|
|
500
|
+
};
|
|
501
|
+
load();
|
|
502
|
+
const unsubscribe = store.subscribeToNode(rowId, () => {
|
|
503
|
+
load();
|
|
504
|
+
});
|
|
505
|
+
return () => {
|
|
506
|
+
mounted = false;
|
|
507
|
+
unsubscribe();
|
|
508
|
+
};
|
|
509
|
+
}, [store, isReady, rowId]);
|
|
510
|
+
const handleUpdate = useCallback3(
|
|
511
|
+
async (values) => {
|
|
512
|
+
if (!storeRef.current) throw new Error("Store not ready");
|
|
513
|
+
optimisticRef.current = { ...optimisticRef.current, ...values };
|
|
514
|
+
setRow(
|
|
515
|
+
(prev) => prev ? {
|
|
516
|
+
...prev,
|
|
517
|
+
cells: { ...prev.cells, ...values }
|
|
518
|
+
} : null
|
|
519
|
+
);
|
|
520
|
+
try {
|
|
521
|
+
await updateCells2(storeRef.current, rowId, values);
|
|
522
|
+
for (const key of Object.keys(values)) {
|
|
523
|
+
delete optimisticRef.current[key];
|
|
524
|
+
}
|
|
525
|
+
} catch (err) {
|
|
526
|
+
setRow((prev) => {
|
|
527
|
+
if (!prev) return null;
|
|
528
|
+
const reverted = { ...prev.cells };
|
|
529
|
+
for (const key of Object.keys(values)) {
|
|
530
|
+
delete reverted[key];
|
|
531
|
+
}
|
|
532
|
+
return { ...prev, cells: reverted };
|
|
533
|
+
});
|
|
534
|
+
throw err;
|
|
535
|
+
}
|
|
536
|
+
},
|
|
537
|
+
[rowId]
|
|
538
|
+
);
|
|
539
|
+
const handleDelete = useCallback3(async () => {
|
|
540
|
+
if (!storeRef.current) throw new Error("Store not ready");
|
|
541
|
+
await deleteRowOp2(storeRef.current, rowId);
|
|
542
|
+
}, [rowId]);
|
|
543
|
+
return {
|
|
544
|
+
row,
|
|
545
|
+
doc,
|
|
546
|
+
update: handleUpdate,
|
|
547
|
+
delete: handleDelete,
|
|
548
|
+
loading,
|
|
549
|
+
error
|
|
550
|
+
};
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
// src/hooks/useCell.ts
|
|
554
|
+
import { updateCell, cellKey } from "@xnetjs/data";
|
|
555
|
+
import { useState as useState4, useEffect as useEffect4, useCallback as useCallback4, useRef as useRef4 } from "react";
|
|
556
|
+
function useCell(rowId, columnId, options = {}) {
|
|
557
|
+
const { store, isReady } = useNodeStore();
|
|
558
|
+
const { debounce: debounceMs = 300 } = options;
|
|
559
|
+
const [value, setValue] = useState4(null);
|
|
560
|
+
const [saving, setSaving] = useState4(false);
|
|
561
|
+
const [error, setError] = useState4(null);
|
|
562
|
+
const debounceRef = useRef4();
|
|
563
|
+
const storeRef = useRef4(store);
|
|
564
|
+
storeRef.current = store;
|
|
565
|
+
useEffect4(() => {
|
|
566
|
+
if (!store || !isReady || !rowId) return;
|
|
567
|
+
store.get(rowId).then((node) => {
|
|
568
|
+
if (node) {
|
|
569
|
+
const key = cellKey(columnId);
|
|
570
|
+
setValue(node.properties[key] ?? null);
|
|
571
|
+
}
|
|
572
|
+
});
|
|
573
|
+
}, [store, isReady, rowId, columnId]);
|
|
574
|
+
useEffect4(() => {
|
|
575
|
+
if (!store || !rowId) return;
|
|
576
|
+
const unsubscribe = store.subscribeToNode(rowId, (event) => {
|
|
577
|
+
if (event.node) {
|
|
578
|
+
const key = cellKey(columnId);
|
|
579
|
+
setValue(event.node.properties[key] ?? null);
|
|
580
|
+
}
|
|
581
|
+
});
|
|
582
|
+
return unsubscribe;
|
|
583
|
+
}, [store, rowId, columnId]);
|
|
584
|
+
const handleSetValue = useCallback4(
|
|
585
|
+
(newValue) => {
|
|
586
|
+
setValue(newValue);
|
|
587
|
+
setError(null);
|
|
588
|
+
if (debounceRef.current) {
|
|
589
|
+
clearTimeout(debounceRef.current);
|
|
590
|
+
}
|
|
591
|
+
debounceRef.current = setTimeout(async () => {
|
|
592
|
+
if (!storeRef.current) return;
|
|
593
|
+
try {
|
|
594
|
+
setSaving(true);
|
|
595
|
+
await updateCell(storeRef.current, rowId, columnId, newValue);
|
|
596
|
+
} catch (err) {
|
|
597
|
+
setError(err instanceof Error ? err : new Error(String(err)));
|
|
598
|
+
} finally {
|
|
599
|
+
setSaving(false);
|
|
600
|
+
}
|
|
601
|
+
}, debounceMs);
|
|
602
|
+
},
|
|
603
|
+
[rowId, columnId, debounceMs]
|
|
604
|
+
);
|
|
605
|
+
const clear = useCallback4(() => {
|
|
606
|
+
handleSetValue(null);
|
|
607
|
+
}, [handleSetValue]);
|
|
608
|
+
useEffect4(() => {
|
|
609
|
+
return () => {
|
|
610
|
+
if (debounceRef.current) {
|
|
611
|
+
clearTimeout(debounceRef.current);
|
|
612
|
+
}
|
|
613
|
+
};
|
|
614
|
+
}, []);
|
|
615
|
+
return {
|
|
616
|
+
value,
|
|
617
|
+
setValue: handleSetValue,
|
|
618
|
+
clear,
|
|
619
|
+
saving,
|
|
620
|
+
error
|
|
621
|
+
};
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
// src/hooks/useRelatedRows.ts
|
|
625
|
+
import { fromCellProperties as fromCellProperties2 } from "@xnetjs/data";
|
|
626
|
+
import { useState as useState5, useEffect as useEffect5, useMemo as useMemo2 } from "react";
|
|
627
|
+
function useRelatedRows(rowIds) {
|
|
628
|
+
const { store, isReady } = useNodeStore();
|
|
629
|
+
const [rows, setRows] = useState5([]);
|
|
630
|
+
const [loading, setLoading] = useState5(true);
|
|
631
|
+
const [error, setError] = useState5(null);
|
|
632
|
+
const idsKey = useMemo2(() => rowIds.join(","), [rowIds]);
|
|
633
|
+
useEffect5(() => {
|
|
634
|
+
if (!store || !isReady) return;
|
|
635
|
+
if (rowIds.length === 0) {
|
|
636
|
+
setRows([]);
|
|
637
|
+
setLoading(false);
|
|
638
|
+
return;
|
|
639
|
+
}
|
|
640
|
+
let cancelled = false;
|
|
641
|
+
const fetchRows = async () => {
|
|
642
|
+
try {
|
|
643
|
+
setLoading(true);
|
|
644
|
+
const results = await Promise.all(
|
|
645
|
+
rowIds.map(async (id) => {
|
|
646
|
+
try {
|
|
647
|
+
return await store.get(id);
|
|
648
|
+
} catch {
|
|
649
|
+
return null;
|
|
650
|
+
}
|
|
651
|
+
})
|
|
652
|
+
);
|
|
653
|
+
if (!cancelled) {
|
|
654
|
+
const loadedRows = results.filter((node) => node !== null).map((node) => nodeToRow2(node));
|
|
655
|
+
setRows(loadedRows);
|
|
656
|
+
setError(null);
|
|
657
|
+
}
|
|
658
|
+
} catch (err) {
|
|
659
|
+
if (!cancelled) {
|
|
660
|
+
setError(err instanceof Error ? err : new Error(String(err)));
|
|
661
|
+
}
|
|
662
|
+
} finally {
|
|
663
|
+
if (!cancelled) {
|
|
664
|
+
setLoading(false);
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
};
|
|
668
|
+
fetchRows();
|
|
669
|
+
return () => {
|
|
670
|
+
cancelled = true;
|
|
671
|
+
};
|
|
672
|
+
}, [store, isReady, idsKey, rowIds]);
|
|
673
|
+
return { rows, loading, error };
|
|
674
|
+
}
|
|
675
|
+
function nodeToRow2(node) {
|
|
676
|
+
const cells = fromCellProperties2(node.properties);
|
|
677
|
+
return {
|
|
678
|
+
id: node.id,
|
|
679
|
+
sortKey: node.properties.sortKey ?? "",
|
|
680
|
+
cells,
|
|
681
|
+
createdAt: node.createdAt,
|
|
682
|
+
createdBy: node.createdBy
|
|
683
|
+
};
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
// src/hooks/useReverseRelations.ts
|
|
687
|
+
import { fromCellProperties as fromCellProperties3, getColumns as getColumns2, isDatabaseDocInitialized as isDatabaseDocInitialized2 } from "@xnetjs/data";
|
|
688
|
+
import { useState as useState6, useEffect as useEffect6 } from "react";
|
|
689
|
+
import * as Y3 from "yjs";
|
|
690
|
+
function useReverseRelations(rowId, databaseId) {
|
|
691
|
+
const { store, isReady } = useNodeStore();
|
|
692
|
+
const [relations, setRelations] = useState6([]);
|
|
693
|
+
const [loading, setLoading] = useState6(true);
|
|
694
|
+
const [error, setError] = useState6(null);
|
|
695
|
+
const [refetchKey, setRefetchKey] = useState6(0);
|
|
696
|
+
useEffect6(() => {
|
|
697
|
+
if (!store || !isReady || !rowId || !databaseId) {
|
|
698
|
+
setLoading(false);
|
|
699
|
+
return;
|
|
700
|
+
}
|
|
701
|
+
let cancelled = false;
|
|
702
|
+
const findReverseRelations = async () => {
|
|
703
|
+
try {
|
|
704
|
+
setLoading(true);
|
|
705
|
+
const allDatabases = await store.list({
|
|
706
|
+
schemaId: "xnet://xnet.fyi/Database"
|
|
707
|
+
});
|
|
708
|
+
const reverseRelations = [];
|
|
709
|
+
for (const db of allDatabases) {
|
|
710
|
+
if (cancelled) break;
|
|
711
|
+
try {
|
|
712
|
+
const docContent = await store.getDocumentContent(db.id);
|
|
713
|
+
if (!docContent || docContent.length === 0) continue;
|
|
714
|
+
const doc = new Y3.Doc({ guid: db.id });
|
|
715
|
+
Y3.applyUpdate(doc, docContent);
|
|
716
|
+
if (!isDatabaseDocInitialized2(doc)) {
|
|
717
|
+
doc.destroy();
|
|
718
|
+
continue;
|
|
719
|
+
}
|
|
720
|
+
const columns = getColumns2(doc);
|
|
721
|
+
const relationColumns = columns.filter(
|
|
722
|
+
(col) => col.type === "relation" && col.config?.targetDatabase === databaseId
|
|
723
|
+
);
|
|
724
|
+
doc.destroy();
|
|
725
|
+
if (relationColumns.length === 0) continue;
|
|
726
|
+
for (const col of relationColumns) {
|
|
727
|
+
if (cancelled) break;
|
|
728
|
+
const allRows = await store.list({
|
|
729
|
+
schemaId: "xnet://xnet.fyi/DatabaseRow"
|
|
730
|
+
});
|
|
731
|
+
const rows = allRows.filter((row) => row.properties.database === db.id);
|
|
732
|
+
for (const node of rows) {
|
|
733
|
+
const cellKey2 = `cell_${col.id}`;
|
|
734
|
+
const cellValue = node.properties[cellKey2];
|
|
735
|
+
if (Array.isArray(cellValue) && cellValue.includes(rowId)) {
|
|
736
|
+
reverseRelations.push({
|
|
737
|
+
row: nodeToRow3(node),
|
|
738
|
+
column: col,
|
|
739
|
+
sourceDatabaseId: db.id,
|
|
740
|
+
sourceDatabaseTitle: db.properties.title ?? "Untitled"
|
|
741
|
+
});
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
} catch {
|
|
746
|
+
continue;
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
if (!cancelled) {
|
|
750
|
+
setRelations(reverseRelations);
|
|
751
|
+
setError(null);
|
|
752
|
+
}
|
|
753
|
+
} catch (err) {
|
|
754
|
+
if (!cancelled) {
|
|
755
|
+
setError(err instanceof Error ? err : new Error(String(err)));
|
|
756
|
+
}
|
|
757
|
+
} finally {
|
|
758
|
+
if (!cancelled) {
|
|
759
|
+
setLoading(false);
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
};
|
|
763
|
+
findReverseRelations();
|
|
764
|
+
return () => {
|
|
765
|
+
cancelled = true;
|
|
766
|
+
};
|
|
767
|
+
}, [store, isReady, rowId, databaseId, refetchKey]);
|
|
768
|
+
const refetch = () => setRefetchKey((k) => k + 1);
|
|
769
|
+
return { relations, loading, error, refetch };
|
|
770
|
+
}
|
|
771
|
+
function nodeToRow3(node) {
|
|
772
|
+
const cells = fromCellProperties3(node.properties);
|
|
773
|
+
return {
|
|
774
|
+
id: node.id,
|
|
775
|
+
sortKey: node.properties.sortKey ?? "",
|
|
776
|
+
cells,
|
|
777
|
+
createdAt: node.createdAt,
|
|
778
|
+
createdBy: node.createdBy
|
|
779
|
+
};
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
// src/hooks/useDatabaseSchema.ts
|
|
783
|
+
import { extractSchemaFromDoc } from "@xnetjs/data";
|
|
784
|
+
import { useState as useState7, useEffect as useEffect7, useMemo as useMemo3, useRef as useRef5 } from "react";
|
|
785
|
+
import * as Y4 from "yjs";
|
|
786
|
+
function useDatabaseSchema(databaseId) {
|
|
787
|
+
const { store, isReady } = useNodeStore();
|
|
788
|
+
const [schema, setSchema] = useState7(null);
|
|
789
|
+
const [metadata, setMetadata] = useState7(null);
|
|
790
|
+
const [schemaIRI, setSchemaIRI] = useState7(null);
|
|
791
|
+
const [loading, setLoading] = useState7(true);
|
|
792
|
+
const [error, setError] = useState7(null);
|
|
793
|
+
const [refreshKey, setRefreshKey] = useState7(0);
|
|
794
|
+
const docRef = useRef5(null);
|
|
795
|
+
useEffect7(() => {
|
|
796
|
+
if (!store || !isReady || !databaseId) {
|
|
797
|
+
setSchema(null);
|
|
798
|
+
setMetadata(null);
|
|
799
|
+
setSchemaIRI(null);
|
|
800
|
+
setLoading(false);
|
|
801
|
+
return;
|
|
802
|
+
}
|
|
803
|
+
let mounted = true;
|
|
804
|
+
const loadSchema = async () => {
|
|
805
|
+
try {
|
|
806
|
+
setLoading(true);
|
|
807
|
+
setError(null);
|
|
808
|
+
const ydoc = new Y4.Doc({ guid: databaseId, gc: false });
|
|
809
|
+
const storedContent = await store.getDocumentContent(databaseId);
|
|
810
|
+
if (storedContent && storedContent.length > 0) {
|
|
811
|
+
Y4.applyUpdate(ydoc, storedContent);
|
|
812
|
+
}
|
|
813
|
+
if (!mounted) {
|
|
814
|
+
ydoc.destroy();
|
|
815
|
+
return;
|
|
816
|
+
}
|
|
817
|
+
docRef.current = ydoc;
|
|
818
|
+
const extractedSchema = extractSchemaFromDoc(databaseId, ydoc);
|
|
819
|
+
if (!extractedSchema) {
|
|
820
|
+
setSchema(null);
|
|
821
|
+
setMetadata(null);
|
|
822
|
+
setSchemaIRI(null);
|
|
823
|
+
return;
|
|
824
|
+
}
|
|
825
|
+
const dataMap = ydoc.getMap("data");
|
|
826
|
+
const meta = dataMap.get("schema");
|
|
827
|
+
if (!mounted) return;
|
|
828
|
+
setSchema(extractedSchema);
|
|
829
|
+
setMetadata(meta ?? null);
|
|
830
|
+
setSchemaIRI(extractedSchema["@id"]);
|
|
831
|
+
} catch (err) {
|
|
832
|
+
if (!mounted) return;
|
|
833
|
+
setError(err instanceof Error ? err : new Error(String(err)));
|
|
834
|
+
} finally {
|
|
835
|
+
if (mounted) setLoading(false);
|
|
836
|
+
}
|
|
837
|
+
};
|
|
838
|
+
void loadSchema();
|
|
839
|
+
return () => {
|
|
840
|
+
mounted = false;
|
|
841
|
+
if (docRef.current) {
|
|
842
|
+
docRef.current.destroy();
|
|
843
|
+
docRef.current = null;
|
|
844
|
+
}
|
|
845
|
+
};
|
|
846
|
+
}, [store, isReady, databaseId, refreshKey]);
|
|
847
|
+
useEffect7(() => {
|
|
848
|
+
const doc = docRef.current;
|
|
849
|
+
if (!doc || !databaseId) return;
|
|
850
|
+
const dataMap = doc.getMap("data");
|
|
851
|
+
const handleSchemaChange = () => {
|
|
852
|
+
const meta = dataMap.get("schema");
|
|
853
|
+
if (!meta) return;
|
|
854
|
+
const extractedSchema = extractSchemaFromDoc(databaseId, doc);
|
|
855
|
+
if (extractedSchema) {
|
|
856
|
+
setSchema(extractedSchema);
|
|
857
|
+
setMetadata(meta);
|
|
858
|
+
setSchemaIRI(extractedSchema["@id"]);
|
|
859
|
+
}
|
|
860
|
+
};
|
|
861
|
+
dataMap.observe(handleSchemaChange);
|
|
862
|
+
return () => {
|
|
863
|
+
dataMap.unobserve(handleSchemaChange);
|
|
864
|
+
};
|
|
865
|
+
}, [databaseId, schema]);
|
|
866
|
+
const refresh = useMemo3(
|
|
867
|
+
() => () => {
|
|
868
|
+
setRefreshKey((k) => k + 1);
|
|
869
|
+
},
|
|
870
|
+
[]
|
|
871
|
+
);
|
|
872
|
+
return useMemo3(
|
|
873
|
+
() => ({
|
|
874
|
+
schema,
|
|
875
|
+
metadata,
|
|
876
|
+
schemaIRI,
|
|
877
|
+
loading,
|
|
878
|
+
error,
|
|
879
|
+
refresh
|
|
880
|
+
}),
|
|
881
|
+
[schema, metadata, schemaIRI, loading, error, refresh]
|
|
882
|
+
);
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
export {
|
|
886
|
+
useDatabaseDoc,
|
|
887
|
+
useDatabase,
|
|
888
|
+
useDatabaseRow,
|
|
889
|
+
useCell,
|
|
890
|
+
useRelatedRows,
|
|
891
|
+
useReverseRelations,
|
|
892
|
+
useDatabaseSchema
|
|
893
|
+
};
|