Compare commits
100 Commits
jazz-react
...
jazz-react
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f122147f03 | ||
|
|
4e1bcde8b2 | ||
|
|
eaef418151 | ||
|
|
8d17b192d0 | ||
|
|
9a56bb3d25 | ||
|
|
b6c6a0ae64 | ||
|
|
e000774b3b | ||
|
|
6f6cf23bc8 | ||
|
|
77a718656c | ||
|
|
6c86c4f7ee | ||
|
|
72508332fb | ||
|
|
0ac88b4c80 | ||
|
|
11460b6f9f | ||
|
|
71b93909e6 | ||
|
|
26646cde0c | ||
|
|
4033e95a50 | ||
|
|
66d59b31d5 | ||
|
|
1e6da19d5e | ||
|
|
10de4b6fc9 | ||
|
|
2433344778 | ||
|
|
4c01459942 | ||
|
|
8dacdd6e2f | ||
|
|
5b0580bfda | ||
|
|
2bed7e845d | ||
|
|
76976026b7 | ||
|
|
10ea8fbf88 | ||
|
|
bd86b159b9 | ||
|
|
c4f5241818 | ||
|
|
181f433477 | ||
|
|
3897a7e137 | ||
|
|
5082ecef3f | ||
|
|
268e433870 | ||
|
|
6c185160c5 | ||
|
|
d18323b74a | ||
|
|
edd91791c9 | ||
|
|
958b13c050 | ||
|
|
bbe140f7be | ||
|
|
1625f82ab7 | ||
|
|
843f729a62 | ||
|
|
cc4631bb42 | ||
|
|
3665ef0088 | ||
|
|
e13039818e | ||
|
|
2e79487982 | ||
|
|
fc2c045a8d | ||
|
|
69bb94be06 | ||
|
|
228c8fa796 | ||
|
|
a34b850824 | ||
|
|
96d518bb97 | ||
|
|
8355f5674d | ||
|
|
5c1d04ee88 | ||
|
|
2ef98d01b0 | ||
|
|
15a86a3014 | ||
|
|
9c49704cf9 | ||
|
|
44f0d8d5c7 | ||
|
|
d7af97d63f | ||
|
|
9d0c9dc6ea | ||
|
|
3b189e4def | ||
|
|
7fe50922cc | ||
|
|
b04e2be665 | ||
|
|
80b1535cdf | ||
|
|
3d882f0442 | ||
|
|
f61d568c9d | ||
|
|
6dd02d289c | ||
|
|
33a4944ba3 | ||
|
|
e367b6056d | ||
|
|
f3f56b9be0 | ||
|
|
4cae6bad34 | ||
|
|
17f2ef57de | ||
|
|
3a4d111a37 | ||
|
|
1e18c7f5fc | ||
|
|
8c7a6b27ed | ||
|
|
91f96e1188 | ||
|
|
28dac10723 | ||
|
|
9cb11e38dd | ||
|
|
f3e4bacb33 | ||
|
|
626d43f07b | ||
|
|
1f5d073035 | ||
|
|
a3b607e799 | ||
|
|
8fb93502af | ||
|
|
36774122e0 | ||
|
|
a6923128c1 | ||
|
|
706ca62feb | ||
|
|
01523dcca3 | ||
|
|
77f039b561 | ||
|
|
d661ba77be | ||
|
|
f8fbc59b6f | ||
|
|
cce0d22007 | ||
|
|
e3ff76e9cb | ||
|
|
4cbf71bff7 | ||
|
|
ceb060243a | ||
|
|
a70bebb96a | ||
|
|
b3b2507c35 | ||
|
|
6a8fa16b49 | ||
|
|
1f08807701 | ||
|
|
ba4a7f6170 | ||
|
|
a2854e3602 | ||
|
|
4ea87dc494 | ||
|
|
d8c87c5314 | ||
|
|
46f624a12e | ||
|
|
86ce770f38 |
@@ -1,5 +1,27 @@
|
||||
# chat-rn-clerk
|
||||
|
||||
## 1.0.90
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [01523dc]
|
||||
- Updated dependencies [4ea87dc]
|
||||
- Updated dependencies [1e6da19]
|
||||
- Updated dependencies [b6c6a0a]
|
||||
- jazz-tools@0.12.0
|
||||
- jazz-react-native@0.12.0
|
||||
- jazz-react-native-auth-clerk@0.12.0
|
||||
- jazz-react-native-media-images@0.12.0
|
||||
|
||||
## 1.0.89
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-react-native@0.11.8
|
||||
- jazz-react-native-auth-clerk@0.11.8
|
||||
- jazz-tools@0.11.8
|
||||
- jazz-react-native-media-images@0.11.8
|
||||
|
||||
## 1.0.88
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -28,7 +28,7 @@ export default function Conversation() {
|
||||
const { me } = useAccount();
|
||||
const [chat, setChat] = useState<Chat>();
|
||||
const [message, setMessage] = useState("");
|
||||
const loadedChat = useCoState(Chat, chat?.id, [{}]);
|
||||
const loadedChat = useCoState(Chat, chat?.id, { resolve: { $each: true } });
|
||||
const navigation = useNavigation();
|
||||
const [isUploading, setIsUploading] = useState(false);
|
||||
|
||||
@@ -71,7 +71,7 @@ export default function Conversation() {
|
||||
|
||||
const loadChat = async (chatId: ID<Chat>) => {
|
||||
try {
|
||||
const chat = await Chat.load(chatId, me, []);
|
||||
const chat = await Chat.load(chatId, me);
|
||||
setChat(chat);
|
||||
} catch (error) {
|
||||
console.log("Error loading chat", error);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "chat-rn-clerk",
|
||||
"main": "index.js",
|
||||
"version": "1.0.88",
|
||||
"version": "1.0.90",
|
||||
"scripts": {
|
||||
"build": "expo export -p ios",
|
||||
"start": "expo start",
|
||||
|
||||
@@ -1,5 +1,23 @@
|
||||
# chat-rn
|
||||
|
||||
## 1.0.86
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [01523dc]
|
||||
- Updated dependencies [4ea87dc]
|
||||
- Updated dependencies [1e6da19]
|
||||
- Updated dependencies [b6c6a0a]
|
||||
- jazz-tools@0.12.0
|
||||
- jazz-react-native@0.12.0
|
||||
|
||||
## 1.0.85
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-react-native@0.11.8
|
||||
- jazz-tools@0.11.8
|
||||
|
||||
## 1.0.84
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "chat-rn",
|
||||
"version": "1.0.84",
|
||||
"version": "1.0.86",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"build": "expo export -p ios",
|
||||
|
||||
@@ -20,7 +20,7 @@ import { Chat, Message } from "./schema";
|
||||
export default function ChatScreen({ navigation }: { navigation: any }) {
|
||||
const { me, logOut } = useAccount();
|
||||
const [chatId, setChatId] = useState<ID<Chat>>();
|
||||
const loadedChat = useCoState(Chat, chatId, [{}]);
|
||||
const loadedChat = useCoState(Chat, chatId, { resolve: { $each: true } });
|
||||
const [message, setMessage] = useState("");
|
||||
const profile = useCoState(Profile, me._refs.profile?.id, {});
|
||||
|
||||
|
||||
@@ -1,5 +1,26 @@
|
||||
# chat-vue
|
||||
|
||||
## 0.0.71
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [01523dc]
|
||||
- Updated dependencies [4ea87dc]
|
||||
- Updated dependencies [1e6da19]
|
||||
- Updated dependencies [b6c6a0a]
|
||||
- Updated dependencies [4c01459]
|
||||
- jazz-tools@0.12.0
|
||||
- jazz-vue@0.12.0
|
||||
- jazz-browser@0.12.0
|
||||
|
||||
## 0.0.70
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-browser@0.11.8
|
||||
- jazz-tools@0.11.8
|
||||
- jazz-vue@0.11.8
|
||||
|
||||
## 0.0.69
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "chat-vue",
|
||||
"version": "0.0.69",
|
||||
"version": "0.0.71",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@@ -49,7 +49,7 @@ export default defineComponent({
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
const chat = useCoState(Chat, props.chatId, [{}]);
|
||||
const chat = useCoState(Chat, props.chatId, { resolve: { $each: true } });
|
||||
const showNLastMessages = ref(30);
|
||||
|
||||
const displayedMessages = computed(() => {
|
||||
|
||||
@@ -1,5 +1,27 @@
|
||||
# jazz-example-chat
|
||||
|
||||
## 0.0.168
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [01523dc]
|
||||
- Updated dependencies [4ea87dc]
|
||||
- Updated dependencies [1e6da19]
|
||||
- Updated dependencies [b6c6a0a]
|
||||
- Updated dependencies [9a56bb3]
|
||||
- jazz-tools@0.12.0
|
||||
- jazz-inspector@0.12.0
|
||||
- jazz-react@0.12.0
|
||||
|
||||
## 0.0.167
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [71b9390]
|
||||
- jazz-inspector@0.11.8
|
||||
- jazz-react@0.11.8
|
||||
- jazz-tools@0.11.8
|
||||
|
||||
## 0.0.166
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "jazz-example-chat",
|
||||
"private": true,
|
||||
"version": "0.0.166",
|
||||
"version": "0.0.168",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -16,8 +16,8 @@ import {
|
||||
} from "./ui.tsx";
|
||||
|
||||
export function ChatScreen(props: { chatID: ID<Chat> }) {
|
||||
const chat = useCoState(Chat, props.chatID, { resolve: { $each: true } });
|
||||
const account = useAccount();
|
||||
const chat = useCoState(Chat, props.chatID, [{}]);
|
||||
const [showNLastMessages, setShowNLastMessages] = useState(30);
|
||||
|
||||
if (!chat)
|
||||
|
||||
@@ -1,5 +1,25 @@
|
||||
# minimal-auth-clerk
|
||||
|
||||
## 0.0.67
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [01523dc]
|
||||
- Updated dependencies [4ea87dc]
|
||||
- Updated dependencies [1e6da19]
|
||||
- Updated dependencies [b6c6a0a]
|
||||
- jazz-tools@0.12.0
|
||||
- jazz-react@0.12.0
|
||||
- jazz-react-auth-clerk@0.12.0
|
||||
|
||||
## 0.0.66
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-react@0.11.8
|
||||
- jazz-react-auth-clerk@0.11.8
|
||||
- jazz-tools@0.11.8
|
||||
|
||||
## 0.0.65
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "clerk",
|
||||
"private": true,
|
||||
"version": "0.0.65",
|
||||
"version": "0.0.67",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
@@ -13,7 +13,7 @@
|
||||
"dependencies": {
|
||||
"@clerk/clerk-react": "^5.4.1",
|
||||
"jazz-react": "workspace:*",
|
||||
"jazz-react-auth-clerk": "workspace:0.11.7",
|
||||
"jazz-react-auth-clerk": "workspace:0.12.0",
|
||||
"jazz-tools": "workspace:*",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1"
|
||||
|
||||
@@ -1,5 +1,23 @@
|
||||
# file-share-svelte
|
||||
|
||||
## 0.0.51
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [01523dc]
|
||||
- Updated dependencies [4ea87dc]
|
||||
- Updated dependencies [1e6da19]
|
||||
- Updated dependencies [b6c6a0a]
|
||||
- jazz-tools@0.12.0
|
||||
- jazz-svelte@0.12.0
|
||||
|
||||
## 0.0.50
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-svelte@0.11.8
|
||||
- jazz-tools@0.11.8
|
||||
|
||||
## 0.0.49
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "file-share-svelte",
|
||||
"version": "0.0.49",
|
||||
"version": "0.0.51",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,5 +1,23 @@
|
||||
# jazz-tailwind-demo-auth-starter
|
||||
|
||||
## 0.0.7
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [01523dc]
|
||||
- Updated dependencies [4ea87dc]
|
||||
- Updated dependencies [1e6da19]
|
||||
- Updated dependencies [b6c6a0a]
|
||||
- jazz-tools@0.12.0
|
||||
- jazz-react@0.12.0
|
||||
|
||||
## 0.0.6
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-react@0.11.8
|
||||
- jazz-tools@0.11.8
|
||||
|
||||
## 0.0.5
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "filestream",
|
||||
"private": true,
|
||||
"version": "0.0.5",
|
||||
"version": "0.0.7",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -1,5 +1,23 @@
|
||||
# form
|
||||
|
||||
## 0.1.9
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [01523dc]
|
||||
- Updated dependencies [4ea87dc]
|
||||
- Updated dependencies [1e6da19]
|
||||
- Updated dependencies [b6c6a0a]
|
||||
- jazz-tools@0.12.0
|
||||
- jazz-react@0.12.0
|
||||
|
||||
## 0.1.8
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-react@0.11.8
|
||||
- jazz-tools@0.11.8
|
||||
|
||||
## 0.1.7
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "form",
|
||||
"private": true,
|
||||
"version": "0.1.7",
|
||||
"version": "0.1.9",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -12,7 +12,9 @@ import {
|
||||
} from "./schema.ts";
|
||||
|
||||
export function CreateOrder() {
|
||||
const { me } = useAccount({ root: { draft: {}, orders: [] } });
|
||||
const { me } = useAccount({
|
||||
resolve: { root: { draft: true, orders: true } },
|
||||
});
|
||||
const router = useIframeHashRouter();
|
||||
const [errors, setErrors] = useState<string[]>([]);
|
||||
|
||||
@@ -60,7 +62,7 @@ function CreateOrderForm({
|
||||
onSave: (draft: DraftBubbleTeaOrder) => void;
|
||||
}) {
|
||||
const draft = useCoState(DraftBubbleTeaOrder, id, {
|
||||
addOns: [],
|
||||
resolve: { addOns: true },
|
||||
});
|
||||
|
||||
if (!draft) return;
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useAccount } from "jazz-react";
|
||||
|
||||
export function DraftIndicator() {
|
||||
const { me } = useAccount({
|
||||
root: { draft: {} },
|
||||
resolve: { root: { draft: true } },
|
||||
});
|
||||
|
||||
if (me?.root.draft?.hasChanges) {
|
||||
|
||||
@@ -6,7 +6,7 @@ import { OrderThumbnail } from "./OrderThumbnail.tsx";
|
||||
import { BubbleTeaOrder } from "./schema.ts";
|
||||
|
||||
export function EditOrder(props: { id: ID<BubbleTeaOrder> }) {
|
||||
const order = useCoState(BubbleTeaOrder, props.id, []);
|
||||
const order = useCoState(BubbleTeaOrder, props.id);
|
||||
|
||||
if (!order) return;
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import { OrderThumbnail } from "./OrderThumbnail.tsx";
|
||||
|
||||
export function Orders() {
|
||||
const { me } = useAccount({
|
||||
root: { orders: [] },
|
||||
resolve: { root: { orders: true } },
|
||||
});
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,5 +1,23 @@
|
||||
# image-upload
|
||||
|
||||
## 0.0.65
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [01523dc]
|
||||
- Updated dependencies [4ea87dc]
|
||||
- Updated dependencies [1e6da19]
|
||||
- Updated dependencies [b6c6a0a]
|
||||
- jazz-tools@0.12.0
|
||||
- jazz-react@0.12.0
|
||||
|
||||
## 0.0.64
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-react@0.11.8
|
||||
- jazz-tools@0.11.8
|
||||
|
||||
## 0.0.63
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "image-upload",
|
||||
"private": true,
|
||||
"version": "0.0.63",
|
||||
"version": "0.0.65",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -1,5 +1,27 @@
|
||||
# jazz-example-inspector
|
||||
|
||||
## 0.0.118
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [01523dc]
|
||||
- Updated dependencies [01523dc]
|
||||
- Updated dependencies [9a56bb3]
|
||||
- cojson@0.12.0
|
||||
- jazz-inspector@0.12.0
|
||||
- cojson-transport-ws@0.12.0
|
||||
|
||||
## 0.0.117
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [71b9390]
|
||||
- Updated dependencies [6c86c4f]
|
||||
- Updated dependencies [9d0c9dc]
|
||||
- jazz-inspector@0.11.8
|
||||
- cojson@0.11.8
|
||||
- cojson-transport-ws@0.11.8
|
||||
|
||||
## 0.0.116
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "jazz-inspector-app",
|
||||
"private": true,
|
||||
"version": "0.0.116",
|
||||
"version": "0.0.118",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
@@ -13,8 +13,8 @@
|
||||
"dependencies": {
|
||||
"jazz-inspector": "workspace:*",
|
||||
"clsx": "^2.0.0",
|
||||
"cojson": "workspace:0.11.7",
|
||||
"cojson-transport-ws": "workspace:0.11.7",
|
||||
"cojson": "workspace:0.12.0",
|
||||
"cojson-transport-ws": "workspace:0.12.0",
|
||||
"hash-slash": "workspace:0.2.2",
|
||||
"lucide-react": "^0.274.0",
|
||||
"react": "^18.3.1",
|
||||
|
||||
@@ -1,5 +1,23 @@
|
||||
# multi-cursors
|
||||
|
||||
## 0.0.61
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [01523dc]
|
||||
- Updated dependencies [4ea87dc]
|
||||
- Updated dependencies [1e6da19]
|
||||
- Updated dependencies [b6c6a0a]
|
||||
- jazz-tools@0.12.0
|
||||
- jazz-react@0.12.0
|
||||
|
||||
## 0.0.60
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-react@0.11.8
|
||||
- jazz-tools@0.11.8
|
||||
|
||||
## 0.0.59
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "multi-cursors",
|
||||
"private": true,
|
||||
"version": "0.0.59",
|
||||
"version": "0.0.61",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -7,7 +7,7 @@ import Canvas from "./Canvas";
|
||||
/** A higher order component that wraps the canvas. */
|
||||
function Container({ cursorFeedID }: { cursorFeedID: ID<CursorFeed> }) {
|
||||
const { me } = useAccount();
|
||||
const cursors = useCoState(CursorFeed, cursorFeedID, []);
|
||||
const cursors = useCoState(CursorFeed, cursorFeedID, { resolve: true });
|
||||
|
||||
return (
|
||||
<Canvas
|
||||
|
||||
@@ -3,7 +3,7 @@ import { CursorContainer, CursorFeed } from "../schema";
|
||||
|
||||
export async function loadGroup(me: Account, groupID: ID<Group>) {
|
||||
const group = await Group.load(groupID, {});
|
||||
if (group === undefined) {
|
||||
if (group === null) {
|
||||
const group = Group.create({
|
||||
owner: me,
|
||||
});
|
||||
@@ -35,15 +35,16 @@ export async function loadCursorContainer(
|
||||
group?.id as ID<Group>,
|
||||
);
|
||||
const cursorContainer = await CursorContainer.load(cursorContainerID, {
|
||||
cursorFeed: [],
|
||||
resolve: {
|
||||
cursorFeed: true,
|
||||
},
|
||||
});
|
||||
if (cursorContainer === undefined) {
|
||||
|
||||
if (cursorContainer === null) {
|
||||
console.log("Global cursors does not exist, creating...");
|
||||
const cursorContainer = CursorContainer.create(
|
||||
{
|
||||
cursorFeed: CursorFeed.create([], {
|
||||
owner: group,
|
||||
}),
|
||||
cursorFeed: CursorFeed.create([], group),
|
||||
},
|
||||
{
|
||||
owner: group,
|
||||
|
||||
@@ -1,5 +1,25 @@
|
||||
# multiauth
|
||||
|
||||
## 0.0.8
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [01523dc]
|
||||
- Updated dependencies [4ea87dc]
|
||||
- Updated dependencies [1e6da19]
|
||||
- Updated dependencies [b6c6a0a]
|
||||
- jazz-tools@0.12.0
|
||||
- jazz-react@0.12.0
|
||||
- jazz-react-auth-clerk@0.12.0
|
||||
|
||||
## 0.0.7
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-react@0.11.8
|
||||
- jazz-react-auth-clerk@0.11.8
|
||||
- jazz-tools@0.11.8
|
||||
|
||||
## 0.0.6
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "multiauth",
|
||||
"private": true,
|
||||
"version": "0.0.6",
|
||||
"version": "0.0.8",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -1,5 +1,27 @@
|
||||
# jazz-example-musicplayer
|
||||
|
||||
## 0.0.89
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [01523dc]
|
||||
- Updated dependencies [4ea87dc]
|
||||
- Updated dependencies [1e6da19]
|
||||
- Updated dependencies [b6c6a0a]
|
||||
- Updated dependencies [9a56bb3]
|
||||
- jazz-tools@0.12.0
|
||||
- jazz-inspector@0.12.0
|
||||
- jazz-react@0.12.0
|
||||
|
||||
## 0.0.88
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [71b9390]
|
||||
- jazz-inspector@0.11.8
|
||||
- jazz-react@0.11.8
|
||||
- jazz-tools@0.11.8
|
||||
|
||||
## 0.0.87
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "jazz-example-music-player",
|
||||
"private": true,
|
||||
"version": "0.0.87",
|
||||
"version": "0.0.89",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
@@ -22,8 +22,8 @@
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"clsx": "^2.0.0",
|
||||
"jazz-inspector": "workspace:*",
|
||||
"jazz-react": "workspace:0.11.7",
|
||||
"jazz-tools": "workspace:0.11.7",
|
||||
"jazz-react": "workspace:0.12.0",
|
||||
"jazz-tools": "workspace:0.12.0",
|
||||
"lucide-react": "^0.274.0",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
|
||||
@@ -24,10 +24,7 @@ export function HomePage({ mediaPlayer }: { mediaPlayer: MediaPlayer }) {
|
||||
* access rights to CoValues. We get it from the top-level provider `<WithJazz/>`.
|
||||
*/
|
||||
const { me } = useAccount({
|
||||
root: {
|
||||
rootPlaylist: {},
|
||||
playlists: [],
|
||||
},
|
||||
resolve: { root: { rootPlaylist: true, playlists: true } },
|
||||
});
|
||||
|
||||
const navigate = useNavigate();
|
||||
@@ -51,8 +48,9 @@ export function HomePage({ mediaPlayer }: { mediaPlayer: MediaPlayer }) {
|
||||
|
||||
const params = useParams<{ playlistId: ID<Playlist> }>();
|
||||
const playlistId = params.playlistId ?? me?.root._refs.rootPlaylist.id;
|
||||
|
||||
const playlist = useCoState(Playlist, playlistId, {
|
||||
tracks: [],
|
||||
resolve: { tracks: true },
|
||||
});
|
||||
|
||||
const isRootPlaylist = !params.playlistId;
|
||||
|
||||
@@ -27,9 +27,11 @@ export async function uploadMusicTracks(
|
||||
isExampleTrack: boolean = false,
|
||||
) {
|
||||
const { root } = await MusicaAccount.getMe().ensureLoaded({
|
||||
root: {
|
||||
rootPlaylist: {
|
||||
tracks: [],
|
||||
resolve: {
|
||||
root: {
|
||||
rootPlaylist: {
|
||||
tracks: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -65,8 +67,10 @@ export async function uploadMusicTracks(
|
||||
|
||||
export async function createNewPlaylist() {
|
||||
const { root } = await MusicaAccount.getMe().ensureLoaded({
|
||||
root: {
|
||||
playlists: [],
|
||||
resolve: {
|
||||
root: {
|
||||
playlists: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -149,9 +153,11 @@ export async function updateMusicTrackTitle(track: MusicTrack, title: string) {
|
||||
|
||||
export async function updateActivePlaylist(playlist?: Playlist) {
|
||||
const { root } = await MusicaAccount.getMe().ensureLoaded({
|
||||
root: {
|
||||
activePlaylist: {},
|
||||
rootPlaylist: {},
|
||||
resolve: {
|
||||
root: {
|
||||
activePlaylist: true,
|
||||
rootPlaylist: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -160,7 +166,9 @@ export async function updateActivePlaylist(playlist?: Playlist) {
|
||||
|
||||
export async function updateActiveTrack(track: MusicTrack) {
|
||||
const { root } = await MusicaAccount.getMe().ensureLoaded({
|
||||
root: {},
|
||||
resolve: {
|
||||
root: {},
|
||||
},
|
||||
});
|
||||
|
||||
root.activeTrack = track;
|
||||
@@ -170,17 +178,23 @@ export async function onAnonymousAccountDiscarded(
|
||||
anonymousAccount: MusicaAccount,
|
||||
) {
|
||||
const { root: anonymousAccountRoot } = await anonymousAccount.ensureLoaded({
|
||||
root: {
|
||||
rootPlaylist: {
|
||||
tracks: [{}],
|
||||
resolve: {
|
||||
root: {
|
||||
rootPlaylist: {
|
||||
tracks: {
|
||||
$each: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const me = await MusicaAccount.getMe().ensureLoaded({
|
||||
root: {
|
||||
rootPlaylist: {
|
||||
tracks: [],
|
||||
resolve: {
|
||||
root: {
|
||||
rootPlaylist: {
|
||||
tracks: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -197,8 +211,10 @@ export async function onAnonymousAccountDiscarded(
|
||||
|
||||
export async function deletePlaylist(playlistId: string) {
|
||||
const { root } = await MusicaAccount.getMe().ensureLoaded({
|
||||
root: {
|
||||
playlists: [],
|
||||
resolve: {
|
||||
root: {
|
||||
playlists: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ import { getNextTrack, getPrevTrack } from "./lib/getters";
|
||||
|
||||
export function useMediaPlayer() {
|
||||
const { me } = useAccount({
|
||||
root: {},
|
||||
resolve: { root: true },
|
||||
});
|
||||
|
||||
const playState = usePlayState();
|
||||
|
||||
@@ -16,8 +16,10 @@ export function InvitePage() {
|
||||
const playlist = await Playlist.load(playlistId, {});
|
||||
|
||||
const me = await MusicaAccount.getMe().ensureLoaded({
|
||||
root: {
|
||||
playlists: [],
|
||||
resolve: {
|
||||
root: {
|
||||
playlists: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -22,9 +22,13 @@ export function AuthModal({ open, onOpenChange }: AuthModalProps) {
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const { me } = useAccount({
|
||||
root: {
|
||||
rootPlaylist: {
|
||||
tracks: [{}],
|
||||
resolve: {
|
||||
root: {
|
||||
rootPlaylist: {
|
||||
tracks: {
|
||||
$each: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -30,9 +30,7 @@ export function MusicTrackRow({
|
||||
const track = useCoState(MusicTrack, trackId);
|
||||
|
||||
const { me } = useAccount({
|
||||
root: {
|
||||
playlists: [{}],
|
||||
},
|
||||
resolve: { root: { playlists: { $each: true } } },
|
||||
});
|
||||
|
||||
const playlists = me?.root.playlists ?? [];
|
||||
|
||||
@@ -12,9 +12,7 @@ export function PlayerControls({ mediaPlayer }: { mediaPlayer: MediaPlayer }) {
|
||||
const isPlaying = playState.value === "play";
|
||||
|
||||
const activePlaylist = useAccount({
|
||||
root: {
|
||||
activePlaylist: {},
|
||||
},
|
||||
resolve: { root: { activePlaylist: true } },
|
||||
}).me?.root.activePlaylist;
|
||||
|
||||
useMediaEndListener(mediaPlayer.playNextTrack);
|
||||
@@ -25,7 +23,7 @@ export function PlayerControls({ mediaPlayer }: { mediaPlayer: MediaPlayer }) {
|
||||
});
|
||||
|
||||
const activeTrack = useCoState(MusicTrack, mediaPlayer.activeTrackId, {
|
||||
waveform: {},
|
||||
resolve: { waveform: true },
|
||||
});
|
||||
|
||||
if (!activeTrack) return null;
|
||||
|
||||
@@ -8,9 +8,7 @@ export function SidePanel() {
|
||||
const { playlistId } = useParams();
|
||||
const navigate = useNavigate();
|
||||
const { me } = useAccount({
|
||||
root: {
|
||||
playlists: [{}],
|
||||
},
|
||||
resolve: { root: { playlists: { $each: true } } },
|
||||
});
|
||||
|
||||
function handleAllTracksClick(evt: React.MouseEvent<HTMLAnchorElement>) {
|
||||
|
||||
@@ -8,7 +8,6 @@ export function Waveform(props: { track: MusicTrack; height: number }) {
|
||||
const waveformData = useCoState(
|
||||
MusicTrackWaveform,
|
||||
track._refs.waveform.id,
|
||||
{},
|
||||
)?.data;
|
||||
const duration = track.duration;
|
||||
|
||||
|
||||
@@ -2,9 +2,11 @@ import { MusicaAccount } from "../1_schema";
|
||||
|
||||
export async function getNextTrack() {
|
||||
const me = await MusicaAccount.getMe().ensureLoaded({
|
||||
root: {
|
||||
activePlaylist: {
|
||||
tracks: [],
|
||||
resolve: {
|
||||
root: {
|
||||
activePlaylist: {
|
||||
tracks: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -21,9 +23,11 @@ export async function getNextTrack() {
|
||||
|
||||
export async function getPrevTrack() {
|
||||
const me = await MusicaAccount.getMe().ensureLoaded({
|
||||
root: {
|
||||
activePlaylist: {
|
||||
tracks: [],
|
||||
resolve: {
|
||||
root: {
|
||||
activePlaylist: {
|
||||
tracks: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -13,7 +13,7 @@ export function useUploadExampleData() {
|
||||
|
||||
async function uploadOnboardingData() {
|
||||
const me = await MusicaAccount.getMe().ensureLoaded({
|
||||
root: {},
|
||||
resolve: { root: true },
|
||||
});
|
||||
|
||||
if (me.root.exampleDataLoaded) return;
|
||||
|
||||
@@ -1,5 +1,23 @@
|
||||
# organization
|
||||
|
||||
## 0.0.61
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [01523dc]
|
||||
- Updated dependencies [4ea87dc]
|
||||
- Updated dependencies [1e6da19]
|
||||
- Updated dependencies [b6c6a0a]
|
||||
- jazz-tools@0.12.0
|
||||
- jazz-react@0.12.0
|
||||
|
||||
## 0.0.60
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-react@0.11.8
|
||||
- jazz-tools@0.11.8
|
||||
|
||||
## 0.0.59
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "organization",
|
||||
"private": true,
|
||||
"version": "0.0.59",
|
||||
"version": "0.0.61",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -5,11 +5,11 @@ import { Organization } from "./schema.ts";
|
||||
|
||||
export function AcceptInvitePage() {
|
||||
const navigate = useNavigate();
|
||||
const { me } = useAccount({ root: { organizations: [] } });
|
||||
const { me } = useAccount({ resolve: { root: { organizations: true } } });
|
||||
|
||||
const onAccept = (organizationId: ID<Organization>) => {
|
||||
if (me?.root?.organizations) {
|
||||
Organization.load(organizationId, me, []).then((organization) => {
|
||||
Organization.load(organizationId).then((organization) => {
|
||||
if (organization) {
|
||||
// avoid duplicates
|
||||
const ids = me.root.organizations.map(
|
||||
|
||||
@@ -5,7 +5,7 @@ import { Heading } from "./components/Heading.tsx";
|
||||
|
||||
export function HomePage() {
|
||||
const { me } = useAccount({
|
||||
root: { organizations: [{}] },
|
||||
resolve: { root: { organizations: true } },
|
||||
});
|
||||
|
||||
if (!me?.root.organizations) return;
|
||||
|
||||
@@ -3,7 +3,7 @@ import { UserIcon } from "lucide-react";
|
||||
|
||||
export function Layout({ children }: { children: React.ReactNode }) {
|
||||
const { me, logOut } = useAccount({
|
||||
root: { draftOrganization: {} },
|
||||
resolve: { root: { draftOrganization: true } },
|
||||
});
|
||||
|
||||
return (
|
||||
|
||||
@@ -13,7 +13,7 @@ export function OrganizationPage() {
|
||||
.organizationId;
|
||||
|
||||
const organization = useCoState(Organization, paramOrganizationId, {
|
||||
projects: [],
|
||||
resolve: { projects: true },
|
||||
});
|
||||
|
||||
if (!organization) return <p>Loading organization...</p>;
|
||||
|
||||
@@ -8,7 +8,7 @@ import { OrganizationForm } from "./OrganizationForm.tsx";
|
||||
|
||||
export function CreateOrganization() {
|
||||
const { me } = useAccount({
|
||||
root: { draftOrganization: {}, organizations: [] },
|
||||
resolve: { root: { draftOrganization: true, organizations: true } },
|
||||
});
|
||||
const [errors, setErrors] = useState<string[]>([]);
|
||||
const navigate = useNavigate();
|
||||
|
||||
@@ -7,7 +7,7 @@ import { Organization } from "../schema.ts";
|
||||
|
||||
export function OrganizationSelector({ className }: { className?: string }) {
|
||||
const { me } = useAccount({
|
||||
root: { organizations: [{}] },
|
||||
resolve: { root: { organizations: { $each: true } } },
|
||||
});
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
@@ -1,5 +1,17 @@
|
||||
# passkey-svelte
|
||||
|
||||
## 0.0.55
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-svelte@0.12.0
|
||||
|
||||
## 0.0.54
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-svelte@0.11.8
|
||||
|
||||
## 0.0.53
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "passkey-svelte",
|
||||
"version": "0.0.53",
|
||||
"version": "0.0.55",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
|
||||
@@ -1,5 +1,23 @@
|
||||
# minimal-auth-passkey
|
||||
|
||||
## 0.0.66
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [01523dc]
|
||||
- Updated dependencies [4ea87dc]
|
||||
- Updated dependencies [1e6da19]
|
||||
- Updated dependencies [b6c6a0a]
|
||||
- jazz-tools@0.12.0
|
||||
- jazz-react@0.12.0
|
||||
|
||||
## 0.0.65
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-react@0.11.8
|
||||
- jazz-tools@0.11.8
|
||||
|
||||
## 0.0.64
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "passkey",
|
||||
"private": true,
|
||||
"version": "0.0.64",
|
||||
"version": "0.0.66",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -1,5 +1,23 @@
|
||||
# passphrase
|
||||
|
||||
## 0.0.63
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [01523dc]
|
||||
- Updated dependencies [4ea87dc]
|
||||
- Updated dependencies [1e6da19]
|
||||
- Updated dependencies [b6c6a0a]
|
||||
- jazz-tools@0.12.0
|
||||
- jazz-react@0.12.0
|
||||
|
||||
## 0.0.62
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-react@0.11.8
|
||||
- jazz-tools@0.11.8
|
||||
|
||||
## 0.0.61
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "passphrase",
|
||||
"private": true,
|
||||
"version": "0.0.61",
|
||||
"version": "0.0.63",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -1,5 +1,23 @@
|
||||
# jazz-password-manager
|
||||
|
||||
## 0.0.87
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [01523dc]
|
||||
- Updated dependencies [4ea87dc]
|
||||
- Updated dependencies [1e6da19]
|
||||
- Updated dependencies [b6c6a0a]
|
||||
- jazz-tools@0.12.0
|
||||
- jazz-react@0.12.0
|
||||
|
||||
## 0.0.86
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-react@0.11.8
|
||||
- jazz-tools@0.11.8
|
||||
|
||||
## 0.0.85
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "jazz-password-manager",
|
||||
"private": true,
|
||||
"version": "0.0.85",
|
||||
"version": "0.0.87",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
@@ -12,8 +12,8 @@
|
||||
"clean-install": "rm -rf node_modules pnpm-lock.yaml && pnpm install"
|
||||
},
|
||||
"dependencies": {
|
||||
"jazz-react": "workspace:0.11.7",
|
||||
"jazz-tools": "workspace:0.11.7",
|
||||
"jazz-react": "workspace:0.12.0",
|
||||
"jazz-tools": "workspace:0.12.0",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-hook-form": "^7.41.5",
|
||||
|
||||
@@ -43,9 +43,11 @@ const VaultPage: React.FC = () => {
|
||||
(item): item is Exclude<typeof item, null> => !!item,
|
||||
) || [],
|
||||
);
|
||||
const folders = useCoState(FolderList, me.root?._refs.folders?.id, [
|
||||
{ items: [{}] },
|
||||
]);
|
||||
const folders = useCoState(FolderList, me.root?._refs.folders?.id, {
|
||||
resolve: {
|
||||
$each: { items: { $each: true } },
|
||||
},
|
||||
});
|
||||
|
||||
const [selectedFolder, setSelectedFolder] = useState<Folder | undefined>();
|
||||
const [isNewItemModalOpen, setIsNewItemModalOpen] = useState(false);
|
||||
|
||||
@@ -60,11 +60,9 @@ export async function addSharedFolder(
|
||||
me: PasswordManagerAccount,
|
||||
) {
|
||||
const [sharedFolder, account] = await Promise.all([
|
||||
Folder.load(sharedFolderId, me, {}),
|
||||
PasswordManagerAccount.load(me.id, me, {
|
||||
root: {
|
||||
folders: [],
|
||||
},
|
||||
Folder.load(sharedFolderId),
|
||||
PasswordManagerAccount.load(me.id, {
|
||||
resolve: { root: { folders: true } },
|
||||
}),
|
||||
]);
|
||||
|
||||
|
||||
@@ -1,5 +1,23 @@
|
||||
# jazz-example-pets
|
||||
|
||||
## 0.0.185
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [01523dc]
|
||||
- Updated dependencies [4ea87dc]
|
||||
- Updated dependencies [1e6da19]
|
||||
- Updated dependencies [b6c6a0a]
|
||||
- jazz-tools@0.12.0
|
||||
- jazz-react@0.12.0
|
||||
|
||||
## 0.0.184
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-react@0.11.8
|
||||
- jazz-tools@0.11.8
|
||||
|
||||
## 0.0.183
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "jazz-example-pets",
|
||||
"private": true,
|
||||
"version": "0.0.183",
|
||||
"version": "0.0.185",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
@@ -19,8 +19,8 @@
|
||||
"@radix-ui/react-toast": "^1.1.4",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"clsx": "^2.0.0",
|
||||
"jazz-react": "workspace:0.11.7",
|
||||
"jazz-tools": "workspace:0.11.7",
|
||||
"jazz-react": "workspace:0.12.0",
|
||||
"jazz-tools": "workspace:0.12.0",
|
||||
"lucide-react": "^0.274.0",
|
||||
"qrcode": "^1.5.3",
|
||||
"react": "^18.3.1",
|
||||
@@ -40,7 +40,7 @@
|
||||
"@vitejs/plugin-react-swc": "^3.3.2",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"is-ci": "^3.0.1",
|
||||
"jazz-run": "workspace:0.11.7",
|
||||
"jazz-run": "workspace:0.12.0",
|
||||
"postcss": "^8.4.27",
|
||||
"tailwindcss": "^3.4.17",
|
||||
"typescript": "~5.6.2",
|
||||
|
||||
@@ -1,5 +1,23 @@
|
||||
# reactions
|
||||
|
||||
## 0.0.65
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [01523dc]
|
||||
- Updated dependencies [4ea87dc]
|
||||
- Updated dependencies [1e6da19]
|
||||
- Updated dependencies [b6c6a0a]
|
||||
- jazz-tools@0.12.0
|
||||
- jazz-react@0.12.0
|
||||
|
||||
## 0.0.64
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-react@0.11.8
|
||||
- jazz-tools@0.11.8
|
||||
|
||||
## 0.0.63
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "reactions",
|
||||
"private": true,
|
||||
"version": "0.0.63",
|
||||
"version": "0.0.65",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -14,7 +14,7 @@ const reactionEmojiMap: {
|
||||
};
|
||||
|
||||
export function ReactionsScreen(props: { id: ID<Reactions> }) {
|
||||
const reactions = useCoState(Reactions, props.id, []);
|
||||
const reactions = useCoState(Reactions, props.id);
|
||||
|
||||
if (!reactions) return;
|
||||
|
||||
|
||||
@@ -1,5 +1,26 @@
|
||||
# todo-vue
|
||||
|
||||
## 0.0.69
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [01523dc]
|
||||
- Updated dependencies [4ea87dc]
|
||||
- Updated dependencies [1e6da19]
|
||||
- Updated dependencies [b6c6a0a]
|
||||
- Updated dependencies [4c01459]
|
||||
- jazz-tools@0.12.0
|
||||
- jazz-vue@0.12.0
|
||||
- jazz-browser@0.12.0
|
||||
|
||||
## 0.0.68
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-browser@0.11.8
|
||||
- jazz-tools@0.11.8
|
||||
- jazz-vue@0.11.8
|
||||
|
||||
## 0.0.67
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "todo-vue",
|
||||
"version": "0.0.67",
|
||||
"version": "0.0.69",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@@ -4,15 +4,15 @@
|
||||
<div class="section-header">
|
||||
<h2>Folders</h2>
|
||||
<div class="new-folder">
|
||||
<input
|
||||
v-model="newFolderName"
|
||||
placeholder="New folder name"
|
||||
<input
|
||||
v-model="newFolderName"
|
||||
placeholder="New folder name"
|
||||
class="input"
|
||||
/>
|
||||
<button class="btn btn-primary" @click="createFolder">Create</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="folder-list">
|
||||
<div
|
||||
v-for="folder in folders"
|
||||
@@ -32,9 +32,9 @@
|
||||
<div class="section-header">
|
||||
<h2>{{ selectedFolder?.name }}</h2>
|
||||
<div class="new-todo">
|
||||
<input
|
||||
v-model="newTodoTitle"
|
||||
placeholder="Add a new task"
|
||||
<input
|
||||
v-model="newTodoTitle"
|
||||
placeholder="Add a new task"
|
||||
class="input"
|
||||
/>
|
||||
<button class="btn btn-primary" @click="createTodo">Add</button>
|
||||
@@ -72,7 +72,9 @@ import { Folder, FolderList, ToDoItem, ToDoList } from "../schema";
|
||||
const { me } = useAccount();
|
||||
|
||||
const computedFoldersId = computed(() => me.value?.root?.folders?.id);
|
||||
const folders = useCoState(FolderList, computedFoldersId, [{ items: [{}] }]);
|
||||
const folders = useCoState(FolderList, computedFoldersId, {
|
||||
resolve: { $each: { items: true } },
|
||||
});
|
||||
|
||||
const selectedFolder = ref<Folder>();
|
||||
const newFolderName = ref("");
|
||||
|
||||
@@ -1,5 +1,23 @@
|
||||
# jazz-example-todo
|
||||
|
||||
## 0.0.184
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [01523dc]
|
||||
- Updated dependencies [4ea87dc]
|
||||
- Updated dependencies [1e6da19]
|
||||
- Updated dependencies [b6c6a0a]
|
||||
- jazz-tools@0.12.0
|
||||
- jazz-react@0.12.0
|
||||
|
||||
## 0.0.183
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- jazz-react@0.11.8
|
||||
- jazz-tools@0.11.8
|
||||
|
||||
## 0.0.182
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "jazz-example-todo",
|
||||
"private": true,
|
||||
"version": "0.0.182",
|
||||
"version": "0.0.184",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
@@ -16,8 +16,8 @@
|
||||
"@radix-ui/react-toast": "^1.1.4",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"clsx": "^2.0.0",
|
||||
"jazz-react": "workspace:0.11.7",
|
||||
"jazz-tools": "workspace:0.11.7",
|
||||
"jazz-react": "workspace:0.12.0",
|
||||
"jazz-tools": "workspace:0.12.0",
|
||||
"lucide-react": "^0.274.0",
|
||||
"qrcode": "^1.5.3",
|
||||
"react": "^18.3.1",
|
||||
|
||||
@@ -121,7 +121,7 @@ export default function App() {
|
||||
|
||||
function HomeScreen() {
|
||||
const { me } = useAccount({
|
||||
root: { projects: [{}] },
|
||||
resolve: { root: { projects: { $each: true } } },
|
||||
});
|
||||
const navigate = useNavigate();
|
||||
|
||||
|
||||
@@ -1,5 +1,27 @@
|
||||
# version-history
|
||||
|
||||
## 0.0.62
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [01523dc]
|
||||
- Updated dependencies [4ea87dc]
|
||||
- Updated dependencies [1e6da19]
|
||||
- Updated dependencies [b6c6a0a]
|
||||
- Updated dependencies [9a56bb3]
|
||||
- jazz-tools@0.12.0
|
||||
- jazz-inspector@0.12.0
|
||||
- jazz-react@0.12.0
|
||||
|
||||
## 0.0.61
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [71b9390]
|
||||
- jazz-inspector@0.11.8
|
||||
- jazz-react@0.11.8
|
||||
- jazz-tools@0.11.8
|
||||
|
||||
## 0.0.60
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "version-history",
|
||||
"private": true,
|
||||
"version": "0.0.60",
|
||||
"version": "0.0.62",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -4,7 +4,9 @@ import { ID } from "jazz-tools";
|
||||
import { IssueComponent } from "./Issue.tsx";
|
||||
import { Issue, Project } from "./schema.ts";
|
||||
export function ProjectComponent({ projectID }: { projectID: ID<Project> }) {
|
||||
const project = useCoState(Project, projectID, { issues: [{}] });
|
||||
const project = useCoState(Project, projectID, {
|
||||
resolve: { issues: { $each: true } },
|
||||
});
|
||||
|
||||
if (!project) return;
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import { clsx } from "clsx";
|
||||
import Link from "next/link";
|
||||
import { forwardRef } from "react";
|
||||
import { Icon } from "./Icon";
|
||||
import type { IconName } from "./Icon";
|
||||
import { Spinner } from "./Spinner";
|
||||
|
||||
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
@@ -9,7 +10,7 @@ interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
size?: "sm" | "md" | "lg";
|
||||
href?: string;
|
||||
newTab?: boolean;
|
||||
icon?: string;
|
||||
icon?: IconName;
|
||||
loading?: boolean;
|
||||
loadingText?: string;
|
||||
children?: React.ReactNode;
|
||||
|
||||
@@ -113,6 +113,8 @@ const strokeWidths = {
|
||||
"9xl": 1,
|
||||
};
|
||||
|
||||
export type IconName = keyof typeof icons;
|
||||
|
||||
export function Icon({
|
||||
name,
|
||||
icon,
|
||||
@@ -120,7 +122,7 @@ export function Icon({
|
||||
className,
|
||||
...svgProps
|
||||
}: {
|
||||
name?: string;
|
||||
name?: IconName;
|
||||
icon?: LucideIcon;
|
||||
size?: keyof typeof sizes;
|
||||
className?: string;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import clsx from "clsx";
|
||||
import { Card } from "../atoms/Card";
|
||||
import { Icon } from "../atoms/Icon";
|
||||
import { Icon, IconName } from "../atoms/Icon";
|
||||
import { Prose } from "./Prose";
|
||||
|
||||
export function FeatureCard({
|
||||
@@ -11,7 +11,7 @@ export function FeatureCard({
|
||||
className,
|
||||
}: {
|
||||
label: React.ReactNode;
|
||||
icon?: string;
|
||||
icon?: IconName;
|
||||
explanation?: React.ReactNode;
|
||||
children?: React.ReactNode;
|
||||
className?: string;
|
||||
|
||||
@@ -13,12 +13,13 @@ import { usePathname } from "next/navigation";
|
||||
import { ComponentType, ReactNode, useEffect, useState } from "react";
|
||||
import { isActive } from "../../utils/nav";
|
||||
import { Icon } from "../atoms/Icon";
|
||||
import type { IconName } from "../atoms/Icon";
|
||||
import { BreadCrumb } from "../molecules/Breadcrumb";
|
||||
import { SocialLinks, SocialLinksProps } from "./SocialLinks";
|
||||
|
||||
type NavItemProps = {
|
||||
href: string;
|
||||
icon?: string;
|
||||
icon?: IconName;
|
||||
title: string;
|
||||
firstOnRight?: boolean;
|
||||
newTab?: boolean;
|
||||
@@ -38,7 +39,7 @@ type NavProps = {
|
||||
export type NavSection = {
|
||||
name: string;
|
||||
content: ReactNode;
|
||||
icon: string;
|
||||
icon: IconName;
|
||||
};
|
||||
|
||||
function NavItem({
|
||||
@@ -134,7 +135,11 @@ export function MobileNav({
|
||||
sections,
|
||||
themeToggle: ThemeToggle,
|
||||
}: NavProps) {
|
||||
const primarySection = {
|
||||
const primarySection: {
|
||||
name: string;
|
||||
icon: IconName;
|
||||
content: ReactNode;
|
||||
} = {
|
||||
name: "Menu",
|
||||
icon: "menu",
|
||||
content: (
|
||||
@@ -254,7 +259,7 @@ export function MobileNav({
|
||||
onClick={() => toggle(section.name)}
|
||||
key={section.name}
|
||||
>
|
||||
<Icon name={section.icon} size="xs" />
|
||||
{section.icon && <Icon name={section.icon} size="xs" />}
|
||||
{section.name}
|
||||
</button>
|
||||
),
|
||||
|
||||
@@ -49,7 +49,7 @@ function highlightPlugin() {
|
||||
token.content.includes("// old"),
|
||||
);
|
||||
const isBinnedLine = line.some((token) =>
|
||||
token.content.includes("// *bin*"),
|
||||
token.content.includes("// [!code --]"),
|
||||
);
|
||||
const isHighlighted = line.some((token) =>
|
||||
token.content.includes("// *highlight*"),
|
||||
@@ -70,7 +70,7 @@ function highlightPlugin() {
|
||||
line
|
||||
.map((token) => {
|
||||
let color = isHighlighted ? "currentColor" : token.color;
|
||||
return `<span style="color: ${color};${isSubduedLine ? "opacity: 0.4;" : ""}">${escape(token.content.replace("// old", "").replace("// *bin*", "").replace("// *highlight*", ""))}</span>`;
|
||||
return `<span style="color: ${color};${isSubduedLine ? "opacity: 0.4;" : ""}">${escape(token.content.replace("// old", "").replace("// [!code --]", "").replace("// *highlight*", ""))}</span>`;
|
||||
})
|
||||
.join("") +
|
||||
"</span>"
|
||||
|
||||
@@ -331,7 +331,7 @@ This works because CoValues
|
||||
|
||||
<CodeGroup>
|
||||
```ts
|
||||
const unsub = issue.subscribe([], (updatedIssue) => console.log(updatedIssue));
|
||||
const unsub = issue.subscribe({ resolve: true }, (updatedIssue) => console.log(updatedIssue));
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
@@ -339,7 +339,7 @@ This works because CoValues
|
||||
|
||||
<CodeGroup>
|
||||
```ts
|
||||
const unsub = Issue.subscribe(issueID, me, [], (updatedIssue) => {
|
||||
const unsub = Issue.subscribe(issueID, me, { resolve: true }, (updatedIssue) => {
|
||||
console.log(updatedIssue);
|
||||
});
|
||||
```
|
||||
@@ -358,7 +358,7 @@ This works because CoValues
|
||||
function useCoState<V extends CoValue>(Schema: CoValueClass<V>, id?: ID<V>): V | undefined {
|
||||
const [value, setValue] = useState<V>();
|
||||
|
||||
useEffect(() => Schema.subscribe(id, [], setValue), [id]);
|
||||
useEffect(() => Schema.subscribe(id, { resolve: true }, setValue), [id]);
|
||||
|
||||
return value;
|
||||
}
|
||||
@@ -454,7 +454,7 @@ function App() { // old
|
||||
const issue = useCoState(Issue, issueID); // old
|
||||
// old
|
||||
const createIssue = () => { // old
|
||||
const group = Group.create({ owner: me });
|
||||
const group = Group.create();
|
||||
group.addMember("everyone", "writer");
|
||||
// old
|
||||
const newIssue = Issue.create( // old
|
||||
@@ -543,7 +543,7 @@ function App() { // old
|
||||
(window.location.search?.replace("?project=", "") || undefined) as ID<Project> | undefined
|
||||
);
|
||||
// old
|
||||
const issue = useCoState(Issue, issueID); // *bin*
|
||||
const issue = useCoState(Issue, issueID); // [!code --]
|
||||
// old
|
||||
const createProject = () => {
|
||||
const group = Group.create();
|
||||
@@ -605,6 +605,8 @@ export function ProjectComponent({ projectID }: { projectID: ID<Project> }) {
|
||||
<button onClick={createAndAddIssue}>Create Issue</button>
|
||||
</div>
|
||||
</div>
|
||||
) : project === null ? (
|
||||
<div>Project not found or access denied</div>
|
||||
) : (
|
||||
<div>Loading project...</div>
|
||||
);
|
||||
@@ -620,7 +622,7 @@ Two things to note here:
|
||||
- We only need to use `useCoState` on the Project, and the nested `ListOfIssues` and each `Issue` will be **automatically loaded and subscribed to when we access them.**
|
||||
- However, because either the `Project`, `ListOfIssues`, or each `Issue` might not be loaded yet, we have to check for them being defined.
|
||||
|
||||
### Precise loading depths
|
||||
### Precise resolve queries
|
||||
|
||||
The load-and-subscribe-on-access is a convenient way to have your rendering drive data loading (including in nested components!) and lets you quickly chuck UIs together without worrying too much about the shape of all data you'll need.
|
||||
|
||||
@@ -635,7 +637,7 @@ import { IssueComponent } from "./Issue.tsx"; // old
|
||||
import { useCoState } from "jazz-react"; // old
|
||||
// old
|
||||
export function ProjectComponent({ projectID }: { projectID: ID<Project> }) {// old
|
||||
const project = useCoState(Project, projectID, { issues: [{}] });
|
||||
const project = useCoState(Project, projectID, { resolve: { issues: { $each: true } } });
|
||||
|
||||
const createAndAddIssue = () => {// old
|
||||
project?.issues.push(Issue.create({
|
||||
@@ -663,7 +665,7 @@ export function ProjectComponent({ projectID }: { projectID: ID<Project> }) {//
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
The loading-depth spec `{ issues: [{}] }` means "in `Project`, load `issues` and load each item in `issues` shallowly". (Since an `Issue` doesn't have any further references, "shallowly" actually means all its properties will be available).
|
||||
The resolve query `{ resolve: { issues: { $each: true } } }` means "in `Project`, load `issues` and load each item in `issues` deeply". (Since an `Issue` doesn't have any further references, "deeply" actually means all its properties will be available).
|
||||
|
||||
- Now, we can get rid of a lot of conditional accesses because we know that once `project` is loaded, `project.issues` and each `Issue` in it will be loaded as well.
|
||||
- This also results in only one rerender and visual update when everything is loaded, which is faster (especially for long lists) and gives you more control over the loading UX.
|
||||
@@ -710,7 +712,7 @@ function App() { // old
|
||||
// old
|
||||
const createProject = () => { // old
|
||||
const group = Group.create(); // old
|
||||
group.addMember("everyone", "writer"); // *bin*
|
||||
group.addMember("everyone", "writer"); // [!code --]
|
||||
// old
|
||||
const newProject = Project.create( // old
|
||||
{ // old
|
||||
@@ -747,7 +749,7 @@ import { createInviteLink } from "jazz-react";
|
||||
// old
|
||||
|
||||
export function ProjectComponent({ projectID }: { projectID: ID<Project> }) {// old
|
||||
const project = useCoState(Project, projectID, { issues: [{}] }); // old
|
||||
const project = useCoState(Project, projectID, { resolve: { issues: { $each: true } } }); // old
|
||||
|
||||
const { me } = useAccount();
|
||||
|
||||
|
||||
@@ -112,7 +112,7 @@ declare module "jazz-react" {// old
|
||||
## Resolving CoValues starting at `profile` or `root`
|
||||
|
||||
<ContentByFramework framework="react">
|
||||
To use per-user data in your app, you typically use `useAccount` somewhere in a high-level component, specifying which references to resolve using a depth-spec (see [Subscribing & deep loading](/docs/using-covalues/subscription-and-loading)).
|
||||
To use per-user data in your app, you typically use `useAccount` somewhere in a high-level component, specifying which references to resolve using a resolve query (see [Subscribing & deep loading](/docs/using-covalues/subscription-and-loading)).
|
||||
|
||||
<CodeGroup>
|
||||
{/* prettier-ignore */}
|
||||
@@ -163,12 +163,12 @@ export class MyAppAccount extends Account {
|
||||
profile = co.ref(MyAppProfile);
|
||||
|
||||
async migrate(this: MyAppAccount, creationProps?: { name: string }) {
|
||||
// we specifically need to check for undefined,
|
||||
// we specifically need to check for undefined,
|
||||
// because the root might simply be not loaded (`null`) yet
|
||||
if (this.root === undefined) {
|
||||
this.root = MyAppRoot.create({
|
||||
// Using a group to set the owner is always a good idea.
|
||||
// This way if in the future we want to share
|
||||
// This way if in the future we want to share
|
||||
// this coValue we can do so easily.
|
||||
myChats: ListOfChats.create([], Group.create()),
|
||||
myContacts: ListOfAccounts.create([], Group.create())
|
||||
@@ -218,8 +218,8 @@ export class MyAppAccount extends Account {
|
||||
root: {},
|
||||
});
|
||||
|
||||
// we specifically need to check for undefined,
|
||||
// because myBookmarks might simply be not loaded (`null`) yet
|
||||
// we specifically need to check for undefined,
|
||||
// because myBookmarks might simply be not loaded (`null`) yet
|
||||
if (root.myBookmarks === undefined) {
|
||||
root.myBookmarks = ListOfBookmarks.create([], Group.create());
|
||||
}
|
||||
@@ -228,9 +228,9 @@ export class MyAppAccount extends Account {
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
{/*
|
||||
TODO: Add best practice: only ever add fields
|
||||
{/*
|
||||
TODO: Add best practice: only ever add fields
|
||||
|
||||
Note: explain and reassure that there will be more guardrails in the future
|
||||
Note: explain and reassure that there will be more guardrails in the future
|
||||
https://github.com/garden-co/jazz/issues/1160
|
||||
*/}
|
||||
|
||||
@@ -29,9 +29,9 @@ Here is what's changed in this release:
|
||||
{/* prettier-ignore */}
|
||||
```tsx
|
||||
<JazzProvider
|
||||
auth={authMethod} // removed // *bin*
|
||||
peer="wss://cloud.jazz.tools/?key=you@example.com" // moved into sync // *bin*
|
||||
sync={{ peer: "wss://cloud.jazz.tools/?key=you@example.com" }} // *add*
|
||||
auth={authMethod} // removed // [!code --]
|
||||
peer="wss://cloud.jazz.tools/?key=you@example.com" // moved into sync // [!code --]
|
||||
sync={{ peer: "wss://cloud.jazz.tools/?key=you@example.com" }} // [!code ++]
|
||||
>
|
||||
<App />
|
||||
</JazzProvider>
|
||||
|
||||
@@ -0,0 +1,299 @@
|
||||
import { CodeGroup } from '@/components/forMdx'
|
||||
|
||||
export const metadata = { title: "Jazz 0.12.0 - Deeply resolved data" };
|
||||
|
||||
# Jazz 0.12.0 - Deeply resolved data
|
||||
|
||||
Jazz 0.12.0 makes it easier and safer to load nested data. You can now specify exactly which nested data you want to load, and Jazz will check permissions and handle missing data gracefully. This helps catch errors earlier during development and makes your code more reliable.
|
||||
|
||||
## What's new?
|
||||
|
||||
- New resolve API for a more type-safe deep loading
|
||||
- A single, consistent load option for all loading methods
|
||||
- Improved permission checks on deep loading
|
||||
- Easier type safety with the `Resolved` type helper
|
||||
|
||||
## Breaking changes
|
||||
|
||||
### New Resolve API
|
||||
|
||||
We're introducing a new resolve API for deep loading, more friendly to TypeScript, IDE autocompletion and LLMs.
|
||||
|
||||
**Major changes:**
|
||||
|
||||
1. Functions and hooks for loading now take the resolve query as an explicit nested `resolve` prop
|
||||
2. Shallowly loading a collection is now done with `true` instead of `[]` or `{}`
|
||||
|
||||
<CodeGroup>
|
||||
```tsx twoslash
|
||||
// @noErrors: 2451
|
||||
import { CoMap, CoList, co, Account } from "jazz-tools";
|
||||
import { useAccount } from "jazz-react";
|
||||
class AccountRoot extends CoMap { friends = co.ref(ListOfAccounts); }
|
||||
class ListOfAccounts extends CoList.Of(co.ref(Account)) {}
|
||||
class MyAppAccount extends Account { root = co.ref(AccountRoot);}
|
||||
declare module "jazz-react" { interface Register { Account: MyAppAccount; } }
|
||||
// ---cut-before---
|
||||
// Before
|
||||
// @ts-expect-error
|
||||
const { me } = useAccount({ root: { friends: [] } }); // [!code --]
|
||||
|
||||
// After
|
||||
const { me } = useAccount({ // [!code ++]
|
||||
resolve: { root: { friends: true } } // [!code ++]
|
||||
}); // [!code ++]
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
3. For collections, resolving items deeply is now done with a special `$each` key.
|
||||
|
||||
For a `CoList`:
|
||||
|
||||
<CodeGroup>
|
||||
```tsx twoslash
|
||||
// @noErrors: 2451
|
||||
import { CoList, co, ID, CoMap } from "jazz-tools";
|
||||
import { useCoState } from "jazz-react";
|
||||
|
||||
// ---cut-before---
|
||||
class Task extends CoMap { }
|
||||
class ListOfTasks extends CoList.Of(co.ref(Task)) {}
|
||||
|
||||
const id = "co_123" as ID<Task>;
|
||||
|
||||
// Before
|
||||
// @ts-expect-error
|
||||
const tasks = useCoState(ListOfTasks, id, [{}]); // [!code --]
|
||||
|
||||
// After
|
||||
const tasks = useCoState(ListOfTasks, id, { resolve: { $each: true } }); // [!code ++]
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
For a `CoMap.Record`:
|
||||
|
||||
<CodeGroup>
|
||||
```tsx twoslash
|
||||
// @noErrors: 2451
|
||||
import { CoMap, co, Account, ID } from "jazz-tools";
|
||||
import { useCoState } from "jazz-react";
|
||||
class MyAppAccount extends Account {}
|
||||
const id = "co_123" as ID<UsersByUsername>;
|
||||
|
||||
// ---cut-before---
|
||||
class UsersByUsername extends CoMap.Record(co.ref(MyAppAccount)) {}
|
||||
|
||||
// Before
|
||||
// @ts-expect-error
|
||||
const usersByUsername = useCoState(UsersByUsername, id, [{}]); // [!code --]
|
||||
|
||||
// After
|
||||
const usersByUsername = useCoState(UsersByUsername, id, { // [!code ++]
|
||||
resolve: { $each: true } // [!code ++]
|
||||
}); // [!code ++]
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
Nested loading — note how it's now less terse, but more readable:
|
||||
|
||||
<CodeGroup>
|
||||
```tsx twoslash
|
||||
// @noErrors: 2451
|
||||
import { CoList, CoMap, co, Account, ID } from "jazz-tools";
|
||||
import { useCoState } from "jazz-react";
|
||||
|
||||
const id = "co_123" as ID<ListOfTasks>;
|
||||
|
||||
// ---cut-before---
|
||||
class Org extends CoMap {
|
||||
name = co.string;
|
||||
}
|
||||
|
||||
class Assignee extends CoMap {
|
||||
name = co.string;
|
||||
org = co.ref(Org);
|
||||
}
|
||||
class ListOfAssignees extends CoList.Of(co.ref(Assignee)) {}
|
||||
|
||||
class Task extends CoMap {
|
||||
content = co.string;
|
||||
assignees = co.ref(ListOfAssignees);
|
||||
}
|
||||
class ListOfTasks extends CoList.Of(co.ref(Task)) {}
|
||||
|
||||
// Before
|
||||
// @ts-expect-error
|
||||
const tasksWithAssigneesAndTheirOrgs = useCoState(ListOfTasks, id, [{ // [!code --]
|
||||
assignees: [{ org: {}}]} // [!code --]
|
||||
]); // [!code --]
|
||||
|
||||
// After
|
||||
const tasksWithAssigneesAndTheirOrgs = useCoState(ListOfTasks, id, { // [!code ++]
|
||||
resolve: { // [!code ++]
|
||||
$each: { // [!code ++]
|
||||
assignees: { // [!code ++]
|
||||
$each: { org: true } // [!code ++]
|
||||
} // [!code ++]
|
||||
} // [!code ++]
|
||||
} // [!code ++]
|
||||
}); // [!code ++]
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
It's also a lot more auto-complete friendly:
|
||||
|
||||
<CodeGroup>
|
||||
```tsx twoslash
|
||||
// @noErrors: 2451 2353 2581
|
||||
import { CoList, CoMap, co, ID } from "jazz-tools";
|
||||
import { useCoState } from "jazz-react";
|
||||
|
||||
class Org extends CoMap { name = co.string; }
|
||||
class Assignee extends CoMap { org = co.ref(Org); }
|
||||
class ListOfAssignees extends CoList.Of(co.ref(Assignee)) {}
|
||||
class Task extends CoMap { assignees = co.ref(ListOfAssignees); }
|
||||
class ListOfTasks extends CoList.Of(co.ref(Task)) {}
|
||||
const id = "co_123" as ID<ListOfTasks>;
|
||||
|
||||
// ---cut-before---
|
||||
const tasksWithAssigneesAndTheirOrgs = useCoState(ListOfTasks, id, {
|
||||
resolve: {
|
||||
$each: {
|
||||
assignees: {
|
||||
$
|
||||
// ^|
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
### A single, consistent load option
|
||||
|
||||
The new API works across all loading methods, and separating out the resolve query means
|
||||
other options with default values are easier to manage, for example: loading a value as a specific account instead of using the implicit current account:
|
||||
|
||||
<CodeGroup>
|
||||
```ts twoslash
|
||||
// @noErrors: 2451
|
||||
import { co, Account, CoMap, CoList, ID } from "jazz-tools";
|
||||
|
||||
class Track extends CoMap {}
|
||||
class ListOfTracks extends CoList.Of(co.ref(Track)) {}
|
||||
class Playlist extends CoMap { tracks = co.ref(ListOfTracks); }
|
||||
const id = "co_123" as ID<Playlist>;
|
||||
const otherAccount = {} as Account;
|
||||
|
||||
// ---cut-before---
|
||||
// Before
|
||||
// @ts-expect-error
|
||||
Playlist.load(id, otherAccount, { // [!code --]
|
||||
tracks: [], // [!code --]
|
||||
}); // [!code --]
|
||||
|
||||
// After
|
||||
Playlist.load(id, { // [!code ++]
|
||||
loadAs: otherAccount, // [!code ++]
|
||||
resolve: { tracks: true } // [!code ++]
|
||||
}); // [!code ++]
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
### Improved permission checks on deep loading
|
||||
|
||||
Now `useCoState` will return `null` when the current user lacks permissions to load requested data.
|
||||
|
||||
Previously, `useCoState` would return `undefined` if the current user lacked permissions, making it hard to tell if the value is loading or if it's missing.
|
||||
|
||||
Now `undefined` means that the value is definitely loading, and `null` means that the value is temporarily missing.
|
||||
|
||||
We also have implemented a more granular permission checking, where if an *optional* CoValue cannot be accessed, `useCoState` will return the data stripped of that CoValue.
|
||||
|
||||
**Note:** The state handling around loading and error states will become more detailed and easy-to-handle in future releases, so this is just a small step towards consistency.
|
||||
|
||||
<CodeGroup>
|
||||
{/* prettier-ignore */}
|
||||
```tsx twoslash
|
||||
// @noErrors: 2451
|
||||
import React from "react";
|
||||
import { CoList, CoMap, co, ID } from "jazz-tools";
|
||||
import { useCoState } from "jazz-react";
|
||||
class Track extends CoMap {}
|
||||
function TrackComponent({ track }: { track: Track }) {return ""}
|
||||
|
||||
// ---cut-before---
|
||||
class ListOfTracks extends CoList.Of(co.optional.ref(Track)) {}
|
||||
|
||||
function TrackListComponent({ id }: { id: ID<ListOfTracks> }) {
|
||||
// Before (ambiguous states)
|
||||
// @ts-expect-error
|
||||
const tracks = useCoState(ListOfTracks, id, [{}]); // [!code --]
|
||||
if (tracks === undefined) return <div>Loading or access denied</div>; // [!code --]
|
||||
if (tracks === null) return <div>Not found</div>; // [!code --]
|
||||
|
||||
// After
|
||||
const tracks = useCoState(ListOfTracks, id, { resolve: { $each: true } }); // [!code ++]
|
||||
if (tracks === undefined) return <div>Loading...</div>; // [!code ++]
|
||||
if (tracks === null) return <div>Not found or access denied</div>; // [!code ++]
|
||||
|
||||
// This will only show tracks that we have access to and that are loaded.
|
||||
return tracks.map(track => track && <TrackComponent track={track} />);
|
||||
}
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
The same change is applied to the load function, so now it returns `null` instead of `undefined` when the value is missing.
|
||||
|
||||
<CodeGroup>
|
||||
```tsx twoslash
|
||||
// @noErrors: 2451
|
||||
import { CoMap, co, ID } from "jazz-tools";
|
||||
import { useAccount } from "jazz-react";
|
||||
|
||||
class MyCoMap extends CoMap {}
|
||||
const id = "co_123" as ID<MyCoMap>;
|
||||
|
||||
// ---cut-before---
|
||||
// Before
|
||||
// @ts-expect-error
|
||||
const map = await MyCoMap.load(id);
|
||||
if (map === undefined) {
|
||||
throw new Error("Map not found");
|
||||
}
|
||||
|
||||
// After
|
||||
const map = await MyCoMap.load(id);
|
||||
if (map === null) {
|
||||
throw new Error("Map not found or access denied");
|
||||
}
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
## New Features
|
||||
|
||||
### The `Resolved` type helper
|
||||
|
||||
The new `Resolved` type can be used to define what kind of deeply loaded data you expect in your parameters, using the same resolve query syntax as the new loading APIs:
|
||||
|
||||
<CodeGroup>
|
||||
```tsx twoslash
|
||||
// @noErrors: 2451
|
||||
import React from "react";
|
||||
import { Resolved, CoMap, CoList, co } from "jazz-tools";
|
||||
class Track extends CoMap {}
|
||||
class ListOfTracks extends CoList.Of(co.ref(Track)) {}
|
||||
class Playlist extends CoMap { tracks = co.ref(ListOfTracks); }
|
||||
function TrackComponent({ track }: { track: Track }) {return ""}
|
||||
|
||||
// ---cut-before---
|
||||
type PlaylistResolved = Resolved<Playlist, {
|
||||
tracks: { $each: true }
|
||||
}>;
|
||||
|
||||
function TrackListComponent({ playlist }: { playlist: PlaylistResolved }) {
|
||||
// Safe access to resolved tracks
|
||||
return playlist.tracks.map(track => <TrackComponent track={track} />);
|
||||
}
|
||||
```
|
||||
</CodeGroup>
|
||||
@@ -25,9 +25,9 @@ So we decided to remove `createJazzRNApp` step and to provide the types through
|
||||
import { JazzProvider, useDemoAuth, DemoAuthBasicUI } from "jazz-react-native";
|
||||
import { MyAppAccount } from "./schema";
|
||||
|
||||
// Remove these lines // *bin*
|
||||
const Jazz = createJazzRNApp({ AccountSchema: MyAppAccount }); // *bin*
|
||||
export const { useAccount, useCoState } = Jazz; // *bin*
|
||||
// Remove these lines // [!code --]
|
||||
const Jazz = createJazzRNApp({ AccountSchema: MyAppAccount }); // [!code --]
|
||||
export const { useAccount, useCoState } = Jazz; // [!code --]
|
||||
|
||||
export function JazzAndAuth({ children }: { children: React.ReactNode }) { // old
|
||||
const [auth, state] = useDemoAuth(); // old
|
||||
@@ -66,8 +66,8 @@ This change improves IDE intellisense support and simplifies imports:
|
||||
{/* prettier-ignore */}
|
||||
```tsx
|
||||
// Replace local imports with "jazz-react-native" imports
|
||||
import { useAccount } from "./main"; // *bin*
|
||||
import { useAccount } from "jazz-react-native"; // *add*
|
||||
import { useAccount } from "./main"; // [!code --]
|
||||
import { useAccount } from "jazz-react-native"; // [!code ++]
|
||||
|
||||
export function Hello() {
|
||||
const { me } = useAccount();
|
||||
|
||||
@@ -24,9 +24,9 @@ So we decided to remove `createJazzReactApp` step and to provide the types throu
|
||||
import { JazzProvider, usePasskeyAuth, PasskeyAuthBasicUI } from "jazz-react";
|
||||
import { MyAppAccount } from "./schema";
|
||||
|
||||
// Remove these lines // *bin*
|
||||
const Jazz = createJazzReactApp({ AccountSchema: MyAppAccount }); // *bin*
|
||||
export const { useAccount, useCoState } = Jazz; // *bin*
|
||||
// Remove these lines // [!code --]
|
||||
const Jazz = createJazzReactApp({ AccountSchema: MyAppAccount }); // [!code --]
|
||||
export const { useAccount, useCoState } = Jazz; // [!code --]
|
||||
|
||||
export function JazzAndAuth({ children }: { children: React.ReactNode }) { // old
|
||||
const [passkeyAuth, passKeyState] = usePasskeyAuth({ appName }); // old
|
||||
@@ -37,7 +37,7 @@ So we decided to remove `createJazzReactApp` step and to provide the types throu
|
||||
<JazzProvider
|
||||
auth={passkeyAuth} // old
|
||||
peer="wss://cloud.jazz.tools/?key=you@example.com" // old
|
||||
AccountSchema={MyAppAccount} {/* The custom Account schema is passed here */} // *add*
|
||||
AccountSchema={MyAppAccount} {/* The custom Account schema is passed here */} // [!code ++]
|
||||
>
|
||||
{children} // old
|
||||
</JazzProvider>
|
||||
@@ -47,11 +47,11 @@ So we decided to remove `createJazzReactApp` step and to provide the types throu
|
||||
}
|
||||
|
||||
// Register the Account schema so `useAccount` returns our custom `MyAppAccount`
|
||||
declare module "jazz-react" { // *add*
|
||||
interface Register { // *add*
|
||||
Account: MyAppAccount; // *add*
|
||||
} // *add*
|
||||
} // *add*
|
||||
declare module "jazz-react" { // [!code ++]
|
||||
interface Register { // [!code ++]
|
||||
Account: MyAppAccount; // [!code ++]
|
||||
} // [!code ++]
|
||||
} // [!code ++]
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
@@ -65,8 +65,8 @@ This change improves IDE intellisense support and simplifies imports:
|
||||
{/* prettier-ignore */}
|
||||
```tsx
|
||||
// Replace local imports with "jazz-react" imports
|
||||
import { useAccount } from "./main"; // *bin*
|
||||
import { useAccount } from "jazz-react"; // *add*
|
||||
import { useAccount } from "./main"; // [!code --]
|
||||
import { useAccount } from "jazz-react"; // [!code ++]
|
||||
|
||||
export function Hello() {
|
||||
const { me } = useAccount();
|
||||
|
||||
@@ -32,7 +32,7 @@ So we decided to remove `createJazzApp` step and to provide the types through na
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { Provider } from '$lib/jazz'; // *bin*
|
||||
import { Provider } from '$lib/jazz'; // [!code --]
|
||||
import { JazzProvider } from 'jazz-svelte';
|
||||
|
||||
// Example configuration for authentication and peer connection
|
||||
@@ -62,8 +62,8 @@ So we decided to remove `createJazzApp` step and to provide the types through na
|
||||
```svelte
|
||||
|
||||
<script lang="ts">
|
||||
import { useAccount } from '$lib/jazz'; // *bin*
|
||||
import { useAccount } from 'jazz-svelte'; // *add*
|
||||
import { useAccount } from '$lib/jazz'; // [!code --]
|
||||
import { useAccount } from 'jazz-svelte'; // [!code ++]
|
||||
|
||||
const { me } = useAccount();
|
||||
</script>
|
||||
|
||||
@@ -28,10 +28,10 @@ import App from "./App.vue"; // old
|
||||
import router from "./router"; // old
|
||||
import { ToDoAccount } from "./schema"; // old
|
||||
|
||||
// Remove these lines // *bin*
|
||||
const Jazz = createJazzVueApp<ToDoAccount>({ AccountSchema: ToDoAccount }); // *bin*
|
||||
export const { useAccount, useCoState } = Jazz; // *bin*
|
||||
const { JazzProvider } = Jazz; // *bin*
|
||||
// Remove these lines // [!code --]
|
||||
const Jazz = createJazzVueApp<ToDoAccount>({ AccountSchema: ToDoAccount }); // [!code --]
|
||||
export const { useAccount, useCoState } = Jazz; // [!code --]
|
||||
const { JazzProvider } = Jazz; // [!code --]
|
||||
|
||||
const RootComponent = defineComponent({ // old
|
||||
name: "RootComponent", // old
|
||||
@@ -86,8 +86,8 @@ This change improves IDE intellisense support and simplifies imports:
|
||||
|
||||
<script setup lang="ts">
|
||||
// Replace local imports with "jazz-vue" imports
|
||||
import { useAccount } from "./main"; // *bin*
|
||||
import { useAccount } from "jazz-vue"; // *add*
|
||||
import { useAccount } from "./main"; // [!code --]
|
||||
import { useAccount } from "jazz-vue"; // [!code ++]
|
||||
|
||||
const { me, logOut } = useAccount();
|
||||
</script>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { CodeGroup } from "@/components/forMdx";
|
||||
|
||||
export const metadata = { title: "Metadata & time-travel" };
|
||||
export const metadata = { title: "History & time-travel" };
|
||||
|
||||
# Metadata & time-travel
|
||||
# History & time-travel
|
||||
|
||||
One of Jazz's most powerful features is that every CoValue automatically tracks its complete edit history. This means you can see who changed what and when, examine the state of your data at any point in time, and build features like audit logs, activity feeds, and undo/redo functionality. This page explores how to access and work with the rich metadata that comes with every CoValue.
|
||||
|
||||
@@ -115,11 +115,11 @@ Activity feeds are a great way to see recent changes to a CoValue, helping users
|
||||
function getRecentActivity(project: Project) {
|
||||
const activity = [];
|
||||
const hourAgo = new Date(Date.now() - 3600000);
|
||||
|
||||
|
||||
for (const field of Object.keys(project)) {
|
||||
// Skip if the field doesn't have edits
|
||||
if (!project._edits[field as keyof typeof project._edits]) continue;
|
||||
|
||||
|
||||
for (const edit of project._edits[field as keyof typeof project._edits].all) {
|
||||
if (edit.madeAt > hourAgo) {
|
||||
activity.push({
|
||||
@@ -128,10 +128,10 @@ function getRecentActivity(project: Project) {
|
||||
by: edit.by,
|
||||
at: edit.madeAt
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return activity.sort((a, b) => b.at.getTime() - a.at.getTime());
|
||||
}
|
||||
|
||||
@@ -148,7 +148,7 @@ recentActivity.forEach(activity => {
|
||||
|
||||
CoValues track their entire history of changes, creating a timeline you can explore. You can see who changed what and when, or even view past states of the data. This capability enables powerful debugging tools and user-facing features like history browsing and restoration of previous versions:
|
||||
|
||||
<CodeGroup>
|
||||
<CodeGroup>
|
||||
```ts
|
||||
class Task extends CoMap {
|
||||
title = co.string;
|
||||
@@ -206,7 +206,7 @@ class Project extends CoMap {
|
||||
function findStatusChange(project: Project, targetStatus: string) {
|
||||
// Get all the edits for the status field
|
||||
const statusEdits = project._edits.status.all;
|
||||
|
||||
|
||||
for (const edit of statusEdits) {
|
||||
if (edit.value === targetStatus) {
|
||||
console.log({
|
||||
@@ -233,7 +233,7 @@ The time travel capabilities of CoValues enable several practical use cases that
|
||||
function getTaskStatusHistory(task: Task, days: number = 7) {
|
||||
const statusHistory = [];
|
||||
const dayInMs = 86400000;
|
||||
|
||||
|
||||
// Check every day for the past week
|
||||
for (let day = 0; day < days; day++) {
|
||||
const timePoint = new Date(Date.now() - day * dayInMs);
|
||||
@@ -245,7 +245,7 @@ function getTaskStatusHistory(task: Task, days: number = 7) {
|
||||
priority: state.priority
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
return statusHistory;
|
||||
}
|
||||
|
||||
@@ -1,272 +0,0 @@
|
||||
import { CodeGroup, ComingSoon, ContentByFramework } from "@/components/forMdx";
|
||||
|
||||
export const metadata = { title: "Reading from CoValues" };
|
||||
|
||||
# Reading from CoValues
|
||||
|
||||
Jazz lets you access your collaborative data with familiar JavaScript patterns while providing TypeScript type safety. Once you have a CoValue, you can read its values, traverse references, and iterate through collections using the same syntax you'd use with regular objects and arrays. This page covers how to read from different types of CoValues and handle loading states effectively.
|
||||
|
||||
<CodeGroup>
|
||||
```ts
|
||||
class Project extends CoMap {
|
||||
name = co.string;
|
||||
tasks = co.ref(ListOfTasks);
|
||||
lead = co.optional.ref(TeamMember);
|
||||
status = co.literal("planning", "active", "completed");
|
||||
}
|
||||
|
||||
// Reading basic fields
|
||||
console.log(project.name); // "Spring Garden Planning"
|
||||
console.log(project.status); // "active"
|
||||
|
||||
// Reading from lists
|
||||
for (const task of project.tasks) {
|
||||
console.log(task.title); // "Plant tomato seedlings"
|
||||
}
|
||||
|
||||
// Checking if an optional field exists
|
||||
if (project.lead) {
|
||||
console.log(project.lead.name); // "Maria Chen"
|
||||
}
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
## Different Types of CoValues
|
||||
|
||||
Jazz provides several CoValue types to represent different kinds of data. Each type has its own access patterns, but they all maintain the familiar JavaScript syntax you already know.
|
||||
|
||||
### CoMaps
|
||||
`CoMap`s work like JavaScript objects, providing named properties you can access with dot notation. These are the most common CoValue type and form the foundation of most Jazz data models:
|
||||
|
||||
<CodeGroup>
|
||||
```ts
|
||||
class TeamMember extends CoMap {
|
||||
name = co.string;
|
||||
role = co.string;
|
||||
active = co.boolean;
|
||||
}
|
||||
|
||||
console.log(member.name); // "Maria Chen"
|
||||
console.log(member.role); // "Garden Coordinator"
|
||||
console.log(member.active); // true
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
### CoLists
|
||||
`CoList`s work like JavaScript arrays, supporting indexed access, iteration methods, and length properties. They're perfect for ordered collections of items where the order matters:
|
||||
|
||||
<CodeGroup>
|
||||
```ts
|
||||
class ListOfTasks extends CoList.Of(co.ref(Task)) {}
|
||||
|
||||
// Access items by index
|
||||
console.log(tasks[0].title); // "Plant tomato seedlings"
|
||||
|
||||
// Use array methods
|
||||
tasks.forEach(task => {
|
||||
console.log(task.title); // "Plant tomato seedlings"
|
||||
});
|
||||
|
||||
// Get list length
|
||||
console.log(tasks.length); // 3
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
### CoFeeds
|
||||
`CoFeed`s provide a specialized way to track data from different sessions (tabs, devices, app instances). They're ideal for activity logs, presence indicators, or other session-specific streams of information. Each account can have multiple sessions, and each session maintains its own append-only log.
|
||||
|
||||
## Type Safety with CoValues
|
||||
|
||||
CoValues are fully typed in TypeScript, giving you the same autocomplete and error checking you'd expect from regular objects. This type safety helps catch errors at compile time rather than runtime, making your application more robust. Here's how the type system works with CoValues:
|
||||
|
||||
<CodeGroup>
|
||||
```ts
|
||||
class Project extends CoMap {
|
||||
name = co.string;
|
||||
memberCount = co.number;
|
||||
priority = co.literal("low", "medium", "high");
|
||||
lead = co.optional.ref(TeamMember);
|
||||
tasks = co.ref(ListOfTasks);
|
||||
}
|
||||
|
||||
// TypeScript knows exactly what fields exist
|
||||
const project = await Project.load(gardenProjectId);
|
||||
|
||||
project.name = "Community Garden"; // ✓ string
|
||||
project.memberCount = "few"; // ✗ Type error: expected number
|
||||
project.priority = "urgent"; // ✗ Type error: must be low/medium/high
|
||||
|
||||
// Optional fields are handled safely
|
||||
if (project.lead) {
|
||||
console.log(project.lead.name); // Type safe
|
||||
}
|
||||
|
||||
// Lists with specific item types
|
||||
project.tasks.forEach(task => {
|
||||
// TypeScript knows each task's structure
|
||||
console.log(`${task.title}: ${task.status}`); // "Plant herbs: in-progress"
|
||||
});
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
## Loading States
|
||||
|
||||
When you load a CoValue, it might not be immediately available due to network latency or data size. Jazz provides patterns to handle these loading states gracefully, and TypeScript helps ensure you check for availability before accessing properties:
|
||||
|
||||
<CodeGroup>
|
||||
```ts
|
||||
const project = await Project.load(gardenProjectId);
|
||||
|
||||
if (!project) {
|
||||
return "Data still loading";
|
||||
}
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
<ContentByFramework framework="react">
|
||||
And in React, `useCoState` provides a similar pattern to allow you to wait for a CoValue to be loaded before accessing it:
|
||||
<CodeGroup>
|
||||
```tsx
|
||||
// Type shows this might be `undefined` while loading
|
||||
const project = useCoState(Project, gardenProjectId, {
|
||||
tasks: [{}]
|
||||
});
|
||||
|
||||
if (!project) {
|
||||
return <div>Loading project data...</div>;
|
||||
}
|
||||
|
||||
// TypeScript now knows project exists and has tasks loaded
|
||||
return <div>{project.tasks.length}</div>;
|
||||
```
|
||||
</CodeGroup>
|
||||
</ContentByFramework>
|
||||
|
||||
### Accessing Nested CoValues
|
||||
|
||||
Nested CoValues need special handling for loading and access. Since each reference might need to be loaded separately, you need patterns to manage these dependencies and handle loading states appropriately throughout your object graph.
|
||||
|
||||
<CodeGroup>
|
||||
```ts
|
||||
class Project extends CoMap {
|
||||
name = co.string;
|
||||
tasks = co.ref(ListOfTasks);
|
||||
resources = co.optional.ref(ResourceList);
|
||||
}
|
||||
|
||||
class Task extends CoMap {
|
||||
title = co.string;
|
||||
status = co.literal("todo", "in-progress", "completed");
|
||||
subtasks = co.ref(ListOfSubtasks);
|
||||
}
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
### Loading
|
||||
|
||||
Loading nested data efficiently is important for performance. Jazz provides depth specifications to control exactly how much of your object graph is loaded, from shallow loading of just the top-level object to deep loading of complex nested structures:
|
||||
|
||||
<CodeGroup>
|
||||
```ts
|
||||
// Basic load - tasks won't be loaded yet
|
||||
const project = await Project.load(gardenProjectId);
|
||||
|
||||
// Load with nested data
|
||||
const projectWithTasks = await Project.load(gardenProjectId, { tasks: {} });
|
||||
|
||||
// Deep load pattern
|
||||
const fullyLoaded = await Project.load(gardenProjectId, {
|
||||
tasks: {
|
||||
subtasks: {}
|
||||
}
|
||||
});
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
More details on loading and subscribing to CoValues can be found in [Subscribing](/docs/using-covalues/subscribing-and-deep-loading).
|
||||
|
||||
### Handling Loading States
|
||||
|
||||
Unloaded references return `undefined`. This means you need to check for undefined values before trying to access properties of nested CoValues.
|
||||
|
||||
For general JavaScript/TypeScript usage, here's a pattern that works across any context:
|
||||
|
||||
<CodeGroup>
|
||||
```ts
|
||||
// Generic pattern for handling nested data
|
||||
function processTaskData(project) {
|
||||
// Check if project and its tasks are loaded
|
||||
if (!project || !project.tasks) {
|
||||
return "Data still loading";
|
||||
}
|
||||
|
||||
// Safe to process tasks
|
||||
const completedTasks = project.tasks.filter(task =>
|
||||
task && task.status === "completed"
|
||||
);
|
||||
|
||||
// Check for subtasks before accessing them
|
||||
const subtaskCount = completedTasks.reduce((count, task) => {
|
||||
if (!(task && task.subtasks)) return count
|
||||
return count + task.subtasks.length;
|
||||
}, 0);
|
||||
|
||||
return {
|
||||
completedCount: completedTasks.length,
|
||||
subtaskCount: subtaskCount
|
||||
};
|
||||
}
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
<ContentByFramework framework="react">
|
||||
Handle these loading states in your components:
|
||||
|
||||
<CodeGroup>
|
||||
```tsx
|
||||
// React pattern for handling nested data
|
||||
function TaskList({ project }: { project: Project }) {
|
||||
if (!project.tasks) {
|
||||
return <div>Loading tasks...</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
{project.tasks.map(task => {
|
||||
// Handle potentially missing nested data
|
||||
if (!task.subtasks) {
|
||||
return <div key={task.id}>Loading subtasks...</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div key={task.id}>
|
||||
{task.title}: {task.subtasks.length} subtasks
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
</CodeGroup>
|
||||
</ContentByFramework>
|
||||
|
||||
Note: We're working on making these patterns more explicit and robust. We'll provide clearer loading states and better error handling patterns. For now, be defensive with your checks for `undefined`.
|
||||
|
||||
<CodeGroup>
|
||||
```ts
|
||||
// Current safest pattern for deep access
|
||||
function getSubtasks(project: Project, taskTitle: string) {
|
||||
const task = project.tasks?.find(t => t.title === taskTitle);
|
||||
const subtasks = task?.subtasks;
|
||||
|
||||
if (!subtasks) {
|
||||
return null; // Could mean loading or error
|
||||
}
|
||||
|
||||
return subtasks.map(st => st.title);
|
||||
}
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
Stay tuned for updates to this API - we're working on making these patterns more robust and explicit.
|
||||
@@ -4,14 +4,29 @@ export const metadata = { title: "Subscriptions & Deep Loading" };
|
||||
|
||||
# Subscriptions & Deep Loading
|
||||
|
||||
When working with collaborative applications, you need to know when data changes and ensure you have all the necessary related data. Jazz provides powerful subscription and deep loading capabilities that make it easy to keep your UI in sync with the underlying data and efficiently load complex object graphs.
|
||||
Jazz's Collaborative Values (such as [CoMaps](/docs/using-covalues/comaps) or [CoLists](/docs/using-covalues/colists)) work like reactive state. By subscribing to them, you can react to both local and remote updates. This is the main way to consume data in your application.
|
||||
|
||||
## Understanding Subscriptions
|
||||
Subscriptions also take care of loading CoValues that are not yet loaded locally and can do so *deeply* — by resolving nested CoValues. To make use of this, we'll show you how to specify the depth of data you need with resolve queries.
|
||||
|
||||
Subscriptions in Jazz allow you to react to changes in CoValues. When a CoValue changes, all subscribers are notified with the updated value. This is essential for building reactive UIs that stay in sync with collaborative data.
|
||||
With each update you can also handle loading states and inaccessible CoValues.
|
||||
|
||||
## Manual subscriptions
|
||||
|
||||
You can subscribe to a CoValue from anywhere in your code (if you have its ID) by using `CoValue.subscribe()`.
|
||||
|
||||
<ContentByFramework framework="vanilla">
|
||||
If you're using React in your project, check out our [React hooks](/docs/react/using-covalues/subscription-and-loading#subscription-hooks) which provide a more streamlined experience with automatic subscription management.
|
||||
</ContentByFramework>
|
||||
|
||||
<ContentByFramework framework={["react", "react-native"]}>
|
||||
**Note:** Unless you're using vanilla JavaScript, this is only used outside of React components - for example in server-side code or in tests. See the section below for convenient subscription *hooks* that you typically use in React.
|
||||
</ContentByFramework>
|
||||
|
||||
<CodeGroup>
|
||||
```ts
|
||||
```ts twoslash
|
||||
import { ID, CoMap, co } from "jazz-tools";
|
||||
const taskId = "co_123" as ID<Task>;
|
||||
// ---cut-before---
|
||||
class Task extends CoMap {
|
||||
title = co.string;
|
||||
description = co.string;
|
||||
@@ -22,144 +37,82 @@ class Task extends CoMap {
|
||||
// ...
|
||||
|
||||
// Subscribe to a Task by ID
|
||||
const unsubscribe = Task.subscribe(taskId, { /* loading depth */ }, (updatedTask) => {
|
||||
const unsubscribe = Task.subscribe(taskId, (updatedTask) => {
|
||||
console.log("Task updated:", updatedTask.title);
|
||||
console.log("New status:", updatedTask.status);
|
||||
});
|
||||
|
||||
// Later, when you're done:
|
||||
// Clean up when you're done
|
||||
unsubscribe();
|
||||
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
If you already have a CoValue instance, you can subscribe to it by calling its `subscribe` method.
|
||||
|
||||
<CodeGroup>
|
||||
```ts twoslash
|
||||
import { ID, CoMap, co } from "jazz-tools";
|
||||
|
||||
class Task extends CoMap {
|
||||
title = co.string;
|
||||
description = co.string;
|
||||
status = co.literal("todo", "in-progress", "completed");
|
||||
assignedTo = co.optional.string;
|
||||
}
|
||||
const otherProps = {} as any;
|
||||
// ---cut-before---
|
||||
const task = Task.create({
|
||||
title: "Cut the grass",
|
||||
...otherProps
|
||||
});
|
||||
|
||||
const unsubscribe = task.subscribe((updatedTask) => {
|
||||
console.log("Task updated:", updatedTask.title);
|
||||
});
|
||||
|
||||
// Clean up when you're done
|
||||
unsubscribe();
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
### Static vs. Instance Subscriptions
|
||||
<ContentByFramework framework={["react", "react-native"]}>
|
||||
## Subscription hooks
|
||||
|
||||
There are two main ways to subscribe to CoValues:
|
||||
### `useCoState`
|
||||
|
||||
1. **Static Subscription** - When you have an ID but don't have the CoValue loaded yet:
|
||||
Jazz provides a `useCoState` hook that provides a convenient way to subscribe to CoValues and handle loading states:
|
||||
|
||||
<CodeGroup>
|
||||
```ts
|
||||
// Subscribe by ID (static method)
|
||||
const unsubscribe = Task.subscribe(taskId, { /* loading depth */ }, (task) => {
|
||||
if (task) {
|
||||
console.log("Task loaded/updated:", task.title);
|
||||
}
|
||||
});
|
||||
```
|
||||
</CodeGroup>
|
||||
```tsx twoslash
|
||||
import React from "react";
|
||||
import { ID, CoMap, co, CoList } from "jazz-tools";
|
||||
|
||||
2. **Instance Subscription** - When you already have a CoValue instance:
|
||||
|
||||
<CodeGroup>
|
||||
```ts
|
||||
// Subscribe to an instance (instance method)
|
||||
const task = Task.create({
|
||||
status: "todo",
|
||||
title: "Cut the grass",
|
||||
});
|
||||
if (task) {
|
||||
const unsubscribe = task.subscribe({ /* loading depth */ }, (updatedTask) => {
|
||||
console.log("Task updated:", updatedTask.title);
|
||||
});
|
||||
class Task extends CoMap {
|
||||
title = co.string;
|
||||
status = co.literal("todo", "in-progress", "completed");
|
||||
}
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
## Deep Loading
|
||||
|
||||
When working with related CoValues (like tasks in a project), you often need to load not just the top-level object but also its nested references. Jazz provides a flexible mechanism for specifying exactly how much of the object graph to load.
|
||||
|
||||
### Loading Depth Specifications
|
||||
|
||||
Loading depth specifications let you declare exactly which references to load and how deep to go:
|
||||
|
||||
<CodeGroup>
|
||||
```ts
|
||||
class Project extends CoMap {
|
||||
name = co.string;
|
||||
tasks = co.ref(ListOfTasks);
|
||||
owner = co.ref(TeamMember);
|
||||
}
|
||||
class ListOfTasks extends CoList.Of(co.ref(Task)) {}
|
||||
|
||||
class Task extends CoMap {
|
||||
title = co.string;
|
||||
subtasks = co.ref(ListOfSubtasks);
|
||||
assignee = co.optional.ref(TeamMember);
|
||||
}
|
||||
// ---cut-before---
|
||||
import { useCoState } from "jazz-react";
|
||||
|
||||
// Load just the project, not its tasks
|
||||
const project = await Project.load(projectId, {});
|
||||
|
||||
// Load the project and its tasks (but not subtasks)
|
||||
const projectWithTasks = await Project.load(projectId, {
|
||||
tasks: {}
|
||||
});
|
||||
|
||||
// Load the project, its tasks, and their subtasks
|
||||
const projectDeep = await Project.load(projectId, {
|
||||
tasks: {
|
||||
subtasks: {}
|
||||
}
|
||||
});
|
||||
|
||||
// Load the project, its tasks, and task assignees
|
||||
const projectWithAssignees = await Project.load(projectId, {
|
||||
tasks: {
|
||||
assignee: {}
|
||||
}
|
||||
});
|
||||
|
||||
// Complex loading pattern: load project, tasks with their subtasks, and the project owner
|
||||
const fullyLoaded = await Project.load(projectId, {
|
||||
tasks: {
|
||||
subtasks: {}
|
||||
},
|
||||
owner: {}
|
||||
});
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
The depth specification object mirrors the structure of your data model, making it intuitive to express which parts of the graph you want to load.
|
||||
|
||||
### Array Notation for Lists
|
||||
|
||||
For lists, you can use array notation to specify how to load the items:
|
||||
|
||||
<CodeGroup>
|
||||
```ts
|
||||
// Load project with all tasks but load each task shallowly
|
||||
const project = await Project.load(projectId, {
|
||||
tasks: [{}]
|
||||
});
|
||||
|
||||
// Load project with tasks and load subtasks for each task
|
||||
const project = await Project.load(projectId, {
|
||||
tasks: [{
|
||||
subtasks: [{}]
|
||||
}]
|
||||
});
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
## Framework Integration
|
||||
|
||||
<ContentByFramework framework="react">
|
||||
### React Integration with useCoState
|
||||
|
||||
In React applications, the `useCoState` hook provides a convenient way to subscribe to CoValues and handle loading states:
|
||||
|
||||
<CodeGroup>
|
||||
```tsx
|
||||
function GardenPlanner({ projectId }: { projectId: ID<Project> }) {
|
||||
// Subscribe to a project and its tasks
|
||||
const project = useCoState(Project, projectId, {
|
||||
tasks: [{}]
|
||||
resolve: {
|
||||
tasks: { $each: true },
|
||||
},
|
||||
});
|
||||
|
||||
// Handle loading state
|
||||
if (!project) {
|
||||
return <div>Loading garden project...</div>;
|
||||
return project === null
|
||||
? "Project not found or not accessible"
|
||||
: "Loading project ...";
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -173,7 +126,7 @@ function GardenPlanner({ projectId }: { projectId: ID<Project> }) {
|
||||
function TaskList({ tasks }: { tasks: Task[] }) {
|
||||
return (
|
||||
<ul>
|
||||
{tasks.map(task => (
|
||||
{tasks.map((task) => (
|
||||
<li key={task.id}>
|
||||
<span>{task.title}</span>
|
||||
<span>{task.status}</span>
|
||||
@@ -186,92 +139,512 @@ function TaskList({ tasks }: { tasks: Task[] }) {
|
||||
</CodeGroup>
|
||||
|
||||
The `useCoState` hook handles subscribing when the component mounts and unsubscribing when it unmounts, making it easy to keep your UI in sync with the underlying data.
|
||||
</ContentByFramework>
|
||||
|
||||
<ContentByFramework framework="vue">
|
||||
### Vue Integration
|
||||
### `useAccount`
|
||||
|
||||
In Vue applications, you can use the `useCoState` composable to subscribe to CoValues:
|
||||
`useAccount` is used to access the current user's account.
|
||||
You can use this at the top-level of your app to subscribe to the current user's [account profile and root](../schemas/accounts-and-migrations#covalues-as-a-graph-of-data-rooted-in-accounts).
|
||||
|
||||
Like `useCoState`, you can specify a resolve query to also subscribe to CoValues referenced in the account profile or root.
|
||||
|
||||
<CodeGroup>
|
||||
```vue
|
||||
<script setup>
|
||||
import { useCoState } from 'jazz-vue';
|
||||
```tsx twoslash
|
||||
import React from "react";
|
||||
import { ID, CoMap, co, CoList, Account } from "jazz-tools";
|
||||
class Task extends CoMap {
|
||||
title = co.string;
|
||||
}
|
||||
class ListOfTasks extends CoList.Of(co.ref(Task)) {}
|
||||
class Project extends CoMap {
|
||||
name = co.string;
|
||||
tasks = co.ref(ListOfTasks);
|
||||
}
|
||||
class ListOfProjects extends CoList.Of(co.ref(Project)) {}
|
||||
|
||||
const props = defineProps({
|
||||
projectId: String
|
||||
});
|
||||
class AccountRoot extends CoMap {
|
||||
myProjects = co.ref(ListOfProjects);
|
||||
}
|
||||
class MyAppAccount extends Account {
|
||||
root = co.ref(AccountRoot);
|
||||
}
|
||||
declare module "jazz-react" { interface Register { Account: MyAppAccount; } }
|
||||
// ---cut-before---
|
||||
import { useAccount } from "jazz-react";
|
||||
|
||||
// Subscribe to a project and its tasks
|
||||
const project = useCoState(Project, props.projectId, {
|
||||
tasks: [{}]
|
||||
});
|
||||
</script>
|
||||
function ProjectList() {
|
||||
const { me } = useAccount({
|
||||
resolve: {
|
||||
profile: true,
|
||||
root: {
|
||||
myProjects: {
|
||||
$each: {
|
||||
tasks: true
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
<template>
|
||||
<div v-if="project">
|
||||
<h1>{{ project.name }}</h1>
|
||||
if (!me) {
|
||||
return <div>Loading...</div>;
|
||||
}
|
||||
|
||||
return <div>
|
||||
<h1>{me.profile.name}'s projects</h1>
|
||||
<ul>
|
||||
<li v-for="task in project.tasks" :key="task.id">
|
||||
{{ task.title }} - {{ task.status }}
|
||||
</li>
|
||||
{me.root.myProjects.map(project => (
|
||||
<li key={project.id}>
|
||||
{project.name} ({project.tasks.length} tasks)
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
<div v-else>
|
||||
Loading garden project...
|
||||
</div>
|
||||
</template>
|
||||
}
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
</ContentByFramework>
|
||||
|
||||
<ContentByFramework framework="svelte">
|
||||
### Svelte Integration
|
||||
## Loading States and Permission Checking
|
||||
|
||||
In Svelte applications, you can use the `useCoState` function to subscribe to CoValues:
|
||||
When subscribing to or loading a CoValue, you need to handle three possible states:
|
||||
|
||||
- `undefined`: The initial loading state, indicating the value is being fetched
|
||||
- `null`: The CoValue was not found or is not accessible (e.g., due to permissions)
|
||||
- `Value`: The successfully loaded CoValue instance
|
||||
|
||||
This allows you to handle loading, error, and success states in your application:
|
||||
|
||||
<CodeGroup>
|
||||
```svelte
|
||||
<script>
|
||||
import { useCoState } from 'jazz-svelte';
|
||||
|
||||
export let projectId;
|
||||
|
||||
// Subscribe to a project and its tasks
|
||||
const project = useCoState(Project, projectId, {
|
||||
tasks: [{}]
|
||||
});
|
||||
</script>
|
||||
```ts twoslash
|
||||
import { ID, CoMap, co } from "jazz-tools";
|
||||
class Task extends CoMap {
|
||||
title = co.string;
|
||||
}
|
||||
|
||||
{#if $project}
|
||||
<h1>{$project.name}</h1>
|
||||
<ul>
|
||||
{#each $project.tasks as task (task.id)}
|
||||
<li>{task.title} - {task.status}</li>
|
||||
{/each}
|
||||
</ul>
|
||||
{:else}
|
||||
<div>Loading garden project...</div>
|
||||
{/if}
|
||||
const taskId = "co_123" as ID<Task>;
|
||||
// ---cut-before---
|
||||
Task.subscribe(taskId, (task) => {
|
||||
if (task === undefined) {
|
||||
console.log("Task is loading...");
|
||||
} else if (task === null) {
|
||||
console.log("Task not found or not accessible");
|
||||
} else {
|
||||
console.log("Task loaded:", task.title);
|
||||
}
|
||||
});
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
## Deep Loading
|
||||
|
||||
When working with related CoValues (like tasks in a project), you often need to load not just the top-level object but also its nested references. This is especially important when working with [CoMaps](/docs/using-covalues/comaps) that contain references to other CoValues or with [CoLists](/docs/using-covalues/colists) that contain multiple items. Jazz provides a flexible mechanism for specifying exactly how much of the object graph to load.
|
||||
|
||||
### Resolve queries
|
||||
|
||||
Resolve queries let you declare exactly which references to load and how deep to go using the `resolve` property:
|
||||
|
||||
<CodeGroup>
|
||||
```ts twoslash
|
||||
import { ID, CoMap, co, CoList } from "jazz-tools";
|
||||
const projectId = "co_123" as ID<Project>;
|
||||
|
||||
// ---cut-before---
|
||||
class Project extends CoMap {
|
||||
name = co.string;
|
||||
tasks = co.ref(ListOfTasks);
|
||||
owner = co.ref(TeamMember);
|
||||
}
|
||||
|
||||
class Task extends CoMap {
|
||||
title = co.string;
|
||||
subtasks = co.ref(ListOfTasks);
|
||||
assignee = co.optional.ref(TeamMember);
|
||||
}
|
||||
|
||||
class TeamMember extends CoMap {
|
||||
name = co.string;
|
||||
}
|
||||
|
||||
class ListOfTasks extends CoList.Of(co.ref(Task)) {}
|
||||
|
||||
// Load just the project, not its references
|
||||
const project = await Project.load(projectId);
|
||||
if (!project) { throw new Error("Project not found or not accessible"); }
|
||||
|
||||
// string - primitive fields are always loaded
|
||||
project.name;
|
||||
// undefined | null | ListOfTasks - non-requested references might not be loaded, or inaccessible
|
||||
project.tasks;
|
||||
|
||||
// Load the project and shallowly load its list of tasks
|
||||
const projectWithTasksShallow = await Project.load(projectId, {
|
||||
resolve: {
|
||||
tasks: true
|
||||
}
|
||||
});
|
||||
if (!projectWithTasksShallow) { throw new Error("Project or required references not found or not accessible"); }
|
||||
|
||||
// ListOfTasks - shallowly loaded
|
||||
projectWithTasksShallow.tasks;
|
||||
// number - length of the list
|
||||
projectWithTasksShallow.tasks.length;
|
||||
// undefined | null | Task - items might not be loaded, or inaccessible
|
||||
projectWithTasksShallow.tasks[0];
|
||||
|
||||
// Load the project and its tasks
|
||||
const projectWithTasks = await Project.load(projectId, {
|
||||
resolve: {
|
||||
tasks: {
|
||||
$each: true
|
||||
}
|
||||
}
|
||||
});
|
||||
if (!projectWithTasks) { throw new Error("Project or required references not found or not accessible"); }
|
||||
|
||||
// ListOfTasks - fully loaded
|
||||
projectWithTasks.tasks;
|
||||
// Task - fully loaded
|
||||
projectWithTasks.tasks[0];
|
||||
// string - primitive fields are always loaded
|
||||
projectWithTasks.tasks[0].title;
|
||||
// undefined | null | ListOfTasks - subtasks might not be loaded, or inaccessible
|
||||
projectWithTasks.tasks[0].subtasks;
|
||||
|
||||
// Load the project, its tasks, and their subtasks
|
||||
const projectDeep = await Project.load(projectId, {
|
||||
resolve: {
|
||||
tasks: {
|
||||
$each: {
|
||||
subtasks: {
|
||||
$each: true
|
||||
},
|
||||
assignee: true
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
if (!projectDeep) { throw new Error("Project or required references not found or not accessible"); }
|
||||
|
||||
// string - primitive fields are always loaded
|
||||
projectDeep.tasks[0].subtasks[0].title;
|
||||
// undefined | null | TeamMember - since assignee is optional:
|
||||
// TeamMember - set and definitely loaded
|
||||
// null - set but unavailable/inaccessible
|
||||
// undefined - not set, or loading (in case of subscription)
|
||||
projectDeep.tasks[0].assignee;
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
The resolve query defines which parts of the graph you want to load, making it intuitive to express complex loading patterns.
|
||||
|
||||
### Loading states and permissions
|
||||
|
||||
When loading data with references, the load operation will fail if one of the references is unavailable or if the user doesn't have read access to it. Let's explore what happens in various scenarios:
|
||||
|
||||
#### Resolved References
|
||||
|
||||
When a user tries to load a reference they don't have access to:
|
||||
|
||||
<CodeGroup>
|
||||
```typescript twoslash
|
||||
import { ID, CoMap, co, CoList } from "jazz-tools";
|
||||
|
||||
class Project extends CoMap {
|
||||
name = co.string;
|
||||
tasks = co.ref(ListOfTasks);
|
||||
owner = co.ref(TeamMember);
|
||||
}
|
||||
|
||||
class Task extends CoMap {
|
||||
title = co.string;
|
||||
subtasks = co.ref(ListOfTasks);
|
||||
assignee = co.optional.ref(TeamMember);
|
||||
}
|
||||
|
||||
class TeamMember extends CoMap {
|
||||
name = co.string;
|
||||
}
|
||||
|
||||
class ListOfTasks extends CoList.Of(co.ref(Task)) {}
|
||||
|
||||
const taskId = "co_123" as ID<Task>;
|
||||
|
||||
// ---cut-before---
|
||||
// If assignee is not accessible to the user:
|
||||
const task = await Task.load(taskId, {
|
||||
resolve: { assignee: true }
|
||||
});
|
||||
|
||||
task // => null
|
||||
```
|
||||
</CodeGroup>
|
||||
The load operation will fail and return `null` if any requested reference is inaccessible. This maintains data consistency by ensuring all requested references are available before returning the object.
|
||||
|
||||
The behavior is the same for optional and required references.
|
||||
|
||||
#### List References
|
||||
|
||||
When a list contains references to items the user can't access:
|
||||
|
||||
<CodeGroup>
|
||||
```typescript twoslash
|
||||
import { ID, CoMap, co, CoList } from "jazz-tools";
|
||||
|
||||
class Project extends CoMap {
|
||||
name = co.string;
|
||||
tasks = co.ref(ListOfTasks);
|
||||
owner = co.ref(TeamMember);
|
||||
}
|
||||
|
||||
class Task extends CoMap {
|
||||
title = co.string;
|
||||
subtasks = co.ref(ListOfTasks);
|
||||
assignee = co.optional.ref(TeamMember);
|
||||
}
|
||||
|
||||
class TeamMember extends CoMap {
|
||||
name = co.string;
|
||||
}
|
||||
|
||||
class ListOfTasks extends CoList.Of(co.ref(Task)) {}
|
||||
|
||||
const projectId = "co_123" as ID<Project>;
|
||||
// ---cut-before---
|
||||
// If any item in the list is not accessible:
|
||||
const project = await Project.load(projectId, {
|
||||
resolve: { tasks: { $each: true } }
|
||||
});
|
||||
|
||||
project // => null
|
||||
```
|
||||
</CodeGroup>
|
||||
If any item in a list is inaccessible to the user, the entire load operation will fail and return `null`. This is because lists expect all their items to be accessible - a partially loaded list could lead to data inconsistencies.
|
||||
|
||||
#### Reading a non-resolved inaccessible reference
|
||||
|
||||
When trying to load an object with an inaccessible reference without directly resolving it:
|
||||
|
||||
<CodeGroup>
|
||||
```typescript twoslash
|
||||
import { ID, CoMap, co, CoList } from "jazz-tools";
|
||||
|
||||
class Project extends CoMap {
|
||||
name = co.string;
|
||||
tasks = co.ref(ListOfTasks);
|
||||
owner = co.ref(TeamMember);
|
||||
}
|
||||
|
||||
class Task extends CoMap {
|
||||
title = co.string;
|
||||
subtasks = co.ref(ListOfTasks);
|
||||
assignee = co.optional.ref(TeamMember);
|
||||
}
|
||||
|
||||
class TeamMember extends CoMap {
|
||||
name = co.string;
|
||||
}
|
||||
|
||||
class ListOfTasks extends CoList.Of(co.ref(Task)) {}
|
||||
|
||||
const projectId = "co_123" as ID<Project>;
|
||||
// ---cut-before---
|
||||
const project = await Project.load(projectId, {
|
||||
resolve: true
|
||||
});
|
||||
|
||||
project // => Project
|
||||
|
||||
// The user doesn't have access to the owner
|
||||
project?.owner // => always null
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
The load operation will succeed and return the object, but the inaccessible reference will always be `null`.
|
||||
|
||||
|
||||
## Type Safety with Resolved Type
|
||||
|
||||
Jazz provides the `Resolved` type to help you define and enforce the structure of deeply loaded data in your application. This makes it easier to ensure that components receive the data they expect with proper TypeScript validation.
|
||||
|
||||
The `Resolved` type is especially useful when passing data between components, as it guarantees that all necessary nested data has been loaded:
|
||||
|
||||
<ContentByFramework framework="react">
|
||||
<CodeGroup>
|
||||
```tsx twoslash
|
||||
import React from "react";
|
||||
import { ID, CoMap, co, CoList, Resolved } from "jazz-tools";
|
||||
|
||||
class Project extends CoMap {
|
||||
name = co.string;
|
||||
tasks = co.ref(ListOfTasks);
|
||||
owner = co.ref(TeamMember);
|
||||
}
|
||||
|
||||
class Task extends CoMap {
|
||||
title = co.string;
|
||||
subtasks = co.ref(ListOfTasks);
|
||||
assignee = co.optional.ref(TeamMember);
|
||||
}
|
||||
|
||||
class TeamMember extends CoMap {
|
||||
name = co.string;
|
||||
}
|
||||
|
||||
class ListOfTasks extends CoList.Of(co.ref(Task)) {}
|
||||
|
||||
|
||||
// ---cut-before---
|
||||
// Define a type that includes resolved nested data
|
||||
type ProjectWithTasks = Resolved<Project, {
|
||||
tasks: { $each: true }
|
||||
}>;
|
||||
|
||||
// Component that expects a fully resolved project
|
||||
function TaskList({ project }: { project: ProjectWithTasks }) {
|
||||
// TypeScript knows tasks are loaded, so this is type-safe
|
||||
return (
|
||||
<ul>
|
||||
{project.tasks.map(task => (
|
||||
<li key={task.id}>{task.title}</li>
|
||||
))}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
|
||||
// For more complex resolutions
|
||||
type FullyLoadedProject = Resolved<Project, {
|
||||
tasks: {
|
||||
$each: {
|
||||
subtasks: true,
|
||||
assignee: true
|
||||
}
|
||||
},
|
||||
owner: true
|
||||
}>;
|
||||
|
||||
// Function that requires deeply resolved data
|
||||
function processProject(project: FullyLoadedProject) {
|
||||
// Safe access to all resolved properties
|
||||
console.log(`Project ${project.name} owned by ${project.owner.name}`);
|
||||
|
||||
project.tasks.forEach(task => {
|
||||
console.log(`Task: ${task.title}, Assigned to: ${task.assignee?.name}`);
|
||||
console.log(`Subtasks: ${task.subtasks.length}`);
|
||||
});
|
||||
}
|
||||
```
|
||||
</CodeGroup>
|
||||
</ContentByFramework>
|
||||
|
||||
<ContentByFramework framework="vanilla">
|
||||
<CodeGroup>
|
||||
```ts twoslash
|
||||
import { ID, CoMap, co, CoList, Resolved } from "jazz-tools";
|
||||
|
||||
class Project extends CoMap {
|
||||
name = co.string;
|
||||
tasks = co.ref(ListOfTasks);
|
||||
owner = co.ref(TeamMember);
|
||||
}
|
||||
|
||||
class Task extends CoMap {
|
||||
title = co.string;
|
||||
subtasks = co.ref(ListOfTasks);
|
||||
assignee = co.optional.ref(TeamMember);
|
||||
}
|
||||
|
||||
class TeamMember extends CoMap {
|
||||
name = co.string;
|
||||
}
|
||||
|
||||
class ListOfTasks extends CoList.Of(co.ref(Task)) {}
|
||||
|
||||
|
||||
// ---cut-before---
|
||||
// Define a type that includes resolved nested data
|
||||
type ProjectWithTasks = Resolved<Project, {
|
||||
tasks: { $each: true }
|
||||
}>;
|
||||
|
||||
// Function that expects resolved data
|
||||
async function taskList({project}: {project: ProjectWithTasks}) {
|
||||
// TypeScript knows tasks are loaded, so this is type-safe
|
||||
return project.tasks
|
||||
.map(task => task.title)
|
||||
.join(`\n - `);
|
||||
}
|
||||
|
||||
// For more complex resolutions
|
||||
type FullyLoadedProject = Resolved<Project, {
|
||||
tasks: {
|
||||
$each: {
|
||||
title: true,
|
||||
subtasks: true,
|
||||
assignee: true
|
||||
}
|
||||
},
|
||||
owner: true
|
||||
}>;
|
||||
|
||||
// Function that requires deeply resolved data
|
||||
function processProject(project: FullyLoadedProject) {
|
||||
// Safe access to all resolved properties
|
||||
console.log(`Project ${project.name} owned by ${project.owner.name}`);
|
||||
|
||||
project.tasks.forEach(task => {
|
||||
console.log(`Task: ${task.title}, Assigned to: ${task.assignee?.name}`);
|
||||
console.log(`Subtasks: ${task.subtasks.length}`);
|
||||
});
|
||||
}
|
||||
```
|
||||
</CodeGroup>
|
||||
</ContentByFramework>
|
||||
|
||||
Using the `Resolved` type helps catch errors at compile time rather than runtime, ensuring that your components and functions receive data with the proper resolution depth. This is especially useful for larger applications where data is passed between many components.
|
||||
|
||||
## Ensuring Data is Loaded
|
||||
|
||||
Sometimes you need to make sure data is loaded before proceeding with an operation. The `ensureLoaded` method lets you guarantee that a CoValue and its referenced data are loaded to a specific depth:
|
||||
|
||||
<CodeGroup>
|
||||
```ts
|
||||
```ts twoslash
|
||||
import { ID, CoMap, co, CoList, Resolved } from "jazz-tools";
|
||||
|
||||
class Project extends CoMap {
|
||||
name = co.string;
|
||||
tasks = co.ref(ListOfTasks);
|
||||
owner = co.ref(TeamMember);
|
||||
}
|
||||
|
||||
class Task extends CoMap {
|
||||
title = co.string;
|
||||
status = co.literal("todo", "in-progress", "completed");
|
||||
subtasks = co.ref(ListOfTasks);
|
||||
assignee = co.optional.ref(TeamMember);
|
||||
}
|
||||
|
||||
class TeamMember extends CoMap {
|
||||
name = co.string;
|
||||
}
|
||||
|
||||
class ListOfTasks extends CoList.Of(co.ref(Task)) {}
|
||||
|
||||
// ---cut-before---
|
||||
async function completeAllTasks(projectId: ID<Project>) {
|
||||
// Ensure the project and its tasks are loaded
|
||||
const project = await Project.load(projectId, {});
|
||||
// Ensure the project is loaded
|
||||
const project = await Project.load(projectId, { resolve: true });
|
||||
if (!project) return;
|
||||
|
||||
|
||||
// Ensure tasks are loaded
|
||||
const loadedProject = await project.ensureLoaded({
|
||||
tasks: [{}]
|
||||
resolve: {
|
||||
tasks: {
|
||||
$each: true
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// Now we can safely access and modify tasks
|
||||
loadedProject.tasks.forEach(task => {
|
||||
task.status = "completed";
|
||||
@@ -280,61 +653,12 @@ async function completeAllTasks(projectId: ID<Project>) {
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
## Performance Considerations
|
||||
|
||||
Loading depth is directly related to performance. Loading too much data can slow down your application, while loading too little can lead to "undefined" references. Here are some guidelines:
|
||||
|
||||
- **Load only what you need** for the current view or operation
|
||||
- **Preload data** that will be needed soon to improve perceived performance
|
||||
- Use **caching** to avoid reloading data that hasn't changed
|
||||
|
||||
{/* TODO: Add a note about supporting pagination */}
|
||||
|
||||
<CodeGroup>
|
||||
```ts
|
||||
// Bad: Loading everything deeply
|
||||
const project = await Project.load(projectId, {
|
||||
tasks: [{
|
||||
subtasks: [{
|
||||
comments: [{}]
|
||||
}]
|
||||
}],
|
||||
members: [{}],
|
||||
resources: [{}]
|
||||
});
|
||||
|
||||
// Better: Loading only what's needed for the current view
|
||||
const project = await Project.load(projectId, {
|
||||
tasks: [{}] // Just load the tasks shallowly
|
||||
});
|
||||
|
||||
// Later, when a task is selected:
|
||||
const task = await Task.load(selectedTaskId, {
|
||||
subtasks: [{}] // Now load its subtasks
|
||||
});
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
## Using a Loading Cache
|
||||
|
||||
By default, Jazz maintains a cache of loaded CoValues to avoid unnecessary network requests. This means that if you've already loaded a CoValue, subsequent load requests will use the cached version unless you explicitly request a refresh.
|
||||
|
||||
<CodeGroup>
|
||||
```ts
|
||||
// First load: fetches from network or local storage
|
||||
const project = await Project.load(projectId, {});
|
||||
|
||||
// Later loads: uses cached version if available
|
||||
const sameProject = await Project.load(projectId, {});
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Be explicit about loading depths**: Always specify exactly what you need
|
||||
2. **Clean up subscriptions**: Always store and call the unsubscribe function when you're done
|
||||
3. **Handle loading states**: Check for undefined/null before accessing properties
|
||||
4. **Use framework integrations**: They handle subscription lifecycle automatically
|
||||
5. **Balance depth and performance**: Load only what you need for the current view
|
||||
|
||||
By effectively using subscriptions and deep loading, you can build responsive, collaborative applications that handle complex data relationships while maintaining good performance.
|
||||
1. **Be explicit about resolution depths**: Always specify exactly what you need
|
||||
2. **Use framework integrations**: They handle subscription lifecycle automatically
|
||||
3. **Clean up subscriptions**: Always store and call the unsubscribe function when you're done
|
||||
4. **Handle all loading states**: Check for undefined (loading), null (not found), and success states
|
||||
5. **Use the Resolved type**: Add compile-time type safety for components that require specific resolution patterns
|
||||
|
||||
@@ -1,175 +0,0 @@
|
||||
export const metadata = { title: "Writing & deleting CoValues" };
|
||||
import { CodeGroup } from "@/components/forMdx";
|
||||
|
||||
# Writing & deleting CoValues
|
||||
|
||||
Collaborative applications need ways to update and remove data. Jazz makes this simple by treating CoValues like regular JavaScript objects while handling all the complexity of syncing changes in the background. This page covers how to modify CoValues, work with collections, handle concurrent edits, and properly remove data when needed.
|
||||
|
||||
## Writing to CoValues
|
||||
|
||||
Once you have a CoValue, modifying it is straightforward. You can update fields like regular JavaScript properties. Changes are applied locally first for immediate feedback, then synchronized to other users with access to the same CoValues. This approach provides a natural programming model while handling all the distributed systems complexity behind the scenes.
|
||||
|
||||
<CodeGroup>
|
||||
```ts
|
||||
class Task extends CoMap {
|
||||
title = co.string;
|
||||
status = co.literal("todo", "in-progress", "completed");
|
||||
assignee = co.optional.string;
|
||||
}
|
||||
|
||||
//...
|
||||
|
||||
// Update fields
|
||||
task.status = "in-progress"; // Direct assignment
|
||||
task.assignee = "Alex"; // Optional field
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
### Working with Lists
|
||||
|
||||
CoLists support familiar array operations, making it easy to work with collections of data. You can add, remove, and modify items using the standard JavaScript array methods, while Jazz handles the collaborative aspects automatically. These operations work correctly even when multiple users are making changes simultaneously.
|
||||
|
||||
<CodeGroup>
|
||||
```ts
|
||||
class Project extends CoMap {
|
||||
name = co.string;
|
||||
tasks = co.ref(ListOfTasks);
|
||||
}
|
||||
|
||||
//...
|
||||
|
||||
// Add items
|
||||
project.tasks.push(Task.create({
|
||||
title: "Build raised beds",
|
||||
status: "todo"
|
||||
}));
|
||||
|
||||
// Remove items
|
||||
project.tasks.splice(2, 1); // Remove third task
|
||||
|
||||
// Update items
|
||||
project.tasks[0].status = "in-progress";
|
||||
|
||||
// Bulk updates
|
||||
project.tasks.forEach(task => {
|
||||
if (task.status === "todo") {
|
||||
task.status = "in-progress";
|
||||
}
|
||||
});
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
Changes sync automatically to everyone with access. Any edits you make are immediately visible in your local view and propagate to other users as they sync.
|
||||
|
||||
## Concurrent Edits
|
||||
|
||||
CoValues use [CRDTs](/docs/schemas/covalues#defining-schemas-covalues) to handle concurrent edits smoothly. In most cases, you don't need to think about conflicts - Jazz handles them automatically. This conflict resolution happens transparently, allowing multiple users to make changes simultaneously without disruption or data loss.
|
||||
|
||||
<CodeGroup>
|
||||
```ts
|
||||
class Dashboard extends CoMap {
|
||||
activeProjects = co.number;
|
||||
status = co.literal("active", "maintenance");
|
||||
notifications = co.ref(ListOfNotifications);
|
||||
}
|
||||
|
||||
//...
|
||||
|
||||
// Multiple users can edit simultaneously
|
||||
// Last-write-wins for simple fields
|
||||
dashboard.status = "maintenance"; // Local change is immediate
|
||||
dashboard.activeProjects = 5; // Syncs automatically
|
||||
|
||||
// Lists handle concurrent edits too
|
||||
dashboard.notifications.push(Notification.create({
|
||||
timestamp: new Date(),
|
||||
message: "System update scheduled"
|
||||
}));
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
## Deleting CoValues
|
||||
|
||||
There are a few ways to delete CoValues, from simple field removal to full cleanup. Jazz provides flexible options for removing data depending on your needs. You can remove references while keeping the underlying data, remove items from lists, or completely delete CoValues when they're no longer needed.
|
||||
|
||||
<CodeGroup>
|
||||
```ts
|
||||
class Project extends CoMap {
|
||||
tasks = co.ref(ListOfTasks);
|
||||
resources = co.optional.ref(ListOfResources);
|
||||
}
|
||||
|
||||
//...
|
||||
|
||||
// Remove a reference
|
||||
project.resources = null; // Removes the reference but resources still exist
|
||||
|
||||
// Remove from a list
|
||||
project.tasks.splice(2, 1); // Removes third team member from list
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
### Best Practices
|
||||
|
||||
- Load everything you plan to delete
|
||||
- Check permissions before attempting deletes
|
||||
- Consider soft deletes for recoverable data
|
||||
|
||||
## Removing Data in CoValues
|
||||
|
||||
You can delete fields from any `CoMap` to remove specific properties while keeping the CoValue itself. This is useful when you need to clear certain data without affecting the rest of your object structure. The deletion operations are also synchronized to all users with access.
|
||||
|
||||
<CodeGroup>
|
||||
```ts
|
||||
class Project extends CoMap {
|
||||
name = co.string;
|
||||
team = co.ref(ListOfMembers);
|
||||
budget = co.optional.ref(Budget);
|
||||
}
|
||||
|
||||
//...
|
||||
|
||||
// Delete fields from a regular CoMap
|
||||
delete project.budget; // Removes the budget reference
|
||||
|
||||
// Delete from a record-type CoMap
|
||||
class ProjectTags extends CoMap.Record(co.string) {}
|
||||
|
||||
const projectTags = ProjectTags.create({
|
||||
"priority-high": "High priority tasks",
|
||||
});
|
||||
|
||||
delete projectTags["priority-high"]; // Removes specific tag
|
||||
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
For `CoList`s, use array methods:
|
||||
|
||||
<CodeGroup>
|
||||
```ts
|
||||
// Remove from lists using splice
|
||||
project.team.splice(2, 1); // Removes third team member
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
### Restoring Data
|
||||
|
||||
For data you might want to restore later, consider using status fields instead of permanent deletion. This "soft delete" pattern is common in applications where users might need to recover previously removed items. By using a boolean field to mark items as archived or deleted, you maintain the ability to restore them later.
|
||||
|
||||
<CodeGroup>
|
||||
```ts
|
||||
class Task extends CoMap {
|
||||
title = co.string;
|
||||
archived = co.boolean;
|
||||
}
|
||||
|
||||
// Mark as archived
|
||||
task.archived = true;
|
||||
|
||||
// Restore later
|
||||
task.archived = false; // Task is back in the active list!
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
Removed data remains in the edit history. If you need to handle sensitive information, plan your data model accordingly.
|
||||
@@ -3,11 +3,7 @@ import DocsLayout from "@/components/docs/DocsLayout";
|
||||
import { DocNav } from "@/components/docs/nav";
|
||||
import { Prose } from "gcmp-design-system/src/app/components/molecules/Prose";
|
||||
|
||||
export default function Layout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
export default function Layout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<TocProvider>
|
||||
<DocsLayout nav={<DocNav />}>
|
||||
|
||||
@@ -227,7 +227,7 @@ const FileUploadIllustration = () => (
|
||||
</div>
|
||||
|
||||
<div className=" w-[12rem] h-2 rounded-full overflow-hidden bg-stone-200 mt-3">
|
||||
<div className="w-3/4 h-full bg-green-500"/>
|
||||
<div className="w-3/4 h-full bg-green-500" />
|
||||
</div>
|
||||
<div className="w-[12rem] flex justify-between text-xs mt-1.5">
|
||||
<p>Uploading...</p>
|
||||
@@ -379,7 +379,8 @@ const reactExamples: Example[] = [
|
||||
{
|
||||
name: "File upload",
|
||||
slug: "filestream",
|
||||
description: "Upload different types of files, and show upload progress, file size, and type.",
|
||||
description:
|
||||
"Upload different types of files, and show upload progress, file size, and type.",
|
||||
tech: [tech.react],
|
||||
features: [features.fileUpload],
|
||||
demoUrl: "https://file-upload-demo.jazz.tools",
|
||||
|
||||
@@ -7,8 +7,7 @@ import { Fragment } from "react";
|
||||
|
||||
const title = "Status";
|
||||
|
||||
export const revalidate = 300
|
||||
|
||||
export const revalidate = 300;
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title,
|
||||
@@ -40,7 +39,6 @@ interface DataRow {
|
||||
p99Latency: number;
|
||||
}
|
||||
|
||||
|
||||
const query = async () => {
|
||||
const res = await fetch("https://gcmp.grafana.net/api/ds/query", {
|
||||
method: "POST",
|
||||
@@ -99,9 +97,9 @@ const query = async () => {
|
||||
}
|
||||
|
||||
const responseData = await res.json();
|
||||
|
||||
|
||||
const byProbe: Record<string, DataRow> = {};
|
||||
|
||||
|
||||
for (const frame of responseData.results.up.frames) {
|
||||
const probe = startCase(frame.schema.fields[1].labels.probe);
|
||||
byProbe[probe] = {
|
||||
@@ -115,7 +113,6 @@ const query = async () => {
|
||||
byProbe[probe].latencyOverTime = frame.data.values;
|
||||
}
|
||||
|
||||
|
||||
for (const frame of responseData.results.avg_latency.frames) {
|
||||
const probe = startCase(frame.schema.fields[1].labels.probe);
|
||||
byProbe[probe].avgLatency = frame.data.values[1];
|
||||
@@ -123,7 +120,7 @@ const query = async () => {
|
||||
|
||||
for (const frame of responseData.results.p99_latency.frames) {
|
||||
const probe = startCase(frame.schema.fields[1].labels.probe);
|
||||
byProbe[probe].p99Latency = frame.data.values[1];
|
||||
byProbe[probe].p99Latency = frame.data.values[1];
|
||||
}
|
||||
|
||||
const byRegion = Object.entries(byProbe).reduce<
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user