@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,472 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Composable condition DSL for the <If> component.
|
|
3
|
+
*
|
|
4
|
+
* Provides a type-safe way to express conditions on FieldRef, HasManyRef, and HasOneRef
|
|
5
|
+
* that works correctly during collection phase. The condition builders ensure that
|
|
6
|
+
* referenced fields are collected for the GraphQL query, even when the condition
|
|
7
|
+
* evaluates to false during collection.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```tsx
|
|
11
|
+
* import { cond } from '@contember/bindx-react'
|
|
12
|
+
*
|
|
13
|
+
* // Simple conditions
|
|
14
|
+
* <If condition={cond.hasItems(task.developers)} then={...} />
|
|
15
|
+
* <If condition={cond.eq(task.status, 'active')} then={...} />
|
|
16
|
+
*
|
|
17
|
+
* // Complex conditions with AND/OR/NOT
|
|
18
|
+
* <If condition={cond.and(
|
|
19
|
+
* cond.hasItems(task.developers),
|
|
20
|
+
* cond.eq(task.status, 'active')
|
|
21
|
+
* )} then={...} />
|
|
22
|
+
*
|
|
23
|
+
* <If condition={cond.or(
|
|
24
|
+
* cond.isEmpty(task.developers),
|
|
25
|
+
* cond.not(cond.isConnected(task.project))
|
|
26
|
+
* )} then={...} />
|
|
27
|
+
* ```
|
|
28
|
+
*
|
|
29
|
+
* @packageDocumentation
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
import type {
|
|
33
|
+
FieldRef,
|
|
34
|
+
FieldRefBase,
|
|
35
|
+
HasManyRef,
|
|
36
|
+
HasManyRefBase,
|
|
37
|
+
HasOneRef,
|
|
38
|
+
HasOneRefBase,
|
|
39
|
+
} from '@contember/bindx'
|
|
40
|
+
|
|
41
|
+
// ============================================================================
|
|
42
|
+
// Symbols and Types
|
|
43
|
+
// ============================================================================
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Symbol to identify condition objects
|
|
47
|
+
*/
|
|
48
|
+
export const CONDITION_META = Symbol('CONDITION_META')
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Internal metadata for a condition.
|
|
52
|
+
*/
|
|
53
|
+
export interface ConditionMeta {
|
|
54
|
+
/** Condition type identifier for debugging */
|
|
55
|
+
readonly type: string
|
|
56
|
+
/** Fields to collect for selection (FieldRef/HasManyRef/HasOneRef) */
|
|
57
|
+
readonly fields: unknown[]
|
|
58
|
+
/** Evaluate condition at runtime */
|
|
59
|
+
readonly evaluate: () => boolean
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* A composable condition for the <If> component.
|
|
64
|
+
*
|
|
65
|
+
* Conditions can be combined using `cond.and()`, `cond.or()`, and `cond.not()`.
|
|
66
|
+
* All referenced fields are automatically collected for the GraphQL query.
|
|
67
|
+
*/
|
|
68
|
+
export interface Condition {
|
|
69
|
+
readonly [CONDITION_META]: ConditionMeta
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// ============================================================================
|
|
73
|
+
// Type Helpers
|
|
74
|
+
// ============================================================================
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Base type for scalar fields - accepts both FieldRef and FieldRefBase.
|
|
78
|
+
*/
|
|
79
|
+
type AnyScalarField<T> = FieldRefBase<T>
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Base type for has-many - accepts both HasManyRef and HasManyRefBase.
|
|
83
|
+
*/
|
|
84
|
+
type AnyHasManyField = HasManyRefBase<any, any, any, any, any>
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Base type for has-one - accepts both HasOneRef and HasOneRefBase.
|
|
88
|
+
*/
|
|
89
|
+
type AnyHasOneField = HasOneRefBase<any, any, any, any, any>
|
|
90
|
+
|
|
91
|
+
// ============================================================================
|
|
92
|
+
// Condition Builder Object
|
|
93
|
+
// ============================================================================
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Condition builder - single import for all condition operations.
|
|
97
|
+
*
|
|
98
|
+
* Use this to create conditions for the <If> component that work correctly
|
|
99
|
+
* during both collection and runtime phases.
|
|
100
|
+
*
|
|
101
|
+
* @example
|
|
102
|
+
* ```tsx
|
|
103
|
+
* import { cond } from '@contember/bindx-react'
|
|
104
|
+
*
|
|
105
|
+
* // Simple conditions
|
|
106
|
+
* <If condition={cond.hasItems(task.developers)} then={...} />
|
|
107
|
+
* <If condition={cond.eq(task.status, 'active')} then={...} />
|
|
108
|
+
*
|
|
109
|
+
* // Complex conditions with AND/OR/NOT
|
|
110
|
+
* <If condition={cond.and(
|
|
111
|
+
* cond.hasItems(task.developers),
|
|
112
|
+
* cond.eq(task.status, 'active')
|
|
113
|
+
* )} then={...} />
|
|
114
|
+
*
|
|
115
|
+
* <If condition={cond.or(
|
|
116
|
+
* cond.isEmpty(task.developers),
|
|
117
|
+
* cond.not(cond.isConnected(task.project))
|
|
118
|
+
* )} then={...} />
|
|
119
|
+
* ```
|
|
120
|
+
*/
|
|
121
|
+
export const cond = {
|
|
122
|
+
// ==================== Has-Many Conditions ====================
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Check if a has-many relation has items.
|
|
126
|
+
*
|
|
127
|
+
* @param field - The has-many field reference
|
|
128
|
+
* @returns A condition that is true when the relation has at least one item
|
|
129
|
+
*
|
|
130
|
+
* @example
|
|
131
|
+
* ```tsx
|
|
132
|
+
* <If condition={cond.hasItems(task.developers)} then={
|
|
133
|
+
* <HasMany field={task.developers}>{dev => <span>{dev.name.value}</span>}</HasMany>
|
|
134
|
+
* } />
|
|
135
|
+
* ```
|
|
136
|
+
*/
|
|
137
|
+
hasItems(field: AnyHasManyField): Condition {
|
|
138
|
+
return {
|
|
139
|
+
[CONDITION_META]: {
|
|
140
|
+
type: 'hasItems',
|
|
141
|
+
fields: [field],
|
|
142
|
+
evaluate: () => (field as HasManyRef<any>).length > 0,
|
|
143
|
+
},
|
|
144
|
+
}
|
|
145
|
+
},
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Check if a has-many relation is empty.
|
|
149
|
+
*
|
|
150
|
+
* @param field - The has-many field reference
|
|
151
|
+
* @returns A condition that is true when the relation has no items
|
|
152
|
+
*
|
|
153
|
+
* @example
|
|
154
|
+
* ```tsx
|
|
155
|
+
* <If condition={cond.isEmpty(task.developers)} then={
|
|
156
|
+
* <span>No developers assigned</span>
|
|
157
|
+
* } />
|
|
158
|
+
* ```
|
|
159
|
+
*/
|
|
160
|
+
isEmpty(field: AnyHasManyField): Condition {
|
|
161
|
+
return {
|
|
162
|
+
[CONDITION_META]: {
|
|
163
|
+
type: 'isEmpty',
|
|
164
|
+
fields: [field],
|
|
165
|
+
evaluate: () => (field as HasManyRef<any>).length === 0,
|
|
166
|
+
},
|
|
167
|
+
}
|
|
168
|
+
},
|
|
169
|
+
|
|
170
|
+
// ==================== Has-One Conditions ====================
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Check if a has-one relation is connected.
|
|
174
|
+
*
|
|
175
|
+
* @param field - The has-one field reference
|
|
176
|
+
* @returns A condition that is true when the relation points to an entity
|
|
177
|
+
*
|
|
178
|
+
* @example
|
|
179
|
+
* ```tsx
|
|
180
|
+
* <If condition={cond.isConnected(task.project)} then={
|
|
181
|
+
* <HasOne field={task.project}>{project => <span>{project.name.value}</span>}</HasOne>
|
|
182
|
+
* } />
|
|
183
|
+
* ```
|
|
184
|
+
*/
|
|
185
|
+
isConnected(field: AnyHasOneField): Condition {
|
|
186
|
+
return {
|
|
187
|
+
[CONDITION_META]: {
|
|
188
|
+
type: 'isConnected',
|
|
189
|
+
fields: [field],
|
|
190
|
+
evaluate: () => (field as HasOneRef<any>).$state === 'connected',
|
|
191
|
+
},
|
|
192
|
+
}
|
|
193
|
+
},
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Check if a has-one relation is disconnected.
|
|
197
|
+
*
|
|
198
|
+
* @param field - The has-one field reference
|
|
199
|
+
* @returns A condition that is true when the relation is not connected
|
|
200
|
+
*
|
|
201
|
+
* @example
|
|
202
|
+
* ```tsx
|
|
203
|
+
* <If condition={cond.isDisconnected(task.project)} then={
|
|
204
|
+
* <span>No project assigned</span>
|
|
205
|
+
* } />
|
|
206
|
+
* ```
|
|
207
|
+
*/
|
|
208
|
+
isDisconnected(field: AnyHasOneField): Condition {
|
|
209
|
+
return {
|
|
210
|
+
[CONDITION_META]: {
|
|
211
|
+
type: 'isDisconnected',
|
|
212
|
+
fields: [field],
|
|
213
|
+
evaluate: () => (field as HasOneRef<any>).$state === 'disconnected',
|
|
214
|
+
},
|
|
215
|
+
}
|
|
216
|
+
},
|
|
217
|
+
|
|
218
|
+
// ==================== Field Value Conditions ====================
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Check if a field equals a value.
|
|
222
|
+
*
|
|
223
|
+
* @param field - The field reference
|
|
224
|
+
* @param value - The value to compare against
|
|
225
|
+
* @returns A condition that is true when the field value equals the given value
|
|
226
|
+
*
|
|
227
|
+
* @example
|
|
228
|
+
* ```tsx
|
|
229
|
+
* <If condition={cond.eq(task.status, 'completed')} then={
|
|
230
|
+
* <span className="text-green-500">Done!</span>
|
|
231
|
+
* } />
|
|
232
|
+
* ```
|
|
233
|
+
*/
|
|
234
|
+
eq<T>(field: AnyScalarField<T>, value: T): Condition {
|
|
235
|
+
return {
|
|
236
|
+
[CONDITION_META]: {
|
|
237
|
+
type: 'eq',
|
|
238
|
+
fields: [field],
|
|
239
|
+
evaluate: () => (field as FieldRef<T>).value === value,
|
|
240
|
+
},
|
|
241
|
+
}
|
|
242
|
+
},
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Check if a field is not equal to a value.
|
|
246
|
+
*
|
|
247
|
+
* @param field - The field reference
|
|
248
|
+
* @param value - The value to compare against
|
|
249
|
+
* @returns A condition that is true when the field value does not equal the given value
|
|
250
|
+
*
|
|
251
|
+
* @example
|
|
252
|
+
* ```tsx
|
|
253
|
+
* <If condition={cond.neq(task.status, 'cancelled')} then={
|
|
254
|
+
* <TaskActions task={task} />
|
|
255
|
+
* } />
|
|
256
|
+
* ```
|
|
257
|
+
*/
|
|
258
|
+
neq<T>(field: AnyScalarField<T>, value: T): Condition {
|
|
259
|
+
return {
|
|
260
|
+
[CONDITION_META]: {
|
|
261
|
+
type: 'neq',
|
|
262
|
+
fields: [field],
|
|
263
|
+
evaluate: () => (field as FieldRef<T>).value !== value,
|
|
264
|
+
},
|
|
265
|
+
}
|
|
266
|
+
},
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Check if a field is null.
|
|
270
|
+
*
|
|
271
|
+
* @param field - The field reference
|
|
272
|
+
* @returns A condition that is true when the field value is null
|
|
273
|
+
*
|
|
274
|
+
* @example
|
|
275
|
+
* ```tsx
|
|
276
|
+
* <If condition={cond.isNull(task.completedAt)} then={
|
|
277
|
+
* <span>In progress</span>
|
|
278
|
+
* } />
|
|
279
|
+
* ```
|
|
280
|
+
*/
|
|
281
|
+
isNull<T>(field: AnyScalarField<T>): Condition {
|
|
282
|
+
return {
|
|
283
|
+
[CONDITION_META]: {
|
|
284
|
+
type: 'isNull',
|
|
285
|
+
fields: [field],
|
|
286
|
+
evaluate: () => (field as FieldRef<T>).value === null,
|
|
287
|
+
},
|
|
288
|
+
}
|
|
289
|
+
},
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Check if a field is not null.
|
|
293
|
+
*
|
|
294
|
+
* @param field - The field reference
|
|
295
|
+
* @returns A condition that is true when the field value is not null
|
|
296
|
+
*
|
|
297
|
+
* @example
|
|
298
|
+
* ```tsx
|
|
299
|
+
* <If condition={cond.isNotNull(task.completedAt)} then={
|
|
300
|
+
* <span>Completed: {task.completedAt.value}</span>
|
|
301
|
+
* } />
|
|
302
|
+
* ```
|
|
303
|
+
*/
|
|
304
|
+
isNotNull<T>(field: AnyScalarField<T>): Condition {
|
|
305
|
+
return {
|
|
306
|
+
[CONDITION_META]: {
|
|
307
|
+
type: 'isNotNull',
|
|
308
|
+
fields: [field],
|
|
309
|
+
evaluate: () => (field as FieldRef<T>).value !== null,
|
|
310
|
+
},
|
|
311
|
+
}
|
|
312
|
+
},
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Check if a field value is truthy.
|
|
316
|
+
*
|
|
317
|
+
* @param field - The field reference (typically boolean)
|
|
318
|
+
* @returns A condition that is true when the field value is truthy
|
|
319
|
+
*
|
|
320
|
+
* @example
|
|
321
|
+
* ```tsx
|
|
322
|
+
* <If condition={cond.isTruthy(task.isUrgent)} then={
|
|
323
|
+
* <Badge variant="destructive">Urgent!</Badge>
|
|
324
|
+
* } />
|
|
325
|
+
* ```
|
|
326
|
+
*/
|
|
327
|
+
isTruthy<T>(field: AnyScalarField<T>): Condition {
|
|
328
|
+
return {
|
|
329
|
+
[CONDITION_META]: {
|
|
330
|
+
type: 'isTruthy',
|
|
331
|
+
fields: [field],
|
|
332
|
+
evaluate: () => Boolean((field as FieldRef<T>).value),
|
|
333
|
+
},
|
|
334
|
+
}
|
|
335
|
+
},
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Check if a field value is falsy.
|
|
339
|
+
*
|
|
340
|
+
* @param field - The field reference (typically boolean)
|
|
341
|
+
* @returns A condition that is true when the field value is falsy
|
|
342
|
+
*
|
|
343
|
+
* @example
|
|
344
|
+
* ```tsx
|
|
345
|
+
* <If condition={cond.isFalsy(task.isArchived)} then={
|
|
346
|
+
* <TaskActions task={task} />
|
|
347
|
+
* } />
|
|
348
|
+
* ```
|
|
349
|
+
*/
|
|
350
|
+
isFalsy<T>(field: AnyScalarField<T>): Condition {
|
|
351
|
+
return {
|
|
352
|
+
[CONDITION_META]: {
|
|
353
|
+
type: 'isFalsy',
|
|
354
|
+
fields: [field],
|
|
355
|
+
evaluate: () => !(field as FieldRef<T>).value,
|
|
356
|
+
},
|
|
357
|
+
}
|
|
358
|
+
},
|
|
359
|
+
|
|
360
|
+
// ==================== Logical Combinators ====================
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* Logical AND - all conditions must be true.
|
|
364
|
+
*
|
|
365
|
+
* @param conditions - Conditions to combine
|
|
366
|
+
* @returns A condition that is true when all given conditions are true
|
|
367
|
+
*
|
|
368
|
+
* @example
|
|
369
|
+
* ```tsx
|
|
370
|
+
* <If condition={cond.and(
|
|
371
|
+
* cond.hasItems(task.developers),
|
|
372
|
+
* cond.eq(task.status, 'active'),
|
|
373
|
+
* cond.isConnected(task.project)
|
|
374
|
+
* )} then={
|
|
375
|
+
* <span>Task is ready</span>
|
|
376
|
+
* } />
|
|
377
|
+
* ```
|
|
378
|
+
*/
|
|
379
|
+
and(...conditions: Condition[]): Condition {
|
|
380
|
+
return {
|
|
381
|
+
[CONDITION_META]: {
|
|
382
|
+
type: 'and',
|
|
383
|
+
fields: conditions.flatMap(c => c[CONDITION_META].fields),
|
|
384
|
+
evaluate: () => conditions.every(c => c[CONDITION_META].evaluate()),
|
|
385
|
+
},
|
|
386
|
+
}
|
|
387
|
+
},
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* Logical OR - at least one condition must be true.
|
|
391
|
+
*
|
|
392
|
+
* @param conditions - Conditions to combine
|
|
393
|
+
* @returns A condition that is true when at least one given condition is true
|
|
394
|
+
*
|
|
395
|
+
* @example
|
|
396
|
+
* ```tsx
|
|
397
|
+
* <If condition={cond.or(
|
|
398
|
+
* cond.eq(task.status, 'completed'),
|
|
399
|
+
* cond.eq(task.status, 'cancelled')
|
|
400
|
+
* )} then={
|
|
401
|
+
* <span>Task is finished</span>
|
|
402
|
+
* } />
|
|
403
|
+
* ```
|
|
404
|
+
*/
|
|
405
|
+
or(...conditions: Condition[]): Condition {
|
|
406
|
+
return {
|
|
407
|
+
[CONDITION_META]: {
|
|
408
|
+
type: 'or',
|
|
409
|
+
fields: conditions.flatMap(c => c[CONDITION_META].fields),
|
|
410
|
+
evaluate: () => conditions.some(c => c[CONDITION_META].evaluate()),
|
|
411
|
+
},
|
|
412
|
+
}
|
|
413
|
+
},
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* Logical NOT - negate a condition.
|
|
417
|
+
*
|
|
418
|
+
* @param condition - Condition to negate
|
|
419
|
+
* @returns A condition that is true when the given condition is false
|
|
420
|
+
*
|
|
421
|
+
* @example
|
|
422
|
+
* ```tsx
|
|
423
|
+
* <If condition={cond.not(cond.isConnected(task.project))} then={
|
|
424
|
+
* <span>No project assigned</span>
|
|
425
|
+
* } />
|
|
426
|
+
* ```
|
|
427
|
+
*/
|
|
428
|
+
not(condition: Condition): Condition {
|
|
429
|
+
return {
|
|
430
|
+
[CONDITION_META]: {
|
|
431
|
+
type: 'not',
|
|
432
|
+
fields: condition[CONDITION_META].fields,
|
|
433
|
+
evaluate: () => !condition[CONDITION_META].evaluate(),
|
|
434
|
+
},
|
|
435
|
+
}
|
|
436
|
+
},
|
|
437
|
+
} as const
|
|
438
|
+
|
|
439
|
+
// ============================================================================
|
|
440
|
+
// Helper Functions
|
|
441
|
+
// ============================================================================
|
|
442
|
+
|
|
443
|
+
/**
|
|
444
|
+
* Check if a value is a Condition object.
|
|
445
|
+
*
|
|
446
|
+
* @param value - Value to check
|
|
447
|
+
* @returns True if the value is a Condition
|
|
448
|
+
*/
|
|
449
|
+
export function isCondition(value: unknown): value is Condition {
|
|
450
|
+
return typeof value === 'object' && value !== null && CONDITION_META in value
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/**
|
|
454
|
+
* Collect all field references from a condition.
|
|
455
|
+
* Used by the selection collector to ensure all fields are included in the query.
|
|
456
|
+
*
|
|
457
|
+
* @param condition - The condition to collect fields from
|
|
458
|
+
* @returns Array of field references
|
|
459
|
+
*/
|
|
460
|
+
export function collectConditionFields(condition: Condition): unknown[] {
|
|
461
|
+
return condition[CONDITION_META].fields
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
/**
|
|
465
|
+
* Evaluate a condition at runtime.
|
|
466
|
+
*
|
|
467
|
+
* @param condition - The condition to evaluate
|
|
468
|
+
* @returns The boolean result of the condition
|
|
469
|
+
*/
|
|
470
|
+
export function evaluateCondition(condition: Condition): boolean {
|
|
471
|
+
return condition[CONDITION_META].evaluate()
|
|
472
|
+
}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Component creation utilities for bindx.
|
|
3
|
+
*
|
|
4
|
+
* The main createComponent function is now provided by createBindx() using the builder API.
|
|
5
|
+
* This module re-exports builder utilities and provides helper functions.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { FluentFragment, AnyBrand } from '@contember/bindx'
|
|
9
|
+
import { ComponentBrand } from '@contember/bindx'
|
|
10
|
+
import { SelectionMetaCollector, mergeSelections } from './SelectionMeta.js'
|
|
11
|
+
import { COMPONENT_BRAND } from './componentBuilder.js'
|
|
12
|
+
|
|
13
|
+
// Re-export from builder module
|
|
14
|
+
export {
|
|
15
|
+
COMPONENT_MARKER,
|
|
16
|
+
COMPONENT_BRAND,
|
|
17
|
+
COMPONENT_SELECTIONS,
|
|
18
|
+
ComponentBuilderImpl,
|
|
19
|
+
createComponentBuilder,
|
|
20
|
+
isBindxComponent,
|
|
21
|
+
getComponentBrand,
|
|
22
|
+
} from './componentBuilder.js'
|
|
23
|
+
|
|
24
|
+
export type {
|
|
25
|
+
SelectionPropMeta,
|
|
26
|
+
BindxComponentBase,
|
|
27
|
+
BindxComponent,
|
|
28
|
+
ComponentBuilder,
|
|
29
|
+
ComponentBuilderState,
|
|
30
|
+
CreateComponentOptions,
|
|
31
|
+
// Interface types
|
|
32
|
+
InterfaceEntityPropConfig,
|
|
33
|
+
ImplicitInterfaceEntityConfig,
|
|
34
|
+
ExplicitInterfaceEntityConfig,
|
|
35
|
+
AddInterfaces,
|
|
36
|
+
InterfaceSelectorsMap,
|
|
37
|
+
AnyEntityPropConfig,
|
|
38
|
+
// Entity prop config types
|
|
39
|
+
EntityPropConfig,
|
|
40
|
+
ImplicitEntityConfig,
|
|
41
|
+
ExplicitEntityConfig,
|
|
42
|
+
// State helpers
|
|
43
|
+
AddImplicitEntity,
|
|
44
|
+
AddExplicitEntity,
|
|
45
|
+
AddImplicitInterfaceEntity,
|
|
46
|
+
AddExplicitInterfaceEntity,
|
|
47
|
+
SetScalarProps,
|
|
48
|
+
// Props building
|
|
49
|
+
BuildEntityProps,
|
|
50
|
+
BuildProps,
|
|
51
|
+
BuildFragmentProps,
|
|
52
|
+
InitialBuilderState,
|
|
53
|
+
} from './componentBuilder.types.js'
|
|
54
|
+
|
|
55
|
+
// ============================================================================
|
|
56
|
+
// Legacy exports for backwards compatibility
|
|
57
|
+
// ============================================================================
|
|
58
|
+
|
|
59
|
+
// These are kept for backwards compatibility with existing code
|
|
60
|
+
// that imports from createComponent.ts
|
|
61
|
+
|
|
62
|
+
export { assignFragmentProperties } from './componentBuilderCompat.js'
|
|
63
|
+
|
|
64
|
+
// ============================================================================
|
|
65
|
+
// Brand Validation
|
|
66
|
+
// ============================================================================
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Flag to enable/disable brand validation.
|
|
70
|
+
* Set to false in production for performance.
|
|
71
|
+
*/
|
|
72
|
+
let brandValidationEnabled = true
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Enables or disables brand validation.
|
|
76
|
+
* Call with false in production to disable warnings.
|
|
77
|
+
*/
|
|
78
|
+
export function setBrandValidation(enabled: boolean): void {
|
|
79
|
+
brandValidationEnabled = enabled
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Validates that an EntityRef has the required component brand.
|
|
84
|
+
* Logs a warning in development mode if the brand is missing.
|
|
85
|
+
*
|
|
86
|
+
* @param entityRef - The EntityRef to validate
|
|
87
|
+
* @param component - The component that requires the brand
|
|
88
|
+
* @param propName - The prop name for error messages
|
|
89
|
+
*/
|
|
90
|
+
export function validateBrand(
|
|
91
|
+
entityRef: { __brands?: Set<symbol> } | null | undefined,
|
|
92
|
+
component: { [key: symbol]: ComponentBrand | undefined },
|
|
93
|
+
propName: string,
|
|
94
|
+
): void {
|
|
95
|
+
if (!brandValidationEnabled || !entityRef) {
|
|
96
|
+
return
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const componentBrand = component[COMPONENT_BRAND] as ComponentBrand | undefined
|
|
100
|
+
if (!componentBrand) {
|
|
101
|
+
return
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const entityBrands = entityRef.__brands
|
|
105
|
+
if (!entityBrands) {
|
|
106
|
+
// EntityRef doesn't have brands yet (might be from legacy code)
|
|
107
|
+
return
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (!entityBrands.has(componentBrand.brandSymbol)) {
|
|
111
|
+
console.warn(
|
|
112
|
+
`[bindx] EntityRef passed to "${propName}" is missing the required component brand. ` +
|
|
113
|
+
`Use mergeFragments() to combine fragment selections when using multiple components. ` +
|
|
114
|
+
`Expected brand: ${componentBrand.name}`,
|
|
115
|
+
)
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// ============================================================================
|
|
120
|
+
// Fragment Merging
|
|
121
|
+
// ============================================================================
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Merges multiple fragment selections into one.
|
|
125
|
+
* Combines both the selection metadata and brand symbols.
|
|
126
|
+
*
|
|
127
|
+
* @example
|
|
128
|
+
* ```tsx
|
|
129
|
+
* const article = useEntity('Article', { id }, e =>
|
|
130
|
+
* e.author(mergeFragments(AuthorInfo.$author, AuthorCard.$author))
|
|
131
|
+
* )
|
|
132
|
+
* ```
|
|
133
|
+
*/
|
|
134
|
+
export function mergeFragments<T, R1 extends object, B1 extends AnyBrand>(
|
|
135
|
+
fragment1: FluentFragment<T, R1, B1>,
|
|
136
|
+
): FluentFragment<T, R1, B1>
|
|
137
|
+
export function mergeFragments<T, R1 extends object, R2 extends object, B1 extends AnyBrand, B2 extends AnyBrand>(
|
|
138
|
+
fragment1: FluentFragment<T, R1, B1>,
|
|
139
|
+
fragment2: FluentFragment<T, R2, B2>,
|
|
140
|
+
): FluentFragment<T, R1 & R2, B1 | B2>
|
|
141
|
+
export function mergeFragments<T, R1 extends object, R2 extends object, R3 extends object, B1 extends AnyBrand, B2 extends AnyBrand, B3 extends AnyBrand>(
|
|
142
|
+
fragment1: FluentFragment<T, R1, B1>,
|
|
143
|
+
fragment2: FluentFragment<T, R2, B2>,
|
|
144
|
+
fragment3: FluentFragment<T, R3, B3>,
|
|
145
|
+
): FluentFragment<T, R1 & R2 & R3, B1 | B2 | B3>
|
|
146
|
+
export function mergeFragments<T>(
|
|
147
|
+
...fragments: FluentFragment<T, object, AnyBrand>[]
|
|
148
|
+
): FluentFragment<T, object, AnyBrand> {
|
|
149
|
+
if (fragments.length === 0) {
|
|
150
|
+
throw new Error('mergeFragments requires at least one fragment')
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (fragments.length === 1) {
|
|
154
|
+
return fragments[0]!
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const mergedSelection = new SelectionMetaCollector()
|
|
158
|
+
const mergedBrands = new Set<symbol>()
|
|
159
|
+
|
|
160
|
+
for (const fragment of fragments) {
|
|
161
|
+
mergeSelections(mergedSelection, fragment.__meta)
|
|
162
|
+
// Merge brand symbols from all fragments
|
|
163
|
+
if (fragment.__brands) {
|
|
164
|
+
fragment.__brands.forEach(b => mergedBrands.add(b))
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return {
|
|
169
|
+
__meta: mergedSelection,
|
|
170
|
+
__resultType: {} as object,
|
|
171
|
+
__modelType: undefined as unknown as T,
|
|
172
|
+
__isFragment: true,
|
|
173
|
+
__brands: mergedBrands,
|
|
174
|
+
}
|
|
175
|
+
}
|