jazz-tools 0.18.6 → 0.18.8
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/.turbo/turbo-build.log +55 -55
- package/CHANGELOG.md +28 -0
- package/dist/better-auth/auth/server.d.ts.map +1 -1
- package/dist/better-auth/auth/server.js +8 -4
- package/dist/better-auth/auth/server.js.map +1 -1
- package/dist/{chunk-45VKEOXG.js → chunk-QF3R3C4N.js} +75 -22
- package/dist/chunk-QF3R3C4N.js.map +1 -0
- package/dist/index.js +1 -1
- package/dist/inspector/{custom-element-IBHKHN27.js → custom-element-G6SPZEBR.js} +292 -31
- package/dist/inspector/custom-element-G6SPZEBR.js.map +1 -0
- package/dist/inspector/index.d.ts +1 -1
- package/dist/inspector/index.js +302 -41
- package/dist/inspector/index.js.map +1 -1
- package/dist/inspector/register-custom-element.js +1 -1
- package/dist/inspector/ui/button.d.ts +1 -1
- package/dist/inspector/ui/button.d.ts.map +1 -1
- package/dist/inspector/ui/heading.d.ts +2 -1
- package/dist/inspector/ui/heading.d.ts.map +1 -1
- package/dist/inspector/ui/input.d.ts.map +1 -1
- package/dist/inspector/ui/modal.d.ts +16 -0
- package/dist/inspector/ui/modal.d.ts.map +1 -0
- package/dist/inspector/viewer/delete-local-data.d.ts +2 -0
- package/dist/inspector/viewer/delete-local-data.d.ts.map +1 -0
- package/dist/inspector/viewer/{inpsector-button.d.ts → inspector-button.d.ts} +1 -1
- package/dist/inspector/viewer/{inpsector-button.d.ts.map → inspector-button.d.ts.map} +1 -1
- package/dist/inspector/viewer/new-app.d.ts +1 -1
- package/dist/inspector/viewer/new-app.d.ts.map +1 -1
- package/dist/react/hooks.d.ts +1 -1
- package/dist/react/hooks.d.ts.map +1 -1
- package/dist/react/index.d.ts +1 -1
- package/dist/react/index.d.ts.map +1 -1
- package/dist/react/index.js +3 -1
- package/dist/react/index.js.map +1 -1
- package/dist/react-core/hooks.d.ts +133 -0
- package/dist/react-core/hooks.d.ts.map +1 -1
- package/dist/react-core/index.js +85 -17
- package/dist/react-core/index.js.map +1 -1
- package/dist/react-core/tests/useCoStateWithSelector.test.d.ts +2 -0
- package/dist/react-core/tests/useCoStateWithSelector.test.d.ts.map +1 -0
- package/dist/react-native-core/hooks.d.ts +1 -1
- package/dist/react-native-core/hooks.d.ts.map +1 -1
- package/dist/react-native-core/index.js +3 -1
- package/dist/react-native-core/index.js.map +1 -1
- package/dist/testing.js +1 -1
- package/dist/tools/coValues/CoFieldInit.d.ts +5 -5
- package/dist/tools/coValues/CoFieldInit.d.ts.map +1 -1
- package/dist/tools/coValues/CoValueBase.d.ts +14 -0
- package/dist/tools/coValues/CoValueBase.d.ts.map +1 -1
- package/dist/tools/coValues/coMap.d.ts +0 -12
- package/dist/tools/coValues/coMap.d.ts.map +1 -1
- package/dist/tools/implementation/createContext.d.ts +2 -1
- package/dist/tools/implementation/createContext.d.ts.map +1 -1
- package/dist/tools/implementation/zodSchema/schemaTypes/CoMapSchema.d.ts +5 -3
- package/dist/tools/implementation/zodSchema/schemaTypes/CoMapSchema.d.ts.map +1 -1
- package/dist/tools/tests/utils.d.ts.map +1 -1
- package/dist/worker/index.d.ts +4 -0
- package/dist/worker/index.d.ts.map +1 -1
- package/dist/worker/index.js +4 -2
- package/dist/worker/index.js.map +1 -1
- package/package.json +6 -4
- package/src/better-auth/auth/server.ts +8 -4
- package/src/better-auth/auth/tests/server.test.ts +2 -2
- package/src/inspector/index.tsx +1 -1
- package/src/inspector/ui/button.tsx +15 -1
- package/src/inspector/ui/heading.tsx +7 -2
- package/src/inspector/ui/input.tsx +6 -2
- package/src/inspector/ui/modal.tsx +158 -0
- package/src/inspector/viewer/delete-local-data.tsx +101 -0
- package/src/inspector/viewer/new-app.tsx +3 -1
- package/src/react/hooks.tsx +1 -0
- package/src/react/index.ts +1 -0
- package/src/react-core/hooks.ts +162 -0
- package/src/react-core/tests/useCoStateWithSelector.test.ts +149 -0
- package/src/react-native-core/hooks.tsx +1 -0
- package/src/tools/coValues/CoFieldInit.ts +5 -5
- package/src/tools/coValues/CoValueBase.ts +32 -0
- package/src/tools/coValues/coList.ts +35 -0
- package/src/tools/coValues/coMap.ts +0 -18
- package/src/tools/implementation/createContext.ts +9 -2
- package/src/tools/implementation/zodSchema/schemaTypes/CoMapSchema.ts +22 -8
- package/src/tools/tests/coList.test.ts +41 -0
- package/src/tools/tests/coMap.test.ts +37 -0
- package/src/tools/tests/coPlainText.test.ts +24 -0
- package/src/tools/tests/createContext.test.ts +24 -0
- package/src/tools/tests/deepLoading.test.ts +2 -0
- package/src/tools/tests/patterns/requestToJoin.test.ts +14 -6
- package/src/tools/tests/utils.ts +1 -0
- package/src/worker/index.ts +6 -0
- package/dist/chunk-45VKEOXG.js.map +0 -1
- package/dist/inspector/custom-element-IBHKHN27.js.map +0 -1
- /package/src/inspector/viewer/{inpsector-button.tsx → inspector-button.tsx} +0 -0
@@ -95,10 +95,14 @@ export const jazzPlugin: () => JazzPlugin = () => {
|
|
95
95
|
contextContainsJazzAuth(context) &&
|
96
96
|
verification.identifier.startsWith("sign-in-otp-")
|
97
97
|
) {
|
98
|
+
const identifier = `jazz-auth-${verification.identifier}`;
|
99
|
+
await context.context.internalAdapter.deleteVerificationByIdentifier(
|
100
|
+
identifier,
|
101
|
+
);
|
98
102
|
await context.context.internalAdapter.createVerificationValue(
|
99
103
|
{
|
100
104
|
value: JSON.stringify({ jazzAuth: context.jazzAuth }),
|
101
|
-
identifier:
|
105
|
+
identifier: identifier,
|
102
106
|
expiresAt: verification.expiresAt,
|
103
107
|
},
|
104
108
|
);
|
@@ -166,7 +170,7 @@ export const jazzPlugin: () => JazzPlugin = () => {
|
|
166
170
|
handler: createAuthMiddleware(async (ctx) => {
|
167
171
|
const state = ctx.query?.state || ctx.body?.state;
|
168
172
|
|
169
|
-
const identifier =
|
173
|
+
const identifier = `jazz-auth-${state}`;
|
170
174
|
|
171
175
|
const data =
|
172
176
|
await ctx.context.internalAdapter.findVerificationValue(
|
@@ -207,7 +211,7 @@ export const jazzPlugin: () => JazzPlugin = () => {
|
|
207
211
|
},
|
208
212
|
handler: createAuthMiddleware(async (ctx) => {
|
209
213
|
const email = ctx.body.email;
|
210
|
-
const identifier = `sign-in-otp-${email}
|
214
|
+
const identifier = `jazz-auth-sign-in-otp-${email}`;
|
211
215
|
|
212
216
|
const data =
|
213
217
|
await ctx.context.internalAdapter.findVerificationValue(
|
@@ -292,7 +296,7 @@ export const jazzPlugin: () => JazzPlugin = () => {
|
|
292
296
|
|
293
297
|
await ctx.context.internalAdapter.createVerificationValue({
|
294
298
|
value,
|
295
|
-
identifier:
|
299
|
+
identifier: `jazz-auth-${state}`,
|
296
300
|
expiresAt,
|
297
301
|
});
|
298
302
|
}),
|
@@ -293,7 +293,7 @@ describe("Better-Auth server plugin", async () => {
|
|
293
293
|
|
294
294
|
expect(verificationCreationSpy).toHaveBeenCalledTimes(2);
|
295
295
|
expect(verificationCreationSpy.mock.calls[1]?.[0]).toMatchObject({
|
296
|
-
identifier: expect.stringMatching("
|
296
|
+
identifier: expect.stringMatching("jazz-auth-"),
|
297
297
|
value: expect.stringContaining('"accountID":"123"'),
|
298
298
|
});
|
299
299
|
});
|
@@ -412,7 +412,7 @@ describe("Better-Auth server plugin", async () => {
|
|
412
412
|
expect(verificationCreationSpy).toHaveBeenCalledTimes(2);
|
413
413
|
expect(verificationCreationSpy.mock.calls[0]?.[0]).toMatchObject(
|
414
414
|
expect.objectContaining({
|
415
|
-
identifier: "sign-in-otp-email@email.it
|
415
|
+
identifier: "jazz-auth-sign-in-otp-email@email.it",
|
416
416
|
value: expect.stringContaining('"accountID":"123"'),
|
417
417
|
}),
|
418
418
|
);
|
package/src/inspector/index.tsx
CHANGED
@@ -23,7 +23,7 @@ import { useJazzContext } from "jazz-tools/react-core";
|
|
23
23
|
import { Account } from "jazz-tools";
|
24
24
|
|
25
25
|
import { JazzInspectorInternal } from "./viewer/new-app.js";
|
26
|
-
import { Position } from "./viewer/
|
26
|
+
import { Position } from "./viewer/inspector-button.js";
|
27
27
|
|
28
28
|
export function JazzInspector({ position = "right" }: { position?: Position }) {
|
29
29
|
const context = useJazzContext<Account>();
|
@@ -2,7 +2,7 @@ import { styled } from "goober";
|
|
2
2
|
import { forwardRef } from "react";
|
3
3
|
|
4
4
|
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
5
|
-
variant?: "primary" | "secondary" | "link" | "plain";
|
5
|
+
variant?: "primary" | "secondary" | "link" | "plain" | "destructive";
|
6
6
|
children?: React.ReactNode;
|
7
7
|
className?: string;
|
8
8
|
disabled?: boolean;
|
@@ -29,6 +29,9 @@ const StyledButton = styled("button")<{ variant: string; disabled?: boolean }>`
|
|
29
29
|
border-color: var(--j-primary-color);
|
30
30
|
color: white;
|
31
31
|
font-weight: 500;
|
32
|
+
&:hover {
|
33
|
+
opacity: 0.8;
|
34
|
+
}
|
32
35
|
`;
|
33
36
|
case "secondary":
|
34
37
|
return `
|
@@ -47,6 +50,17 @@ const StyledButton = styled("button")<{ variant: string; disabled?: boolean }>`
|
|
47
50
|
text-decoration: underline;
|
48
51
|
}
|
49
52
|
`;
|
53
|
+
case "destructive":
|
54
|
+
return `
|
55
|
+
padding: 0.375rem 0.75rem;
|
56
|
+
background-color: var(--j-destructive-color);
|
57
|
+
border-color: var(--j-destructive-color);
|
58
|
+
color: white;
|
59
|
+
font-weight: 500;
|
60
|
+
&:hover {
|
61
|
+
opacity: 0.8;
|
62
|
+
}
|
63
|
+
`;
|
50
64
|
default:
|
51
65
|
return "";
|
52
66
|
}
|
@@ -10,6 +10,11 @@ const StyledHeading = styled("h1")<{ className?: string }>`
|
|
10
10
|
export function Heading({
|
11
11
|
children,
|
12
12
|
className,
|
13
|
-
|
14
|
-
|
13
|
+
id,
|
14
|
+
}: React.PropsWithChildren<{ className?: string; id?: string }>) {
|
15
|
+
return (
|
16
|
+
<StyledHeading className={className} id={id}>
|
17
|
+
{children}
|
18
|
+
</StyledHeading>
|
19
|
+
);
|
15
20
|
}
|
@@ -27,7 +27,7 @@ const StyledInput = styled("input")`
|
|
27
27
|
box-shadow: var(--j-shadow-sm);
|
28
28
|
font-weight: 500;
|
29
29
|
background-color: white;
|
30
|
-
color: var(--text-color-strong);
|
30
|
+
color: var(--j-text-color-strong);
|
31
31
|
|
32
32
|
@media (prefers-color-scheme: dark) {
|
33
33
|
background-color: var(--j-foreground);
|
@@ -41,7 +41,11 @@ export const Input = forwardRef<HTMLInputElement, InputProps>(
|
|
41
41
|
|
42
42
|
return (
|
43
43
|
<Container className={className}>
|
44
|
-
<label
|
44
|
+
<label
|
45
|
+
htmlFor={id}
|
46
|
+
className={hideLabel ? "j-sr-only" : ""}
|
47
|
+
style={{ color: "var(--j-text-color)" }}
|
48
|
+
>
|
45
49
|
{label}
|
46
50
|
</label>
|
47
51
|
<StyledInput ref={ref} {...inputProps} id={id} />
|
@@ -0,0 +1,158 @@
|
|
1
|
+
import { styled } from "goober";
|
2
|
+
import { forwardRef, useEffect, useRef } from "react";
|
3
|
+
import { Button } from "./button.js";
|
4
|
+
import { Heading } from "./heading.js";
|
5
|
+
|
6
|
+
interface ModalProps {
|
7
|
+
isOpen: boolean;
|
8
|
+
onClose: () => void;
|
9
|
+
heading: string;
|
10
|
+
text?: string;
|
11
|
+
children?: React.ReactNode;
|
12
|
+
confirmText?: string;
|
13
|
+
cancelText?: string;
|
14
|
+
onConfirm?: () => void;
|
15
|
+
onCancel?: () => void;
|
16
|
+
showButtons?: boolean;
|
17
|
+
className?: string;
|
18
|
+
}
|
19
|
+
|
20
|
+
const ModalContent = styled("dialog")`
|
21
|
+
background-color: var(--j-background);
|
22
|
+
border-radius: var(--j-radius-lg);
|
23
|
+
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.2);
|
24
|
+
border: 1px solid var(--j-border-color);
|
25
|
+
max-width: 32rem;
|
26
|
+
margin-block: auto;
|
27
|
+
margin-inline: auto;
|
28
|
+
&::backdrop {
|
29
|
+
background-color: rgba(0, 0, 0, 0.7);
|
30
|
+
}
|
31
|
+
|
32
|
+
`;
|
33
|
+
|
34
|
+
const ModalHeader = styled("div")`
|
35
|
+
display: flex;
|
36
|
+
justify-content: space-between;
|
37
|
+
align-items: flex-start;
|
38
|
+
padding: 1.5rem 1.5rem 0 1.5rem;
|
39
|
+
gap: 1rem;
|
40
|
+
`;
|
41
|
+
|
42
|
+
const ModalBody = styled("div")`
|
43
|
+
padding: 1rem 1.5rem;
|
44
|
+
flex: 1;
|
45
|
+
`;
|
46
|
+
|
47
|
+
const ModalFooter = styled("div")`
|
48
|
+
display: flex;
|
49
|
+
justify-content: flex-end;
|
50
|
+
gap: 0.75rem;
|
51
|
+
padding: 0 1.5rem 1.5rem 1.5rem;
|
52
|
+
`;
|
53
|
+
|
54
|
+
const CloseButton = styled("button")`
|
55
|
+
background: none;
|
56
|
+
border: none;
|
57
|
+
cursor: pointer;
|
58
|
+
padding: 0.25rem;
|
59
|
+
border-radius: var(--j-radius-sm);
|
60
|
+
color: var(--j-text-color);
|
61
|
+
font-size: 1.25rem;
|
62
|
+
line-height: 1;
|
63
|
+
display: flex;
|
64
|
+
align-items: center;
|
65
|
+
justify-content: center;
|
66
|
+
min-width: 2rem;
|
67
|
+
min-height: 2rem;
|
68
|
+
|
69
|
+
&:hover {
|
70
|
+
background-color: var(--j-foreground);
|
71
|
+
}
|
72
|
+
|
73
|
+
&:focus-visible {
|
74
|
+
outline: 2px solid var(--j-border-focus);
|
75
|
+
outline-offset: 2px;
|
76
|
+
}
|
77
|
+
`;
|
78
|
+
|
79
|
+
export const Modal = forwardRef<HTMLDialogElement, ModalProps>(
|
80
|
+
(
|
81
|
+
{
|
82
|
+
isOpen,
|
83
|
+
onClose,
|
84
|
+
heading,
|
85
|
+
text,
|
86
|
+
children,
|
87
|
+
confirmText = "Confirm",
|
88
|
+
cancelText = "Cancel",
|
89
|
+
onConfirm,
|
90
|
+
onCancel,
|
91
|
+
showButtons = true,
|
92
|
+
className,
|
93
|
+
},
|
94
|
+
ref,
|
95
|
+
) => {
|
96
|
+
const modalRef = useRef<HTMLDialogElement>(null);
|
97
|
+
|
98
|
+
useEffect(() => {
|
99
|
+
if (isOpen) {
|
100
|
+
modalRef.current?.showModal();
|
101
|
+
} else {
|
102
|
+
onClose();
|
103
|
+
modalRef.current?.close();
|
104
|
+
}
|
105
|
+
}, [isOpen, onClose]);
|
106
|
+
|
107
|
+
const handleConfirm = () => {
|
108
|
+
onConfirm?.();
|
109
|
+
onClose();
|
110
|
+
};
|
111
|
+
|
112
|
+
const handleCancel = () => {
|
113
|
+
onCancel?.();
|
114
|
+
onClose();
|
115
|
+
};
|
116
|
+
|
117
|
+
if (!isOpen) return null;
|
118
|
+
|
119
|
+
return (
|
120
|
+
<ModalContent
|
121
|
+
ref={ref || modalRef}
|
122
|
+
className={className}
|
123
|
+
role="dialog"
|
124
|
+
aria-labelledby="modal-heading"
|
125
|
+
onClose={onClose}
|
126
|
+
>
|
127
|
+
<ModalHeader>
|
128
|
+
<Heading id="modal-heading">{heading}</Heading>
|
129
|
+
<CloseButton onClick={onClose} aria-label="Close modal" type="button">
|
130
|
+
×
|
131
|
+
</CloseButton>
|
132
|
+
</ModalHeader>
|
133
|
+
|
134
|
+
<ModalBody>
|
135
|
+
{text && (
|
136
|
+
<p style={{ margin: "0 0 1rem 0", color: "var(--j-text-color)" }}>
|
137
|
+
{text}
|
138
|
+
</p>
|
139
|
+
)}
|
140
|
+
{children}
|
141
|
+
</ModalBody>
|
142
|
+
|
143
|
+
{showButtons && (
|
144
|
+
<ModalFooter>
|
145
|
+
<Button variant="secondary" onClick={handleCancel}>
|
146
|
+
{cancelText}
|
147
|
+
</Button>
|
148
|
+
<Button variant="primary" onClick={handleConfirm}>
|
149
|
+
{confirmText}
|
150
|
+
</Button>
|
151
|
+
</ModalFooter>
|
152
|
+
)}
|
153
|
+
</ModalContent>
|
154
|
+
);
|
155
|
+
},
|
156
|
+
);
|
157
|
+
|
158
|
+
Modal.displayName = "Modal";
|
@@ -0,0 +1,101 @@
|
|
1
|
+
import { Button } from "../ui/button.js";
|
2
|
+
import { Modal } from "../ui/modal.js";
|
3
|
+
import { Input } from "../ui/input.js";
|
4
|
+
import { useState } from "react";
|
5
|
+
|
6
|
+
const DELETE_LOCAL_DATA_STRING = "delete my local data";
|
7
|
+
|
8
|
+
export function DeleteLocalData() {
|
9
|
+
const [showDeleteModal, setShowDeleteModal] = useState(false);
|
10
|
+
const [confirmDeleteString, setConfirmDeleteString] = useState("");
|
11
|
+
|
12
|
+
return (
|
13
|
+
<>
|
14
|
+
<Button variant="destructive" onClick={() => setShowDeleteModal(true)}>
|
15
|
+
Delete my local data
|
16
|
+
</Button>
|
17
|
+
<Modal
|
18
|
+
isOpen={showDeleteModal}
|
19
|
+
onClose={() => setShowDeleteModal(false)}
|
20
|
+
heading="Delete Local Data"
|
21
|
+
showButtons={false}
|
22
|
+
>
|
23
|
+
<div
|
24
|
+
style={{
|
25
|
+
margin: "0 0 1rem 0",
|
26
|
+
color: "var(--j-text-color)",
|
27
|
+
display: "flex",
|
28
|
+
flexDirection: "column",
|
29
|
+
gap: "0.5rem",
|
30
|
+
}}
|
31
|
+
>
|
32
|
+
<p>
|
33
|
+
This action <strong>cannot</strong> be undone.
|
34
|
+
</p>
|
35
|
+
<p>
|
36
|
+
Be aware that the following data will be{" "}
|
37
|
+
<strong>permanently</strong> deleted:
|
38
|
+
</p>
|
39
|
+
<ul style={{ listStyleType: "disc", paddingLeft: "1rem" }}>
|
40
|
+
<li>
|
41
|
+
Unsynced data for <strong>all apps</strong> on{" "}
|
42
|
+
<code>{window.location.origin}</code>
|
43
|
+
</li>
|
44
|
+
<li>Accounts</li>
|
45
|
+
<li>Logged in sessions</li>
|
46
|
+
</ul>
|
47
|
+
<p></p>
|
48
|
+
</div>
|
49
|
+
<Input
|
50
|
+
label={`Type "${DELETE_LOCAL_DATA_STRING}" to confirm`}
|
51
|
+
placeholder={DELETE_LOCAL_DATA_STRING}
|
52
|
+
value={confirmDeleteString}
|
53
|
+
onChange={(e) => {
|
54
|
+
setConfirmDeleteString(e.target.value);
|
55
|
+
}}
|
56
|
+
/>
|
57
|
+
<p
|
58
|
+
style={{
|
59
|
+
margin: "0 0 1rem 0",
|
60
|
+
color: "var(--j-text-color)",
|
61
|
+
display: "flex",
|
62
|
+
flexDirection: "column",
|
63
|
+
gap: "0.5rem",
|
64
|
+
}}
|
65
|
+
>
|
66
|
+
<small>
|
67
|
+
Data synced to a sync server will <strong>not</strong> be deleted,
|
68
|
+
and will be synced when you log in again.
|
69
|
+
</small>
|
70
|
+
</p>
|
71
|
+
<div
|
72
|
+
style={{
|
73
|
+
display: "flex",
|
74
|
+
marginTop: "0.5rem",
|
75
|
+
justifyContent: "flex-end",
|
76
|
+
gap: "0.5rem",
|
77
|
+
}}
|
78
|
+
>
|
79
|
+
<Button variant="secondary" onClick={() => setShowDeleteModal(false)}>
|
80
|
+
Cancel
|
81
|
+
</Button>
|
82
|
+
<Button
|
83
|
+
variant="destructive"
|
84
|
+
disabled={confirmDeleteString !== DELETE_LOCAL_DATA_STRING}
|
85
|
+
onClick={() => {
|
86
|
+
const jazzKeys = Object.keys(localStorage).filter(
|
87
|
+
(key) => key.startsWith("jazz-") || key.startsWith("co_z"),
|
88
|
+
);
|
89
|
+
jazzKeys.forEach((key) => localStorage.removeItem(key));
|
90
|
+
indexedDB.deleteDatabase("jazz-storage");
|
91
|
+
window.location.reload();
|
92
|
+
setShowDeleteModal(false);
|
93
|
+
}}
|
94
|
+
>
|
95
|
+
I'm sure, delete my local data
|
96
|
+
</Button>
|
97
|
+
</div>
|
98
|
+
</Modal>
|
99
|
+
</>
|
100
|
+
);
|
101
|
+
}
|
@@ -9,8 +9,9 @@ import { usePagePath } from "./use-page-path.js";
|
|
9
9
|
|
10
10
|
import { GlobalStyles } from "../ui/global-styles.js";
|
11
11
|
import { Heading } from "../ui/heading.js";
|
12
|
-
import { InspectorButton, type Position } from "./
|
12
|
+
import { InspectorButton, type Position } from "./inspector-button.js";
|
13
13
|
import { useOpenInspector } from "./use-open-inspector.js";
|
14
|
+
import { DeleteLocalData } from "./delete-local-data.js";
|
14
15
|
|
15
16
|
const InspectorContainer = styled("div")`
|
16
17
|
position: fixed;
|
@@ -102,6 +103,7 @@ export function JazzInspectorInternal({
|
|
102
103
|
/>
|
103
104
|
)}
|
104
105
|
</Form>
|
106
|
+
<DeleteLocalData />
|
105
107
|
<Button variant="plain" type="button" onClick={() => setOpen(false)}>
|
106
108
|
Close
|
107
109
|
</Button>
|
package/src/react/hooks.tsx
CHANGED
package/src/react/index.ts
CHANGED
package/src/react-core/hooks.ts
CHANGED
@@ -1,3 +1,4 @@
|
|
1
|
+
import { useSyncExternalStoreWithSelector } from "use-sync-external-store/shim/with-selector";
|
1
2
|
import React, {
|
2
3
|
useCallback,
|
3
4
|
useContext,
|
@@ -261,6 +262,167 @@ export function useCoState<
|
|
261
262
|
return value;
|
262
263
|
}
|
263
264
|
|
265
|
+
/**
|
266
|
+
* React hook for subscribing to CoValues with selective data extraction and custom equality checking.
|
267
|
+
*
|
268
|
+
* This hook extends `useCoState` by allowing you to select only specific parts of the CoValue data
|
269
|
+
* through a selector function, which helps reduce unnecessary re-renders by narrowing down the
|
270
|
+
* returned data. Additionally, you can provide a custom equality function to further optimize
|
271
|
+
* performance by controlling when the component should re-render based on the selected data.
|
272
|
+
*
|
273
|
+
* The hook automatically handles the subscription lifecycle and supports deep loading of nested
|
274
|
+
* CoValues through resolve queries, just like `useCoState`.
|
275
|
+
*
|
276
|
+
* @returns The result of the selector function applied to the loaded CoValue data
|
277
|
+
*
|
278
|
+
* @example
|
279
|
+
* ```tsx
|
280
|
+
* // Select only specific fields to reduce re-renders
|
281
|
+
* const Project = co.map({
|
282
|
+
* name: z.string(),
|
283
|
+
* description: z.string(),
|
284
|
+
* tasks: co.list(Task),
|
285
|
+
* lastModified: z.date(),
|
286
|
+
* });
|
287
|
+
*
|
288
|
+
* function ProjectTitle({ projectId }: { projectId: string }) {
|
289
|
+
* // Only re-render when the project name changes, not other fields
|
290
|
+
* const projectName = useCoStateWithSelector(
|
291
|
+
* Project,
|
292
|
+
* projectId,
|
293
|
+
* {
|
294
|
+
* select: (project) => project?.name ?? "Loading...",
|
295
|
+
* }
|
296
|
+
* );
|
297
|
+
*
|
298
|
+
* return <h1>{projectName}</h1>;
|
299
|
+
* }
|
300
|
+
* ```
|
301
|
+
*
|
302
|
+
* @example
|
303
|
+
* ```tsx
|
304
|
+
* // Use custom equality function for complex data structures
|
305
|
+
* const TaskList = co.list(Task);
|
306
|
+
*
|
307
|
+
* function TaskCount({ listId }: { listId: string }) {
|
308
|
+
* const taskStats = useCoStateWithSelector(
|
309
|
+
* TaskList,
|
310
|
+
* listId,
|
311
|
+
* {
|
312
|
+
* resolve: { $each: true },
|
313
|
+
* select: (tasks) => {
|
314
|
+
* if (!tasks) return { total: 0, completed: 0 };
|
315
|
+
* return {
|
316
|
+
* total: tasks.length,
|
317
|
+
* completed: tasks.filter(task => task.completed).length,
|
318
|
+
* };
|
319
|
+
* },
|
320
|
+
* // Custom equality to prevent re-renders when stats haven't changed
|
321
|
+
* equalityFn: (a, b) => a.total === b.total && a.completed === b.completed,
|
322
|
+
* }
|
323
|
+
* );
|
324
|
+
*
|
325
|
+
* return (
|
326
|
+
* <div>
|
327
|
+
* {taskStats.completed} of {taskStats.total} tasks completed
|
328
|
+
* </div>
|
329
|
+
* );
|
330
|
+
* }
|
331
|
+
* ```
|
332
|
+
*
|
333
|
+
* @example
|
334
|
+
* ```tsx
|
335
|
+
* // Combine with deep loading and complex selectors
|
336
|
+
* const Team = co.map({
|
337
|
+
* name: z.string(),
|
338
|
+
* members: co.list(TeamMember),
|
339
|
+
* projects: co.list(Project),
|
340
|
+
* });
|
341
|
+
*
|
342
|
+
* function TeamSummary({ teamId }: { teamId: string }) {
|
343
|
+
* const summary = useCoStateWithSelector(
|
344
|
+
* Team,
|
345
|
+
* teamId,
|
346
|
+
* {
|
347
|
+
* resolve: {
|
348
|
+
* members: { $each: true },
|
349
|
+
* projects: { $each: { tasks: { $each: true } } },
|
350
|
+
* },
|
351
|
+
* select: (team) => {
|
352
|
+
* if (!team) return null;
|
353
|
+
*
|
354
|
+
* const totalTasks = team.projects.reduce(
|
355
|
+
* (sum, project) => sum + project.tasks.length,
|
356
|
+
* 0
|
357
|
+
* );
|
358
|
+
*
|
359
|
+
* return {
|
360
|
+
* teamName: team.name,
|
361
|
+
* memberCount: team.members.length,
|
362
|
+
* projectCount: team.projects.length,
|
363
|
+
* totalTasks,
|
364
|
+
* };
|
365
|
+
* },
|
366
|
+
* }
|
367
|
+
* );
|
368
|
+
*
|
369
|
+
* if (!summary) return <div>Loading team summary...</div>;
|
370
|
+
*
|
371
|
+
* return (
|
372
|
+
* <div>
|
373
|
+
* <h2>{summary.teamName}</h2>
|
374
|
+
* <p>{summary.memberCount} members</p>
|
375
|
+
* <p>{summary.projectCount} projects</p>
|
376
|
+
* <p>{summary.totalTasks} total tasks</p>
|
377
|
+
* </div>
|
378
|
+
* );
|
379
|
+
* }
|
380
|
+
* ```
|
381
|
+
*
|
382
|
+
* For more examples, see the [subscription and deep loading](https://jazz.tools/docs/react/using-covalues/subscription-and-loading) documentation.
|
383
|
+
*/
|
384
|
+
export function useCoStateWithSelector<
|
385
|
+
S extends CoValueClassOrSchema,
|
386
|
+
TSelectorReturn,
|
387
|
+
const R extends ResolveQuery<S> = true,
|
388
|
+
>(
|
389
|
+
/** The CoValue schema or class constructor */
|
390
|
+
Schema: S,
|
391
|
+
/** The ID of the CoValue to subscribe to. If `undefined`, returns the result of selector called with `null` */
|
392
|
+
id: string | undefined,
|
393
|
+
/** Optional configuration for the subscription */
|
394
|
+
options: {
|
395
|
+
/** Resolve query to specify which nested CoValues to load */
|
396
|
+
resolve?: ResolveQueryStrict<S, R>;
|
397
|
+
/** Select which value to return */
|
398
|
+
select: (value: Loaded<S, R> | undefined | null) => TSelectorReturn;
|
399
|
+
/** Equality function to determine if the selected value has changed, defaults to `Object.is` */
|
400
|
+
equalityFn?: (a: TSelectorReturn, b: TSelectorReturn) => boolean;
|
401
|
+
},
|
402
|
+
): TSelectorReturn {
|
403
|
+
const subscription = useCoValueSubscription(Schema, id, options);
|
404
|
+
|
405
|
+
return useSyncExternalStoreWithSelector<
|
406
|
+
Loaded<S, R> | undefined | null,
|
407
|
+
TSelectorReturn
|
408
|
+
>(
|
409
|
+
React.useCallback(
|
410
|
+
(callback) => {
|
411
|
+
if (!subscription) {
|
412
|
+
return () => {};
|
413
|
+
}
|
414
|
+
|
415
|
+
return subscription.subscribe(callback);
|
416
|
+
},
|
417
|
+
[subscription],
|
418
|
+
),
|
419
|
+
() => (subscription ? subscription.getCurrentValue() : null),
|
420
|
+
() => (subscription ? subscription.getCurrentValue() : null),
|
421
|
+
options.select,
|
422
|
+
options.equalityFn ?? Object.is,
|
423
|
+
);
|
424
|
+
}
|
425
|
+
|
264
426
|
function useAccountSubscription<
|
265
427
|
S extends AccountClass<Account> | AnyAccountSchema,
|
266
428
|
const R extends ResolveQuery<S>,
|