jazz-tools 0.18.31 → 0.18.33

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 (53) hide show
  1. package/.svelte-kit/__package__/media/image.svelte +1 -1
  2. package/.svelte-kit/__package__/tests/media/image.svelte.test.js +1 -1
  3. package/.turbo/turbo-build.log +58 -58
  4. package/CHANGELOG.md +23 -0
  5. package/dist/{chunk-6BIYT3KH.js → chunk-OSQ7S47Q.js} +3 -3
  6. package/dist/chunk-OSQ7S47Q.js.map +1 -0
  7. package/dist/index.js +1 -1
  8. package/dist/inspector/{custom-element-RQTLPAPJ.js → custom-element-RBBL46TI.js} +636 -193
  9. package/dist/inspector/custom-element-RBBL46TI.js.map +1 -0
  10. package/dist/inspector/index.js +626 -183
  11. package/dist/inspector/index.js.map +1 -1
  12. package/dist/inspector/register-custom-element.js +1 -1
  13. package/dist/inspector/tests/ui/data-table.test.d.ts +2 -0
  14. package/dist/inspector/tests/ui/data-table.test.d.ts.map +1 -0
  15. package/dist/inspector/tests/viewer/history-view.test.d.ts +2 -0
  16. package/dist/inspector/tests/viewer/history-view.test.d.ts.map +1 -0
  17. package/dist/inspector/ui/data-table.d.ts +23 -0
  18. package/dist/inspector/ui/data-table.d.ts.map +1 -0
  19. package/dist/inspector/ui/index.d.ts +1 -0
  20. package/dist/inspector/ui/index.d.ts.map +1 -1
  21. package/dist/inspector/viewer/history-view.d.ts +6 -0
  22. package/dist/inspector/viewer/history-view.d.ts.map +1 -0
  23. package/dist/inspector/viewer/page.d.ts.map +1 -1
  24. package/dist/svelte/media/image.svelte +1 -1
  25. package/dist/svelte/tests/media/image.svelte.test.js +1 -1
  26. package/dist/testing.js +1 -1
  27. package/dist/tools/coValues/coFeed.d.ts +1 -1
  28. package/dist/tools/coValues/coFeed.d.ts.map +1 -1
  29. package/dist/tools/implementation/zodSchema/schemaTypes/FileStreamSchema.d.ts +2 -2
  30. package/dist/tools/implementation/zodSchema/schemaTypes/FileStreamSchema.d.ts.map +1 -1
  31. package/dist/worker/index.d.ts +2 -1
  32. package/dist/worker/index.d.ts.map +1 -1
  33. package/dist/worker/index.js +2 -1
  34. package/dist/worker/index.js.map +1 -1
  35. package/package.json +4 -4
  36. package/src/inspector/tests/ui/data-table.test.tsx +296 -0
  37. package/src/inspector/tests/viewer/history-view.test.tsx +246 -0
  38. package/src/inspector/ui/data-table.tsx +265 -0
  39. package/src/inspector/ui/index.ts +1 -0
  40. package/src/inspector/viewer/history-view.tsx +379 -0
  41. package/src/inspector/viewer/page.tsx +2 -0
  42. package/src/media/create-image-factory.test.ts +2 -2
  43. package/src/svelte/media/image.svelte +1 -1
  44. package/src/svelte/tests/media/image.svelte.test.ts +1 -1
  45. package/src/tools/coValues/coFeed.ts +2 -2
  46. package/src/tools/coValues/interfaces.ts +1 -1
  47. package/src/tools/implementation/zodSchema/schemaTypes/FileStreamSchema.ts +1 -1
  48. package/src/tools/tests/CoValueCoreSubscription.test.ts +1 -1
  49. package/src/tools/tests/coFeed.test.ts +1 -1
  50. package/src/tools/tests/load.test.ts +1 -1
  51. package/src/worker/index.ts +9 -1
  52. package/dist/chunk-6BIYT3KH.js.map +0 -1
  53. package/dist/inspector/custom-element-RQTLPAPJ.js.map +0 -1
@@ -0,0 +1,246 @@
1
+ // @vitest-environment happy-dom
2
+ import { afterEach, beforeAll, describe, expect, it } from "vitest";
3
+ import { createJazzTestAccount, setupJazzTestSync } from "jazz-tools/testing";
4
+ import { co, z } from "jazz-tools";
5
+ import {
6
+ cleanup,
7
+ fireEvent,
8
+ render,
9
+ screen,
10
+ waitFor,
11
+ } from "@testing-library/react";
12
+ import { HistoryView } from "../../viewer/history-view";
13
+ import { setup } from "goober";
14
+ import React from "react";
15
+
16
+ function extractAction(row: HTMLElement | null | undefined) {
17
+ if (!row) return "";
18
+ // index 0: author, index 1: action, index 2: timestamp
19
+ return row.querySelectorAll("td")?.[1]?.textContent ?? "";
20
+ }
21
+
22
+ function extractActions(): string[] {
23
+ // slice 2 to skip header and filters
24
+ return screen.getAllByRole("row").slice(2).map(extractAction);
25
+ }
26
+
27
+ describe("HistoryView", async () => {
28
+ const account = await setupJazzTestSync();
29
+ const account2 = await createJazzTestAccount();
30
+
31
+ beforeAll(() => {
32
+ // setup goober
33
+ setup(React.createElement);
34
+ });
35
+
36
+ afterEach(() => {
37
+ cleanup();
38
+ });
39
+
40
+ it("should render a history card", async () => {
41
+ const value = co
42
+ .map({
43
+ foo: z.string(),
44
+ })
45
+ .create({ foo: "bar" }, account);
46
+
47
+ render(
48
+ <HistoryView coValue={value.$jazz.raw} node={value.$jazz.localNode} />,
49
+ );
50
+
51
+ expect(
52
+ screen.getAllByText('Property "foo" has been set to "bar"'),
53
+ ).toHaveLength(1);
54
+ });
55
+
56
+ describe("co.map", () => {
57
+ it("should render co.map changes", async () => {
58
+ const value = co
59
+ .map({
60
+ pet: z.string(),
61
+ age: z.number(),
62
+ certified: z.boolean().optional(),
63
+ })
64
+ .create({ pet: "dog", age: 10, certified: false }, account);
65
+
66
+ value.$jazz.set("pet", "cat");
67
+ value.$jazz.set("age", 20);
68
+ value.$jazz.set("certified", true);
69
+ value.$jazz.delete("certified");
70
+
71
+ render(
72
+ <HistoryView coValue={value.$jazz.raw} node={value.$jazz.localNode} />,
73
+ );
74
+
75
+ const history = [
76
+ 'Property "pet" has been set to "dog"',
77
+ 'Property "age" has been set to "10"',
78
+ 'Property "certified" has been set to "false"',
79
+ 'Property "pet" has been set to "cat"',
80
+ 'Property "age" has been set to "20"',
81
+ 'Property "certified" has been set to "true"',
82
+ 'Property "certified" has been deleted',
83
+ ].toReversed(); // Default sort is descending
84
+
85
+ expect(screen.getAllByRole("row")).toHaveLength(history.length + 2);
86
+
87
+ await waitFor(() => {
88
+ expect(screen.getAllByRole("row")[2]?.textContent).toContain(
89
+ account.$jazz.id,
90
+ );
91
+ });
92
+
93
+ expect(extractActions()).toEqual(history);
94
+ });
95
+ });
96
+
97
+ describe("co.list", () => {
98
+ it("should render simple co.list changes", async () => {
99
+ const value = co.list(z.string()).create(["dog", "cat"], account);
100
+
101
+ value.$jazz.push("bird");
102
+
103
+ value.$jazz.splice(1, 0, "fish");
104
+
105
+ value.$jazz.shift();
106
+
107
+ render(
108
+ <HistoryView coValue={value.$jazz.raw} node={value.$jazz.localNode} />,
109
+ );
110
+
111
+ const history = [
112
+ '"dog" has been appended',
113
+ '"cat" has been appended',
114
+ '"bird" has been inserted after "cat"',
115
+ '"fish" has been inserted after "dog"',
116
+ '"dog" has been deleted',
117
+ ].toReversed(); // Default sort is descending
118
+
119
+ expect(extractActions()).toEqual(history);
120
+ });
121
+
122
+ it("should render changes of a co.list of co.maps", async () => {
123
+ const Animal = co.map({
124
+ pet: z.string(),
125
+ age: z.number(),
126
+ certified: z.boolean(),
127
+ });
128
+
129
+ const dog = Animal.create(
130
+ { pet: "dog", age: 10, certified: false },
131
+ account,
132
+ );
133
+ const cat = Animal.create(
134
+ { pet: "cat", age: 20, certified: true },
135
+ account,
136
+ );
137
+ const fish = Animal.create(
138
+ { pet: "fish", age: 30, certified: false },
139
+ account,
140
+ );
141
+ const bird = Animal.create(
142
+ { pet: "bird", age: 40, certified: true },
143
+ account,
144
+ );
145
+
146
+ const value = co.list(Animal).create([dog, cat], account);
147
+
148
+ value.$jazz.push(bird);
149
+
150
+ value.$jazz.splice(1, 0, fish);
151
+
152
+ value.$jazz.shift();
153
+
154
+ render(
155
+ <HistoryView coValue={value.$jazz.raw} node={value.$jazz.localNode} />,
156
+ );
157
+
158
+ const history = [
159
+ `"${dog.$jazz.id}" has been appended`,
160
+ `"${cat.$jazz.id}" has been appended`,
161
+ `"${bird.$jazz.id}" has been inserted after "${cat.$jazz.id}"`,
162
+ `"${fish.$jazz.id}" has been inserted after "${dog.$jazz.id}"`,
163
+ `"${dog.$jazz.id}" has been deleted`,
164
+ ].toReversed(); // Default sort is descending
165
+
166
+ expect(extractActions()).toEqual(history);
167
+ });
168
+ });
169
+
170
+ describe("co.group", () => {
171
+ it("should render co.group changes", async () => {
172
+ const group = co.group().create(account);
173
+
174
+ const group2 = co.group().create(account);
175
+
176
+ group.addMember(group2, "writer");
177
+
178
+ group.addMember(account2, "reader");
179
+ group.removeMember(account2);
180
+
181
+ const group3 = co.group().create(account);
182
+ group3.addMember(group, "inherit");
183
+
184
+ const { container } = render(
185
+ <HistoryView coValue={group.$jazz.raw} node={group.$jazz.localNode} />,
186
+ );
187
+
188
+ const history = [
189
+ `${account.$jazz.id} has been promoted to admin`,
190
+ expect.stringContaining(` has been revealed to `), // key revelation
191
+ expect.stringContaining('Property "readKey" has been set to'),
192
+ `Group ${group2.$jazz.id} has been promoted to writer`,
193
+ expect.stringContaining(" has been revealed to"),
194
+ `${account2.$jazz.id} has been promoted to reader`,
195
+ expect.stringContaining(" has been revealed to"),
196
+ // Member revocation: key rotation
197
+ expect.stringContaining(" has been revealed to"),
198
+ expect.stringContaining(" has been revealed to"),
199
+ expect.stringContaining('Property "readKey" has been set to'),
200
+ expect.stringContaining(" has been revealed to"),
201
+ `${account2.$jazz.id} has been revoked`,
202
+
203
+ // Group extension
204
+ // `Group become a member of ${group3.$jazz.id}`,
205
+ ].toReversed(); // Default sort is descending
206
+
207
+ const historyPage1 = history.slice(0, 10);
208
+ const historyPage2 = history.slice(10, 20);
209
+
210
+ // Page 1: 10 rows
211
+ expect(extractActions()).toEqual(historyPage1);
212
+
213
+ // Go to page 2
214
+ fireEvent.click(screen.getByText("»"));
215
+
216
+ // Page 2: 3 rows
217
+ expect(extractActions()).toEqual(historyPage2);
218
+ });
219
+ });
220
+
221
+ describe("co.account", () => {
222
+ it("should render co.account changes", async () => {
223
+ const account = await createJazzTestAccount({
224
+ creationProps: {
225
+ name: "John Doe",
226
+ },
227
+ });
228
+
229
+ const history = [
230
+ expect.stringContaining(' has been set to "admin"'),
231
+ expect.stringContaining(" has been revealed to "),
232
+ expect.stringContaining('Property "readKey" has been set to '),
233
+ `Property "profile" has been set to "${account.profile!.$jazz.id}"`,
234
+ ].toReversed(); // Default sort is descending
235
+
236
+ render(
237
+ <HistoryView
238
+ coValue={account.$jazz.raw}
239
+ node={account.$jazz.localNode}
240
+ />,
241
+ );
242
+
243
+ expect(extractActions()).toEqual(history);
244
+ });
245
+ });
246
+ });
@@ -0,0 +1,265 @@
1
+ import { useEffect, useMemo, useState } from "react";
2
+ import {
3
+ Table,
4
+ TableBody,
5
+ TableCell,
6
+ TableHead,
7
+ TableHeader,
8
+ TableRow,
9
+ } from "./table";
10
+ import { Button } from "./button";
11
+ import { Input } from "./input";
12
+
13
+ export type ColumnDef<T> = {
14
+ id: string;
15
+ header: string;
16
+ accessor: (row: T) => React.ReactNode;
17
+ sortable?: boolean;
18
+ filterable?: boolean;
19
+ sortFn?: (a: T, b: T) => number;
20
+ filterFn?: (row: T, filterValue: string) => boolean;
21
+ };
22
+
23
+ export type SortConfig = {
24
+ columnId: string;
25
+ direction: "asc" | "desc";
26
+ } | null;
27
+
28
+ export type DataTableProps<T> = {
29
+ columns: ColumnDef<T>[];
30
+ data: T[];
31
+ pageSize?: number;
32
+ initialSort?: SortConfig;
33
+ getRowKey: (row: T, index: number) => string;
34
+ emptyMessage?: string;
35
+ };
36
+
37
+ export function DataTable<T>({
38
+ columns,
39
+ data,
40
+ pageSize = 10,
41
+ initialSort = null,
42
+ getRowKey,
43
+ emptyMessage = "No data available",
44
+ }: DataTableProps<T>) {
45
+ const [currentPage, setCurrentPage] = useState(1);
46
+ const [sortConfig, setSortConfig] = useState<SortConfig>(initialSort);
47
+ const [filters, setFilters] = useState<Record<string, string>>({});
48
+
49
+ // Apply filtering
50
+ const filteredData = useMemo(() => {
51
+ return data.filter((row) => {
52
+ return Object.entries(filters).every(([columnId, filterValue]) => {
53
+ if (!filterValue) return true;
54
+
55
+ const column = columns.find((col) => col.id === columnId);
56
+ if (!column?.filterable) return true;
57
+
58
+ if (column.filterFn) {
59
+ return column.filterFn(row, filterValue);
60
+ }
61
+
62
+ // Default filter: convert to string and check inclusion
63
+ const cellValue = String(column.accessor(row));
64
+ return cellValue.toLowerCase().includes(filterValue.toLowerCase());
65
+ });
66
+ });
67
+ }, [data, filters, columns]);
68
+
69
+ // Apply sorting
70
+ const sortedData = useMemo(() => {
71
+ if (!sortConfig) return filteredData;
72
+
73
+ const column = columns.find((col) => col.id === sortConfig.columnId);
74
+ if (!column?.sortable) return filteredData;
75
+
76
+ const sorted = [...filteredData].sort((a, b) => {
77
+ if (column.sortFn) {
78
+ return column.sortFn(a, b);
79
+ }
80
+
81
+ // Default sort: compare string values
82
+ const aValue = String(column.accessor(a));
83
+ const bValue = String(column.accessor(b));
84
+ return aValue.localeCompare(bValue);
85
+ });
86
+
87
+ return sortConfig.direction === "desc" ? sorted.reverse() : sorted;
88
+ }, [filteredData, sortConfig, columns]);
89
+
90
+ // Calculate pagination
91
+ const totalPages = Math.ceil(sortedData.length / pageSize);
92
+ const showPagination = sortedData.length > pageSize;
93
+ const startIndex = (currentPage - 1) * pageSize;
94
+ const endIndex = startIndex + pageSize;
95
+ const paginatedData = sortedData.slice(startIndex, endIndex);
96
+
97
+ // Reset to page 1 when filters change
98
+ useEffect(() => {
99
+ setCurrentPage(1);
100
+ }, [filters]);
101
+
102
+ const handleSort = (columnId: string) => {
103
+ const column = columns.find((col) => col.id === columnId);
104
+ if (!column?.sortable) return;
105
+
106
+ setSortConfig((current) => {
107
+ if (current?.columnId === columnId) {
108
+ if (current.direction === "asc") {
109
+ return { columnId, direction: "desc" };
110
+ }
111
+ return null; // Remove sorting
112
+ }
113
+ return { columnId, direction: "asc" };
114
+ });
115
+ };
116
+
117
+ const handleFilterChange = (columnId: string, value: string) => {
118
+ setFilters((current) => ({
119
+ ...current,
120
+ [columnId]: value,
121
+ }));
122
+ };
123
+
124
+ const handlePageChange = (page: number) => {
125
+ setCurrentPage(Math.max(1, Math.min(page, totalPages)));
126
+ };
127
+
128
+ return (
129
+ <>
130
+ <Table>
131
+ <TableHead>
132
+ <TableRow>
133
+ {columns.map((column) => (
134
+ <TableHeader key={column.id}>
135
+ <div
136
+ style={{
137
+ display: "flex",
138
+ alignItems: "center",
139
+ gap: "8px",
140
+ cursor: column.sortable ? "pointer" : "default",
141
+ }}
142
+ onClick={() => handleSort(column.id)}
143
+ >
144
+ <span>{column.header}</span>
145
+ {column.sortable && (
146
+ <span
147
+ style={{
148
+ fontSize: "12px",
149
+ opacity: 0.7,
150
+ }}
151
+ >
152
+ {sortConfig?.columnId === column.id
153
+ ? sortConfig.direction === "asc"
154
+ ? "↑"
155
+ : "↓"
156
+ : "↕"}
157
+ </span>
158
+ )}
159
+ </div>
160
+ </TableHeader>
161
+ ))}
162
+ </TableRow>
163
+ </TableHead>
164
+ <TableBody>
165
+ {columns.some((column) => column.filterable) && (
166
+ <TableRow>
167
+ {columns.map((column) => (
168
+ <TableCell key={column.id}>
169
+ {column.filterable && (
170
+ <Input
171
+ label="Filter"
172
+ hideLabel
173
+ type="search"
174
+ placeholder={`Filter ${column.header.toLowerCase()}`}
175
+ value={filters[column.id] || ""}
176
+ onChange={(e) =>
177
+ handleFilterChange(column.id, e.target.value)
178
+ }
179
+ onClick={(e) => e.stopPropagation()}
180
+ />
181
+ )}
182
+ </TableCell>
183
+ ))}
184
+ </TableRow>
185
+ )}
186
+ {paginatedData.length === 0 ? (
187
+ <TableRow>
188
+ <TableCell colSpan={columns.length}>
189
+ <div
190
+ style={{
191
+ textAlign: "center",
192
+ padding: "20px",
193
+ opacity: 0.6,
194
+ }}
195
+ >
196
+ {emptyMessage}
197
+ </div>
198
+ </TableCell>
199
+ </TableRow>
200
+ ) : (
201
+ paginatedData.map((row, index) => (
202
+ <TableRow key={getRowKey(row, startIndex + index)}>
203
+ {columns.map((column) => (
204
+ <TableCell key={column.id}>{column.accessor(row)}</TableCell>
205
+ ))}
206
+ </TableRow>
207
+ ))
208
+ )}
209
+ </TableBody>
210
+ </Table>
211
+
212
+ {showPagination && (
213
+ <div
214
+ style={{
215
+ display: "flex",
216
+ justifyContent: "space-between",
217
+ alignItems: "center",
218
+ marginTop: "16px",
219
+ padding: "8px 0",
220
+ }}
221
+ >
222
+ <div style={{ fontSize: "14px", opacity: 0.7 }}>
223
+ Showing {startIndex + 1} to {Math.min(endIndex, sortedData.length)}{" "}
224
+ of {sortedData.length} entries
225
+ {Object.keys(filters).some((key) => filters[key]) &&
226
+ ` (filtered from ${data.length})`}
227
+ </div>
228
+ <div style={{ display: "flex", gap: "8px", alignItems: "center" }}>
229
+ <Button
230
+ variant="secondary"
231
+ onClick={() => handlePageChange(1)}
232
+ disabled={currentPage === 1}
233
+ >
234
+ ««
235
+ </Button>
236
+ <Button
237
+ variant="secondary"
238
+ onClick={() => handlePageChange(currentPage - 1)}
239
+ disabled={currentPage === 1}
240
+ >
241
+ «
242
+ </Button>
243
+ <span style={{ fontSize: "14px" }}>
244
+ Page {currentPage} of {totalPages}
245
+ </span>
246
+ <Button
247
+ variant="secondary"
248
+ onClick={() => handlePageChange(currentPage + 1)}
249
+ disabled={currentPage === totalPages}
250
+ >
251
+ »
252
+ </Button>
253
+ <Button
254
+ variant="secondary"
255
+ onClick={() => handlePageChange(totalPages)}
256
+ disabled={currentPage === totalPages}
257
+ >
258
+ »»
259
+ </Button>
260
+ </div>
261
+ </div>
262
+ )}
263
+ </>
264
+ );
265
+ }
@@ -3,3 +3,4 @@ export { Icon } from "./icon.js";
3
3
  export { Modal } from "./modal.js";
4
4
  export { Input } from "./input.js";
5
5
  export { Select } from "./select.js";
6
+ export { DataTable } from "./data-table.js";