@contember/bindx-react 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/batching/QueryBatcher.d.ts +31 -0
- package/dist/batching/QueryBatcher.d.ts.map +1 -0
- package/dist/batching/index.d.ts +2 -0
- package/dist/batching/index.d.ts.map +1 -0
- package/dist/components/EntityScope.d.ts +28 -0
- package/dist/components/EntityScope.d.ts.map +1 -0
- package/dist/dataview/DataGrid.d.ts +45 -0
- package/dist/dataview/DataGrid.d.ts.map +1 -0
- package/dist/dataview/DataGridPagination.d.ts +24 -0
- package/dist/dataview/DataGridPagination.d.ts.map +1 -0
- package/dist/dataview/DataGridToolbar.d.ts +16 -0
- package/dist/dataview/DataGridToolbar.d.ts.map +1 -0
- package/dist/dataview/DataViewContext.d.ts +32 -0
- package/dist/dataview/DataViewContext.d.ts.map +1 -0
- package/dist/dataview/columns.d.ts +115 -0
- package/dist/dataview/columns.d.ts.map +1 -0
- package/dist/dataview/contextHooks.d.ts +48 -0
- package/dist/dataview/contextHooks.d.ts.map +1 -0
- package/dist/dataview/dataAttribute.d.ts +6 -0
- package/dist/dataview/dataAttribute.d.ts.map +1 -0
- package/dist/dataview/export.d.ts +46 -0
- package/dist/dataview/export.d.ts.map +1 -0
- package/dist/dataview/filterComponents.d.ts +127 -0
- package/dist/dataview/filterComponents.d.ts.map +1 -0
- package/dist/dataview/filterContext.d.ts +10 -0
- package/dist/dataview/filterContext.d.ts.map +1 -0
- package/dist/dataview/filterHooks.d.ts +74 -0
- package/dist/dataview/filterHooks.d.ts.map +1 -0
- package/dist/dataview/filterScope.d.ts +54 -0
- package/dist/dataview/filterScope.d.ts.map +1 -0
- package/dist/dataview/filterWrappers.d.ts +65 -0
- package/dist/dataview/filterWrappers.d.ts.map +1 -0
- package/dist/dataview/index.d.ts +21 -0
- package/dist/dataview/index.d.ts.map +1 -0
- package/dist/dataview/infiniteLoad.d.ts +32 -0
- package/dist/dataview/infiniteLoad.d.ts.map +1 -0
- package/dist/dataview/loaderComponents.d.ts +80 -0
- package/dist/dataview/loaderComponents.d.ts.map +1 -0
- package/dist/dataview/pagingComponents.d.ts +50 -0
- package/dist/dataview/pagingComponents.d.ts.map +1 -0
- package/dist/dataview/renderCellValue.d.ts +9 -0
- package/dist/dataview/renderCellValue.d.ts.map +1 -0
- package/dist/dataview/selectionComponents.d.ts +62 -0
- package/dist/dataview/selectionComponents.d.ts.map +1 -0
- package/dist/dataview/sortingComponents.d.ts +39 -0
- package/dist/dataview/sortingComponents.d.ts.map +1 -0
- package/dist/dataview/stateStorage.d.ts +19 -0
- package/dist/dataview/stateStorage.d.ts.map +1 -0
- package/dist/dataview/useDataView.d.ts +58 -0
- package/dist/dataview/useDataView.d.ts.map +1 -0
- package/dist/dataview/useDataViewState.d.ts +89 -0
- package/dist/dataview/useDataViewState.d.ts.map +1 -0
- package/dist/hooks/BackendAdapterContext.d.ts +110 -0
- package/dist/hooks/BackendAdapterContext.d.ts.map +1 -0
- package/dist/hooks/ContemberBindxProvider.d.ts +67 -0
- package/dist/hooks/ContemberBindxProvider.d.ts.map +1 -0
- package/dist/hooks/createBindx.d.ts +39 -0
- package/dist/hooks/createBindx.d.ts.map +1 -0
- package/dist/hooks/index.d.ts +10 -0
- package/dist/hooks/index.d.ts.map +1 -0
- package/dist/hooks/useBindxEvents.d.ts +83 -0
- package/dist/hooks/useBindxEvents.d.ts.map +1 -0
- package/dist/hooks/useEntity.d.ts +123 -0
- package/dist/hooks/useEntity.d.ts.map +1 -0
- package/dist/hooks/useEntityBeforePersist.d.ts +22 -0
- package/dist/hooks/useEntityBeforePersist.d.ts.map +1 -0
- package/dist/hooks/useEntityCore.d.ts +52 -0
- package/dist/hooks/useEntityCore.d.ts.map +1 -0
- package/dist/hooks/useEntityImpl.d.ts +105 -0
- package/dist/hooks/useEntityImpl.d.ts.map +1 -0
- package/dist/hooks/useEntityList.d.ts +102 -0
- package/dist/hooks/useEntityList.d.ts.map +1 -0
- package/dist/hooks/useEntityListCore.d.ts +49 -0
- package/dist/hooks/useEntityListCore.d.ts.map +1 -0
- package/dist/hooks/useEntityListImpl.d.ts +70 -0
- package/dist/hooks/useEntityListImpl.d.ts.map +1 -0
- package/dist/hooks/useErrors.d.ts +72 -0
- package/dist/hooks/useErrors.d.ts.map +1 -0
- package/dist/hooks/usePersist.d.ts +115 -0
- package/dist/hooks/usePersist.d.ts.map +1 -0
- package/dist/hooks/useSelectionCollection.d.ts +41 -0
- package/dist/hooks/useSelectionCollection.d.ts.map +1 -0
- package/dist/hooks/useSelectionCollectionForList.d.ts +40 -0
- package/dist/hooks/useSelectionCollectionForList.d.ts.map +1 -0
- package/dist/hooks/useStableSelectionMeta.d.ts +11 -0
- package/dist/hooks/useStableSelectionMeta.d.ts.map +1 -0
- package/dist/hooks/useStoreSubscription.d.ts +19 -0
- package/dist/hooks/useStoreSubscription.d.ts.map +1 -0
- package/dist/hooks/useUndo.d.ts +45 -0
- package/dist/hooks/useUndo.d.ts.map +1 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/jsx/SelectionMeta.d.ts +2 -0
- package/dist/jsx/SelectionMeta.d.ts.map +1 -0
- package/dist/jsx/analyzer.d.ts +22 -0
- package/dist/jsx/analyzer.d.ts.map +1 -0
- package/dist/jsx/collectorProxy.d.ts +14 -0
- package/dist/jsx/collectorProxy.d.ts.map +1 -0
- package/dist/jsx/componentBuilder.d.ts +51 -0
- package/dist/jsx/componentBuilder.d.ts.map +1 -0
- package/dist/jsx/componentBuilder.types.d.ts +268 -0
- package/dist/jsx/componentBuilder.types.d.ts.map +1 -0
- package/dist/jsx/componentBuilderCompat.d.ts +17 -0
- package/dist/jsx/componentBuilderCompat.d.ts.map +1 -0
- package/dist/jsx/componentFactory.d.ts +39 -0
- package/dist/jsx/componentFactory.d.ts.map +1 -0
- package/dist/jsx/components/Entity.d.ts +82 -0
- package/dist/jsx/components/Entity.d.ts.map +1 -0
- package/dist/jsx/components/EntityList.d.ts +48 -0
- package/dist/jsx/components/EntityList.d.ts.map +1 -0
- package/dist/jsx/components/Field.d.ts +27 -0
- package/dist/jsx/components/Field.d.ts.map +1 -0
- package/dist/jsx/components/HasMany.d.ts +34 -0
- package/dist/jsx/components/HasMany.d.ts.map +1 -0
- package/dist/jsx/components/HasOne.d.ts +25 -0
- package/dist/jsx/components/HasOne.d.ts.map +1 -0
- package/dist/jsx/components/HasRole.d.ts +51 -0
- package/dist/jsx/components/HasRole.d.ts.map +1 -0
- package/dist/jsx/components/If.d.ts +51 -0
- package/dist/jsx/components/If.d.ts.map +1 -0
- package/dist/jsx/components/Show.d.ts +33 -0
- package/dist/jsx/components/Show.d.ts.map +1 -0
- package/dist/jsx/components/index.d.ts +8 -0
- package/dist/jsx/components/index.d.ts.map +1 -0
- package/dist/jsx/conditions.d.ts +310 -0
- package/dist/jsx/conditions.d.ts.map +1 -0
- package/dist/jsx/createComponent.d.ts +44 -0
- package/dist/jsx/createComponent.d.ts.map +1 -0
- package/dist/jsx/index.d.ts +19 -0
- package/dist/jsx/index.d.ts.map +1 -0
- package/dist/jsx/inlineProxy.d.ts +14 -0
- package/dist/jsx/inlineProxy.d.ts.map +1 -0
- package/dist/jsx/legacyTypes.d.ts +26 -0
- package/dist/jsx/legacyTypes.d.ts.map +1 -0
- package/dist/jsx/proxy.d.ts +2 -0
- package/dist/jsx/proxy.d.ts.map +1 -0
- package/dist/jsx/proxyShared.d.ts +14 -0
- package/dist/jsx/proxyShared.d.ts.map +1 -0
- package/dist/jsx/runtimeProxy.d.ts +11 -0
- package/dist/jsx/runtimeProxy.d.ts.map +1 -0
- package/dist/jsx/standaloneCreateComponent.d.ts +30 -0
- package/dist/jsx/standaloneCreateComponent.d.ts.map +1 -0
- package/dist/jsx/types.d.ts +109 -0
- package/dist/jsx/types.d.ts.map +1 -0
- package/dist/jsx/withCollector.d.ts +39 -0
- package/dist/jsx/withCollector.d.ts.map +1 -0
- package/dist/roles/RoleContext.d.ts +31 -0
- package/dist/roles/RoleContext.d.ts.map +1 -0
- package/dist/roles/index.d.ts +2 -0
- package/dist/roles/index.d.ts.map +1 -0
- package/package.json +34 -0
- package/src/batching/QueryBatcher.ts +114 -0
- package/src/batching/index.ts +1 -0
- package/src/components/EntityScope.tsx +43 -0
- package/src/hooks/BackendAdapterContext.tsx +214 -0
- package/src/hooks/ContemberBindxProvider.tsx +158 -0
- package/src/hooks/index.ts +61 -0
- package/src/hooks/useBindxEvents.ts +229 -0
- package/src/hooks/useEntity.ts +489 -0
- package/src/hooks/useEntityBeforePersist.ts +32 -0
- package/src/hooks/useEntityList.ts +444 -0
- package/src/hooks/useErrors.ts +150 -0
- package/src/hooks/usePersist.ts +377 -0
- package/src/hooks/useSelectionCollection.ts +122 -0
- package/src/hooks/useStoreSubscription.ts +51 -0
- package/src/hooks/useUndo.ts +83 -0
- package/src/index.ts +357 -0
- package/src/jsx/SelectionMeta.ts +2 -0
- package/src/jsx/analyzer.ts +261 -0
- package/src/jsx/collectorProxy.ts +282 -0
- package/src/jsx/componentBuilder.ts +180 -0
- package/src/jsx/componentBuilder.types.ts +463 -0
- package/src/jsx/componentBuilderCompat.ts +39 -0
- package/src/jsx/componentFactory.ts +400 -0
- package/src/jsx/components/Entity.tsx +383 -0
- package/src/jsx/components/EntityList.tsx +147 -0
- package/src/jsx/components/Field.tsx +79 -0
- package/src/jsx/components/HasMany.tsx +109 -0
- package/src/jsx/components/HasOne.tsx +78 -0
- package/src/jsx/components/HasRole.tsx +95 -0
- package/src/jsx/components/If.tsx +127 -0
- package/src/jsx/components/Show.tsx +56 -0
- package/src/jsx/components/index.ts +20 -0
- package/src/jsx/conditions.ts +472 -0
- package/src/jsx/createComponent.ts +175 -0
- package/src/jsx/index.ts +135 -0
- package/src/jsx/legacyTypes.ts +30 -0
- package/src/jsx/proxy.ts +1 -0
- package/src/jsx/proxyShared.ts +42 -0
- package/src/jsx/standaloneCreateComponent.ts +47 -0
- package/src/jsx/types.ts +192 -0
- package/src/jsx/withCollector.ts +45 -0
- package/src/roles/RoleContext.tsx +57 -0
- package/src/roles/index.ts +1 -0
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
/**
|
|
3
|
+
* Attaches a static render function to a component for selection collection.
|
|
4
|
+
*
|
|
5
|
+
* During the collection phase, the analyzer calls `staticRender(props)` instead of
|
|
6
|
+
* analyzing runtime children. Props contain collector proxies, so field accesses
|
|
7
|
+
* are tracked automatically. The returned JSX is analyzed for nested components.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```tsx
|
|
11
|
+
* // Simple: children is a render prop receiving has-one entity
|
|
12
|
+
* export const SelectField = withCollector(
|
|
13
|
+
* function SelectField({ field, children, ... }) { ... },
|
|
14
|
+
* (props) => props.children(props.field.$entity)
|
|
15
|
+
* )
|
|
16
|
+
*
|
|
17
|
+
* // Composing with HasMany for iteration
|
|
18
|
+
* export const DefaultRepeater = withCollector(
|
|
19
|
+
* function DefaultRepeater({ field, children, ... }) { ... },
|
|
20
|
+
* (props) => (
|
|
21
|
+
* <HasMany field={props.field}>
|
|
22
|
+
* {item => props.children(item, collectionItemInfo)}
|
|
23
|
+
* </HasMany>
|
|
24
|
+
* )
|
|
25
|
+
* )
|
|
26
|
+
*
|
|
27
|
+
* // Programmatic field access (no JSX needed)
|
|
28
|
+
* export const Uploader = withCollector(
|
|
29
|
+
* function Uploader({ field, fileType }) { ... },
|
|
30
|
+
* (props) => {
|
|
31
|
+
* const entity = props.field.$entity
|
|
32
|
+
* for (const ext of props.fileType.extractors) entity[ext.fieldName]
|
|
33
|
+
* return null
|
|
34
|
+
* }
|
|
35
|
+
* )
|
|
36
|
+
* ```
|
|
37
|
+
*/
|
|
38
|
+
export declare function withCollector<TComponent extends (...args: never[]) => ReactNode>(component: TComponent, staticRender: (props: Parameters<TComponent>[0]) => ReactNode): TComponent;
|
|
39
|
+
//# sourceMappingURL=withCollector.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"withCollector.d.ts","sourceRoot":"","sources":["../../src/jsx/withCollector.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAEtC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AACH,wBAAgB,aAAa,CAAC,UAAU,SAAS,CAAC,GAAG,IAAI,EAAE,KAAK,EAAE,KAAK,SAAS,EAC/E,SAAS,EAAE,UAAU,EACrB,YAAY,EAAE,CAAC,KAAK,EAAE,UAAU,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,KAAK,SAAS,GAC3D,UAAU,CAGZ"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Role provider context for runtime role checking.
|
|
3
|
+
*
|
|
4
|
+
* HasRole uses this to determine if the current user has the required roles.
|
|
5
|
+
* Without a RoleProvider, HasRole always renders (no gating).
|
|
6
|
+
*/
|
|
7
|
+
import { type ReactNode, type ReactElement } from 'react';
|
|
8
|
+
type HasRoleFn = (role: string) => boolean;
|
|
9
|
+
export interface RoleProviderProps {
|
|
10
|
+
children: ReactNode;
|
|
11
|
+
/** Function that checks if the current user has a given role */
|
|
12
|
+
hasRole: HasRoleFn;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Provides runtime role checking for HasRole components.
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* ```tsx
|
|
19
|
+
* <RoleProvider hasRole={role => currentUser.roles.includes(role)}>
|
|
20
|
+
* <App />
|
|
21
|
+
* </RoleProvider>
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
24
|
+
export declare function RoleProvider({ children, hasRole }: RoleProviderProps): ReactElement;
|
|
25
|
+
/**
|
|
26
|
+
* Returns a function to check if the current user has a given role.
|
|
27
|
+
* If no RoleProvider is present, returns a function that always returns true.
|
|
28
|
+
*/
|
|
29
|
+
export declare function useHasRole(): HasRoleFn;
|
|
30
|
+
export {};
|
|
31
|
+
//# sourceMappingURL=RoleContext.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"RoleContext.d.ts","sourceRoot":"","sources":["../../src/roles/RoleContext.tsx"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAc,EAA6B,KAAK,SAAS,EAAE,KAAK,YAAY,EAAE,MAAM,OAAO,CAAA;AAM3F,KAAK,SAAS,GAAG,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAA;AAQ1C,MAAM,WAAW,iBAAiB;IACjC,QAAQ,EAAE,SAAS,CAAA;IACnB,gEAAgE;IAChE,OAAO,EAAE,SAAS,CAAA;CAClB;AAED;;;;;;;;;GASG;AACH,wBAAgB,YAAY,CAAC,EAAE,QAAQ,EAAE,OAAO,EAAE,EAAE,iBAAiB,GAAG,YAAY,CAEnF;AAMD;;;GAGG;AACH,wBAAgB,UAAU,IAAI,SAAS,CAGtC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/roles/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,UAAU,EAAE,KAAK,iBAAiB,EAAE,MAAM,kBAAkB,CAAA"}
|
package/package.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@contember/bindx-react",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Type-safe React data binding framework",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./src/index.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./src/index.ts"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"build": "tsc --build",
|
|
12
|
+
"typecheck": "tsc --build"
|
|
13
|
+
},
|
|
14
|
+
"peerDependencies": {
|
|
15
|
+
"react": ">=18.0.0"
|
|
16
|
+
},
|
|
17
|
+
"dependencies": {
|
|
18
|
+
"@contember/bindx": "0.1.0",
|
|
19
|
+
"@contember/client-content": "^2.1.0-alpha.30",
|
|
20
|
+
"@contember/graphql-client": "^2.1.0-alpha.30",
|
|
21
|
+
"@radix-ui/primitive": "^1.1.3",
|
|
22
|
+
"@radix-ui/react-slot": "^1.2.4"
|
|
23
|
+
},
|
|
24
|
+
"files": [
|
|
25
|
+
"dist",
|
|
26
|
+
"src"
|
|
27
|
+
],
|
|
28
|
+
"license": "MIT",
|
|
29
|
+
"repository": {
|
|
30
|
+
"type": "git",
|
|
31
|
+
"url": "https://github.com/contember/bindx.git",
|
|
32
|
+
"directory": "packages/bindx-react"
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import type { BackendAdapter, Query, QueryResult, QueryOptions } from '@contember/bindx'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Pending query with its resolution callbacks
|
|
5
|
+
*/
|
|
6
|
+
interface PendingQuery {
|
|
7
|
+
readonly query: Query
|
|
8
|
+
readonly resolve: (result: QueryResult) => void
|
|
9
|
+
readonly reject: (error: Error) => void
|
|
10
|
+
readonly signal?: AbortSignal
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Batches queries from multiple components into single adapter calls.
|
|
15
|
+
*
|
|
16
|
+
* Uses queueMicrotask to collect all queries during a React render cycle
|
|
17
|
+
* and execute them together in a single batch.
|
|
18
|
+
*/
|
|
19
|
+
export class QueryBatcher {
|
|
20
|
+
private pendingQueries: PendingQuery[] = []
|
|
21
|
+
private flushScheduled = false
|
|
22
|
+
|
|
23
|
+
constructor(private readonly adapter: BackendAdapter) {}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Queue a query for batched execution.
|
|
27
|
+
* Returns a promise that resolves when the query completes.
|
|
28
|
+
*/
|
|
29
|
+
enqueue(query: Query, options?: QueryOptions): Promise<QueryResult> {
|
|
30
|
+
return new Promise((resolve, reject) => {
|
|
31
|
+
// If provided signal is already aborted, reject immediately
|
|
32
|
+
if (options?.signal?.aborted) {
|
|
33
|
+
reject(new DOMException('Aborted', 'AbortError'))
|
|
34
|
+
return
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const pending: PendingQuery = { query, resolve, reject, signal: options?.signal }
|
|
38
|
+
|
|
39
|
+
// Handle abort for this specific query
|
|
40
|
+
if (options?.signal) {
|
|
41
|
+
const abortHandler = (): void => {
|
|
42
|
+
const index = this.pendingQueries.indexOf(pending)
|
|
43
|
+
if (index >= 0) {
|
|
44
|
+
this.pendingQueries.splice(index, 1)
|
|
45
|
+
reject(new DOMException('Aborted', 'AbortError'))
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
options.signal.addEventListener('abort', abortHandler, { once: true })
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
this.pendingQueries.push(pending)
|
|
52
|
+
this.scheduleFlush()
|
|
53
|
+
})
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Schedule flush using microtask to batch within render cycle
|
|
58
|
+
*/
|
|
59
|
+
private scheduleFlush(): void {
|
|
60
|
+
if (this.flushScheduled) return
|
|
61
|
+
this.flushScheduled = true
|
|
62
|
+
|
|
63
|
+
queueMicrotask(() => {
|
|
64
|
+
this.flush()
|
|
65
|
+
})
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Execute all pending queries in a batch
|
|
70
|
+
*/
|
|
71
|
+
private async flush(): Promise<void> {
|
|
72
|
+
this.flushScheduled = false
|
|
73
|
+
|
|
74
|
+
if (this.pendingQueries.length === 0) return
|
|
75
|
+
|
|
76
|
+
// Take all pending queries
|
|
77
|
+
const batch = this.pendingQueries.splice(0)
|
|
78
|
+
|
|
79
|
+
// Find queries that aren't aborted
|
|
80
|
+
const activeBatch = batch.filter(p => !p.signal?.aborted)
|
|
81
|
+
|
|
82
|
+
if (activeBatch.length === 0) return
|
|
83
|
+
|
|
84
|
+
try {
|
|
85
|
+
const queries = activeBatch.map(p => p.query)
|
|
86
|
+
const results = await this.adapter.query(queries)
|
|
87
|
+
|
|
88
|
+
// Resolve each query with its result
|
|
89
|
+
for (let i = 0; i < activeBatch.length; i++) {
|
|
90
|
+
const pending = activeBatch[i]
|
|
91
|
+
const result = results[i]
|
|
92
|
+
if (pending && result) {
|
|
93
|
+
pending.resolve(result)
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
} catch (error) {
|
|
97
|
+
// Reject all queries in batch
|
|
98
|
+
for (const pending of activeBatch) {
|
|
99
|
+
pending.reject(error instanceof Error ? error : new Error(String(error)))
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Cancel all pending queries
|
|
106
|
+
*/
|
|
107
|
+
cancelAll(): void {
|
|
108
|
+
const error = new DOMException('Cancelled', 'AbortError')
|
|
109
|
+
for (const pending of this.pendingQueries) {
|
|
110
|
+
pending.reject(error)
|
|
111
|
+
}
|
|
112
|
+
this.pendingQueries = []
|
|
113
|
+
}
|
|
114
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { QueryBatcher } from './QueryBatcher.js'
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { createContext, useContext, type ReactNode } from 'react'
|
|
2
|
+
import type { EntityHandle } from '@contember/bindx'
|
|
3
|
+
|
|
4
|
+
const EntityScopeContext = createContext<EntityHandle | null>(null)
|
|
5
|
+
|
|
6
|
+
export interface EntityScopeProps {
|
|
7
|
+
entity: EntityHandle
|
|
8
|
+
children: ReactNode
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Provides an EntityHandle into React context for nested components.
|
|
13
|
+
* Useful for scoping entity access in editors, repeaters, selects, etc.
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* ```tsx
|
|
17
|
+
* <EntityScope entity={handle}>
|
|
18
|
+
* <MyComponent /> // can use useEntityScope() inside
|
|
19
|
+
* </EntityScope>
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
export function EntityScope({ entity, children }: EntityScopeProps): ReactNode {
|
|
23
|
+
return <EntityScopeContext.Provider value={entity}>{children}</EntityScopeContext.Provider>
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Reads the closest EntityHandle from EntityScope context.
|
|
28
|
+
* Throws if used outside of an EntityScope.
|
|
29
|
+
*/
|
|
30
|
+
export function useEntityScope(): EntityHandle {
|
|
31
|
+
const entity = useContext(EntityScopeContext)
|
|
32
|
+
if (!entity) {
|
|
33
|
+
throw new Error('useEntityScope must be used within an EntityScope')
|
|
34
|
+
}
|
|
35
|
+
return entity
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Reads the closest EntityHandle from EntityScope context, or null if not in scope.
|
|
40
|
+
*/
|
|
41
|
+
export function useOptionalEntityScope(): EntityHandle | null {
|
|
42
|
+
return useContext(EntityScopeContext)
|
|
43
|
+
}
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import { createContext, useContext, useMemo, type ReactNode } from 'react'
|
|
2
|
+
import type { BackendAdapter, MutationDataCollector, SchemaDefinition, UndoManagerConfig, UpdateMode } from '@contember/bindx'
|
|
3
|
+
import { SnapshotStore } from '@contember/bindx'
|
|
4
|
+
import { ActionDispatcher } from '@contember/bindx'
|
|
5
|
+
import { BatchPersister } from '@contember/bindx'
|
|
6
|
+
import { MutationCollector } from '@contember/bindx'
|
|
7
|
+
import { SchemaRegistry } from '@contember/bindx'
|
|
8
|
+
import { UndoManager } from '@contember/bindx'
|
|
9
|
+
import { QueryBatcher } from '../batching/QueryBatcher.js'
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Context value containing all bindx services
|
|
13
|
+
*/
|
|
14
|
+
export interface BindxContextValue {
|
|
15
|
+
/** Backend adapter for data fetching/persistence */
|
|
16
|
+
adapter: BackendAdapter
|
|
17
|
+
/** Query batcher for combining multiple queries into single requests */
|
|
18
|
+
batcher: QueryBatcher
|
|
19
|
+
/** Immutable snapshot store */
|
|
20
|
+
store: SnapshotStore
|
|
21
|
+
/** Action dispatcher for mutations */
|
|
22
|
+
dispatcher: ActionDispatcher
|
|
23
|
+
/** Batch persister for multi-entity persistence */
|
|
24
|
+
batchPersister: BatchPersister
|
|
25
|
+
/** Schema registry for entity metadata */
|
|
26
|
+
schema: SchemaRegistry
|
|
27
|
+
/** Undo manager (if enabled) */
|
|
28
|
+
undoManager: UndoManager | null
|
|
29
|
+
/** Whether debug logging is enabled */
|
|
30
|
+
debug: boolean
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export const BindxContext = createContext<BindxContextValue | null>(null)
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Props for BindxProvider
|
|
37
|
+
*/
|
|
38
|
+
export interface BindxProviderProps {
|
|
39
|
+
/** The backend adapter to use for data fetching/persistence */
|
|
40
|
+
adapter: BackendAdapter
|
|
41
|
+
/** Optional custom snapshot store (useful for testing) */
|
|
42
|
+
store?: SnapshotStore
|
|
43
|
+
/** Schema definition for entity metadata and mutation collection */
|
|
44
|
+
schema: SchemaDefinition<Record<string, object>>
|
|
45
|
+
/** Optional custom mutation collector (overrides default behavior) */
|
|
46
|
+
mutationCollector?: MutationDataCollector
|
|
47
|
+
/** Enable undo/redo functionality */
|
|
48
|
+
enableUndo?: boolean
|
|
49
|
+
/** Configuration for undo manager */
|
|
50
|
+
undoConfig?: UndoManagerConfig
|
|
51
|
+
/**
|
|
52
|
+
* Default update mode for persistence operations.
|
|
53
|
+
* - 'optimistic': Update UI immediately, revert on failure (default)
|
|
54
|
+
* - 'pessimistic': Wait for server confirmation before updating UI
|
|
55
|
+
*/
|
|
56
|
+
defaultUpdateMode?: UpdateMode
|
|
57
|
+
/** Enable debug logging (e.g., selection collection output) */
|
|
58
|
+
debug?: boolean
|
|
59
|
+
children: ReactNode
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Provider component that supplies the bindx services
|
|
64
|
+
* to all child components using bindx hooks.
|
|
65
|
+
*
|
|
66
|
+
* @example
|
|
67
|
+
* ```tsx
|
|
68
|
+
* const adapter = new MockAdapter(initialData)
|
|
69
|
+
*
|
|
70
|
+
* function App() {
|
|
71
|
+
* return (
|
|
72
|
+
* <BindxProvider adapter={adapter} schema={testSchema}>
|
|
73
|
+
* <ArticleEditor id="123" />
|
|
74
|
+
* </BindxProvider>
|
|
75
|
+
* )
|
|
76
|
+
* }
|
|
77
|
+
* ```
|
|
78
|
+
*/
|
|
79
|
+
export function BindxProvider({
|
|
80
|
+
adapter,
|
|
81
|
+
store: customStore,
|
|
82
|
+
schema: schemaDefinition,
|
|
83
|
+
mutationCollector: customMutationCollector,
|
|
84
|
+
enableUndo = false,
|
|
85
|
+
undoConfig,
|
|
86
|
+
defaultUpdateMode,
|
|
87
|
+
debug = false,
|
|
88
|
+
children,
|
|
89
|
+
}: BindxProviderProps) {
|
|
90
|
+
// Create services - memoized to maintain stable references
|
|
91
|
+
const services = useMemo(() => {
|
|
92
|
+
const store = customStore ?? new SnapshotStore()
|
|
93
|
+
const dispatcher = new ActionDispatcher(store)
|
|
94
|
+
const batcher = new QueryBatcher(adapter)
|
|
95
|
+
|
|
96
|
+
// Create undo manager if enabled
|
|
97
|
+
const undoManager = enableUndo ? new UndoManager(store, undoConfig) : null
|
|
98
|
+
if (undoManager) {
|
|
99
|
+
dispatcher.addMiddleware(undoManager.createMiddleware())
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Create schema registry from definition
|
|
103
|
+
const schemaRegistry = new SchemaRegistry(schemaDefinition)
|
|
104
|
+
|
|
105
|
+
// Create mutation collector - use custom, or auto-create from schema
|
|
106
|
+
// SchemaRegistry implements MutationSchemaProvider interface
|
|
107
|
+
const mutationCollector =
|
|
108
|
+
customMutationCollector ?? new MutationCollector(store, schemaRegistry)
|
|
109
|
+
|
|
110
|
+
const batchPersister = new BatchPersister(adapter, store, dispatcher, {
|
|
111
|
+
mutationCollector,
|
|
112
|
+
undoManager: undoManager ?? undefined,
|
|
113
|
+
schema: schemaRegistry,
|
|
114
|
+
defaultUpdateMode,
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
return {
|
|
118
|
+
adapter,
|
|
119
|
+
batcher,
|
|
120
|
+
store,
|
|
121
|
+
dispatcher,
|
|
122
|
+
batchPersister,
|
|
123
|
+
schema: schemaRegistry,
|
|
124
|
+
undoManager,
|
|
125
|
+
debug,
|
|
126
|
+
}
|
|
127
|
+
}, [adapter, customStore, schemaDefinition, customMutationCollector, enableUndo, undoConfig, defaultUpdateMode, debug])
|
|
128
|
+
|
|
129
|
+
return <BindxContext.Provider value={services}>{children}</BindxContext.Provider>
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Hook to access the schema registry.
|
|
134
|
+
* Must be used within a BindxProvider or ContemberBindxProvider.
|
|
135
|
+
*/
|
|
136
|
+
export function useSchemaRegistry(): SchemaRegistry {
|
|
137
|
+
const context = useContext(BindxContext)
|
|
138
|
+
if (!context) {
|
|
139
|
+
throw new Error('useSchemaRegistry must be used within a BindxProvider')
|
|
140
|
+
}
|
|
141
|
+
return context.schema
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Hook to access the backend adapter.
|
|
146
|
+
* Must be used within a BindxProvider.
|
|
147
|
+
*/
|
|
148
|
+
export function useBackendAdapter(): BackendAdapter {
|
|
149
|
+
const context = useContext(BindxContext)
|
|
150
|
+
if (!context) {
|
|
151
|
+
throw new Error('useBackendAdapter must be used within a BindxProvider')
|
|
152
|
+
}
|
|
153
|
+
return context.adapter
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Hook to access the snapshot store.
|
|
158
|
+
* Must be used within a BindxProvider.
|
|
159
|
+
*/
|
|
160
|
+
export function useSnapshotStore(): SnapshotStore {
|
|
161
|
+
const context = useContext(BindxContext)
|
|
162
|
+
if (!context) {
|
|
163
|
+
throw new Error('useSnapshotStore must be used within a BindxProvider')
|
|
164
|
+
}
|
|
165
|
+
return context.store
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Hook to access the action dispatcher.
|
|
170
|
+
* Must be used within a BindxProvider.
|
|
171
|
+
*/
|
|
172
|
+
export function useDispatcher(): ActionDispatcher {
|
|
173
|
+
const context = useContext(BindxContext)
|
|
174
|
+
if (!context) {
|
|
175
|
+
throw new Error('useDispatcher must be used within a BindxProvider')
|
|
176
|
+
}
|
|
177
|
+
return context.dispatcher
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Hook to access the batch persister.
|
|
182
|
+
* Must be used within a BindxProvider.
|
|
183
|
+
*/
|
|
184
|
+
export function useBatchPersister(): BatchPersister {
|
|
185
|
+
const context = useContext(BindxContext)
|
|
186
|
+
if (!context) {
|
|
187
|
+
throw new Error('useBatchPersister must be used within a BindxProvider')
|
|
188
|
+
}
|
|
189
|
+
return context.batchPersister
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Hook to access all bindx services.
|
|
194
|
+
* Must be used within a BindxProvider.
|
|
195
|
+
*/
|
|
196
|
+
export function useBindxContext(): BindxContextValue {
|
|
197
|
+
const context = useContext(BindxContext)
|
|
198
|
+
if (!context) {
|
|
199
|
+
throw new Error('useBindxContext must be used within a BindxProvider')
|
|
200
|
+
}
|
|
201
|
+
return context
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Hook to access the query batcher.
|
|
206
|
+
* Must be used within a BindxProvider.
|
|
207
|
+
*/
|
|
208
|
+
export function useQueryBatcher(): QueryBatcher {
|
|
209
|
+
const context = useContext(BindxContext)
|
|
210
|
+
if (!context) {
|
|
211
|
+
throw new Error('useQueryBatcher must be used within a BindxProvider')
|
|
212
|
+
}
|
|
213
|
+
return context.batcher
|
|
214
|
+
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { memo, useMemo, type ReactNode } from 'react'
|
|
2
|
+
import { GraphQlClient } from '@contember/graphql-client'
|
|
3
|
+
import { ContentClient, ContentQueryBuilder, type SchemaNames } from '@contember/client-content'
|
|
4
|
+
import { ContemberAdapter, SnapshotStore, ActionDispatcher, BatchPersister, MutationCollector, ContemberSchemaMutationAdapter, UndoManager, SchemaRegistry, type SchemaDefinition, type FieldDef, type UndoManagerConfig, type UpdateMode } from '@contember/bindx'
|
|
5
|
+
import { BindxContext, type BindxContextValue } from './BackendAdapterContext.js'
|
|
6
|
+
import { QueryBatcher } from '../batching/QueryBatcher.js'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Converts SchemaNames (Contember format) to SchemaDefinition (bindx format)
|
|
10
|
+
* so that a SchemaRegistry can be created for standalone hooks.
|
|
11
|
+
*/
|
|
12
|
+
export function schemaNamesToDef(schemaNames: SchemaNames): SchemaDefinition<Record<string, object>> {
|
|
13
|
+
const enumsMap = (schemaNames as { enums?: Record<string, readonly string[]> }).enums ?? {}
|
|
14
|
+
const entities: Record<string, { fields: Record<string, FieldDef> }> = {}
|
|
15
|
+
for (const [entityName, entity] of Object.entries(schemaNames.entities)) {
|
|
16
|
+
const fields: Record<string, FieldDef> = {}
|
|
17
|
+
for (const [fieldName, fieldDef] of Object.entries(entity.fields)) {
|
|
18
|
+
if (fieldDef.type === 'column') {
|
|
19
|
+
const enumName = (fieldDef as { enumName?: string }).enumName
|
|
20
|
+
const columnType = (fieldDef as { columnType?: string }).columnType
|
|
21
|
+
const enumValues = enumName ? enumsMap[enumName] : undefined
|
|
22
|
+
if (enumName && enumValues) {
|
|
23
|
+
fields[fieldName] = { type: 'enum', enumName, values: enumValues }
|
|
24
|
+
} else {
|
|
25
|
+
fields[fieldName] = { type: 'scalar', columnType }
|
|
26
|
+
}
|
|
27
|
+
} else if (fieldDef.type === 'one') {
|
|
28
|
+
fields[fieldName] = { type: 'hasOne', target: fieldDef.entity }
|
|
29
|
+
} else if (fieldDef.type === 'many') {
|
|
30
|
+
fields[fieldName] = { type: 'hasMany', target: fieldDef.entity }
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
entities[entityName] = { fields }
|
|
34
|
+
}
|
|
35
|
+
return { entities }
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Props for ContemberBindxProvider
|
|
40
|
+
*/
|
|
41
|
+
export interface ContemberBindxProviderProps {
|
|
42
|
+
/** Contember schema names for query building */
|
|
43
|
+
schema: SchemaNames
|
|
44
|
+
/** Optional custom snapshot store (useful for testing) */
|
|
45
|
+
store?: SnapshotStore
|
|
46
|
+
/** Children */
|
|
47
|
+
children: ReactNode
|
|
48
|
+
|
|
49
|
+
client: GraphQlClient
|
|
50
|
+
|
|
51
|
+
/** Enable undo/redo functionality. Pass true to auto-create an UndoManager, or pass an UndoManager instance. */
|
|
52
|
+
undoManager?: UndoManager | boolean
|
|
53
|
+
/** Configuration for auto-created undo manager (only used when undoManager={true}) */
|
|
54
|
+
undoConfig?: UndoManagerConfig
|
|
55
|
+
/**
|
|
56
|
+
* Default update mode for persistence operations.
|
|
57
|
+
* - 'optimistic': Update UI immediately, revert on failure (default)
|
|
58
|
+
* - 'pessimistic': Wait for server confirmation before updating UI
|
|
59
|
+
*/
|
|
60
|
+
defaultUpdateMode?: UpdateMode
|
|
61
|
+
/** Enable debug logging (e.g., selection collection output) */
|
|
62
|
+
debug?: boolean
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Provider component that combines Contember authentication with bindx data binding.
|
|
67
|
+
*
|
|
68
|
+
* @example
|
|
69
|
+
* ```tsx
|
|
70
|
+
* import { ContemberBindxProvider, useUndo } from '@contember/bindx-react'
|
|
71
|
+
* import { schema } from './generated/schema'
|
|
72
|
+
*
|
|
73
|
+
* function App() {
|
|
74
|
+
* return (
|
|
75
|
+
* <ContemberBindxProvider
|
|
76
|
+
* client={graphQlClient}
|
|
77
|
+
* schema={schema}
|
|
78
|
+
* undoManager={true}
|
|
79
|
+
* >
|
|
80
|
+
* <ArticleEditor id="123" />
|
|
81
|
+
* </ContemberBindxProvider>
|
|
82
|
+
* )
|
|
83
|
+
* }
|
|
84
|
+
*
|
|
85
|
+
* // Access undo/redo in child components
|
|
86
|
+
* function UndoControls() {
|
|
87
|
+
* const { canUndo, canRedo, undo, redo } = useUndo()
|
|
88
|
+
* return (
|
|
89
|
+
* <div>
|
|
90
|
+
* <button onClick={undo} disabled={!canUndo}>Undo</button>
|
|
91
|
+
* <button onClick={redo} disabled={!canRedo}>Redo</button>
|
|
92
|
+
* </div>
|
|
93
|
+
* )
|
|
94
|
+
* }
|
|
95
|
+
* ```
|
|
96
|
+
*/
|
|
97
|
+
export const ContemberBindxProvider = memo(function ContemberBindxProvider({
|
|
98
|
+
schema,
|
|
99
|
+
store: customStore,
|
|
100
|
+
children,
|
|
101
|
+
client: graphQlClient,
|
|
102
|
+
undoManager: undoManagerProp,
|
|
103
|
+
undoConfig,
|
|
104
|
+
defaultUpdateMode,
|
|
105
|
+
debug = false,
|
|
106
|
+
}: ContemberBindxProviderProps) {
|
|
107
|
+
|
|
108
|
+
// Create GraphQL client and adapter
|
|
109
|
+
const bindxValue = useMemo((): BindxContextValue => {
|
|
110
|
+
|
|
111
|
+
const adapter = new ContemberAdapter(new ContentClient(graphQlClient), new ContentQueryBuilder(schema))
|
|
112
|
+
const batcher = new QueryBatcher(adapter)
|
|
113
|
+
|
|
114
|
+
const store = customStore ?? new SnapshotStore()
|
|
115
|
+
const dispatcher = new ActionDispatcher(store)
|
|
116
|
+
|
|
117
|
+
// Create undo manager if enabled
|
|
118
|
+
const undoManager = undoManagerProp === true
|
|
119
|
+
? new UndoManager(store, undoConfig)
|
|
120
|
+
: undoManagerProp instanceof UndoManager
|
|
121
|
+
? undoManagerProp
|
|
122
|
+
: null
|
|
123
|
+
if (undoManager) {
|
|
124
|
+
dispatcher.addMiddleware(undoManager.createMiddleware())
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Create schema registry from SchemaNames for standalone hooks
|
|
128
|
+
const schemaRegistry = new SchemaRegistry(schemaNamesToDef(schema))
|
|
129
|
+
|
|
130
|
+
// Create mutation collector for proper nested operations
|
|
131
|
+
// Use ContemberSchemaMutationAdapter to wrap SchemaNames
|
|
132
|
+
const schemaAdapter = new ContemberSchemaMutationAdapter(schema)
|
|
133
|
+
const mutationCollector = new MutationCollector(store, schemaAdapter)
|
|
134
|
+
|
|
135
|
+
const batchPersister = new BatchPersister(adapter, store, dispatcher, {
|
|
136
|
+
mutationCollector,
|
|
137
|
+
undoManager: undoManager ?? undefined,
|
|
138
|
+
defaultUpdateMode,
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
return {
|
|
142
|
+
adapter,
|
|
143
|
+
batcher,
|
|
144
|
+
store,
|
|
145
|
+
dispatcher,
|
|
146
|
+
batchPersister,
|
|
147
|
+
schema: schemaRegistry,
|
|
148
|
+
undoManager,
|
|
149
|
+
debug,
|
|
150
|
+
}
|
|
151
|
+
}, [schema, customStore, undoManagerProp, undoConfig, defaultUpdateMode, debug])
|
|
152
|
+
|
|
153
|
+
return (
|
|
154
|
+
<BindxContext.Provider value={bindxValue}>
|
|
155
|
+
{children}
|
|
156
|
+
</BindxContext.Provider>
|
|
157
|
+
)
|
|
158
|
+
})
|