@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.
Files changed (194) hide show
  1. package/dist/batching/QueryBatcher.d.ts +31 -0
  2. package/dist/batching/QueryBatcher.d.ts.map +1 -0
  3. package/dist/batching/index.d.ts +2 -0
  4. package/dist/batching/index.d.ts.map +1 -0
  5. package/dist/components/EntityScope.d.ts +28 -0
  6. package/dist/components/EntityScope.d.ts.map +1 -0
  7. package/dist/dataview/DataGrid.d.ts +45 -0
  8. package/dist/dataview/DataGrid.d.ts.map +1 -0
  9. package/dist/dataview/DataGridPagination.d.ts +24 -0
  10. package/dist/dataview/DataGridPagination.d.ts.map +1 -0
  11. package/dist/dataview/DataGridToolbar.d.ts +16 -0
  12. package/dist/dataview/DataGridToolbar.d.ts.map +1 -0
  13. package/dist/dataview/DataViewContext.d.ts +32 -0
  14. package/dist/dataview/DataViewContext.d.ts.map +1 -0
  15. package/dist/dataview/columns.d.ts +115 -0
  16. package/dist/dataview/columns.d.ts.map +1 -0
  17. package/dist/dataview/contextHooks.d.ts +48 -0
  18. package/dist/dataview/contextHooks.d.ts.map +1 -0
  19. package/dist/dataview/dataAttribute.d.ts +6 -0
  20. package/dist/dataview/dataAttribute.d.ts.map +1 -0
  21. package/dist/dataview/export.d.ts +46 -0
  22. package/dist/dataview/export.d.ts.map +1 -0
  23. package/dist/dataview/filterComponents.d.ts +127 -0
  24. package/dist/dataview/filterComponents.d.ts.map +1 -0
  25. package/dist/dataview/filterContext.d.ts +10 -0
  26. package/dist/dataview/filterContext.d.ts.map +1 -0
  27. package/dist/dataview/filterHooks.d.ts +74 -0
  28. package/dist/dataview/filterHooks.d.ts.map +1 -0
  29. package/dist/dataview/filterScope.d.ts +54 -0
  30. package/dist/dataview/filterScope.d.ts.map +1 -0
  31. package/dist/dataview/filterWrappers.d.ts +65 -0
  32. package/dist/dataview/filterWrappers.d.ts.map +1 -0
  33. package/dist/dataview/index.d.ts +21 -0
  34. package/dist/dataview/index.d.ts.map +1 -0
  35. package/dist/dataview/infiniteLoad.d.ts +32 -0
  36. package/dist/dataview/infiniteLoad.d.ts.map +1 -0
  37. package/dist/dataview/loaderComponents.d.ts +80 -0
  38. package/dist/dataview/loaderComponents.d.ts.map +1 -0
  39. package/dist/dataview/pagingComponents.d.ts +50 -0
  40. package/dist/dataview/pagingComponents.d.ts.map +1 -0
  41. package/dist/dataview/renderCellValue.d.ts +9 -0
  42. package/dist/dataview/renderCellValue.d.ts.map +1 -0
  43. package/dist/dataview/selectionComponents.d.ts +62 -0
  44. package/dist/dataview/selectionComponents.d.ts.map +1 -0
  45. package/dist/dataview/sortingComponents.d.ts +39 -0
  46. package/dist/dataview/sortingComponents.d.ts.map +1 -0
  47. package/dist/dataview/stateStorage.d.ts +19 -0
  48. package/dist/dataview/stateStorage.d.ts.map +1 -0
  49. package/dist/dataview/useDataView.d.ts +58 -0
  50. package/dist/dataview/useDataView.d.ts.map +1 -0
  51. package/dist/dataview/useDataViewState.d.ts +89 -0
  52. package/dist/dataview/useDataViewState.d.ts.map +1 -0
  53. package/dist/hooks/BackendAdapterContext.d.ts +110 -0
  54. package/dist/hooks/BackendAdapterContext.d.ts.map +1 -0
  55. package/dist/hooks/ContemberBindxProvider.d.ts +67 -0
  56. package/dist/hooks/ContemberBindxProvider.d.ts.map +1 -0
  57. package/dist/hooks/createBindx.d.ts +39 -0
  58. package/dist/hooks/createBindx.d.ts.map +1 -0
  59. package/dist/hooks/index.d.ts +10 -0
  60. package/dist/hooks/index.d.ts.map +1 -0
  61. package/dist/hooks/useBindxEvents.d.ts +83 -0
  62. package/dist/hooks/useBindxEvents.d.ts.map +1 -0
  63. package/dist/hooks/useEntity.d.ts +123 -0
  64. package/dist/hooks/useEntity.d.ts.map +1 -0
  65. package/dist/hooks/useEntityBeforePersist.d.ts +22 -0
  66. package/dist/hooks/useEntityBeforePersist.d.ts.map +1 -0
  67. package/dist/hooks/useEntityCore.d.ts +52 -0
  68. package/dist/hooks/useEntityCore.d.ts.map +1 -0
  69. package/dist/hooks/useEntityImpl.d.ts +105 -0
  70. package/dist/hooks/useEntityImpl.d.ts.map +1 -0
  71. package/dist/hooks/useEntityList.d.ts +102 -0
  72. package/dist/hooks/useEntityList.d.ts.map +1 -0
  73. package/dist/hooks/useEntityListCore.d.ts +49 -0
  74. package/dist/hooks/useEntityListCore.d.ts.map +1 -0
  75. package/dist/hooks/useEntityListImpl.d.ts +70 -0
  76. package/dist/hooks/useEntityListImpl.d.ts.map +1 -0
  77. package/dist/hooks/useErrors.d.ts +72 -0
  78. package/dist/hooks/useErrors.d.ts.map +1 -0
  79. package/dist/hooks/usePersist.d.ts +115 -0
  80. package/dist/hooks/usePersist.d.ts.map +1 -0
  81. package/dist/hooks/useSelectionCollection.d.ts +41 -0
  82. package/dist/hooks/useSelectionCollection.d.ts.map +1 -0
  83. package/dist/hooks/useSelectionCollectionForList.d.ts +40 -0
  84. package/dist/hooks/useSelectionCollectionForList.d.ts.map +1 -0
  85. package/dist/hooks/useStableSelectionMeta.d.ts +11 -0
  86. package/dist/hooks/useStableSelectionMeta.d.ts.map +1 -0
  87. package/dist/hooks/useStoreSubscription.d.ts +19 -0
  88. package/dist/hooks/useStoreSubscription.d.ts.map +1 -0
  89. package/dist/hooks/useUndo.d.ts +45 -0
  90. package/dist/hooks/useUndo.d.ts.map +1 -0
  91. package/dist/index.d.ts +17 -0
  92. package/dist/index.d.ts.map +1 -0
  93. package/dist/jsx/SelectionMeta.d.ts +2 -0
  94. package/dist/jsx/SelectionMeta.d.ts.map +1 -0
  95. package/dist/jsx/analyzer.d.ts +22 -0
  96. package/dist/jsx/analyzer.d.ts.map +1 -0
  97. package/dist/jsx/collectorProxy.d.ts +14 -0
  98. package/dist/jsx/collectorProxy.d.ts.map +1 -0
  99. package/dist/jsx/componentBuilder.d.ts +51 -0
  100. package/dist/jsx/componentBuilder.d.ts.map +1 -0
  101. package/dist/jsx/componentBuilder.types.d.ts +268 -0
  102. package/dist/jsx/componentBuilder.types.d.ts.map +1 -0
  103. package/dist/jsx/componentBuilderCompat.d.ts +17 -0
  104. package/dist/jsx/componentBuilderCompat.d.ts.map +1 -0
  105. package/dist/jsx/componentFactory.d.ts +39 -0
  106. package/dist/jsx/componentFactory.d.ts.map +1 -0
  107. package/dist/jsx/components/Entity.d.ts +82 -0
  108. package/dist/jsx/components/Entity.d.ts.map +1 -0
  109. package/dist/jsx/components/EntityList.d.ts +48 -0
  110. package/dist/jsx/components/EntityList.d.ts.map +1 -0
  111. package/dist/jsx/components/Field.d.ts +27 -0
  112. package/dist/jsx/components/Field.d.ts.map +1 -0
  113. package/dist/jsx/components/HasMany.d.ts +34 -0
  114. package/dist/jsx/components/HasMany.d.ts.map +1 -0
  115. package/dist/jsx/components/HasOne.d.ts +25 -0
  116. package/dist/jsx/components/HasOne.d.ts.map +1 -0
  117. package/dist/jsx/components/HasRole.d.ts +51 -0
  118. package/dist/jsx/components/HasRole.d.ts.map +1 -0
  119. package/dist/jsx/components/If.d.ts +51 -0
  120. package/dist/jsx/components/If.d.ts.map +1 -0
  121. package/dist/jsx/components/Show.d.ts +33 -0
  122. package/dist/jsx/components/Show.d.ts.map +1 -0
  123. package/dist/jsx/components/index.d.ts +8 -0
  124. package/dist/jsx/components/index.d.ts.map +1 -0
  125. package/dist/jsx/conditions.d.ts +310 -0
  126. package/dist/jsx/conditions.d.ts.map +1 -0
  127. package/dist/jsx/createComponent.d.ts +44 -0
  128. package/dist/jsx/createComponent.d.ts.map +1 -0
  129. package/dist/jsx/index.d.ts +19 -0
  130. package/dist/jsx/index.d.ts.map +1 -0
  131. package/dist/jsx/inlineProxy.d.ts +14 -0
  132. package/dist/jsx/inlineProxy.d.ts.map +1 -0
  133. package/dist/jsx/legacyTypes.d.ts +26 -0
  134. package/dist/jsx/legacyTypes.d.ts.map +1 -0
  135. package/dist/jsx/proxy.d.ts +2 -0
  136. package/dist/jsx/proxy.d.ts.map +1 -0
  137. package/dist/jsx/proxyShared.d.ts +14 -0
  138. package/dist/jsx/proxyShared.d.ts.map +1 -0
  139. package/dist/jsx/runtimeProxy.d.ts +11 -0
  140. package/dist/jsx/runtimeProxy.d.ts.map +1 -0
  141. package/dist/jsx/standaloneCreateComponent.d.ts +30 -0
  142. package/dist/jsx/standaloneCreateComponent.d.ts.map +1 -0
  143. package/dist/jsx/types.d.ts +109 -0
  144. package/dist/jsx/types.d.ts.map +1 -0
  145. package/dist/jsx/withCollector.d.ts +39 -0
  146. package/dist/jsx/withCollector.d.ts.map +1 -0
  147. package/dist/roles/RoleContext.d.ts +31 -0
  148. package/dist/roles/RoleContext.d.ts.map +1 -0
  149. package/dist/roles/index.d.ts +2 -0
  150. package/dist/roles/index.d.ts.map +1 -0
  151. package/package.json +34 -0
  152. package/src/batching/QueryBatcher.ts +114 -0
  153. package/src/batching/index.ts +1 -0
  154. package/src/components/EntityScope.tsx +43 -0
  155. package/src/hooks/BackendAdapterContext.tsx +214 -0
  156. package/src/hooks/ContemberBindxProvider.tsx +158 -0
  157. package/src/hooks/index.ts +61 -0
  158. package/src/hooks/useBindxEvents.ts +229 -0
  159. package/src/hooks/useEntity.ts +489 -0
  160. package/src/hooks/useEntityBeforePersist.ts +32 -0
  161. package/src/hooks/useEntityList.ts +444 -0
  162. package/src/hooks/useErrors.ts +150 -0
  163. package/src/hooks/usePersist.ts +377 -0
  164. package/src/hooks/useSelectionCollection.ts +122 -0
  165. package/src/hooks/useStoreSubscription.ts +51 -0
  166. package/src/hooks/useUndo.ts +83 -0
  167. package/src/index.ts +357 -0
  168. package/src/jsx/SelectionMeta.ts +2 -0
  169. package/src/jsx/analyzer.ts +261 -0
  170. package/src/jsx/collectorProxy.ts +282 -0
  171. package/src/jsx/componentBuilder.ts +180 -0
  172. package/src/jsx/componentBuilder.types.ts +463 -0
  173. package/src/jsx/componentBuilderCompat.ts +39 -0
  174. package/src/jsx/componentFactory.ts +400 -0
  175. package/src/jsx/components/Entity.tsx +383 -0
  176. package/src/jsx/components/EntityList.tsx +147 -0
  177. package/src/jsx/components/Field.tsx +79 -0
  178. package/src/jsx/components/HasMany.tsx +109 -0
  179. package/src/jsx/components/HasOne.tsx +78 -0
  180. package/src/jsx/components/HasRole.tsx +95 -0
  181. package/src/jsx/components/If.tsx +127 -0
  182. package/src/jsx/components/Show.tsx +56 -0
  183. package/src/jsx/components/index.ts +20 -0
  184. package/src/jsx/conditions.ts +472 -0
  185. package/src/jsx/createComponent.ts +175 -0
  186. package/src/jsx/index.ts +135 -0
  187. package/src/jsx/legacyTypes.ts +30 -0
  188. package/src/jsx/proxy.ts +1 -0
  189. package/src/jsx/proxyShared.ts +42 -0
  190. package/src/jsx/standaloneCreateComponent.ts +47 -0
  191. package/src/jsx/types.ts +192 -0
  192. package/src/jsx/withCollector.ts +45 -0
  193. package/src/roles/RoleContext.tsx +57 -0
  194. 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'