Skip to content

Commit b1abbd0

Browse files
committed
create PublishDiff component
1 parent 2904bb3 commit b1abbd0

File tree

10 files changed

+385
-20
lines changed

10 files changed

+385
-20
lines changed

apps/events/src/components/todos2.tsx

Lines changed: 42 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
import { smartAccountWalletClient } from '@/lib/smart-account';
22
import { cn } from '@/lib/utils';
3+
import type { Op } from '@graphprotocol/grc-20';
4+
import type { PublishDiffInfo } from '@graphprotocol/hypergraph-react';
35
import {
6+
PublishDiff,
47
_generateDeleteOps,
58
publishOps,
69
useCreateEntity,
@@ -17,6 +20,7 @@ import { Todo2 } from '../schema';
1720
import { Spinner } from './spinner';
1821
import { Button } from './ui/button';
1922
import { Input } from './ui/input';
23+
import { Modal } from './ui/modal';
2024
export const Todos2 = () => {
2125
const { data: kgPublicData, isLoading: kgPublicIsLoading, isError: kgPublicIsError } = useQueryPublicKg(Todo2);
2226
const { data: dataPublic, isLoading: isLoadingPublic, isError: isErrorPublic } = useQuery(Todo2, { mode: 'public' });
@@ -29,7 +33,8 @@ export const Todos2 = () => {
2933
const hardDeleteEntity = useHardDeleteEntity();
3034
const [newTodoName, setNewTodoName] = useState('');
3135
const queryClient = useQueryClient();
32-
36+
const [publishData, setPublishData] = useState<{ diff: PublishDiffInfo<typeof Todo2>; ops: Array<Op> } | null>(null);
37+
const [isPublishDiffModalOpen, setIsPublishDiffModalOpen] = useState(false);
3338
return (
3439
<>
3540
<div className="flex flex-row gap-4 items-center">
@@ -76,22 +81,45 @@ export const Todos2 = () => {
7681

7782
console.log('ops & diff', result);
7883
if (result) {
79-
const publishOpsResult = await publishOps({
80-
ops: result.ops,
81-
walletClient: smartAccountWalletClient,
82-
space,
83-
});
84-
console.log('publishOpsResult', publishOpsResult);
85-
setTimeout(() => {
86-
queryClient.invalidateQueries({ queryKey: [`entities:${Todo2.name}`] });
87-
queryClient.invalidateQueries({ queryKey: [`entities:geo:${Todo2.name}`] });
88-
}, 1000);
84+
setPublishData(result);
85+
setIsPublishDiffModalOpen(true);
8986
}
9087
}}
9188
>
92-
Prepare Publish and Publish
89+
Prepare Publish
9390
</Button>
9491

92+
<Modal isOpen={isPublishDiffModalOpen} onOpenChange={setIsPublishDiffModalOpen}>
93+
<div className="p-4 flex flex-col gap-4 min-w-96">
94+
<PublishDiff<typeof Todo2>
95+
newEntities={publishData?.diff.newEntities ?? []}
96+
deletedEntities={publishData?.diff.deletedEntities ?? []}
97+
updatedEntities={publishData?.diff.updatedEntities ?? []}
98+
/>
99+
<Button
100+
onClick={async () => {
101+
if (publishData) {
102+
const publishOpsResult = await publishOps({
103+
ops: publishData.ops,
104+
walletClient: smartAccountWalletClient,
105+
space,
106+
});
107+
console.log('publishOpsResult', publishOpsResult);
108+
setIsPublishDiffModalOpen(false);
109+
setPublishData(null);
110+
setTimeout(() => {
111+
queryClient.invalidateQueries({ queryKey: [`entities:${Todo2.name}`] });
112+
queryClient.invalidateQueries({ queryKey: [`entities:geo:${Todo2.name}`] });
113+
}, 1000);
114+
}
115+
}}
116+
disabled={publishData?.ops.length === 0}
117+
>
118+
Publish
119+
</Button>
120+
</div>
121+
</Modal>
122+
95123
<h2 className="text-2xl font-bold">Todos (Local)</h2>
96124
{todosLocalData.map((todo) => (
97125
<div key={todo.id} className="flex flex-row items-center gap-2">
@@ -129,7 +157,7 @@ export const Todos2 = () => {
129157
<div key={todo.id} className="flex flex-row items-center gap-2">
130158
<h2>{todo.name}</h2>
131159
<div className="text-xs">{todo.id}</div>
132-
<input type="checkbox" checked={todo.checked} />
160+
<input type="checkbox" checked={todo.checked} readOnly />
133161
<Button
134162
onClick={async () => {
135163
const ops = await _generateDeleteOps({ id: todo.id, space });
@@ -151,7 +179,7 @@ export const Todos2 = () => {
151179
<div key={todo.id} className="flex flex-row items-center gap-2">
152180
<h2>{todo.name}</h2>
153181
<div className="text-xs">{todo.id}</div>
154-
<input type="checkbox" checked={todo.checked} />
182+
<input type="checkbox" checked={todo.checked} readOnly />
155183
<Button
156184
onClick={async () => {
157185
const ops = await _generateDeleteOps({ id: todo.id, space });
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { useEffect } from 'react';
2+
3+
type ModalProps = {
4+
isOpen: boolean;
5+
onOpenChange: (open: boolean) => void;
6+
children: React.ReactNode;
7+
};
8+
9+
export function Modal({ isOpen, onOpenChange, children }: ModalProps) {
10+
useEffect(() => {
11+
const handleEsc = (event: KeyboardEvent) => {
12+
if (event.key === 'Escape') {
13+
onOpenChange(false);
14+
}
15+
};
16+
17+
if (isOpen) {
18+
document.addEventListener('keydown', handleEsc);
19+
document.body.style.overflow = 'hidden';
20+
}
21+
22+
return () => {
23+
document.removeEventListener('keydown', handleEsc);
24+
document.body.style.overflow = 'unset';
25+
};
26+
}, [isOpen, onOpenChange]);
27+
28+
if (!isOpen) return null;
29+
30+
return (
31+
<div className="fixed inset-0 z-50">
32+
{/* biome-ignore lint/a11y/useKeyWithClickEvents: Modal has keyboard support via Escape key */}
33+
<div className="fixed inset-0 bg-black/50" onClick={() => onOpenChange(false)} />
34+
<div className="fixed left-[50%] top-[50%] z-50 translate-x-[-50%] translate-y-[-50%] max-h-[90vh] w-[90vw] max-w-2xl">
35+
<div className="bg-white rounded shadow-lg max-h-[90vh] overflow-y-auto">{children}</div>
36+
</div>
37+
</div>
38+
);
39+
}

apps/events/tailwind.config.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import tailwindcssAnimate from 'tailwindcss-animate';
33
/** @type {import('tailwindcss').Config} */
44
export default {
55
darkMode: ['class'],
6-
content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'],
6+
content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}', '../../packages/hypergraph-react/src/**/*.{js,ts,jsx,tsx}'],
77
theme: {
88
extend: {
99
borderRadius: {
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export const IGNORED_PROPERTIES = ['id', '__deleted', '__version', 'type'];
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import type { Entity } from '@graphprotocol/hypergraph';
2+
import { useState } from 'react';
3+
import { IGNORED_PROPERTIES } from './constants.js';
4+
5+
type EntityCardProps<S extends Entity.AnyNoContext> = {
6+
entity: Entity.Entity<S>;
7+
type: 'new' | 'deleted';
8+
};
9+
10+
export const EntityCard = <S extends Entity.AnyNoContext>({ entity, type }: EntityCardProps<S>) => {
11+
const [isExpanded, setIsExpanded] = useState(true);
12+
13+
const headerBgColor = type === 'new' ? 'bg-green-50' : 'bg-red-50';
14+
const borderColor = type === 'new' ? 'border-green-200' : 'border-red-200';
15+
const textColor = type === 'new' ? 'text-green-800' : 'text-red-800';
16+
17+
return (
18+
<div className={`border rounded-sm ${borderColor} overflow-hidden text-xs`}>
19+
<div
20+
className={`p-3 flex justify-between items-center cursor-pointer ${headerBgColor}`}
21+
onClick={() => setIsExpanded(!isExpanded)}
22+
onKeyDown={(e) => {
23+
if (e.key === 'Enter' || e.key === ' ') {
24+
setIsExpanded(!isExpanded);
25+
}
26+
}}
27+
>
28+
<div className={`font-medium ${textColor}`}>ID: {entity.id}</div>
29+
<button className="text-gray-500" type="button">
30+
{isExpanded ? (
31+
<svg
32+
className="w-4 h-4"
33+
viewBox="0 0 24 24"
34+
fill="none"
35+
stroke="currentColor"
36+
strokeWidth="2"
37+
strokeLinecap="round"
38+
strokeLinejoin="round"
39+
>
40+
<title>Collapse</title>
41+
<path d="m18 15-6-6-6 6" />
42+
</svg>
43+
) : (
44+
<svg
45+
className="w-4 h-4"
46+
viewBox="0 0 24 24"
47+
fill="none"
48+
stroke="currentColor"
49+
strokeWidth="2"
50+
strokeLinecap="round"
51+
strokeLinejoin="round"
52+
>
53+
<title>Expand</title>
54+
<path d="m6 9 6 6 6-6" />
55+
</svg>
56+
)}
57+
</button>
58+
</div>
59+
60+
{isExpanded && (
61+
<div className="px-3 pt-0 border-t border-dashed border-gray-200 bg-white">
62+
<table className="w-full table-fixed">
63+
<colgroup>
64+
<col className="w-1/3" />
65+
<col className="w-2/3" />
66+
</colgroup>
67+
<tbody>
68+
{Object.entries(entity)
69+
.filter(([key]) => !IGNORED_PROPERTIES.includes(key))
70+
.map(([key, value]) => (
71+
<tr key={key} className="border-b border-gray-100 last:border-0">
72+
<td className="py-1.5 font-medium text-gray-600 pr-3">
73+
{key.charAt(0).toUpperCase() + key.slice(1)}
74+
</td>
75+
<td className="py-1.5">{value.toString()}</td>
76+
</tr>
77+
))}
78+
</tbody>
79+
</table>
80+
</div>
81+
)}
82+
</div>
83+
);
84+
};
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
'use client';
2+
3+
import type { Entity } from '@graphprotocol/hypergraph';
4+
import type { DiffEntry } from '../../types.js';
5+
import { EntityCard } from './entity-card.js';
6+
import { UpdatedEntityCard } from './updated-entity-card.js';
7+
8+
type PublishDiffProps<S extends Entity.AnyNoContext> = {
9+
newEntities: Entity.Entity<S>[];
10+
deletedEntities: Entity.Entity<S>[];
11+
updatedEntities: {
12+
id: string;
13+
current: Entity.Entity<S>;
14+
next: Entity.Entity<S>;
15+
diff: DiffEntry<S>;
16+
}[];
17+
};
18+
19+
export const PublishDiff = <S extends Entity.AnyNoContext>({
20+
newEntities = [],
21+
deletedEntities = [],
22+
updatedEntities = [],
23+
}: PublishDiffProps<S>) => {
24+
return (
25+
<div className="space-y-6 text-sm">
26+
{newEntities.length > 0 && (
27+
<section>
28+
<h2 className="text-base font-semibold mb-3 flex items-center">
29+
<svg
30+
className="w-4 h-4 mr-2 text-green-500"
31+
viewBox="0 0 24 24"
32+
fill="none"
33+
stroke="currentColor"
34+
strokeWidth="2"
35+
strokeLinecap="round"
36+
strokeLinejoin="round"
37+
aria-label="New Entities"
38+
>
39+
<title>New Entities</title>
40+
<path d="M12 5v14M5 12h14" />
41+
</svg>
42+
New Entities ({newEntities.length})
43+
</h2>
44+
<div className="space-y-3">
45+
{newEntities.map((entity) => (
46+
<EntityCard<S> key={entity.id} entity={entity} type="new" />
47+
))}
48+
</div>
49+
</section>
50+
)}
51+
52+
{updatedEntities.length > 0 && (
53+
<section>
54+
<h2 className="text-base font-semibold mb-3">Updated Entities ({updatedEntities.length})</h2>
55+
<div className="space-y-3">
56+
{updatedEntities.map((entity) => (
57+
<UpdatedEntityCard<S> key={entity.id} entity={entity} />
58+
))}
59+
</div>
60+
</section>
61+
)}
62+
63+
{deletedEntities.length > 0 && (
64+
<section>
65+
<h2 className="text-base font-semibold mb-3 flex items-center">
66+
<svg
67+
className="w-4 h-4 mr-2 text-red-500"
68+
viewBox="0 0 24 24"
69+
fill="none"
70+
stroke="currentColor"
71+
strokeWidth="2"
72+
strokeLinecap="round"
73+
strokeLinejoin="round"
74+
>
75+
<title>Deleted Entities</title>
76+
<path d="M3 6h18M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2" />
77+
</svg>
78+
Deleted Entities ({deletedEntities.length})
79+
</h2>
80+
<div className="space-y-3">
81+
{deletedEntities.map((entity) => (
82+
<EntityCard<S> key={entity.id} entity={entity} type="deleted" />
83+
))}
84+
</div>
85+
</section>
86+
)}
87+
88+
{newEntities.length === 0 && updatedEntities.length === 0 && deletedEntities.length === 0 && (
89+
<div className="text-center py-6 text-xs text-muted-foreground">No changes detected</div>
90+
)}
91+
</div>
92+
);
93+
};

0 commit comments

Comments
 (0)