From d32cc4951554ff534ad3e3bdf475db694c5847be Mon Sep 17 00:00:00 2001 From: Nik Graf Date: Thu, 6 Feb 2025 10:24:14 +0100 Subject: [PATCH 01/59] first version of merged query --- apps/events/package.json | 3 + .../create-properties-and-types.tsx | 63 +++ apps/events/src/components/playground.tsx | 10 + apps/events/src/components/todos2.tsx | 147 ++++++ apps/events/src/lib/smart-account.ts | 8 + apps/events/src/routeTree.gen.ts | 115 ++++- apps/events/src/routes/__root.tsx | 3 + apps/events/src/routes/index.tsx | 8 +- apps/events/src/routes/playground.lazy.tsx | 19 + apps/events/src/routes/space/$spaceId.tsx | 100 ++-- .../src/routes/space/$spaceId/index.tsx | 72 +++ .../space/$spaceId/public-integration.tsx | 42 ++ apps/events/src/schema.ts | 35 ++ docs/public-graph-integration.md | 59 +++ package.json | 4 +- packages/hypergraph-react/package.json | 17 +- .../src/HypergraphAppContext.tsx | 120 +++-- .../src/HypergraphSpaceContext.tsx | 28 +- .../src/create-wallet-client.ts | 31 ++ .../src/generate-delete-ops.tsx | 45 ++ packages/hypergraph-react/src/index.ts | 10 + .../src/internal/constants.ts | 4 + .../src/internal/create-version-id.ts | 25 + .../src/internal/use-generate-create-ops.tsx | 38 ++ .../src/internal/use-query-public-geo.tsx | 133 +++++ .../src/internal/use-query-public-kg.tsx | 78 +++ packages/hypergraph-react/src/publish-ops.ts | 39 ++ packages/hypergraph-react/src/types.ts | 10 + packages/hypergraph-react/src/use-query.tsx | 45 ++ .../test/internal/create-version-id.test.ts | 48 ++ packages/hypergraph/src/entity/create.ts | 7 +- packages/hypergraph/src/entity/delete.ts | 22 +- packages/hypergraph/src/entity/findMany.ts | 60 ++- .../src/space-events/create-space.ts | 5 +- pnpm-lock.yaml | 484 +++++++++++++++++- 35 files changed, 1757 insertions(+), 180 deletions(-) create mode 100644 apps/events/src/components/create-properties-and-types.tsx create mode 100644 apps/events/src/components/playground.tsx create mode 100644 apps/events/src/components/todos2.tsx create mode 100644 apps/events/src/lib/smart-account.ts create mode 100644 apps/events/src/routes/playground.lazy.tsx create mode 100644 apps/events/src/routes/space/$spaceId/index.tsx create mode 100644 apps/events/src/routes/space/$spaceId/public-integration.tsx create mode 100644 docs/public-graph-integration.md create mode 100644 packages/hypergraph-react/src/create-wallet-client.ts create mode 100644 packages/hypergraph-react/src/generate-delete-ops.tsx create mode 100644 packages/hypergraph-react/src/internal/constants.ts create mode 100644 packages/hypergraph-react/src/internal/create-version-id.ts create mode 100644 packages/hypergraph-react/src/internal/use-generate-create-ops.tsx create mode 100644 packages/hypergraph-react/src/internal/use-query-public-geo.tsx create mode 100644 packages/hypergraph-react/src/internal/use-query-public-kg.tsx create mode 100644 packages/hypergraph-react/src/publish-ops.ts create mode 100644 packages/hypergraph-react/src/types.ts create mode 100644 packages/hypergraph-react/src/use-query.tsx create mode 100644 packages/hypergraph-react/test/internal/create-version-id.test.ts diff --git a/apps/events/package.json b/apps/events/package.json index 20dde2b7..7bb598f0 100644 --- a/apps/events/package.json +++ b/apps/events/package.json @@ -11,6 +11,7 @@ "@automerge/automerge": "^v2.2.9-alpha.3", "@automerge/automerge-repo": "^2.0.0-alpha.14", "@automerge/automerge-repo-react-hooks": "^2.0.0-alpha.14", + "@graphprotocol/grc-20": "^0.10.0", "@graphprotocol/hypergraph": "workspace:*", "@graphprotocol/hypergraph-react": "workspace:*", "@noble/hashes": "^1.7.0", @@ -18,11 +19,13 @@ "@radix-ui/react-avatar": "^1.1.2", "@radix-ui/react-icons": "^1.3.2", "@radix-ui/react-slot": "^1.1.1", + "@tanstack/react-query": "^5.67.1", "@tanstack/react-router": "^1.97.1", "@xstate/store": "^2.6.2", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "effect": "^3.12.4", + "graphql-request": "^7.1.2", "isomorphic-ws": "^5.0.0", "lucide-react": "^0.471.1", "react": "^19.0.0", diff --git a/apps/events/src/components/create-properties-and-types.tsx b/apps/events/src/components/create-properties-and-types.tsx new file mode 100644 index 00000000..67f638a1 --- /dev/null +++ b/apps/events/src/components/create-properties-and-types.tsx @@ -0,0 +1,63 @@ +import { smartAccountWalletClient } from '@/lib/smart-account'; +import { type GeoSmartAccount, Graph, type Op } from '@graphprotocol/grc-20'; +import { publishOps, useHypergraphSpace } from '@graphprotocol/hypergraph-react'; +import { useState } from 'react'; +import { Button } from './ui/button'; +import { Card, CardContent } from './ui/card'; + +const createPropertiesAndTypes = async ({ + smartAccountWalletClient, + space, +}: { smartAccountWalletClient: GeoSmartAccount; space: string }) => { + const ops: Array = []; + const { id: checkedPropertyId, ops: createCheckedPropertyOps } = Graph.createProperty({ + type: 'CHECKBOX', + name: 'Checked', + }); + ops.push(...createCheckedPropertyOps); + + const { id: todoTypeId, ops: createTodoTypeOps } = Graph.createType({ + name: 'Todo', + properties: [checkedPropertyId], + }); + ops.push(...createTodoTypeOps); + + const result = await publishOps({ ops, walletClient: smartAccountWalletClient, space }); + return { result, todoTypeId, checkedPropertyId }; +}; + +export const CreatePropertiesAndTypes = () => { + const [mapping, setMapping] = useState(''); + const space = useHypergraphSpace(); + + return ( +
+ {mapping && ( + + +
{mapping}
+
+
+ )} + +
+ ); +}; diff --git a/apps/events/src/components/playground.tsx b/apps/events/src/components/playground.tsx new file mode 100644 index 00000000..ea2893ab --- /dev/null +++ b/apps/events/src/components/playground.tsx @@ -0,0 +1,10 @@ +import { usePublicQueryEntities } from '@graphprotocol/hypergraph-react'; +import { NewsStory } from '../schema'; + +export const Playground = () => { + const { data: entityData, isLoading, isError } = usePublicQueryEntities(NewsStory); + + console.log({ isLoading, isError, entityData }); + + return
{JSON.stringify(entityData, null, 2)}
; +}; diff --git a/apps/events/src/components/todos2.tsx b/apps/events/src/components/todos2.tsx new file mode 100644 index 00000000..33f689bf --- /dev/null +++ b/apps/events/src/components/todos2.tsx @@ -0,0 +1,147 @@ +import { smartAccountWalletClient } from '@/lib/smart-account'; +import type { Op } from '@graphprotocol/grc-20'; +import { + generateDeleteOps, + publishOps, + useCreateEntity, + useDeleteEntity, + useGenerateCreateOps, + useHardDeleteEntity, + useHypergraphSpace, + _useQueryPublicGeo as usePublicQueryGeo, + _useQueryPublicKg as usePublicQueryKg, + useQuery, + useQueryEntities, +} from '@graphprotocol/hypergraph-react'; +import { useState } from 'react'; +import { Todo2 } from '../schema'; +import { Button } from './ui/button'; +import { Input } from './ui/input'; + +export const Todos2 = () => { + const { data: kgPublicData, isLoading: kgPublicIsLoading, isError: kgPublicIsError } = usePublicQueryKg(Todo2); + const { data: geoPublicData, isLoading: geoPublicIsLoading, isError: geoPublicIsError } = usePublicQueryGeo(Todo2); + const generateTodoOps = useGenerateCreateOps(Todo2); + const space = useHypergraphSpace(); + const todos = useQueryEntities(Todo2); + const createEntity = useCreateEntity(Todo2); + const deleteEntity = useDeleteEntity(); + const hardDeleteEntity = useHardDeleteEntity(); + const [newTodoName, setNewTodoName] = useState(''); + const { data, isLoading, isError } = useQuery(Todo2); + + return ( + <> +

Todos (Merged)

+ {isLoading &&
Loading...
} + {isError &&
Error loading todos
} + {data.map((todo) => ( +
+

{todo.name}

+
{todo.id}
+ +
{todo.__version}
+ +
+ ))} + +
+ setNewTodoName(e.target.value)} /> + +
+ + + +

Todos (Local)

+ {todos.map((todo) => ( +
+

{todo.name}

+
{todo.id}
+ +
{todo.__deleted ? 'deleted' : 'not deleted'}
+
{todo.__version}
+ +
+ ))} + +

Todos (Public KG)

+ {kgPublicIsLoading &&
Loading...
} + {kgPublicIsError &&
Error loading todos
} + {kgPublicData.map((todo) => ( +
+

{todo.name}

+
{todo.id}
+ + +
+ ))} + +

Todos (Public Geo)

+ {geoPublicIsLoading &&
Loading...
} + {geoPublicIsError &&
Error loading todos
} + {geoPublicData.map((todo) => ( +
+

{todo.name}

+
{todo.id}
+ + +
+ ))} + + ); +}; diff --git a/apps/events/src/lib/smart-account.ts b/apps/events/src/lib/smart-account.ts new file mode 100644 index 00000000..c0bb2abc --- /dev/null +++ b/apps/events/src/lib/smart-account.ts @@ -0,0 +1,8 @@ +import { getSmartAccountWalletClient } from '@graphprotocol/grc-20'; +import type { Hex } from 'viem'; + +const privateKey = `0x${import.meta.env.VITE_ACCOUNT_KEY}` as Hex; + +export const smartAccountWalletClient = await getSmartAccountWalletClient({ + privateKey, +}); diff --git a/apps/events/src/routeTree.gen.ts b/apps/events/src/routeTree.gen.ts index 4894b762..6982dda4 100644 --- a/apps/events/src/routeTree.gen.ts +++ b/apps/events/src/routeTree.gen.ts @@ -16,13 +16,22 @@ import { Route as rootRoute } from './routes/__root' import { Route as IndexImport } from './routes/index' import { Route as SpaceSpaceIdImport } from './routes/space/$spaceId' import { Route as SettingsExportWalletImport } from './routes/settings/export-wallet' +import { Route as SpaceSpaceIdIndexImport } from './routes/space/$spaceId/index' +import { Route as SpaceSpaceIdPublicIntegrationImport } from './routes/space/$spaceId/public-integration' // Create Virtual Routes +const PlaygroundLazyImport = createFileRoute('/playground')() const LoginLazyImport = createFileRoute('/login')() // Create/Update Routes +const PlaygroundLazyRoute = PlaygroundLazyImport.update({ + id: '/playground', + path: '/playground', + getParentRoute: () => rootRoute, +} as any).lazy(() => import('./routes/playground.lazy').then((d) => d.Route)) + const LoginLazyRoute = LoginLazyImport.update({ id: '/login', path: '/login', @@ -47,6 +56,19 @@ const SettingsExportWalletRoute = SettingsExportWalletImport.update({ getParentRoute: () => rootRoute, } as any) +const SpaceSpaceIdIndexRoute = SpaceSpaceIdIndexImport.update({ + id: '/', + path: '/', + getParentRoute: () => SpaceSpaceIdRoute, +} as any) + +const SpaceSpaceIdPublicIntegrationRoute = + SpaceSpaceIdPublicIntegrationImport.update({ + id: '/public-integration', + path: '/public-integration', + getParentRoute: () => SpaceSpaceIdRoute, + } as any) + // Populate the FileRoutesByPath interface declare module '@tanstack/react-router' { @@ -65,6 +87,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof LoginLazyImport parentRoute: typeof rootRoute } + '/playground': { + id: '/playground' + path: '/playground' + fullPath: '/playground' + preLoaderRoute: typeof PlaygroundLazyImport + parentRoute: typeof rootRoute + } '/settings/export-wallet': { id: '/settings/export-wallet' path: '/settings/export-wallet' @@ -79,59 +108,113 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof SpaceSpaceIdImport parentRoute: typeof rootRoute } + '/space/$spaceId/public-integration': { + id: '/space/$spaceId/public-integration' + path: '/public-integration' + fullPath: '/space/$spaceId/public-integration' + preLoaderRoute: typeof SpaceSpaceIdPublicIntegrationImport + parentRoute: typeof SpaceSpaceIdImport + } + '/space/$spaceId/': { + id: '/space/$spaceId/' + path: '/' + fullPath: '/space/$spaceId/' + preLoaderRoute: typeof SpaceSpaceIdIndexImport + parentRoute: typeof SpaceSpaceIdImport + } } } // Create and export the route tree +interface SpaceSpaceIdRouteChildren { + SpaceSpaceIdPublicIntegrationRoute: typeof SpaceSpaceIdPublicIntegrationRoute + SpaceSpaceIdIndexRoute: typeof SpaceSpaceIdIndexRoute +} + +const SpaceSpaceIdRouteChildren: SpaceSpaceIdRouteChildren = { + SpaceSpaceIdPublicIntegrationRoute: SpaceSpaceIdPublicIntegrationRoute, + SpaceSpaceIdIndexRoute: SpaceSpaceIdIndexRoute, +} + +const SpaceSpaceIdRouteWithChildren = SpaceSpaceIdRoute._addFileChildren( + SpaceSpaceIdRouteChildren, +) + export interface FileRoutesByFullPath { '/': typeof IndexRoute '/login': typeof LoginLazyRoute + '/playground': typeof PlaygroundLazyRoute '/settings/export-wallet': typeof SettingsExportWalletRoute - '/space/$spaceId': typeof SpaceSpaceIdRoute + '/space/$spaceId': typeof SpaceSpaceIdRouteWithChildren + '/space/$spaceId/public-integration': typeof SpaceSpaceIdPublicIntegrationRoute + '/space/$spaceId/': typeof SpaceSpaceIdIndexRoute } export interface FileRoutesByTo { '/': typeof IndexRoute '/login': typeof LoginLazyRoute + '/playground': typeof PlaygroundLazyRoute '/settings/export-wallet': typeof SettingsExportWalletRoute - '/space/$spaceId': typeof SpaceSpaceIdRoute + '/space/$spaceId/public-integration': typeof SpaceSpaceIdPublicIntegrationRoute + '/space/$spaceId': typeof SpaceSpaceIdIndexRoute } export interface FileRoutesById { __root__: typeof rootRoute '/': typeof IndexRoute '/login': typeof LoginLazyRoute + '/playground': typeof PlaygroundLazyRoute '/settings/export-wallet': typeof SettingsExportWalletRoute - '/space/$spaceId': typeof SpaceSpaceIdRoute + '/space/$spaceId': typeof SpaceSpaceIdRouteWithChildren + '/space/$spaceId/public-integration': typeof SpaceSpaceIdPublicIntegrationRoute + '/space/$spaceId/': typeof SpaceSpaceIdIndexRoute } export interface FileRouteTypes { fileRoutesByFullPath: FileRoutesByFullPath - fullPaths: '/' | '/login' | '/settings/export-wallet' | '/space/$spaceId' + fullPaths: + | '/' + | '/login' + | '/playground' + | '/settings/export-wallet' + | '/space/$spaceId' + | '/space/$spaceId/public-integration' + | '/space/$spaceId/' fileRoutesByTo: FileRoutesByTo - to: '/' | '/login' | '/settings/export-wallet' | '/space/$spaceId' + to: + | '/' + | '/login' + | '/playground' + | '/settings/export-wallet' + | '/space/$spaceId/public-integration' + | '/space/$spaceId' id: | '__root__' | '/' | '/login' + | '/playground' | '/settings/export-wallet' | '/space/$spaceId' + | '/space/$spaceId/public-integration' + | '/space/$spaceId/' fileRoutesById: FileRoutesById } export interface RootRouteChildren { IndexRoute: typeof IndexRoute LoginLazyRoute: typeof LoginLazyRoute + PlaygroundLazyRoute: typeof PlaygroundLazyRoute SettingsExportWalletRoute: typeof SettingsExportWalletRoute - SpaceSpaceIdRoute: typeof SpaceSpaceIdRoute + SpaceSpaceIdRoute: typeof SpaceSpaceIdRouteWithChildren } const rootRouteChildren: RootRouteChildren = { IndexRoute: IndexRoute, LoginLazyRoute: LoginLazyRoute, + PlaygroundLazyRoute: PlaygroundLazyRoute, SettingsExportWalletRoute: SettingsExportWalletRoute, - SpaceSpaceIdRoute: SpaceSpaceIdRoute, + SpaceSpaceIdRoute: SpaceSpaceIdRouteWithChildren, } export const routeTree = rootRoute @@ -146,6 +229,7 @@ export const routeTree = rootRoute "children": [ "/", "/login", + "/playground", "/settings/export-wallet", "/space/$spaceId" ] @@ -156,11 +240,26 @@ export const routeTree = rootRoute "/login": { "filePath": "login.lazy.tsx" }, + "/playground": { + "filePath": "playground.lazy.tsx" + }, "/settings/export-wallet": { "filePath": "settings/export-wallet.tsx" }, "/space/$spaceId": { - "filePath": "space/$spaceId.tsx" + "filePath": "space/$spaceId.tsx", + "children": [ + "/space/$spaceId/public-integration", + "/space/$spaceId/" + ] + }, + "/space/$spaceId/public-integration": { + "filePath": "space/$spaceId/public-integration.tsx", + "parent": "/space/$spaceId" + }, + "/space/$spaceId/": { + "filePath": "space/$spaceId/index.tsx", + "parent": "/space/$spaceId" } } } diff --git a/apps/events/src/routes/__root.tsx b/apps/events/src/routes/__root.tsx index 92992d86..8ac3218f 100644 --- a/apps/events/src/routes/__root.tsx +++ b/apps/events/src/routes/__root.tsx @@ -31,6 +31,9 @@ export const Route = createRootRoute({ diff --git a/apps/events/src/routes/space/$spaceId/playground.tsx b/apps/events/src/routes/space/$spaceId/playground.tsx new file mode 100644 index 00000000..e841fd57 --- /dev/null +++ b/apps/events/src/routes/space/$spaceId/playground.tsx @@ -0,0 +1,42 @@ +import { CreatePropertiesAndTypes } from '@/components/create-properties-and-types'; +import { TodosPublicGeo } from '@/components/todo/todos-public-geo'; +import { mapping } from '@/schema'; +import { store } from '@graphprotocol/hypergraph'; +import { HypergraphSpaceProvider, useHypergraphApp } from '@graphprotocol/hypergraph-react'; +import { createFileRoute } from '@tanstack/react-router'; +import { useSelector } from '@xstate/store/react'; +import { useEffect } from 'react'; + +export const Route = createFileRoute('/space/$spaceId/playground')({ + component: PlaygroundRouteComponent, +}); + +function PlaygroundRouteComponent() { + const { spaceId } = Route.useParams(); + const spaces = useSelector(store, (state) => state.context.spaces); + const { subscribeToSpace, isConnecting, isLoadingSpaces } = useHypergraphApp(); + useEffect(() => { + if (!isConnecting) { + subscribeToSpace({ spaceId }); + } + }, [isConnecting, subscribeToSpace, spaceId]); + + const space = spaces.find((space) => space.id === spaceId); + + if (isConnecting || isLoadingSpaces[spaceId]) { + return
Loading …
; + } + + if (!space) { + return
Space not found
; + } + + return ( +
+ + + + +
+ ); +} diff --git a/apps/events/src/schema.ts b/apps/events/src/schema.ts index 0881008c..1d552f14 100644 --- a/apps/events/src/schema.ts +++ b/apps/events/src/schema.ts @@ -22,6 +22,7 @@ export class Todo2 extends Entity.Class('Todo2')({ id: Entity.Generated(Entity.Text), name: Entity.Text, checked: Entity.Checkbox, + assignees: Entity.Reference(Entity.ReferenceArray(User)), __deleted: Entity.Generated(Entity.Checkbox), __version: Entity.Generated(Entity.Text), }) {} @@ -50,6 +51,9 @@ export const mapping: Mapping = { name: Id.Id('LuBWqZAu6pz54eiJS5mLv8'), checked: Id.Id('A8UfGTFYCmfpTsDj7fC8dY'), }, + relations: { + assignees: Id.Id('JhxaewiF4zgzpawv4vt9SB'), + }, }, User: { typeIds: [Id.Id('JhxaewiF4zgzpawv4vt9SB')], diff --git a/packages/hypergraph-react/src/internal/generate-delete-ops-kg.tsx b/packages/hypergraph-react/src/internal/generate-delete-ops-kg.tsx index 0eea8f50..fed10dd5 100644 --- a/packages/hypergraph-react/src/internal/generate-delete-ops-kg.tsx +++ b/packages/hypergraph-react/src/internal/generate-delete-ops-kg.tsx @@ -1,6 +1,6 @@ import { type Op, Relation, Triple } from '@graphprotocol/grc-20'; import { gql, request } from 'graphql-request'; -import { KG_ENDPOINT } from './internal/constants.js'; +import { KG_ENDPOINT } from './constants.js'; const deleteEntityQueryDocument = gql` query deleteEntity($spaceId: String!, $id: String!) { diff --git a/packages/hypergraph-react/src/internal/use-generate-create-ops.tsx b/packages/hypergraph-react/src/internal/use-generate-create-ops.tsx index 4f1a4d93..990ec448 100644 --- a/packages/hypergraph-react/src/internal/use-generate-create-ops.tsx +++ b/packages/hypergraph-react/src/internal/use-generate-create-ops.tsx @@ -17,7 +17,7 @@ export function useGenerateCreateOps(type: } const fields = type.fields; const grcProperties: PropertiesParam = {}; - for (const [key, value] of Object.entries(mappingEntry.properties)) { + for (const [key, value] of Object.entries(mappingEntry.properties || {})) { let valueType: ValueType = 'TEXT'; let serializedValue: string = properties[key]; if (fields[key] === Entity.Checkbox) { @@ -31,6 +31,14 @@ export function useGenerateCreateOps(type: }; } + for (const [key, value] of Object.entries(mappingEntry.relations || {})) { + const toIds: { to: Id.Id }[] = []; + for (const toId of properties[key]) { + toIds.push({ to: Id.Id(toId) }); + } + grcProperties[value] = toIds; + } + const { ops, id } = Graph.createEntity({ types: mappingEntry.typeIds, properties: grcProperties, diff --git a/packages/hypergraph-react/src/internal/use-query-public-geo.tsx b/packages/hypergraph-react/src/internal/use-query-public-geo.tsx index 6d236ed6..8c4eb244 100644 --- a/packages/hypergraph-react/src/internal/use-query-public-geo.tsx +++ b/packages/hypergraph-react/src/internal/use-query-public-geo.tsx @@ -1,20 +1,13 @@ -import type { Id as Grc20Id } from '@graphprotocol/grc-20'; import { Entity } from '@graphprotocol/hypergraph'; import { useQuery as useQueryTanstack } from '@tanstack/react-query'; import * as Either from 'effect/Either'; import * as Schema from 'effect/Schema'; import { gql, request } from 'graphql-request'; import { useHypergraph } from '../HypergraphSpaceContext.js'; +import type { Mapping, MappingEntry } from '../types.js'; import { GEO_ENDPOINT } from './constants.js'; import type { QueryPublicParams } from './types.js'; -type MappingEntry = { - typeIds: Grc20Id.Id[]; - properties: { - [key: string]: Grc20Id.Id; - }; -}; - const entitiesQueryDocument = gql` query entities($spaceId: String!, $typeId: String!) { entities( @@ -43,7 +36,13 @@ query entities($spaceId: String!, $typeId: String!) { } relationsByFromVersionId { nodes { + toEntity { + nodeId + id + name + } typeOf { + id name } } @@ -75,6 +74,12 @@ type EntityQueryResult = { nodes: { typeOf: { name: string; + id: string; + }; + toEntity: { + nodeId: string; + id: string; + name: string; }; }[]; }; @@ -88,6 +93,7 @@ export const parseResult = ( queryData: EntityQueryResult, type: S, mappingEntry: MappingEntry, + mapping: Mapping, ) => { const decode = Schema.decodeUnknownEither(type); const data: Entity.Entity[] = []; @@ -95,7 +101,7 @@ export const parseResult = ( for (const queryEntity of queryData.entities.nodes) { const queryEntityVersion = queryEntity.currentVersion.version; - const rawEntity: Record = { + const rawEntity: Record = { id: queryEntity.id, }; // take the mappingEntry and assign the attributes to the rawEntity @@ -109,11 +115,62 @@ export const parseResult = ( } } } + + for (const [key, value] of Object.entries(mappingEntry?.relations ?? {})) { + const property = queryEntityVersion.relationsByFromVersionId.nodes.find((a) => a.typeOf.id === value); + if (!property) { + rawEntity[key] = [] as unknown[]; + continue; + } + + const field = type.fields[key]; + if (!field) { + // @ts-expect-error TODO: properly access the type.name + console.error(`Field ${key} not found in ${type.name}`); + continue; + } + // @ts-expect-error TODO: properly access the type.name + const annotations = field.ast.rest[0].type.to.annotations; + + // TODO: fix this access using proper effect types + const relationTypeName = + annotations[ + Object.getOwnPropertySymbols(annotations).find((sym) => sym.description === 'effect/annotation/Identifier') + ]; + + const relationMappingEntry = mapping[relationTypeName]; + if (!relationMappingEntry) { + console.error(`Relation mapping entry for ${relationTypeName} not found`); + continue; + } + + const newRelationEntity = { + id: property.toEntity.id, + name: property.toEntity.name, + type: relationMappingEntry.typeIds[0], + // TODO: should be determined by the actual value + __deleted: false, + // TODO: should be determined by the actual value + __version: '', + }; + + if (rawEntity[key]) { + rawEntity[key] = [ + // @ts-expect-error TODO: properly access the type.name + ...rawEntity[key], + newRelationEntity, + ]; + } else { + rawEntity[key] = [newRelationEntity]; + } + } + const decodeResult = decode({ ...rawEntity, __deleted: false, __version: queryEntity.currentVersion.versionId, }); + if (Either.isRight(decodeResult)) { data.push(decodeResult.right); } else { @@ -151,7 +208,7 @@ export const useQueryPublic = (type: S, par let invalidEntities: Record[] = []; if (result.data && mappingEntry) { - const parsedData = parseResult(result.data, type, mappingEntry); + const parsedData = parseResult(result.data, type, mappingEntry, mapping); data = parsedData.data; invalidEntities = parsedData.invalidEntities; } diff --git a/packages/hypergraph-react/src/types.ts b/packages/hypergraph-react/src/types.ts index bb359eb3..5e549ab0 100644 --- a/packages/hypergraph-react/src/types.ts +++ b/packages/hypergraph-react/src/types.ts @@ -2,15 +2,20 @@ import type { Id as Grc20Id, Op } from '@graphprotocol/grc-20'; import type { Entity } from '@graphprotocol/hypergraph'; import type * as Schema from 'effect/Schema'; -export type Mapping = { - [key: string]: { - typeIds: Grc20Id.Id[]; - properties: { - [key: string]: Grc20Id.Id; - }; +export type MappingEntry = { + typeIds: Grc20Id.Id[]; + properties?: { + [key: string]: Grc20Id.Id; + }; + relations?: { + [key: string]: Grc20Id.Id; }; }; +export type Mapping = { + [key: string]: MappingEntry; +}; + export type DiffEntry = Partial>> & { id: string; }; From fd76a0bb51979a4cee7c2e4900a6aa7756be55a5 Mon Sep 17 00:00:00 2001 From: Nik Graf Date: Sun, 30 Mar 2025 07:34:26 +0200 Subject: [PATCH 32/59] add users tab --- .../src/components/users/users-local.tsx | 35 +++++++++++++++ .../src/components/users/users-merged.tsx | 29 +++++++++++++ .../src/components/users/users-public-geo.tsx | 36 ++++++++++++++++ apps/events/src/routeTree.gen.ts | 27 ++++++++++++ apps/events/src/routes/space/$spaceId.tsx | 3 ++ .../src/routes/space/$spaceId/users.tsx | 43 +++++++++++++++++++ 6 files changed, 173 insertions(+) create mode 100644 apps/events/src/components/users/users-local.tsx create mode 100644 apps/events/src/components/users/users-merged.tsx create mode 100644 apps/events/src/components/users/users-public-geo.tsx create mode 100644 apps/events/src/routes/space/$spaceId/users.tsx diff --git a/apps/events/src/components/users/users-local.tsx b/apps/events/src/components/users/users-local.tsx new file mode 100644 index 00000000..239949c4 --- /dev/null +++ b/apps/events/src/components/users/users-local.tsx @@ -0,0 +1,35 @@ +import { useHardDeleteEntity, useQuery } from '@graphprotocol/hypergraph-react'; +import { User } from '../../schema'; +import { Button } from '../ui/button'; + +export const UsersLocal = () => { + const hardDeleteEntity = useHardDeleteEntity(); + const { data: usersLocalData, deleted: deletedUsersLocalData } = useQuery(User, { mode: 'local' }); + + return ( + <> +

Users (Local)

+ {usersLocalData.map((user) => ( +
+

{user.name}

+
{user.id}
+
{user.__deleted ? 'deleted' : 'not deleted'}
+
{user.__version}
+ +
+ ))} +

Deleted Users (Local)

+ {deletedUsersLocalData.map((user) => ( +
+

{user.name}

+
{user.id}
+ +
+ ))} + + ); +}; diff --git a/apps/events/src/components/users/users-merged.tsx b/apps/events/src/components/users/users-merged.tsx new file mode 100644 index 00000000..76124488 --- /dev/null +++ b/apps/events/src/components/users/users-merged.tsx @@ -0,0 +1,29 @@ +import { useDeleteEntity, useQuery } from '@graphprotocol/hypergraph-react'; +import { User } from '../../schema'; +import { Spinner } from '../spinner'; +import { Button } from '../ui/button'; + +export const UsersMerged = () => { + const { data, isLoading, isError } = useQuery(User); + const deleteEntity = useDeleteEntity(); + + return ( + <> +
+

Users (Merged)

+ {isLoading && } +
+ {isError &&
Error loading users
} + {data.map((user) => ( +
+

{user.name}

+
{user.id}
+ + +
+ ))} + + ); +}; diff --git a/apps/events/src/components/users/users-public-geo.tsx b/apps/events/src/components/users/users-public-geo.tsx new file mode 100644 index 00000000..f6f3b9e0 --- /dev/null +++ b/apps/events/src/components/users/users-public-geo.tsx @@ -0,0 +1,36 @@ +import { smartAccountWalletClient } from '@/lib/smart-account'; +import { _generateDeleteOps, publishOps, useHypergraphSpace, useQuery } from '@graphprotocol/hypergraph-react'; +import { User } from '../../schema'; +import { Spinner } from '../spinner'; +import { Button } from '../ui/button'; + +export const UsersPublicGeo = () => { + const space = useHypergraphSpace(); + const { data: dataPublic, isLoading: isLoadingPublic, isError: isErrorPublic } = useQuery(User, { mode: 'public' }); + + return ( + <> +
+

Users (Public Geo)

+ {isLoadingPublic && } +
+ {isErrorPublic &&
Error loading users
} + {dataPublic.map((user) => ( +
+

{user.name}

+
{user.id}
+ + +
+ ))} + + ); +}; diff --git a/apps/events/src/routeTree.gen.ts b/apps/events/src/routeTree.gen.ts index 355cbf72..18471442 100644 --- a/apps/events/src/routeTree.gen.ts +++ b/apps/events/src/routeTree.gen.ts @@ -17,6 +17,7 @@ import { Route as IndexImport } from './routes/index' import { Route as SpaceSpaceIdImport } from './routes/space/$spaceId' import { Route as SettingsExportWalletImport } from './routes/settings/export-wallet' import { Route as SpaceSpaceIdIndexImport } from './routes/space/$spaceId/index' +import { Route as SpaceSpaceIdUsersImport } from './routes/space/$spaceId/users' import { Route as SpaceSpaceIdPublicIntegrationImport } from './routes/space/$spaceId/public-integration' import { Route as SpaceSpaceIdPlaygroundImport } from './routes/space/$spaceId/playground' @@ -63,6 +64,12 @@ const SpaceSpaceIdIndexRoute = SpaceSpaceIdIndexImport.update({ getParentRoute: () => SpaceSpaceIdRoute, } as any) +const SpaceSpaceIdUsersRoute = SpaceSpaceIdUsersImport.update({ + id: '/users', + path: '/users', + getParentRoute: () => SpaceSpaceIdRoute, +} as any) + const SpaceSpaceIdPublicIntegrationRoute = SpaceSpaceIdPublicIntegrationImport.update({ id: '/public-integration', @@ -129,6 +136,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof SpaceSpaceIdPublicIntegrationImport parentRoute: typeof SpaceSpaceIdImport } + '/space/$spaceId/users': { + id: '/space/$spaceId/users' + path: '/users' + fullPath: '/space/$spaceId/users' + preLoaderRoute: typeof SpaceSpaceIdUsersImport + parentRoute: typeof SpaceSpaceIdImport + } '/space/$spaceId/': { id: '/space/$spaceId/' path: '/' @@ -144,12 +158,14 @@ declare module '@tanstack/react-router' { interface SpaceSpaceIdRouteChildren { SpaceSpaceIdPlaygroundRoute: typeof SpaceSpaceIdPlaygroundRoute SpaceSpaceIdPublicIntegrationRoute: typeof SpaceSpaceIdPublicIntegrationRoute + SpaceSpaceIdUsersRoute: typeof SpaceSpaceIdUsersRoute SpaceSpaceIdIndexRoute: typeof SpaceSpaceIdIndexRoute } const SpaceSpaceIdRouteChildren: SpaceSpaceIdRouteChildren = { SpaceSpaceIdPlaygroundRoute: SpaceSpaceIdPlaygroundRoute, SpaceSpaceIdPublicIntegrationRoute: SpaceSpaceIdPublicIntegrationRoute, + SpaceSpaceIdUsersRoute: SpaceSpaceIdUsersRoute, SpaceSpaceIdIndexRoute: SpaceSpaceIdIndexRoute, } @@ -165,6 +181,7 @@ export interface FileRoutesByFullPath { '/space/$spaceId': typeof SpaceSpaceIdRouteWithChildren '/space/$spaceId/playground': typeof SpaceSpaceIdPlaygroundRoute '/space/$spaceId/public-integration': typeof SpaceSpaceIdPublicIntegrationRoute + '/space/$spaceId/users': typeof SpaceSpaceIdUsersRoute '/space/$spaceId/': typeof SpaceSpaceIdIndexRoute } @@ -175,6 +192,7 @@ export interface FileRoutesByTo { '/settings/export-wallet': typeof SettingsExportWalletRoute '/space/$spaceId/playground': typeof SpaceSpaceIdPlaygroundRoute '/space/$spaceId/public-integration': typeof SpaceSpaceIdPublicIntegrationRoute + '/space/$spaceId/users': typeof SpaceSpaceIdUsersRoute '/space/$spaceId': typeof SpaceSpaceIdIndexRoute } @@ -187,6 +205,7 @@ export interface FileRoutesById { '/space/$spaceId': typeof SpaceSpaceIdRouteWithChildren '/space/$spaceId/playground': typeof SpaceSpaceIdPlaygroundRoute '/space/$spaceId/public-integration': typeof SpaceSpaceIdPublicIntegrationRoute + '/space/$spaceId/users': typeof SpaceSpaceIdUsersRoute '/space/$spaceId/': typeof SpaceSpaceIdIndexRoute } @@ -200,6 +219,7 @@ export interface FileRouteTypes { | '/space/$spaceId' | '/space/$spaceId/playground' | '/space/$spaceId/public-integration' + | '/space/$spaceId/users' | '/space/$spaceId/' fileRoutesByTo: FileRoutesByTo to: @@ -209,6 +229,7 @@ export interface FileRouteTypes { | '/settings/export-wallet' | '/space/$spaceId/playground' | '/space/$spaceId/public-integration' + | '/space/$spaceId/users' | '/space/$spaceId' id: | '__root__' @@ -219,6 +240,7 @@ export interface FileRouteTypes { | '/space/$spaceId' | '/space/$spaceId/playground' | '/space/$spaceId/public-integration' + | '/space/$spaceId/users' | '/space/$spaceId/' fileRoutesById: FileRoutesById } @@ -273,6 +295,7 @@ export const routeTree = rootRoute "children": [ "/space/$spaceId/playground", "/space/$spaceId/public-integration", + "/space/$spaceId/users", "/space/$spaceId/" ] }, @@ -284,6 +307,10 @@ export const routeTree = rootRoute "filePath": "space/$spaceId/public-integration.tsx", "parent": "/space/$spaceId" }, + "/space/$spaceId/users": { + "filePath": "space/$spaceId/users.tsx", + "parent": "/space/$spaceId" + }, "/space/$spaceId/": { "filePath": "space/$spaceId/index.tsx", "parent": "/space/$spaceId" diff --git a/apps/events/src/routes/space/$spaceId.tsx b/apps/events/src/routes/space/$spaceId.tsx index 6e923073..54e668bf 100644 --- a/apps/events/src/routes/space/$spaceId.tsx +++ b/apps/events/src/routes/space/$spaceId.tsx @@ -44,6 +44,9 @@ function RouteComponent() { > Playground + + Users + diff --git a/apps/events/src/routes/space/$spaceId/users.tsx b/apps/events/src/routes/space/$spaceId/users.tsx new file mode 100644 index 00000000..29d79727 --- /dev/null +++ b/apps/events/src/routes/space/$spaceId/users.tsx @@ -0,0 +1,43 @@ +import { mapping } from '@/schema'; +import { store } from '@graphprotocol/hypergraph'; +import { HypergraphSpaceProvider, useHypergraphApp } from '@graphprotocol/hypergraph-react'; +import { createFileRoute } from '@tanstack/react-router'; +import { useSelector } from '@xstate/store/react'; +import { useEffect } from 'react'; +import { UsersLocal } from '../../../components/users/users-local'; +import { UsersMerged } from '../../../components/users/users-merged'; +import { UsersPublicGeo } from '../../../components/users/users-public-geo'; +export const Route = createFileRoute('/space/$spaceId/users')({ + component: UsersRouteComponent, +}); + +function UsersRouteComponent() { + const { spaceId } = Route.useParams(); + const spaces = useSelector(store, (state) => state.context.spaces); + const { subscribeToSpace, isConnecting, isLoadingSpaces } = useHypergraphApp(); + useEffect(() => { + if (!isConnecting) { + subscribeToSpace({ spaceId }); + } + }, [isConnecting, subscribeToSpace, spaceId]); + + const space = spaces.find((space) => space.id === spaceId); + + if (isConnecting || isLoadingSpaces[spaceId]) { + return
Loading …
; + } + + if (!space) { + return
Space not found
; + } + + return ( +
+ + + + + +
+ ); +} From 3f71c47e02d717e4d9819b78d31c42e33e0e01f9 Mon Sep 17 00:00:00 2001 From: Nik Graf Date: Mon, 31 Mar 2025 12:40:12 +0200 Subject: [PATCH 33/59] ensure stable references to the data array for use-query --- .../src/HypergraphSpaceContext.tsx | 31 +++--- .../src/internal/use-query-public-geo.tsx | 15 ++- packages/hypergraph-react/src/use-query.tsx | 101 +++++++++--------- 3 files changed, 70 insertions(+), 77 deletions(-) diff --git a/packages/hypergraph-react/src/HypergraphSpaceContext.tsx b/packages/hypergraph-react/src/HypergraphSpaceContext.tsx index ba77d5dd..71aa1e27 100644 --- a/packages/hypergraph-react/src/HypergraphSpaceContext.tsx +++ b/packages/hypergraph-react/src/HypergraphSpaceContext.tsx @@ -5,7 +5,7 @@ import { useRepo } from '@automerge/automerge-repo-react-hooks'; import { Entity, Utils } from '@graphprotocol/hypergraph'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import * as Schema from 'effect/Schema'; -import { type ReactNode, createContext, useContext, useRef, useState, useSyncExternalStore } from 'react'; +import { type ReactNode, createContext, useContext, useMemo, useRef, useState, useSyncExternalStore } from 'react'; import type { Mapping } from './types.js'; export type HypergraphContext = { @@ -85,7 +85,6 @@ type QueryParams = { export function useQueryLocal(type: S, params?: QueryParams) { const { enabled = true } = params ?? {}; const entitiesRef = useRef[]>([]); - const deletedEntitiesRef = useRef[]>([]); const hypergraph = useHypergraph(); const [subscription] = useState(() => { @@ -101,22 +100,22 @@ export function useQueryLocal(type: S, para // TODO: allow to change the enabled state - const entities = useSyncExternalStore(subscription.subscribe, subscription.getEntities, () => entitiesRef.current); - - entitiesRef.current.splice(0, entitiesRef.current.length); - deletedEntitiesRef.current.splice(0, deletedEntitiesRef.current.length); - for (const entity of entities) { - if (entity.__deleted === true) { - deletedEntitiesRef.current.push(entity); - } else { - entitiesRef.current.push(entity); + const allEntities = useSyncExternalStore(subscription.subscribe, subscription.getEntities, () => entitiesRef.current); + + const { entities, deletedEntities } = useMemo(() => { + const entities: Entity.Entity[] = []; + const deletedEntities: Entity.Entity[] = []; + for (const entity of allEntities) { + if (entity.__deleted === true) { + deletedEntities.push(entity); + } else { + entities.push(entity); + } } - } + return { entities, deletedEntities }; + }, [allEntities]); - return { - entities: entitiesRef.current, - deletedEntities: deletedEntitiesRef.current, - }; + return { entities, deletedEntities }; } export function useQueryEntity(type: S, id: string) { diff --git a/packages/hypergraph-react/src/internal/use-query-public-geo.tsx b/packages/hypergraph-react/src/internal/use-query-public-geo.tsx index 8c4eb244..fe1329a5 100644 --- a/packages/hypergraph-react/src/internal/use-query-public-geo.tsx +++ b/packages/hypergraph-react/src/internal/use-query-public-geo.tsx @@ -3,6 +3,7 @@ import { useQuery as useQueryTanstack } from '@tanstack/react-query'; import * as Either from 'effect/Either'; import * as Schema from 'effect/Schema'; import { gql, request } from 'graphql-request'; +import { useMemo } from 'react'; import { useHypergraph } from '../HypergraphSpaceContext.js'; import type { Mapping, MappingEntry } from '../types.js'; import { GEO_ENDPOINT } from './constants.js'; @@ -204,14 +205,12 @@ export const useQueryPublic = (type: S, par enabled, }); - let data: Entity.Entity[] = []; - let invalidEntities: Record[] = []; - - if (result.data && mappingEntry) { - const parsedData = parseResult(result.data, type, mappingEntry, mapping); - data = parsedData.data; - invalidEntities = parsedData.invalidEntities; - } + const { data, invalidEntities } = useMemo(() => { + if (result.data && mappingEntry) { + return parseResult(result.data, type, mappingEntry, mapping); + } + return { data: [], invalidEntities: [] }; + }, [result.data, type, mappingEntry, mapping]); return { ...result, data, invalidEntities }; }; diff --git a/packages/hypergraph-react/src/use-query.tsx b/packages/hypergraph-react/src/use-query.tsx index 26f6c657..9dabb355 100644 --- a/packages/hypergraph-react/src/use-query.tsx +++ b/packages/hypergraph-react/src/use-query.tsx @@ -1,4 +1,5 @@ import type { Entity } from '@graphprotocol/hypergraph'; +import { useMemo } from 'react'; import { useHypergraph, useQueryLocal } from './HypergraphSpaceContext.js'; import { generateDeleteOps } from './internal/generate-delete-ops-geo.js'; import { useGenerateCreateOps } from './internal/use-generate-create-ops.js'; @@ -95,6 +96,13 @@ export function useQuery(type: S, params?: const generateCreateOps = useGenerateCreateOps(type, mode === 'merged'); const generateUpdateOps = useGenerateUpdateOps(type); + const mergedData = useMemo(() => { + if (mode !== 'merged' || publicResult.isLoading) { + return localResult.entities; + } + return mergeEntities(publicResult.data, localResult.entities, localResult.deletedEntities); + }, [mode, publicResult.isLoading, publicResult.data, localResult.entities, localResult.deletedEntities]); + if (mode === 'public') { return { ...publicResult, @@ -112,61 +120,48 @@ export function useQuery(type: S, params?: }; } - if (!publicResult.isLoading) { - const mergedData: Entity.Entity[] = mergeEntities( - publicResult.data, - localResult.entities, - localResult.deletedEntities, - ); - - return { - ...publicResult, - data: mergedData, - deleted: localResult.deletedEntities, - preparePublish: async (): Promise => { - // @ts-expect-error TODO should use the actual type instead of the name in the mapping - const typeName = type.name; - const mappingEntry = mapping?.[typeName]; - if (!mappingEntry) { - throw new Error(`Mapping entry for ${typeName} not found`); - } - - const result = await publicResult.refetch(); - if (!result.data) { - throw new Error('No data found'); - } - const diff = getDiff( - parseResult(result.data, type, mappingEntry).data, - localResult.entities, - localResult.deletedEntities, - ); - - const newEntities = diff.newEntities.map((entity) => { - const { ops: createOps } = generateCreateOps(entity); - return { id: entity.id, entity, ops: createOps }; - }); - - const updatedEntities = diff.updatedEntities.map((updatedEntityInfo) => { - const { ops: updateOps } = generateUpdateOps({ ...updatedEntityInfo.diff, id: updatedEntityInfo.id }); - return { ...updatedEntityInfo, ops: updateOps }; - }); - - const deletedEntities = await Promise.all( - diff.deletedEntities.map(async (entity) => { - const deleteOps = await generateDeleteOps(entity); - return { id: entity.id, entity, ops: deleteOps }; - }), - ); - - return { newEntities, updatedEntities, deletedEntities }; - }, - }; - } - return { ...publicResult, - data: localResult.entities, + data: mergedData, deleted: localResult.deletedEntities, - preparePublish: preparePublishDummy, + preparePublish: !publicResult.isLoading + ? async (): Promise => { + // @ts-expect-error TODO should use the actual type instead of the name in the mapping + const typeName = type.name; + const mappingEntry = mapping?.[typeName]; + if (!mappingEntry) { + throw new Error(`Mapping entry for ${typeName} not found`); + } + + const result = await publicResult.refetch(); + if (!result.data) { + throw new Error('No data found'); + } + const diff = getDiff( + parseResult(result.data, type, mappingEntry, mapping).data, + localResult.entities, + localResult.deletedEntities, + ); + + const newEntities = diff.newEntities.map((entity) => { + const { ops: createOps } = generateCreateOps(entity); + return { id: entity.id, entity, ops: createOps }; + }); + + const updatedEntities = diff.updatedEntities.map((updatedEntityInfo) => { + const { ops: updateOps } = generateUpdateOps({ ...updatedEntityInfo.diff, id: updatedEntityInfo.id }); + return { ...updatedEntityInfo, ops: updateOps }; + }); + + const deletedEntities = await Promise.all( + diff.deletedEntities.map(async (entity) => { + const deleteOps = await generateDeleteOps(entity); + return { id: entity.id, entity, ops: deleteOps }; + }), + ); + + return { newEntities, updatedEntities, deletedEntities }; + } + : preparePublishDummy, }; } From 6f4a4041016d775a9f02e97c94efbb91f23f23f8 Mon Sep 17 00:00:00 2001 From: Nik Graf Date: Mon, 31 Mar 2025 13:20:08 +0200 Subject: [PATCH 34/59] add assignee selector --- apps/events/src/components/todos2.tsx | 45 +++++++++++++++++++++++++-- 1 file changed, 42 insertions(+), 3 deletions(-) diff --git a/apps/events/src/components/todos2.tsx b/apps/events/src/components/todos2.tsx index 0e61b2e2..0649699e 100644 --- a/apps/events/src/components/todos2.tsx +++ b/apps/events/src/components/todos2.tsx @@ -11,7 +11,8 @@ import { useUpdateEntity, } from '@graphprotocol/hypergraph-react'; import { useQueryClient } from '@tanstack/react-query'; -import { useState } from 'react'; +import { useEffect, useState } from 'react'; +import Select from 'react-select'; import { Todo2, User } from '../schema'; import { Spinner } from './spinner'; import { TodosLocal } from './todo/todos-local'; @@ -40,6 +41,7 @@ export const Todos2 = () => { const createUser = useCreateEntity(User); const deleteEntity = useDeleteEntity(); const [newTodoName, setNewTodoName] = useState(''); + const [newTodoAssignees, setNewTodoAssignees] = useState<{ value: string; label: string }[]>([]); const [newUserName, setNewUserName] = useState(''); const queryClient = useQueryClient(); const [publishData, setPublishData] = useState(null); @@ -47,6 +49,15 @@ export const Todos2 = () => { const [isPreparingPublish, setIsPreparingPublish] = useState(false); const [isPublishing, setIsPublishing] = useState(false); + useEffect(() => { + setNewTodoAssignees((prevFilteredAssignees) => { + // filter out assignees that are not in the users array whenever users change + return prevFilteredAssignees.filter((assignee) => dataUsers.some((user) => user.id === assignee.value)); + }); + }, [dataUsers]); + + const userOptions = dataUsers.map((user) => ({ value: user.id, label: user.name })); + return ( <>
@@ -98,6 +109,27 @@ export const Todos2 = () => { checked={todo.checked} onChange={(e) => updateTodo(todo.id, { checked: e.target.checked })} /> + {todo.assignees.length > 0 && ( + + Assigned to:{' '} + {todo.assignees.map((assignee) => ( + + {assignee.name} + {/* */} + + ))} + + )}
{todo.__version}