Skip to content

Commit d02241b

Browse files
committed
feat(352 | typesync.v2): add type and property search functionality for browser
1 parent ba9a5af commit d02241b

File tree

15 files changed

+537
-450
lines changed

15 files changed

+537
-450
lines changed

apps/events/src/schema.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,13 @@ import { Entity, Type } from '@graphprotocol/hypergraph';
22

33
export class User extends Entity.Class<User>('User')({
44
name: Type.String,
5-
created: Type.Date
5+
created: Type.Date,
66
}) {}
77

88
export class Todo extends Entity.Class<Todo>('Todo')({
99
name: Type.String,
1010
completed: Type.Boolean,
11-
assignees: Type.Relation(User)
11+
assignees: Type.Relation(User),
1212
}) {}
1313

1414
export class Todo2 extends Entity.Class<Todo2>('Todo2')({
@@ -18,21 +18,21 @@ export class Todo2 extends Entity.Class<Todo2>('Todo2')({
1818
due: Type.Date,
1919
amount: Type.Number,
2020
point: Type.Point,
21-
website: Type.String
21+
website: Type.String,
2222
}) {}
2323

2424
export class JobOffer extends Entity.Class<JobOffer>('JobOffer')({
2525
name: Type.String,
26-
salary: Type.Number
26+
salary: Type.Number,
2727
}) {}
2828

2929
export class Company extends Entity.Class<Company>('Company')({
3030
name: Type.String,
31-
jobOffers: Type.Relation(JobOffer)
31+
jobOffers: Type.Relation(JobOffer),
3232
}) {}
3333

3434
export class Event extends Entity.Class<Event>('Event')({
3535
name: Type.String,
3636
description: Type.optional(Type.String),
37-
sponsors: Type.Relation(Company)
38-
}) {}
37+
sponsors: Type.Relation(Company),
38+
}) {}

biome.jsonc

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@
1717
"!**/tsconfig.*.json",
1818
"!**/variant-schema.ts",
1919
"!**/apps/create-hypergraph/template-*/**",
20-
"!**/*.css"
20+
"!**/*.css",
21+
"!packages/hypergraph/typesync-studio/src/generated/**/*.ts"
2122
]
2223
},
2324
"formatter": {

packages/hypergraph/typesync-studio/graphql.codegen.ts

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,6 @@ const config = {
3232
},
3333
},
3434
},
35-
hooks: {
36-
afterAllFileWrite: 'pnpm run lint:fix',
37-
},
3835
} as const satisfies CodegenConfig;
3936

4037
export default config;

packages/hypergraph/typesync-studio/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
"effect": "^3.17.4",
3030
"graphql": "^16.11.0",
3131
"graphql-request": "^7.2.0",
32+
"lodash.debounce": "^4.0.8",
3233
"react": "^19.1.1",
3334
"react-dom": "^19.1.1",
3435
"tailwindcss": "^4.1.11",
@@ -42,6 +43,7 @@
4243
"@tailwindcss/vite": "^4.1.11",
4344
"@testing-library/dom": "^10.4.1",
4445
"@testing-library/react": "^16.3.0",
46+
"@types/lodash.debounce": "^4.0.9",
4547
"@types/node": "^24.1.0",
4648
"@types/react": "^19.1.9",
4749
"@types/react-dom": "^19.1.7",

packages/hypergraph/typesync-studio/src/Components/Schema/KnowledgeGraphBrowser.tsx

Lines changed: 46 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,52 @@
11
'use client';
22

3-
import { Disclosure, DisclosureButton, DisclosurePanel, Input } from '@headlessui/react';
3+
import { Disclosure, DisclosureButton, DisclosurePanel } from '@headlessui/react';
44
import { CaretDownIcon, CaretRightIcon, PlusIcon } from '@phosphor-icons/react';
5-
import { Array as EffectArray, pipe } from 'effect';
5+
import { createFormHook } from '@tanstack/react-form';
6+
import { Array as EffectArray, String as EffectString } from 'effect';
67
import { useState } from 'react';
78

9+
import { fieldContext, formContext } from '@/Components/Form/form.ts';
10+
import { TextField } from '@/Components/Form/TextField.tsx';
811
import { type ExtendedSchemaBrowserType, useSchemaBrowserQuery } from '@/hooks/useKnowledgeGraph.tsx';
912
import { mapKGDataTypeToPrimitiveType } from '@/utils/type-mapper.ts';
1013
import { InlineCode } from '../InlineCode.tsx';
1114
import { Loading } from '../Loading.tsx';
1215

16+
const { useAppForm } = createFormHook({
17+
fieldComponents: {
18+
TextField,
19+
},
20+
formComponents: {},
21+
fieldContext,
22+
formContext,
23+
});
24+
1325
export type KnowledgeGraphBrowserProps = Readonly<{
1426
typeSelected(type: ExtendedSchemaBrowserType): void;
1527
}>;
1628
export function KnowledgeGraphBrowser({ typeSelected }: KnowledgeGraphBrowserProps) {
1729
const [typeSearch, setTypeSearch] = useState('');
18-
19-
const { data: types, isLoading } = useSchemaBrowserQuery({
20-
refetchOnMount: false,
21-
refetchOnWindowFocus: false,
22-
select(data) {
23-
if (!typeSearch) {
24-
return data;
25-
}
26-
return pipe(
27-
data,
28-
EffectArray.filter((type) => type.slug.includes(typeSearch.toLowerCase())),
29-
);
30+
const schemaBrowserForm = useAppForm({
31+
defaultValues: {
32+
search: '',
33+
} as {
34+
search: string;
3035
},
36+
asyncDebounceMs: 300,
3137
});
3238

39+
const { data: types, isLoading } = useSchemaBrowserQuery(
40+
{
41+
query: EffectString.isNonEmpty(typeSearch) ? EffectString.toLowerCase(typeSearch) : null,
42+
first: 100,
43+
},
44+
{
45+
refetchOnMount: false,
46+
refetchOnWindowFocus: false,
47+
},
48+
);
49+
3350
return (
3451
<div className="flex flex-col gap-y-6">
3552
<div className="border-b border-gray-200 dark:border-white/20 pb-5 h-20">
@@ -43,15 +60,21 @@ export function KnowledgeGraphBrowser({ typeSelected }: KnowledgeGraphBrowserPro
4360
</div>
4461
<div className="bg-gray-100 dark:bg-slate-900 rounded-lg flex flex-col gap-y-3 pt-2">
4562
<div className="px-3 mt-2">
46-
<Input
47-
id="SchemaBrowserSearch"
48-
name="SchemaBrowserSearch"
49-
value={typeSearch}
50-
onChange={(e) => setTypeSearch(e.target.value || '')}
51-
type="search"
52-
placeholder="Search types..."
53-
className="block min-w-0 grow py-1.5 pl-2 pr-3 rounded-md bg-white dark:bg-slate-700 data-[state=invalid]:pr-10 text-base text-gray-900 dark:text-white placeholder:text-gray-400 dark:placeholder:text-gray-500 focus:outline sm:text-sm/6 focus-visible:outline-none w-full"
54-
/>
63+
<schemaBrowserForm.AppField
64+
name="search"
65+
listeners={{
66+
// wait 300ms before setting the TypeSearch state value.
67+
// an update on that value will update the useSchemaBrowserQuery vars, sending a search query to the Knowledge Graph
68+
onChangeDebounceMs: 300,
69+
onChange({ value }) {
70+
setTypeSearch(value);
71+
},
72+
}}
73+
>
74+
{(field) => (
75+
<field.TextField id="search" name="search" type="search" placeholder="Search Knowledge Graph types..." />
76+
)}
77+
</schemaBrowserForm.AppField>
5578
</div>
5679
<ul className="px-4 divide-y divide-gray-100 dark:divide-gray-700 overflow-y-auto h-[500px] 2xl:h-[850px]">
5780
{(types ?? []).map((_type) => {

packages/hypergraph/typesync-studio/src/Components/Schema/PropertyCombobox.tsx

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@ import {
1111
} from '@headlessui/react';
1212
import { CaretUpDownIcon, CheckIcon } from '@phosphor-icons/react';
1313
import { useStore } from '@tanstack/react-form';
14-
import { Array as EffectArray, String as EffectString } from 'effect';
14+
import { String as EffectString } from 'effect';
15+
import debounce from 'lodash.debounce';
1516
import { useState } from 'react';
1617

1718
import { type ExtendedProperty, usePropertiesQuery } from '@/hooks/useKnowledgeGraph.tsx';
@@ -34,17 +35,20 @@ export function PropertyCombobox({ id, label, propertySelected, ...rest }: Reado
3435
const hasErrors = errors.length > 0 && touched;
3536

3637
const [propsFilter, setPropsFilter] = useState('');
38+
const debounceSearch = debounce<(val: string) => void>((val: string) => {
39+
setPropsFilter(val);
40+
}, 300);
3741

38-
const { data } = usePropertiesQuery({
39-
refetchOnMount: false,
40-
refetchOnWindowFocus: false,
41-
select(data) {
42-
if (EffectString.isEmpty(propsFilter)) {
43-
return data;
44-
}
45-
return EffectArray.filter(data, (prop) => prop.slug.includes(propsFilter.toLowerCase()));
42+
const { data } = usePropertiesQuery(
43+
{
44+
query: EffectString.isNonEmpty(propsFilter) ? EffectString.toLowerCase(propsFilter) : null,
45+
first: 50,
4646
},
47-
});
47+
{
48+
refetchOnMount: false,
49+
refetchOnWindowFocus: false,
50+
},
51+
);
4852
const props = data ?? [];
4953

5054
return (
@@ -62,7 +66,7 @@ export function PropertyCombobox({ id, label, propertySelected, ...rest }: Reado
6266
field.handleChange(val.name || val.id);
6367
propertySelected(val);
6468
}}
65-
onClose={() => setPropsFilter('')}
69+
onClose={() => debounceSearch('')}
6670
immediate
6771
>
6872
{label != null ? (
@@ -76,7 +80,7 @@ export function PropertyCombobox({ id, label, propertySelected, ...rest }: Reado
7680
className="block min-w-0 grow py-1.5 pr-12 data-[state=invalid]:pr-10 text-base bg-white dark:bg-white/5 pl-3 outline -outline-offset-1 outline-gray-300 dark:outline-white/10 rounded-md text-gray-900 dark:text-white data-[state=invalid]:text-red-900 dark:data-[state=invalid]:text-red-700 placeholder:text-gray-400 dark:placeholder:text-gray-500 data-[state=invalid]:placeholder:text-red-700 dark:data-[state=invalid]:placeholder:text-red-400 focus:outline sm:text-sm/6 focus-visible:outline-none"
7781
onChange={(event) => {
7882
const value = event.target.value;
79-
setPropsFilter(value);
83+
debounceSearch(value);
8084
field.handleChange(value);
8185
}}
8286
onBlur={() => setPropsFilter('')}

packages/hypergraph/typesync-studio/src/Components/Schema/TypeCombobox.tsx

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@ import {
1111
} from '@headlessui/react';
1212
import { CaretUpDownIcon, CheckIcon } from '@phosphor-icons/react';
1313
import { useStore } from '@tanstack/react-form';
14-
import { Array as EffectArray, String as EffectString } from 'effect';
14+
import { String as EffectString } from 'effect';
15+
import debounce from 'lodash.debounce';
1516
import { useState } from 'react';
1617

1718
import { type ExtendedSchemaBrowserType, useSchemaBrowserQuery } from '@/hooks/useKnowledgeGraph.tsx';
@@ -33,16 +34,20 @@ export function TypeCombobox({ id, label, typeSelected, ...rest }: Readonly<Type
3334
const hasErrors = errors.length > 0 && touched;
3435

3536
const [typesFilter, setTypesFilter] = useState('');
37+
const debounceSearch = debounce<(val: string) => void>((val: string) => {
38+
setTypesFilter(val);
39+
}, 300);
3640

37-
const { data } = useSchemaBrowserQuery({
38-
refetchOnMount: false,
39-
select(data) {
40-
if (EffectString.isEmpty(typesFilter)) {
41-
return data;
42-
}
43-
return EffectArray.filter(data, (type) => type.slug.includes(typesFilter.toLowerCase()));
41+
const { data } = useSchemaBrowserQuery(
42+
{
43+
query: EffectString.isNonEmpty(typesFilter) ? EffectString.toLowerCase(typesFilter) : null,
44+
first: 50,
4445
},
45-
});
46+
{
47+
refetchOnMount: false,
48+
refetchOnWindowFocus: false,
49+
},
50+
);
4651
const types = data ?? [];
4752

4853
return (
@@ -61,7 +66,9 @@ export function TypeCombobox({ id, label, typeSelected, ...rest }: Readonly<Type
6166
// emit the change
6267
typeSelected(val);
6368
}}
64-
onClose={() => setTypesFilter('')}
69+
onClose={() => {
70+
debounceSearch('');
71+
}}
6572
immediate
6673
>
6774
{label != null ? (
@@ -75,11 +82,12 @@ export function TypeCombobox({ id, label, typeSelected, ...rest }: Readonly<Type
7582
className="block min-w-0 grow py-1.5 pr-12 data-[state=invalid]:pr-10 text-base bg-white dark:bg-white/5 pl-3 outline -outline-offset-1 outline-gray-300 dark:outline-white/10 rounded-md text-gray-900 dark:text-white data-[state=invalid]:text-red-900 dark:data-[state=invalid]:text-red-700 placeholder:text-gray-400 dark:placeholder:text-gray-500 data-[state=invalid]:placeholder:text-red-700 dark:data-[state=invalid]:placeholder:text-red-400 focus:outline sm:text-sm/6 focus-visible:outline-none"
7683
onChange={(event) => {
7784
const value = event.target.value;
78-
setTypesFilter(value);
85+
debounceSearch(value);
7986
field.handleChange(value);
8087
}}
8188
onBlur={() => setTypesFilter('')}
8289
displayValue={(selectedType: string) => selectedType}
90+
type="search"
8391
/>
8492
<ComboboxButton className="absolute inset-y-0 right-0 flex items-center rounded-r-md px-2 focus:outline-hidden cursor-pointer">
8593
<CaretUpDownIcon className="size-5 text-gray-400 dark:text-gray-50" aria-hidden="true" />

0 commit comments

Comments
 (0)