jazz-tools 0.18.28 → 0.18.30
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/.svelte-kit/__package__/media/image.svelte +7 -4
- package/.svelte-kit/__package__/media/image.svelte.d.ts.map +1 -1
- package/.svelte-kit/__package__/media/image.types.d.ts +1 -0
- package/.svelte-kit/__package__/media/image.types.d.ts.map +1 -1
- package/.svelte-kit/__package__/tests/media/image.svelte.test.js +63 -0
- package/.turbo/turbo-build.log +60 -60
- package/CHANGELOG.md +27 -0
- package/dist/better-auth/auth/client.d.ts +1 -1
- package/dist/better-auth/auth/server.d.ts +1 -1
- package/dist/better-auth/auth/server.d.ts.map +1 -1
- package/dist/better-auth/auth/server.js.map +1 -1
- package/dist/better-auth/database-adapter/index.d.ts +3 -3
- package/dist/better-auth/database-adapter/index.d.ts.map +1 -1
- package/dist/better-auth/database-adapter/index.js +6 -2
- package/dist/better-auth/database-adapter/index.js.map +1 -1
- package/dist/better-auth/database-adapter/utils.d.ts.map +1 -1
- package/dist/browser/index.d.ts +2 -1
- package/dist/browser/index.d.ts.map +1 -1
- package/dist/browser/index.js.map +1 -1
- package/dist/{chunk-YOL3XDDW.js → chunk-6BIYT3KH.js} +84 -50
- package/dist/chunk-6BIYT3KH.js.map +1 -0
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/inspector/{custom-element-A7UAELEG.js → custom-element-RQTLPAPJ.js} +2137 -1848
- package/dist/inspector/custom-element-RQTLPAPJ.js.map +1 -0
- package/dist/inspector/custom-element.d.ts.map +1 -1
- package/dist/inspector/index.d.ts +1 -1
- package/dist/inspector/index.d.ts.map +1 -1
- package/dist/inspector/index.js +570 -339
- package/dist/inspector/index.js.map +1 -1
- package/dist/inspector/register-custom-element.js +1 -1
- package/dist/inspector/ui/index.d.ts +6 -0
- package/dist/inspector/ui/index.d.ts.map +1 -0
- package/dist/inspector/viewer/group-view.d.ts +3 -2
- package/dist/inspector/viewer/group-view.d.ts.map +1 -1
- package/dist/inspector/viewer/page.d.ts.map +1 -1
- package/dist/react/index.js +2 -2
- package/dist/react/index.js.map +1 -1
- package/dist/react/media/image.d.ts +8 -0
- package/dist/react/media/image.d.ts.map +1 -1
- package/dist/react-native-core/index.js +3 -3
- package/dist/react-native-core/index.js.map +1 -1
- package/dist/react-native-core/media/image.d.ts +15 -0
- package/dist/react-native-core/media/image.d.ts.map +1 -1
- package/dist/svelte/media/image.svelte +7 -4
- package/dist/svelte/media/image.svelte.d.ts.map +1 -1
- package/dist/svelte/media/image.types.d.ts +1 -0
- package/dist/svelte/media/image.types.d.ts.map +1 -1
- package/dist/svelte/tests/media/image.svelte.test.js +63 -0
- package/dist/testing.js +8 -1
- package/dist/testing.js.map +1 -1
- package/dist/tools/coValues/account.d.ts +1 -0
- package/dist/tools/coValues/account.d.ts.map +1 -1
- package/dist/tools/coValues/group.d.ts +3 -3
- package/dist/tools/coValues/group.d.ts.map +1 -1
- package/dist/tools/coValues/interfaces.d.ts +9 -2
- package/dist/tools/coValues/interfaces.d.ts.map +1 -1
- package/dist/tools/exports.d.ts +2 -2
- package/dist/tools/exports.d.ts.map +1 -1
- package/dist/tools/implementation/invites.d.ts +2 -2
- package/dist/tools/implementation/invites.d.ts.map +1 -1
- package/dist/tools/implementation/zodSchema/runtimeConverters/schemaFieldToCoFieldDef.d.ts +1 -1
- package/dist/tools/implementation/zodSchema/runtimeConverters/schemaFieldToCoFieldDef.d.ts.map +1 -1
- package/dist/tools/implementation/zodSchema/schemaTypes/AccountSchema.d.ts.map +1 -1
- package/dist/tools/implementation/zodSchema/typeConverters/TypeOfZodSchema.d.ts +3 -1
- package/dist/tools/implementation/zodSchema/typeConverters/TypeOfZodSchema.d.ts.map +1 -1
- package/dist/tools/implementation/zodSchema/zodCo.d.ts.map +1 -1
- package/dist/tools/implementation/zodSchema/zodReExport.d.ts +1 -1
- package/dist/tools/implementation/zodSchema/zodReExport.d.ts.map +1 -1
- package/dist/tools/subscribe/CoValueCoreSubscription.d.ts.map +1 -1
- package/dist/tools/subscribe/SubscriptionScope.d.ts +0 -2
- package/dist/tools/subscribe/SubscriptionScope.d.ts.map +1 -1
- package/dist/tools/testing.d.ts +1 -0
- package/dist/tools/testing.d.ts.map +1 -1
- package/dist/tools/tests/CoValueCoreSubscription.test.d.ts.map +1 -0
- package/package.json +5 -5
- package/src/better-auth/auth/server.ts +7 -2
- package/src/better-auth/auth/tests/server.test.ts +39 -17
- package/src/better-auth/database-adapter/index.ts +8 -5
- package/src/better-auth/database-adapter/utils.ts +4 -0
- package/src/browser/index.ts +2 -1
- package/src/inspector/custom-element.tsx +4 -0
- package/src/inspector/index.tsx +0 -2
- package/src/inspector/ui/index.ts +5 -0
- package/src/inspector/viewer/group-view.tsx +304 -20
- package/src/inspector/viewer/new-app.tsx +4 -4
- package/src/inspector/viewer/page.tsx +16 -2
- package/src/react/media/image.tsx +11 -2
- package/src/react/tests/media/image.test.tsx +94 -0
- package/src/react-native-core/media/image.tsx +11 -3
- package/src/svelte/media/image.svelte +7 -4
- package/src/svelte/media/image.types.ts +1 -0
- package/src/svelte/tests/media/image.svelte.test.ts +85 -0
- package/src/tools/coValues/account.ts +30 -5
- package/src/tools/coValues/coList.ts +3 -3
- package/src/tools/coValues/coMap.ts +3 -3
- package/src/tools/coValues/group.ts +13 -12
- package/src/tools/coValues/inbox.ts +5 -5
- package/src/tools/coValues/interfaces.ts +49 -18
- package/src/tools/exports.ts +1 -1
- package/src/tools/implementation/invites.ts +3 -8
- package/src/tools/implementation/zodSchema/runtimeConverters/schemaFieldToCoFieldDef.ts +5 -1
- package/src/tools/implementation/zodSchema/schemaTypes/AccountSchema.ts +2 -0
- package/src/tools/implementation/zodSchema/typeConverters/TypeOfZodSchema.ts +63 -50
- package/src/tools/implementation/zodSchema/zodReExport.ts +2 -2
- package/src/tools/subscribe/CoValueCoreSubscription.ts +17 -0
- package/src/tools/subscribe/SubscriptionScope.ts +1 -27
- package/src/tools/testing.ts +7 -0
- package/src/tools/{subscribe → tests}/CoValueCoreSubscription.test.ts +233 -3
- package/src/tools/tests/coFeed.branch.test.ts +14 -5
- package/src/tools/tests/coMap.test.ts +139 -42
- package/src/tools/tests/coMap.unique.test.ts +106 -1
- package/src/tools/tests/coOptional.test.ts +9 -1
- package/src/tools/tests/groupsAndAccounts.test.ts +156 -1
- package/src/tools/tests/load.test.ts +198 -1
- package/src/tools/tests/zod.test-d.ts +0 -2
- package/src/tools/tests/zod.test.ts +43 -40
- package/dist/chunk-YOL3XDDW.js.map +0 -1
- package/dist/inspector/custom-element-A7UAELEG.js.map +0 -1
- package/dist/tools/subscribe/CoValueCoreSubscription.test.d.ts.map +0 -1
- /package/dist/tools/{subscribe → tests}/CoValueCoreSubscription.test.d.ts +0 -0
|
@@ -1,5 +1,13 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
Everyone,
|
|
3
|
+
JsonObject,
|
|
4
|
+
LocalNode,
|
|
5
|
+
RawAccount,
|
|
6
|
+
RawCoValue,
|
|
7
|
+
RawGroup,
|
|
8
|
+
} from "cojson";
|
|
2
9
|
import { CoID } from "cojson";
|
|
10
|
+
import { useState } from "react";
|
|
3
11
|
import {
|
|
4
12
|
Table,
|
|
5
13
|
TableBody,
|
|
@@ -8,60 +16,336 @@ import {
|
|
|
8
16
|
TableHeader,
|
|
9
17
|
TableRow,
|
|
10
18
|
} from "../ui/table.js";
|
|
11
|
-
import { Text } from "../ui/text.js";
|
|
12
19
|
import { AccountOrGroupText } from "./account-or-group-text.js";
|
|
13
20
|
import { RawDataCard } from "./raw-data-card.js";
|
|
14
21
|
import { PageInfo, isCoId } from "./types.js";
|
|
22
|
+
import { Button, Icon, Modal, Input, Select } from "../ui/index.js";
|
|
23
|
+
|
|
24
|
+
function partitionMembers(data: Record<string, string>) {
|
|
25
|
+
const everyone = Object.entries(data)
|
|
26
|
+
.filter(([key]) => key === "everyone")
|
|
27
|
+
.map(([key, value]) => ({
|
|
28
|
+
id: key as CoID<RawCoValue>,
|
|
29
|
+
role: value as string,
|
|
30
|
+
}));
|
|
31
|
+
|
|
32
|
+
const members = Object.entries(data)
|
|
33
|
+
.filter(([key]) => isCoId(key))
|
|
34
|
+
.map(([key, value]) => ({
|
|
35
|
+
id: key as CoID<RawCoValue>,
|
|
36
|
+
role: value,
|
|
37
|
+
}));
|
|
38
|
+
|
|
39
|
+
const parentGroups = Object.entries(data)
|
|
40
|
+
.filter(([key]) => key.startsWith("parent_co_"))
|
|
41
|
+
.map(([key, value]) => ({
|
|
42
|
+
id: key.slice(7) as CoID<RawCoValue>,
|
|
43
|
+
role: value,
|
|
44
|
+
}));
|
|
45
|
+
|
|
46
|
+
const childGroups = Object.entries(data)
|
|
47
|
+
.filter(
|
|
48
|
+
([key, value]) => key.startsWith("child_co_") && value !== "revoked",
|
|
49
|
+
)
|
|
50
|
+
.map(([key, value]) => ({
|
|
51
|
+
id: key.slice(6) as CoID<RawCoValue>,
|
|
52
|
+
role: value,
|
|
53
|
+
}));
|
|
54
|
+
|
|
55
|
+
return { everyone, members, parentGroups, childGroups };
|
|
56
|
+
}
|
|
15
57
|
|
|
16
58
|
export function GroupView({
|
|
59
|
+
coValue,
|
|
17
60
|
data,
|
|
18
61
|
onNavigate,
|
|
19
62
|
node,
|
|
20
63
|
}: {
|
|
64
|
+
coValue: RawCoValue;
|
|
21
65
|
data: JsonObject;
|
|
22
66
|
onNavigate: (pages: PageInfo[]) => void;
|
|
23
67
|
node: LocalNode;
|
|
24
68
|
}) {
|
|
69
|
+
const [addMemberType, setAddMemberType] = useState<
|
|
70
|
+
null | "account" | "group"
|
|
71
|
+
>(null);
|
|
72
|
+
|
|
73
|
+
const { everyone, members, parentGroups, childGroups } = partitionMembers(
|
|
74
|
+
data as Record<string, string>,
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
const onRemoveMember = async (id: CoID<RawCoValue>) => {
|
|
78
|
+
if (confirm("Are you sure you want to remove this member?") === false) {
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
try {
|
|
82
|
+
const group = await node.load(coValue.id);
|
|
83
|
+
if (group === "unavailable") {
|
|
84
|
+
throw new Error("Group not found");
|
|
85
|
+
}
|
|
86
|
+
const rawGroup = group as RawGroup;
|
|
87
|
+
rawGroup.removeMember(id as any);
|
|
88
|
+
} catch (error) {
|
|
89
|
+
console.error(error);
|
|
90
|
+
throw error;
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const onRemoveGroup = async (id: CoID<RawCoValue>) => {
|
|
95
|
+
if (confirm("Are you sure you want to remove this group?") === false) {
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
try {
|
|
99
|
+
const group = await node.load(coValue.id);
|
|
100
|
+
if (group === "unavailable") {
|
|
101
|
+
throw new Error("Group not found");
|
|
102
|
+
}
|
|
103
|
+
const rawGroup = group as RawGroup;
|
|
104
|
+
const targetGroup = await node.load(id);
|
|
105
|
+
if (targetGroup === "unavailable") {
|
|
106
|
+
throw new Error("Group not found");
|
|
107
|
+
}
|
|
108
|
+
const rawTargetGroup = targetGroup as RawGroup;
|
|
109
|
+
rawGroup.revokeExtend(rawTargetGroup);
|
|
110
|
+
} catch (error) {
|
|
111
|
+
console.error(error);
|
|
112
|
+
throw error;
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
const handleAddMemberSubmit = async (
|
|
117
|
+
event: React.FormEvent<HTMLFormElement>,
|
|
118
|
+
) => {
|
|
119
|
+
event.preventDefault();
|
|
120
|
+
const form = event.currentTarget;
|
|
121
|
+
|
|
122
|
+
const memberId = (form.elements.namedItem("memberId") as HTMLInputElement)
|
|
123
|
+
?.value;
|
|
124
|
+
const role = (form.elements.namedItem("role") as HTMLSelectElement)?.value;
|
|
125
|
+
|
|
126
|
+
try {
|
|
127
|
+
const group = await node.load(coValue.id);
|
|
128
|
+
if (group === "unavailable") {
|
|
129
|
+
throw new Error("Group not found");
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const rawGroup = group as RawGroup;
|
|
133
|
+
|
|
134
|
+
// Adding an account
|
|
135
|
+
if (addMemberType === "account") {
|
|
136
|
+
let rawAccount: RawAccount | Everyone = "everyone";
|
|
137
|
+
|
|
138
|
+
if (memberId !== "everyone") {
|
|
139
|
+
const account = await node.load(memberId as CoID<RawCoValue>);
|
|
140
|
+
if (account === "unavailable") {
|
|
141
|
+
throw new Error("Account not found");
|
|
142
|
+
}
|
|
143
|
+
rawAccount = account as RawAccount;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
rawGroup.addMember(rawAccount, role as "reader" | "writer" | "admin");
|
|
147
|
+
}
|
|
148
|
+
// Adding a group
|
|
149
|
+
else if (addMemberType === "group") {
|
|
150
|
+
const targetGroup = await node.load(memberId as CoID<RawCoValue>);
|
|
151
|
+
if (targetGroup === "unavailable") {
|
|
152
|
+
throw new Error("Group not found");
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const rawTargetGroup = targetGroup as RawGroup;
|
|
156
|
+
rawGroup.extend(
|
|
157
|
+
rawTargetGroup,
|
|
158
|
+
role as "reader" | "writer" | "admin" | "inherit",
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
setAddMemberType(null);
|
|
163
|
+
} catch (error: any) {
|
|
164
|
+
console.error(error);
|
|
165
|
+
alert(`Failed to add ${addMemberType}: ${error.message}`);
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
|
|
25
169
|
return (
|
|
26
170
|
<>
|
|
27
|
-
<Text strong>Members</Text>
|
|
28
|
-
|
|
29
171
|
<Table>
|
|
30
172
|
<TableHead>
|
|
31
173
|
<TableRow>
|
|
32
|
-
<TableHeader>
|
|
174
|
+
<TableHeader>Member</TableHeader>
|
|
33
175
|
<TableHeader>Permission</TableHeader>
|
|
176
|
+
<TableHeader></TableHeader>
|
|
34
177
|
</TableRow>
|
|
35
178
|
</TableHead>
|
|
36
179
|
<TableBody>
|
|
37
|
-
{
|
|
38
|
-
<TableRow>
|
|
39
|
-
<TableCell>
|
|
40
|
-
<TableCell>{
|
|
180
|
+
{everyone.map((member) => (
|
|
181
|
+
<TableRow key={member.id}>
|
|
182
|
+
<TableCell>{member.id}</TableCell>
|
|
183
|
+
<TableCell>{member.role}</TableCell>
|
|
184
|
+
<TableCell>
|
|
185
|
+
{member.role !== "revoked" && (
|
|
186
|
+
<Button
|
|
187
|
+
variant="secondary"
|
|
188
|
+
onClick={() => onRemoveMember(member.id)}
|
|
189
|
+
>
|
|
190
|
+
<Icon name="delete" />
|
|
191
|
+
</Button>
|
|
192
|
+
)}
|
|
193
|
+
</TableCell>
|
|
194
|
+
</TableRow>
|
|
195
|
+
))}
|
|
196
|
+
{members.map((member) => (
|
|
197
|
+
<TableRow key={member.id}>
|
|
198
|
+
<TableCell>
|
|
199
|
+
<AccountOrGroupText
|
|
200
|
+
coId={member.id}
|
|
201
|
+
node={node}
|
|
202
|
+
showId
|
|
203
|
+
onClick={() => {
|
|
204
|
+
onNavigate([{ coId: member.id, name: member.id }]);
|
|
205
|
+
}}
|
|
206
|
+
/>
|
|
207
|
+
</TableCell>
|
|
208
|
+
<TableCell>{member.role}</TableCell>
|
|
209
|
+
<TableCell>
|
|
210
|
+
{member.role !== "revoked" && (
|
|
211
|
+
<Button
|
|
212
|
+
variant="secondary"
|
|
213
|
+
onClick={() => onRemoveMember(member.id)}
|
|
214
|
+
>
|
|
215
|
+
<Icon name="delete" />
|
|
216
|
+
</Button>
|
|
217
|
+
)}
|
|
218
|
+
</TableCell>
|
|
219
|
+
</TableRow>
|
|
220
|
+
))}
|
|
221
|
+
{parentGroups.map((group) => (
|
|
222
|
+
<TableRow key={group.id}>
|
|
223
|
+
<TableCell>
|
|
224
|
+
<AccountOrGroupText
|
|
225
|
+
coId={group.id}
|
|
226
|
+
node={node}
|
|
227
|
+
showId
|
|
228
|
+
onClick={() => {
|
|
229
|
+
onNavigate([{ coId: group.id, name: group.id }]);
|
|
230
|
+
}}
|
|
231
|
+
/>
|
|
232
|
+
</TableCell>
|
|
233
|
+
<TableCell>{group.role}</TableCell>
|
|
234
|
+
<TableCell>
|
|
235
|
+
{group.role !== "revoked" && (
|
|
236
|
+
<Button
|
|
237
|
+
variant="secondary"
|
|
238
|
+
onClick={() => onRemoveGroup(group.id)}
|
|
239
|
+
>
|
|
240
|
+
<Icon name="delete" />
|
|
241
|
+
</Button>
|
|
242
|
+
)}
|
|
243
|
+
</TableCell>
|
|
41
244
|
</TableRow>
|
|
42
|
-
)
|
|
245
|
+
))}
|
|
246
|
+
</TableBody>
|
|
247
|
+
</Table>
|
|
43
248
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
249
|
+
<div
|
|
250
|
+
style={{
|
|
251
|
+
display: "flex",
|
|
252
|
+
justifyContent: "flex-end",
|
|
253
|
+
gap: "0.75rem",
|
|
254
|
+
marginTop: "1rem",
|
|
255
|
+
}}
|
|
256
|
+
>
|
|
257
|
+
<Button variant="primary" onClick={() => setAddMemberType("account")}>
|
|
258
|
+
Add Account
|
|
259
|
+
</Button>
|
|
260
|
+
<Button variant="primary" onClick={() => setAddMemberType("group")}>
|
|
261
|
+
Add Group
|
|
262
|
+
</Button>
|
|
263
|
+
</div>
|
|
264
|
+
|
|
265
|
+
{childGroups.length > 0 && (
|
|
266
|
+
<Table>
|
|
267
|
+
<TableHead>
|
|
268
|
+
<TableRow>
|
|
269
|
+
<TableHeader>Member of</TableHeader>
|
|
270
|
+
</TableRow>
|
|
271
|
+
</TableHead>
|
|
272
|
+
<TableBody>
|
|
273
|
+
{childGroups.map((group) => (
|
|
274
|
+
<TableRow key={group.id}>
|
|
47
275
|
<TableCell>
|
|
48
276
|
<AccountOrGroupText
|
|
49
|
-
coId={
|
|
277
|
+
coId={group.id}
|
|
50
278
|
node={node}
|
|
51
279
|
showId
|
|
52
280
|
onClick={() => {
|
|
53
|
-
onNavigate([{ coId:
|
|
281
|
+
onNavigate([{ coId: group.id, name: group.id }]);
|
|
54
282
|
}}
|
|
55
283
|
/>
|
|
56
284
|
</TableCell>
|
|
57
|
-
<TableCell>{value as string}</TableCell>
|
|
58
285
|
</TableRow>
|
|
59
|
-
)
|
|
60
|
-
|
|
61
|
-
</
|
|
62
|
-
|
|
286
|
+
))}
|
|
287
|
+
</TableBody>
|
|
288
|
+
</Table>
|
|
289
|
+
)}
|
|
63
290
|
|
|
64
291
|
<RawDataCard data={data} />
|
|
292
|
+
|
|
293
|
+
<Modal
|
|
294
|
+
isOpen={addMemberType !== null}
|
|
295
|
+
onClose={() => setAddMemberType(null)}
|
|
296
|
+
heading={addMemberType === "account" ? "Add Account" : "Add Group"}
|
|
297
|
+
showButtons={false}
|
|
298
|
+
>
|
|
299
|
+
<form onSubmit={handleAddMemberSubmit}>
|
|
300
|
+
<div
|
|
301
|
+
style={{ display: "flex", flexDirection: "column", gap: "1rem" }}
|
|
302
|
+
>
|
|
303
|
+
<Input
|
|
304
|
+
name="memberId"
|
|
305
|
+
label={addMemberType === "account" ? "Account ID" : "Group ID"}
|
|
306
|
+
placeholder={
|
|
307
|
+
addMemberType === "account"
|
|
308
|
+
? "Enter account ID"
|
|
309
|
+
: "Enter group ID"
|
|
310
|
+
}
|
|
311
|
+
required
|
|
312
|
+
/>
|
|
313
|
+
<Select name="role" label="Role">
|
|
314
|
+
<option value="reader">Reader</option>
|
|
315
|
+
<option value="writer">Writer</option>
|
|
316
|
+
<option value="admin">Admin</option>
|
|
317
|
+
{addMemberType === "account" ? (
|
|
318
|
+
<>
|
|
319
|
+
<option value="writeOnly">Write Only</option>
|
|
320
|
+
</>
|
|
321
|
+
) : (
|
|
322
|
+
<>
|
|
323
|
+
<option value="inherit">Inherit</option>
|
|
324
|
+
</>
|
|
325
|
+
)}
|
|
326
|
+
</Select>
|
|
327
|
+
<div
|
|
328
|
+
style={{
|
|
329
|
+
display: "flex",
|
|
330
|
+
gap: "0.75rem",
|
|
331
|
+
justifyContent: "flex-end",
|
|
332
|
+
marginTop: "0.5rem",
|
|
333
|
+
}}
|
|
334
|
+
>
|
|
335
|
+
<Button
|
|
336
|
+
type="button"
|
|
337
|
+
variant="secondary"
|
|
338
|
+
onClick={() => setAddMemberType(null)}
|
|
339
|
+
>
|
|
340
|
+
Cancel
|
|
341
|
+
</Button>
|
|
342
|
+
<Button type="submit" variant="primary">
|
|
343
|
+
Add
|
|
344
|
+
</Button>
|
|
345
|
+
</div>
|
|
346
|
+
</div>
|
|
347
|
+
</form>
|
|
348
|
+
</Modal>
|
|
65
349
|
</>
|
|
66
350
|
);
|
|
67
351
|
}
|
|
@@ -91,8 +91,8 @@ export function JazzInspectorInternal({
|
|
|
91
91
|
<InspectorContainer as={GlobalStyles} style={{ zIndex: 999 }}>
|
|
92
92
|
<HeaderContainer>
|
|
93
93
|
<Breadcrumbs path={path} onBreadcrumbClick={goToIndex} />
|
|
94
|
-
|
|
95
|
-
{
|
|
94
|
+
{path.length !== 0 && (
|
|
95
|
+
<Form onSubmit={handleCoValueIdSubmit}>
|
|
96
96
|
<Input
|
|
97
97
|
label="CoValue ID"
|
|
98
98
|
style={{ fontFamily: "monospace" }}
|
|
@@ -101,8 +101,8 @@ export function JazzInspectorInternal({
|
|
|
101
101
|
value={coValueId}
|
|
102
102
|
onChange={(e) => setCoValueId(e.target.value as CoID<RawCoValue>)}
|
|
103
103
|
/>
|
|
104
|
-
|
|
105
|
-
|
|
104
|
+
</Form>
|
|
105
|
+
)}
|
|
106
106
|
<DeleteLocalData />
|
|
107
107
|
<Button variant="plain" type="button" onClick={() => setOpen(false)}>
|
|
108
108
|
Close
|
|
@@ -1,4 +1,11 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
CoID,
|
|
3
|
+
LocalNode,
|
|
4
|
+
RawCoList,
|
|
5
|
+
RawCoStream,
|
|
6
|
+
RawCoValue,
|
|
7
|
+
RawGroup,
|
|
8
|
+
} from "cojson";
|
|
2
9
|
import { styled } from "goober";
|
|
3
10
|
import React from "react";
|
|
4
11
|
import { Badge } from "../ui/badge.js";
|
|
@@ -117,7 +124,14 @@ function View(
|
|
|
117
124
|
}
|
|
118
125
|
|
|
119
126
|
if (extendedType === "group") {
|
|
120
|
-
return
|
|
127
|
+
return (
|
|
128
|
+
<GroupView
|
|
129
|
+
coValue={value}
|
|
130
|
+
data={snapshot}
|
|
131
|
+
node={node}
|
|
132
|
+
onNavigate={onNavigate}
|
|
133
|
+
/>
|
|
134
|
+
);
|
|
121
135
|
}
|
|
122
136
|
|
|
123
137
|
if (extendedType === "account") {
|
|
@@ -51,6 +51,13 @@ export type ImageProps = Omit<
|
|
|
51
51
|
* ```
|
|
52
52
|
*/
|
|
53
53
|
height?: number | "original";
|
|
54
|
+
/**
|
|
55
|
+
* A custom placeholder to display while an image is loading. This will
|
|
56
|
+
* be passed as the src of the img tag, so a data URL works well here.
|
|
57
|
+
* This will be used as a fallback if no images are ready and no placeholder
|
|
58
|
+
* is available otherwise.
|
|
59
|
+
*/
|
|
60
|
+
placeholder?: string;
|
|
54
61
|
};
|
|
55
62
|
|
|
56
63
|
/**
|
|
@@ -69,11 +76,13 @@ export type ImageProps = Omit<
|
|
|
69
76
|
* height={100}
|
|
70
77
|
* alt="Avatar"
|
|
71
78
|
* style={{ borderRadius: "50%", objectFit: "cover" }}
|
|
79
|
+
placeholder={myPlaceholder}
|
|
72
80
|
* />
|
|
73
81
|
* );
|
|
74
82
|
* }
|
|
75
83
|
* ```
|
|
76
84
|
*/
|
|
85
|
+
|
|
77
86
|
export const Image = forwardRef<HTMLImageElement, ImageProps>(function Image(
|
|
78
87
|
{ imageId, width, height, ...props },
|
|
79
88
|
ref,
|
|
@@ -150,7 +159,7 @@ export const Image = forwardRef<HTMLImageElement, ImageProps>(function Image(
|
|
|
150
159
|
dimensions.height || dimensions.width || 9999,
|
|
151
160
|
);
|
|
152
161
|
|
|
153
|
-
if (!bestImage) return image.placeholderDataURL;
|
|
162
|
+
if (!bestImage) return image.placeholderDataURL ?? props?.placeholder;
|
|
154
163
|
if (lastBestImage.current?.[0] === bestImage.image.$jazz.id)
|
|
155
164
|
return lastBestImage.current?.[1];
|
|
156
165
|
|
|
@@ -163,7 +172,7 @@ export const Image = forwardRef<HTMLImageElement, ImageProps>(function Image(
|
|
|
163
172
|
return url;
|
|
164
173
|
}
|
|
165
174
|
|
|
166
|
-
return image.placeholderDataURL;
|
|
175
|
+
return image.placeholderDataURL ?? props?.placeholder;
|
|
167
176
|
}, [image, dimensions.width, dimensions.height, waitingLazyLoading]);
|
|
168
177
|
|
|
169
178
|
const onThresholdReached = useCallback(() => {
|
|
@@ -101,6 +101,100 @@ describe("Image", async () => {
|
|
|
101
101
|
expect(img!.src).toBe(placeholderDataUrl);
|
|
102
102
|
});
|
|
103
103
|
|
|
104
|
+
it("should not override actual placeholders", async () => {
|
|
105
|
+
const placeholderDataUrl =
|
|
106
|
+
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=";
|
|
107
|
+
const customPlaceholder =
|
|
108
|
+
"data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIGNsYXNzPSJsdWNpZGUgbHVjaWRlLXVzZXItaWNvbiBsdWNpZGUtdXNlciI+PHBhdGggZD0iTTE5IDIxdi0yYTQgNCAwIDAgMC00LTRIOWE0IDQgMCAwIDAtNCA0djIiLz48Y2lyY2xlIGN4PSIxMiIgY3k9IjciIHI9IjQiLz48L3N2Zz4=";
|
|
109
|
+
|
|
110
|
+
const original = FileStream.create({ owner: account });
|
|
111
|
+
original.start({ mimeType: "image/jpeg" });
|
|
112
|
+
// Don't end original, so it has no chunks
|
|
113
|
+
|
|
114
|
+
const im = ImageDefinition.create(
|
|
115
|
+
{
|
|
116
|
+
original,
|
|
117
|
+
originalSize: [100, 100],
|
|
118
|
+
progressive: false,
|
|
119
|
+
placeholderDataURL: placeholderDataUrl,
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
owner: account,
|
|
123
|
+
},
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
const { container } = render(
|
|
127
|
+
<Image
|
|
128
|
+
imageId={im.$jazz.id}
|
|
129
|
+
alt="test"
|
|
130
|
+
placeholder={customPlaceholder}
|
|
131
|
+
/>,
|
|
132
|
+
{
|
|
133
|
+
account,
|
|
134
|
+
},
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
const img = container.querySelector("img");
|
|
138
|
+
expect(img).toBeDefined();
|
|
139
|
+
expect(img!.src).toBe(placeholderDataUrl);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it("should show custom placeholder while loading and replace with loaded image", async () => {
|
|
143
|
+
const createObjectURLSpy = vi
|
|
144
|
+
.spyOn(URL, "createObjectURL")
|
|
145
|
+
.mockImplementation((blob) => {
|
|
146
|
+
if (!(blob instanceof Blob)) {
|
|
147
|
+
throw new Error("Blob expected");
|
|
148
|
+
}
|
|
149
|
+
return `blob:test-${blob.size}`;
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
const customPlaceholder =
|
|
153
|
+
"data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIGNsYXNzPSJsdWNpZGUgbHVjaWRlLXVzZXItaWNvbiBsdWNpZGUtdXNlciI+PHBhdGggZD0iTTE5IDIxdi0yYTQgNCAwIDAgMC00LTRIOWE0IDQgMCAwIDAtNCA0djIiLz48Y2lyY2xlIGN4PSIxMiIgY3k9IjciIHI9IjQiLz48L3N2Zz4=";
|
|
154
|
+
|
|
155
|
+
// Create an image with no chunks initially (loading state)
|
|
156
|
+
const original = FileStream.create({ owner: account });
|
|
157
|
+
original.start({ mimeType: "image/jpeg" });
|
|
158
|
+
// Don't end original, so it has no chunks
|
|
159
|
+
|
|
160
|
+
const im = ImageDefinition.create(
|
|
161
|
+
{
|
|
162
|
+
original,
|
|
163
|
+
originalSize: [100, 100],
|
|
164
|
+
progressive: false,
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
owner: account,
|
|
168
|
+
},
|
|
169
|
+
);
|
|
170
|
+
|
|
171
|
+
const { container } = render(
|
|
172
|
+
<Image
|
|
173
|
+
imageId={im.$jazz.id}
|
|
174
|
+
alt="test-loading-custom-placeholder"
|
|
175
|
+
placeholder={customPlaceholder}
|
|
176
|
+
/>,
|
|
177
|
+
{ account },
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
// Initially should show custom placeholder
|
|
181
|
+
let img = container.querySelector("img");
|
|
182
|
+
expect(img).toBeDefined();
|
|
183
|
+
expect(img!.src).toBe(customPlaceholder);
|
|
184
|
+
|
|
185
|
+
// Now add the actual image data
|
|
186
|
+
const imageData = await createDummyFileStream(100, account);
|
|
187
|
+
im.$jazz.set("100x100", imageData);
|
|
188
|
+
|
|
189
|
+
// Wait for the image to load and replace the placeholder
|
|
190
|
+
await waitFor(() => {
|
|
191
|
+
img = container.querySelector("img");
|
|
192
|
+
expect(img!.src).toBe("blob:test-100");
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
expect(createObjectURLSpy).toHaveBeenCalledTimes(1);
|
|
196
|
+
});
|
|
197
|
+
|
|
104
198
|
it("should render the original image once loaded", async () => {
|
|
105
199
|
const createObjectURLSpy = vi
|
|
106
200
|
.spyOn(URL, "createObjectURL")
|
|
@@ -35,6 +35,13 @@ export type ImageProps = Omit<RNImageProps, "width" | "height" | "source"> & {
|
|
|
35
35
|
* ```
|
|
36
36
|
*/
|
|
37
37
|
height?: number | "original";
|
|
38
|
+
/**
|
|
39
|
+
* A custom placeholder to display while an image is loading. This will
|
|
40
|
+
* be passed as the src of the img tag, so a data URL works well here.
|
|
41
|
+
* This will be used as a fallback if no images are ready and no placeholder
|
|
42
|
+
* is available otherwise.
|
|
43
|
+
*/
|
|
44
|
+
placeholder?: string;
|
|
38
45
|
};
|
|
39
46
|
|
|
40
47
|
/**
|
|
@@ -53,6 +60,7 @@ export type ImageProps = Omit<RNImageProps, "width" | "height" | "source"> & {
|
|
|
53
60
|
* width={100}
|
|
54
61
|
* height={100}
|
|
55
62
|
* resizeMode="cover"
|
|
63
|
+
* placeholder="/placeholder.png"
|
|
56
64
|
* />
|
|
57
65
|
* );
|
|
58
66
|
* }
|
|
@@ -65,7 +73,7 @@ export type ImageProps = Omit<RNImageProps, "width" | "height" | "source"> & {
|
|
|
65
73
|
* ```
|
|
66
74
|
*/
|
|
67
75
|
export const Image = forwardRef<RNImage, ImageProps>(function Image(
|
|
68
|
-
{ imageId, width, height, ...props },
|
|
76
|
+
{ imageId, width, height, placeholder, ...props },
|
|
69
77
|
ref,
|
|
70
78
|
) {
|
|
71
79
|
const image = useCoState(ImageDefinition, imageId);
|
|
@@ -117,7 +125,7 @@ export const Image = forwardRef<RNImage, ImageProps>(function Image(
|
|
|
117
125
|
if (!image) return;
|
|
118
126
|
|
|
119
127
|
let lastBestImage: FileStream | string | undefined =
|
|
120
|
-
image
|
|
128
|
+
image?.placeholderDataURL ?? placeholder;
|
|
121
129
|
|
|
122
130
|
const unsub = image.$jazz.subscribe({}, (update) => {
|
|
123
131
|
if (lastBestImage === undefined && update.placeholderDataURL) {
|
|
@@ -146,7 +154,7 @@ export const Image = forwardRef<RNImage, ImageProps>(function Image(
|
|
|
146
154
|
return unsub;
|
|
147
155
|
}, [image]);
|
|
148
156
|
|
|
149
|
-
if (!
|
|
157
|
+
if (!src) {
|
|
150
158
|
return null;
|
|
151
159
|
}
|
|
152
160
|
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
import { CoState } from "../jazz.class.svelte";
|
|
6
6
|
import type { ImageProps } from "./image.types.js";
|
|
7
7
|
|
|
8
|
-
const { imageId, width, height, ...rest }: ImageProps = $props();
|
|
8
|
+
const { imageId, width, height, placeholder, ...rest }: ImageProps = $props();
|
|
9
9
|
|
|
10
10
|
const imageState = new CoState(ImageDefinition, () => imageId);
|
|
11
11
|
let lastBestImage: [string, string] | null = null;
|
|
@@ -69,7 +69,10 @@
|
|
|
69
69
|
|
|
70
70
|
const image = imageState.current;
|
|
71
71
|
if (image === undefined)
|
|
72
|
-
return
|
|
72
|
+
return (
|
|
73
|
+
placeholder ??
|
|
74
|
+
"data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw=="
|
|
75
|
+
);
|
|
73
76
|
|
|
74
77
|
if (!image) return undefined;
|
|
75
78
|
|
|
@@ -79,7 +82,7 @@
|
|
|
79
82
|
dimensions.height || dimensions.width || 9999,
|
|
80
83
|
);
|
|
81
84
|
|
|
82
|
-
if (!bestImage) return image.placeholderDataURL;
|
|
85
|
+
if (!bestImage) return image.placeholderDataURL ?? placeholder;
|
|
83
86
|
if (lastBestImage?.[0] === bestImage.image.$jazz.id)
|
|
84
87
|
return lastBestImage?.[1];
|
|
85
88
|
|
|
@@ -92,7 +95,7 @@
|
|
|
92
95
|
return url;
|
|
93
96
|
}
|
|
94
97
|
|
|
95
|
-
return image.placeholderDataURL;
|
|
98
|
+
return image.placeholderDataURL ?? placeholder;
|
|
96
99
|
});
|
|
97
100
|
|
|
98
101
|
// Cleanup object URL on component destroy
|