@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,109 @@
|
|
|
1
|
+
import React, { memo, type ReactElement, type ReactNode } from 'react'
|
|
2
|
+
import type { HasManyProps, SelectionFieldMeta, SelectionMeta, SelectionProvider, EntityRef, AnyBrand, HasManyRef } from '../types.js'
|
|
3
|
+
import { FIELD_REF_META, BINDX_COMPONENT, SCOPE_REF } from '../types.js'
|
|
4
|
+
import { mergeSelections } from '../SelectionMeta.js'
|
|
5
|
+
import { SelectionScope, generateHasManyAlias } from '@contember/bindx'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* HasMany component - renders a has-many relation
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```tsx
|
|
12
|
+
* // Basic usage
|
|
13
|
+
* <HasMany field={author.fields.articles}>
|
|
14
|
+
* {article => (
|
|
15
|
+
* <div>
|
|
16
|
+
* <Field field={article.fields.title} />
|
|
17
|
+
* </div>
|
|
18
|
+
* )}
|
|
19
|
+
* </HasMany>
|
|
20
|
+
*
|
|
21
|
+
* // With query parameters
|
|
22
|
+
* <HasMany field={author.fields.articles} limit={5} orderBy={{ publishedAt: 'desc' }}>
|
|
23
|
+
* {(article, index) => (
|
|
24
|
+
* <div key={article.id}>
|
|
25
|
+
* {index + 1}. <Field field={article.fields.title} />
|
|
26
|
+
* </div>
|
|
27
|
+
* )}
|
|
28
|
+
* </HasMany>
|
|
29
|
+
* ```
|
|
30
|
+
*/
|
|
31
|
+
function HasManyImpl<
|
|
32
|
+
TEntity,
|
|
33
|
+
TSelected = TEntity,
|
|
34
|
+
TBrand extends AnyBrand = AnyBrand,
|
|
35
|
+
TEntityName extends string = string,
|
|
36
|
+
TSchema extends Record<string, object> = Record<string, object>,
|
|
37
|
+
>({ field, children }: HasManyProps<TEntity, TSelected, TBrand, TEntityName, TSchema>): ReactElement {
|
|
38
|
+
// At runtime, field is always a full HasManyRef (proxy provides all properties)
|
|
39
|
+
// Props accept HasManyRefBase for type compatibility with both implicit and explicit modes
|
|
40
|
+
const fullField = field as HasManyRef<TEntity, TSelected, TBrand, TEntityName, TSchema>
|
|
41
|
+
const items = fullField.map((item, index) => {
|
|
42
|
+
return <React.Fragment key={item.id}>{children(item, index)}</React.Fragment>
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
return <>{items}</>
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export const HasMany = memo(HasManyImpl) as typeof HasManyImpl
|
|
49
|
+
|
|
50
|
+
// Static method for selection extraction
|
|
51
|
+
const hasManyWithSelection = HasMany as typeof HasMany & SelectionProvider & { [BINDX_COMPONENT]: true }
|
|
52
|
+
|
|
53
|
+
hasManyWithSelection.getSelection = (
|
|
54
|
+
props: HasManyProps<unknown>,
|
|
55
|
+
collectNested: (children: ReactNode) => SelectionMeta,
|
|
56
|
+
): SelectionFieldMeta => {
|
|
57
|
+
const meta = props.field[FIELD_REF_META]
|
|
58
|
+
// At runtime, field is always a full HasManyRef (proxy provides all properties)
|
|
59
|
+
const fullField = props.field as HasManyRef<unknown>
|
|
60
|
+
|
|
61
|
+
// Use field's map function to get a properly configured collector with schema info
|
|
62
|
+
// The map() creates a collector that knows about entity types and relations
|
|
63
|
+
const result: { syntheticChildren: ReactNode; childScope: SelectionScope | null } = {
|
|
64
|
+
syntheticChildren: null,
|
|
65
|
+
childScope: null,
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
fullField.map((item, index) => {
|
|
69
|
+
// Get the scope from the collector entity (it has SCOPE_REF)
|
|
70
|
+
const scopeRef = (item as unknown as Record<symbol, unknown>)[SCOPE_REF]
|
|
71
|
+
if (scopeRef instanceof SelectionScope) {
|
|
72
|
+
result.childScope = scopeRef
|
|
73
|
+
}
|
|
74
|
+
result.syntheticChildren = props.children(item, index)
|
|
75
|
+
return null
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
// Also analyze the JSX structure
|
|
79
|
+
const jsxSelection = result.syntheticChildren ? collectNested(result.syntheticChildren) : { fields: new Map() }
|
|
80
|
+
|
|
81
|
+
// Convert scope to SelectionMeta and merge with JSX selection
|
|
82
|
+
const nestedSelection = result.childScope ? result.childScope.toSelectionMeta() : { fields: new Map() }
|
|
83
|
+
mergeSelections(nestedSelection, jsxSelection)
|
|
84
|
+
|
|
85
|
+
// Build hasManyParams object
|
|
86
|
+
const hasManyParams = {
|
|
87
|
+
filter: props.filter,
|
|
88
|
+
orderBy: props.orderBy,
|
|
89
|
+
limit: props.limit,
|
|
90
|
+
offset: props.offset,
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Generate alias based on params if any are provided
|
|
94
|
+
const alias = generateHasManyAlias(meta.fieldName, hasManyParams)
|
|
95
|
+
|
|
96
|
+
return {
|
|
97
|
+
fieldName: meta.fieldName,
|
|
98
|
+
alias,
|
|
99
|
+
path: meta.path,
|
|
100
|
+
isArray: true,
|
|
101
|
+
isRelation: true,
|
|
102
|
+
nested: nestedSelection,
|
|
103
|
+
hasManyParams,
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
hasManyWithSelection[BINDX_COMPONENT] = true
|
|
108
|
+
|
|
109
|
+
export { hasManyWithSelection as HasManyWithMeta }
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import React, { memo, type ReactElement, type ReactNode } from 'react'
|
|
2
|
+
import type { HasOneProps, SelectionFieldMeta, SelectionMeta, SelectionProvider, AnyBrand, HasOneRef } from '../types.js'
|
|
3
|
+
import { FIELD_REF_META, BINDX_COMPONENT, SCOPE_REF } from '../types.js'
|
|
4
|
+
import { mergeSelections } from '../SelectionMeta.js'
|
|
5
|
+
import { SelectionScope } from '@contember/bindx'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* HasOne component - renders a has-one relation
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```tsx
|
|
12
|
+
* <HasOne field={article.fields.author}>
|
|
13
|
+
* {author => (
|
|
14
|
+
* <div>
|
|
15
|
+
* <Field field={author.fields.name} />
|
|
16
|
+
* <Field field={author.fields.email} />
|
|
17
|
+
* </div>
|
|
18
|
+
* )}
|
|
19
|
+
* </HasOne>
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
function HasOneImpl<
|
|
23
|
+
TEntity,
|
|
24
|
+
TSelected = TEntity,
|
|
25
|
+
TBrand extends AnyBrand = AnyBrand,
|
|
26
|
+
TEntityName extends string = string,
|
|
27
|
+
TSchema extends Record<string, object> = Record<string, object>,
|
|
28
|
+
>({ field, children }: HasOneProps<TEntity, TSelected, TBrand, TEntityName, TSchema>): ReactElement {
|
|
29
|
+
// At runtime, field is always a full HasOneRef (proxy provides all properties)
|
|
30
|
+
// Props accept HasOneRefBase for type compatibility with both implicit and explicit modes
|
|
31
|
+
const fullField = field as HasOneRef<TEntity, TSelected, TBrand, TEntityName, TSchema>
|
|
32
|
+
// Get the related entity reference (always available, may be placeholder with id=null)
|
|
33
|
+
return <>{children(fullField.$entity)}</>
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export const HasOne = memo(HasOneImpl) as typeof HasOneImpl
|
|
37
|
+
|
|
38
|
+
// Static method for selection extraction
|
|
39
|
+
const hasOneWithSelection = HasOne as typeof HasOne & SelectionProvider & { [BINDX_COMPONENT]: true }
|
|
40
|
+
|
|
41
|
+
hasOneWithSelection.getSelection = (
|
|
42
|
+
props: HasOneProps<unknown>,
|
|
43
|
+
collectNested: (children: ReactNode) => SelectionMeta,
|
|
44
|
+
): SelectionFieldMeta => {
|
|
45
|
+
const meta = props.field[FIELD_REF_META]
|
|
46
|
+
// At runtime, field is always a full HasOneRef (proxy provides all properties)
|
|
47
|
+
const fullField = props.field as HasOneRef<unknown>
|
|
48
|
+
|
|
49
|
+
// Use field's $entity which has a properly configured scope with schema info
|
|
50
|
+
const nestedCollector = fullField.$entity
|
|
51
|
+
|
|
52
|
+
// Get the scope from the collector entity (it has SCOPE_REF)
|
|
53
|
+
const scopeRef = (nestedCollector as unknown as Record<symbol, unknown>)[SCOPE_REF]
|
|
54
|
+
const childScope = scopeRef instanceof SelectionScope ? scopeRef : null
|
|
55
|
+
|
|
56
|
+
// Call children once to gather nested field access
|
|
57
|
+
const syntheticChildren = props.children(nestedCollector)
|
|
58
|
+
|
|
59
|
+
// Also analyze the JSX structure
|
|
60
|
+
const jsxSelection = collectNested(syntheticChildren)
|
|
61
|
+
|
|
62
|
+
// Convert scope to SelectionMeta and merge with JSX selection
|
|
63
|
+
const nestedSelection = childScope ? childScope.toSelectionMeta() : { fields: new Map() }
|
|
64
|
+
mergeSelections(nestedSelection, jsxSelection)
|
|
65
|
+
|
|
66
|
+
return {
|
|
67
|
+
fieldName: meta.fieldName,
|
|
68
|
+
alias: meta.fieldName,
|
|
69
|
+
path: meta.path,
|
|
70
|
+
isArray: false,
|
|
71
|
+
isRelation: true,
|
|
72
|
+
nested: nestedSelection,
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
hasOneWithSelection[BINDX_COMPONENT] = true
|
|
77
|
+
|
|
78
|
+
export { hasOneWithSelection as HasOneWithMeta }
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HasRole component — runtime role gating with optional entity type narrowing.
|
|
3
|
+
*
|
|
4
|
+
* Mode 1 (context gate): Renders children only if user has at least one specified role.
|
|
5
|
+
* Mode 2 (entity re-typing): Narrows entity accessor type to the role-specific entity type.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import React, { type ReactNode, type ReactElement } from 'react'
|
|
9
|
+
import type {
|
|
10
|
+
EntityAccessor,
|
|
11
|
+
EntityForRoles,
|
|
12
|
+
RoleNames,
|
|
13
|
+
ExtractRoleMap,
|
|
14
|
+
} from '@contember/bindx'
|
|
15
|
+
import { useHasRole } from '../../roles/RoleContext.js'
|
|
16
|
+
|
|
17
|
+
// ============================================================================
|
|
18
|
+
// Props
|
|
19
|
+
// ============================================================================
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Props for HasRole in context-gate mode (no entity).
|
|
23
|
+
*/
|
|
24
|
+
interface HasRoleGateProps<TRoles extends string> {
|
|
25
|
+
/** Roles to check — renders if user has at least one */
|
|
26
|
+
roles: readonly TRoles[]
|
|
27
|
+
/** Children to render if role check passes */
|
|
28
|
+
children: ReactNode
|
|
29
|
+
entity?: never
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Props for HasRole in entity-narrowing mode.
|
|
34
|
+
* TRoleMap is inferred from the entity accessor's __roleMap phantom type.
|
|
35
|
+
*/
|
|
36
|
+
interface HasRoleEntityProps<
|
|
37
|
+
TRoleMap extends Record<string, object>,
|
|
38
|
+
TRoles extends RoleNames<TRoleMap>,
|
|
39
|
+
> {
|
|
40
|
+
/** Roles to expand to */
|
|
41
|
+
roles: readonly TRoles[]
|
|
42
|
+
/** Entity accessor to narrow */
|
|
43
|
+
entity: EntityAccessor<any, any, any, any, any, TRoleMap>
|
|
44
|
+
/** Render function receiving the role-expanded entity */
|
|
45
|
+
children: (entity: EntityAccessor<EntityForRoles<TRoleMap, TRoles>>) => ReactNode
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export type HasRoleProps<
|
|
49
|
+
TRoleMap extends Record<string, object> = Record<string, object>,
|
|
50
|
+
TRoles extends RoleNames<TRoleMap> = RoleNames<TRoleMap>,
|
|
51
|
+
> = HasRoleGateProps<TRoles> | HasRoleEntityProps<TRoleMap, TRoles>
|
|
52
|
+
|
|
53
|
+
// ============================================================================
|
|
54
|
+
// Component
|
|
55
|
+
// ============================================================================
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* HasRole component — conditionally renders based on user roles.
|
|
59
|
+
*
|
|
60
|
+
* @example Context gate mode
|
|
61
|
+
* ```tsx
|
|
62
|
+
* <HasRole roles={['admin']}>
|
|
63
|
+
* <AdminPanel />
|
|
64
|
+
* </HasRole>
|
|
65
|
+
* ```
|
|
66
|
+
*
|
|
67
|
+
* @example Entity narrowing mode
|
|
68
|
+
* ```tsx
|
|
69
|
+
* <HasRole roles={['admin']} entity={article}>
|
|
70
|
+
* {adminArticle => <span>{adminArticle.internalNotes.value}</span>}
|
|
71
|
+
* </HasRole>
|
|
72
|
+
* ```
|
|
73
|
+
*/
|
|
74
|
+
export function HasRole<
|
|
75
|
+
TRoleMap extends Record<string, object>,
|
|
76
|
+
TRoles extends RoleNames<TRoleMap>,
|
|
77
|
+
>(props: HasRoleProps<TRoleMap, TRoles>): ReactElement | null {
|
|
78
|
+
const hasRole = useHasRole()
|
|
79
|
+
|
|
80
|
+
// Check if user has at least one of the specified roles
|
|
81
|
+
const hasAccess = props.roles.some(role => hasRole(role))
|
|
82
|
+
if (!hasAccess) {
|
|
83
|
+
return null
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Entity narrowing mode
|
|
87
|
+
if ('entity' in props && props.entity !== undefined) {
|
|
88
|
+
const { entity, children } = props as HasRoleEntityProps<TRoleMap, TRoles>
|
|
89
|
+
// At runtime, the entity handle is the same object — we just widen the TS type
|
|
90
|
+
return <>{children(entity as unknown as EntityAccessor<EntityForRoles<TRoleMap, TRoles>>)}</>
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Context gate mode
|
|
94
|
+
return <>{props.children}</>
|
|
95
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import React, { memo, type ReactElement, type ReactNode } from 'react'
|
|
2
|
+
import type { IfProps, SelectionFieldMeta, SelectionMeta, SelectionProvider, FieldRef } from '../types.js'
|
|
3
|
+
import { FIELD_REF_META, BINDX_COMPONENT } from '../types.js'
|
|
4
|
+
import { mergeSelections, createEmptySelection } from '../SelectionMeta.js'
|
|
5
|
+
import {
|
|
6
|
+
type Condition,
|
|
7
|
+
isCondition,
|
|
8
|
+
evaluateCondition,
|
|
9
|
+
collectConditionFields,
|
|
10
|
+
CONDITION_META,
|
|
11
|
+
} from '../conditions.js'
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* If component - conditional rendering that ensures both branches are analyzed
|
|
15
|
+
*
|
|
16
|
+
* During collection phase, BOTH branches are analyzed to capture all required fields.
|
|
17
|
+
* During runtime, only the matching branch is rendered.
|
|
18
|
+
*
|
|
19
|
+
* Supports three types of conditions:
|
|
20
|
+
* 1. Boolean literals: `condition={true}` or `condition={someBoolean}`
|
|
21
|
+
* 2. FieldRef<boolean>: `condition={task.isActive}` - field value is used
|
|
22
|
+
* 3. Condition objects: `condition={cond.hasItems(task.developers)}` - composable DSL
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* ```tsx
|
|
26
|
+
* // With boolean condition
|
|
27
|
+
* <If condition={showBio} then={
|
|
28
|
+
* <Field field={author.bio} />
|
|
29
|
+
* } />
|
|
30
|
+
*
|
|
31
|
+
* // With field condition
|
|
32
|
+
* <If
|
|
33
|
+
* condition={author.isPublished}
|
|
34
|
+
* then={<Field field={author.publishedAt} />}
|
|
35
|
+
* else={<span>Draft</span>}
|
|
36
|
+
* />
|
|
37
|
+
*
|
|
38
|
+
* // With condition DSL
|
|
39
|
+
* <If
|
|
40
|
+
* condition={cond.hasItems(task.developers)}
|
|
41
|
+
* then={<HasMany field={task.developers}>{dev => ...}</HasMany>}
|
|
42
|
+
* />
|
|
43
|
+
*
|
|
44
|
+
* // Complex conditions
|
|
45
|
+
* <If
|
|
46
|
+
* condition={cond.and(
|
|
47
|
+
* cond.hasItems(task.developers),
|
|
48
|
+
* cond.eq(task.status, 'active')
|
|
49
|
+
* )}
|
|
50
|
+
* then={<span>Ready to work</span>}
|
|
51
|
+
* />
|
|
52
|
+
* ```
|
|
53
|
+
*/
|
|
54
|
+
function IfImpl({ condition, then: thenBranch, else: elseBranch }: IfProps): ReactElement | null {
|
|
55
|
+
let conditionValue: boolean
|
|
56
|
+
|
|
57
|
+
if (typeof condition === 'boolean') {
|
|
58
|
+
// Boolean literal
|
|
59
|
+
conditionValue = condition
|
|
60
|
+
} else if (isCondition(condition)) {
|
|
61
|
+
// Condition DSL object
|
|
62
|
+
conditionValue = evaluateCondition(condition)
|
|
63
|
+
} else {
|
|
64
|
+
// FieldRef<boolean>
|
|
65
|
+
conditionValue = (condition as FieldRef<boolean>).value ?? false
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return conditionValue ? <>{thenBranch}</> : <>{elseBranch ?? null}</>
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export const If = memo(IfImpl)
|
|
72
|
+
|
|
73
|
+
// Static method for selection extraction - analyzes BOTH branches
|
|
74
|
+
const ifWithSelection = If as typeof If & SelectionProvider & { [BINDX_COMPONENT]: true }
|
|
75
|
+
|
|
76
|
+
ifWithSelection.getSelection = (
|
|
77
|
+
props: IfProps,
|
|
78
|
+
collectNested: (children: ReactNode) => SelectionMeta,
|
|
79
|
+
): SelectionFieldMeta[] | null => {
|
|
80
|
+
const thenSelection = collectNested(props.then)
|
|
81
|
+
const elseSelection = props.else ? collectNested(props.else) : createEmptySelection()
|
|
82
|
+
|
|
83
|
+
// Merge both selections - we need fields from both branches
|
|
84
|
+
mergeSelections(thenSelection, elseSelection)
|
|
85
|
+
|
|
86
|
+
// Return all fields from merged selection
|
|
87
|
+
const result: SelectionFieldMeta[] = []
|
|
88
|
+
for (const field of thenSelection.fields.values()) {
|
|
89
|
+
result.push(field)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Collect fields from condition
|
|
93
|
+
if (typeof props.condition !== 'boolean') {
|
|
94
|
+
if (isCondition(props.condition)) {
|
|
95
|
+
// Condition DSL object - collect all referenced fields
|
|
96
|
+
const conditionFields = collectConditionFields(props.condition)
|
|
97
|
+
for (const field of conditionFields) {
|
|
98
|
+
if (field && typeof field === 'object' && FIELD_REF_META in field) {
|
|
99
|
+
const meta = (field as { [FIELD_REF_META]: { fieldName: string; path: string[]; isArray?: boolean; isRelation?: boolean } })[FIELD_REF_META]
|
|
100
|
+
result.push({
|
|
101
|
+
fieldName: meta.fieldName,
|
|
102
|
+
alias: meta.fieldName,
|
|
103
|
+
path: meta.path,
|
|
104
|
+
isArray: meta.isArray ?? false,
|
|
105
|
+
isRelation: meta.isRelation ?? false,
|
|
106
|
+
})
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
} else {
|
|
110
|
+
// FieldRef<boolean> - add that field
|
|
111
|
+
const meta = (props.condition as { [FIELD_REF_META]: { fieldName: string; path: string[]; isArray?: boolean; isRelation?: boolean } })[FIELD_REF_META]
|
|
112
|
+
result.push({
|
|
113
|
+
fieldName: meta.fieldName,
|
|
114
|
+
alias: meta.fieldName,
|
|
115
|
+
path: meta.path,
|
|
116
|
+
isArray: false,
|
|
117
|
+
isRelation: false,
|
|
118
|
+
})
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return result.length > 0 ? result : null
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
ifWithSelection[BINDX_COMPONENT] = true
|
|
126
|
+
|
|
127
|
+
export { ifWithSelection as IfWithMeta }
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import React, { memo, type ReactElement, type ReactNode } from 'react'
|
|
2
|
+
import type { FieldRef, SelectionFieldMeta, SelectionMeta, SelectionProvider } from '../types.js'
|
|
3
|
+
import { FIELD_REF_META, BINDX_COMPONENT } from '../types.js'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Props for Show component
|
|
7
|
+
*/
|
|
8
|
+
export interface ShowProps<T> {
|
|
9
|
+
field: FieldRef<T>
|
|
10
|
+
children: (value: NonNullable<T>) => ReactNode
|
|
11
|
+
fallback?: ReactNode
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Show component - renders content only if field has a value
|
|
16
|
+
* Useful for nullable fields.
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* ```tsx
|
|
20
|
+
* <Show field={article.fields.publishedAt}>
|
|
21
|
+
* {value => <time>{value.toISOString()}</time>}
|
|
22
|
+
* </Show>
|
|
23
|
+
*
|
|
24
|
+
* <Show field={author.fields.bio} fallback={<span>No bio</span>}>
|
|
25
|
+
* {bio => <p>{bio}</p>}
|
|
26
|
+
* </Show>
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
29
|
+
function ShowImpl<T>({ field, children, fallback }: ShowProps<T>): ReactElement | null {
|
|
30
|
+
if (field.value === null || field.value === undefined) {
|
|
31
|
+
return fallback ? <>{fallback}</> : null
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return <>{children(field.value as NonNullable<T>)}</>
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export const Show = memo(ShowImpl) as typeof ShowImpl
|
|
38
|
+
|
|
39
|
+
// Static method for selection extraction
|
|
40
|
+
const showWithSelection = Show as typeof Show & SelectionProvider & { [BINDX_COMPONENT]: true }
|
|
41
|
+
|
|
42
|
+
showWithSelection.getSelection = (props: ShowProps<unknown>): SelectionFieldMeta | null => {
|
|
43
|
+
const meta = props.field[FIELD_REF_META]
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
fieldName: meta.fieldName,
|
|
47
|
+
alias: meta.fieldName,
|
|
48
|
+
path: meta.path,
|
|
49
|
+
isArray: false,
|
|
50
|
+
isRelation: false,
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
showWithSelection[BINDX_COMPONENT] = true
|
|
55
|
+
|
|
56
|
+
export { showWithSelection as ShowWithMeta }
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// Field component
|
|
2
|
+
export { Field, FieldWithMeta } from './Field.js'
|
|
3
|
+
|
|
4
|
+
// HasMany component
|
|
5
|
+
export { HasMany, HasManyWithMeta } from './HasMany.js'
|
|
6
|
+
|
|
7
|
+
// HasOne component
|
|
8
|
+
export { HasOne, HasOneWithMeta } from './HasOne.js'
|
|
9
|
+
|
|
10
|
+
// If component
|
|
11
|
+
export { If, IfWithMeta } from './If.js'
|
|
12
|
+
|
|
13
|
+
// Show component
|
|
14
|
+
export { Show, ShowWithMeta, type ShowProps } from './Show.js'
|
|
15
|
+
|
|
16
|
+
// Entity component
|
|
17
|
+
export { Entity, type EntityProps } from './Entity.js'
|
|
18
|
+
|
|
19
|
+
// EntityList component
|
|
20
|
+
export { EntityList, type EntityListProps } from './EntityList.js'
|