@contember/bindx-dataview 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/DataGrid.d.ts +38 -0
- package/dist/DataGrid.d.ts.map +1 -0
- package/dist/DataViewContext.d.ts +39 -0
- package/dist/DataViewContext.d.ts.map +1 -0
- package/dist/DataViewKeyProvider.d.ts +8 -0
- package/dist/DataViewKeyProvider.d.ts.map +1 -0
- package/dist/HasManyDataGrid.d.ts +51 -0
- package/dist/HasManyDataGrid.d.ts.map +1 -0
- package/dist/columnLeaf.d.ts +72 -0
- package/dist/columnLeaf.d.ts.map +1 -0
- package/dist/columnTypes.d.ts +35 -0
- package/dist/columnTypes.d.ts.map +1 -0
- package/dist/columns.d.ts +115 -0
- package/dist/columns.d.ts.map +1 -0
- package/dist/contextHooks.d.ts +45 -0
- package/dist/contextHooks.d.ts.map +1 -0
- package/dist/createColumn.d.ts +37 -0
- package/dist/createColumn.d.ts.map +1 -0
- package/dist/createRelationColumn.d.ts +57 -0
- package/dist/createRelationColumn.d.ts.map +1 -0
- package/dist/dataAttribute.d.ts +6 -0
- package/dist/dataAttribute.d.ts.map +1 -0
- package/dist/export.d.ts +46 -0
- package/dist/export.d.ts.map +1 -0
- package/dist/filterComponents.d.ts +127 -0
- package/dist/filterComponents.d.ts.map +1 -0
- package/dist/filterContext.d.ts +10 -0
- package/dist/filterContext.d.ts.map +1 -0
- package/dist/filterHooks.d.ts +74 -0
- package/dist/filterHooks.d.ts.map +1 -0
- package/dist/filterScope.d.ts +54 -0
- package/dist/filterScope.d.ts.map +1 -0
- package/dist/filterWrappers.d.ts +66 -0
- package/dist/filterWrappers.d.ts.map +1 -0
- package/dist/index.d.ts +28 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/infiniteLoad.d.ts +32 -0
- package/dist/infiniteLoad.d.ts.map +1 -0
- package/dist/loaderComponents.d.ts +75 -0
- package/dist/loaderComponents.d.ts.map +1 -0
- package/dist/markers.d.ts +36 -0
- package/dist/markers.d.ts.map +1 -0
- package/dist/pagingComponents.d.ts +50 -0
- package/dist/pagingComponents.d.ts.map +1 -0
- package/dist/renderCellValue.d.ts +9 -0
- package/dist/renderCellValue.d.ts.map +1 -0
- package/dist/select/MultiSelect.d.ts +27 -0
- package/dist/select/MultiSelect.d.ts.map +1 -0
- package/dist/select/Select.d.ts +27 -0
- package/dist/select/Select.d.ts.map +1 -0
- package/dist/select/SelectDataView.d.ts +46 -0
- package/dist/select/SelectDataView.d.ts.map +1 -0
- package/dist/select/SelectEachValue.d.ts +20 -0
- package/dist/select/SelectEachValue.d.ts.map +1 -0
- package/dist/select/SelectItemTrigger.d.ts +25 -0
- package/dist/select/SelectItemTrigger.d.ts.map +1 -0
- package/dist/select/SelectOption.d.ts +23 -0
- package/dist/select/SelectOption.d.ts.map +1 -0
- package/dist/select/SelectPlaceholder.d.ts +16 -0
- package/dist/select/SelectPlaceholder.d.ts.map +1 -0
- package/dist/select/index.d.ts +9 -0
- package/dist/select/index.d.ts.map +1 -0
- package/dist/select/selectContext.d.ts +20 -0
- package/dist/select/selectContext.d.ts.map +1 -0
- package/dist/selectionComponents.d.ts +62 -0
- package/dist/selectionComponents.d.ts.map +1 -0
- package/dist/sortingComponents.d.ts +39 -0
- package/dist/sortingComponents.d.ts.map +1 -0
- package/dist/stateStorage.d.ts +20 -0
- package/dist/stateStorage.d.ts.map +1 -0
- package/dist/useDataGridSetup.d.ts +53 -0
- package/dist/useDataGridSetup.d.ts.map +1 -0
- package/dist/useDataView.d.ts +57 -0
- package/dist/useDataView.d.ts.map +1 -0
- package/dist/useDataViewState.d.ts +89 -0
- package/dist/useDataViewState.d.ts.map +1 -0
- package/package.json +33 -0
- package/src/DataGrid.tsx +173 -0
- package/src/DataViewContext.tsx +56 -0
- package/src/DataViewKeyProvider.tsx +16 -0
- package/src/HasManyDataGrid.tsx +319 -0
- package/src/columnLeaf.ts +148 -0
- package/src/columnTypes.ts +160 -0
- package/src/columns.tsx +306 -0
- package/src/contextHooks.ts +157 -0
- package/src/createColumn.ts +128 -0
- package/src/createRelationColumn.tsx +203 -0
- package/src/dataAttribute.ts +7 -0
- package/src/export.tsx +219 -0
- package/src/filterComponents.tsx +617 -0
- package/src/filterContext.ts +27 -0
- package/src/filterHooks.ts +386 -0
- package/src/filterScope.tsx +89 -0
- package/src/filterWrappers.tsx +187 -0
- package/src/index.ts +356 -0
- package/src/infiniteLoad.tsx +188 -0
- package/src/loaderComponents.tsx +239 -0
- package/src/markers.ts +54 -0
- package/src/pagingComponents.tsx +149 -0
- package/src/select/MultiSelect.tsx +88 -0
- package/src/select/Select.tsx +88 -0
- package/src/select/SelectDataView.tsx +231 -0
- package/src/select/SelectEachValue.tsx +34 -0
- package/src/select/SelectItemTrigger.tsx +51 -0
- package/src/select/SelectOption.tsx +40 -0
- package/src/select/SelectPlaceholder.tsx +22 -0
- package/src/select/index.ts +17 -0
- package/src/select/selectContext.ts +82 -0
- package/src/selectionComponents.tsx +160 -0
- package/src/sortingComponents.tsx +104 -0
- package/src/stateStorage.ts +188 -0
- package/src/useDataGridSetup.ts +230 -0
- package/src/useDataView.ts +148 -0
- package/src/useDataViewState.ts +404 -0
|
@@ -0,0 +1,386 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Filter hooks — typed state access and action hooks for filter components.
|
|
3
|
+
*
|
|
4
|
+
* These hooks match the Contember react-dataview API:
|
|
5
|
+
* - useDataViewFilter: generic [state, setFilter, meta] tuple
|
|
6
|
+
* - useDataViewBooleanFilter: [current, set] with action types
|
|
7
|
+
* - useDataViewEnumFilter: [current, set] with include/exclude/toggle actions
|
|
8
|
+
* - useDataViewRelationFilter: [current, set] with include/exclude/toggle actions
|
|
9
|
+
* - useDataViewTextFilterInput: { value, onChange } with debounce
|
|
10
|
+
* - useDataViewTextFilterMatchMode: [isCurrent, set]
|
|
11
|
+
* - useDataViewNullFilter: [state, set] with include/exclude/toggle actions
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { type ChangeEvent, type SetStateAction, useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
|
15
|
+
import { useDataViewContext } from './DataViewContext.js'
|
|
16
|
+
import type {
|
|
17
|
+
FilterArtifact,
|
|
18
|
+
TextFilterArtifact,
|
|
19
|
+
BooleanFilterArtifact,
|
|
20
|
+
EnumFilterArtifact,
|
|
21
|
+
RelationFilterArtifact,
|
|
22
|
+
NumberRangeFilterArtifact,
|
|
23
|
+
} from '@contember/bindx'
|
|
24
|
+
|
|
25
|
+
// ============================================================================
|
|
26
|
+
// Generic useDataViewFilter — matches Contember's [state, setFilter, meta] API
|
|
27
|
+
// ============================================================================
|
|
28
|
+
|
|
29
|
+
export type UseDataViewFilterResult<T extends FilterArtifact> = [
|
|
30
|
+
state: T | undefined,
|
|
31
|
+
setFilter: (filter: SetStateAction<T | undefined>) => void,
|
|
32
|
+
meta: { isEmpty?: boolean },
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
export function useDataViewFilter<T extends FilterArtifact>(key: string): UseDataViewFilterResult<T> {
|
|
36
|
+
const { filtering } = useDataViewContext()
|
|
37
|
+
const state = filtering.getArtifact(key) as T | undefined
|
|
38
|
+
const filter = filtering.filters.get(key)
|
|
39
|
+
const isEmpty = state ? !filter?.handler.isActive(state) : true
|
|
40
|
+
|
|
41
|
+
const setFilter = useCallback(
|
|
42
|
+
(filterOrUpdater: SetStateAction<T | undefined>): void => {
|
|
43
|
+
if (typeof filterOrUpdater === 'function') {
|
|
44
|
+
const current = filtering.getArtifact(key) as T | undefined
|
|
45
|
+
const next = (filterOrUpdater as (prev: T | undefined) => T | undefined)(current)
|
|
46
|
+
if (next !== undefined) {
|
|
47
|
+
filtering.setArtifact(key, next)
|
|
48
|
+
} else {
|
|
49
|
+
filtering.resetFilter(key)
|
|
50
|
+
}
|
|
51
|
+
} else if (filterOrUpdater !== undefined) {
|
|
52
|
+
filtering.setArtifact(key, filterOrUpdater)
|
|
53
|
+
} else {
|
|
54
|
+
filtering.resetFilter(key)
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
[filtering, key],
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
return [state, setFilter, { isEmpty }]
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// ============================================================================
|
|
64
|
+
// Boolean Filter Hook
|
|
65
|
+
// ============================================================================
|
|
66
|
+
|
|
67
|
+
export type DataViewSetBooleanFilterAction = 'include' | 'unset' | 'toggle'
|
|
68
|
+
export type DataViewBooleanFilterCurrent = 'include' | 'none'
|
|
69
|
+
export type UseDataViewBooleanFilterResult = [
|
|
70
|
+
current: DataViewBooleanFilterCurrent,
|
|
71
|
+
set: (action: DataViewSetBooleanFilterAction) => void,
|
|
72
|
+
]
|
|
73
|
+
|
|
74
|
+
export function useDataViewBooleanFilter(name: string, value: boolean): UseDataViewBooleanFilterResult {
|
|
75
|
+
const factory = useDataViewBooleanFilterFactory(name)
|
|
76
|
+
return useMemo(() => factory(value), [factory, value])
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function useDataViewBooleanFilterFactory(name: string): (value: boolean) => UseDataViewBooleanFilterResult {
|
|
80
|
+
const [filter, setFilter] = useDataViewFilter<BooleanFilterArtifact>(name)
|
|
81
|
+
|
|
82
|
+
return useCallback((value: boolean): UseDataViewBooleanFilterResult => {
|
|
83
|
+
const key = value ? 'includeTrue' : 'includeFalse'
|
|
84
|
+
const current: DataViewBooleanFilterCurrent = filter?.[key] ? 'include' : 'none'
|
|
85
|
+
|
|
86
|
+
const set = (action: DataViewSetBooleanFilterAction = 'include'): void => {
|
|
87
|
+
switch (action) {
|
|
88
|
+
case 'unset':
|
|
89
|
+
setFilter(it => ({ ...it, [key]: undefined }))
|
|
90
|
+
return
|
|
91
|
+
case 'toggle':
|
|
92
|
+
setFilter(it => ({ ...it, [key]: it?.[key] === true ? undefined : true }))
|
|
93
|
+
return
|
|
94
|
+
case 'include':
|
|
95
|
+
setFilter(it => ({ ...it, [key]: true }))
|
|
96
|
+
return
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return [current, set]
|
|
101
|
+
}, [filter, setFilter])
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// ============================================================================
|
|
105
|
+
// Enum Filter Hook
|
|
106
|
+
// ============================================================================
|
|
107
|
+
|
|
108
|
+
export type DataViewSetEnumFilterAction = 'include' | 'exclude' | 'unset' | 'toggleInclude' | 'toggleExclude'
|
|
109
|
+
export type DataViewEnumFilterCurrent = 'include' | 'exclude' | 'none'
|
|
110
|
+
export type UseDataViewEnumFilterResult = [
|
|
111
|
+
current: DataViewEnumFilterCurrent,
|
|
112
|
+
set: (action: DataViewSetEnumFilterAction) => void,
|
|
113
|
+
]
|
|
114
|
+
|
|
115
|
+
export function useDataViewEnumFilter(name: string, value: string): UseDataViewEnumFilterResult {
|
|
116
|
+
const factory = useDataViewEnumFilterFactory(name)
|
|
117
|
+
return useMemo(() => factory(value), [factory, value])
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function useDataViewEnumFilterFactory(name: string): (value: string) => UseDataViewEnumFilterResult {
|
|
121
|
+
const [filter, setFilter] = useDataViewFilter<EnumFilterArtifact>(name)
|
|
122
|
+
|
|
123
|
+
return useCallback((value: string): UseDataViewEnumFilterResult => {
|
|
124
|
+
const current: DataViewEnumFilterCurrent = (() => {
|
|
125
|
+
if (filter?.values?.includes(value)) return 'include'
|
|
126
|
+
if (filter?.notValues?.includes(value)) return 'exclude'
|
|
127
|
+
return 'none'
|
|
128
|
+
})()
|
|
129
|
+
|
|
130
|
+
const set = (action: DataViewSetEnumFilterAction = 'include'): void => {
|
|
131
|
+
switch (action) {
|
|
132
|
+
case 'unset':
|
|
133
|
+
setFilter(it => ({
|
|
134
|
+
...it,
|
|
135
|
+
values: it?.values?.filter(v => v !== value),
|
|
136
|
+
notValues: it?.notValues?.filter(v => v !== value),
|
|
137
|
+
}))
|
|
138
|
+
return
|
|
139
|
+
case 'toggleInclude':
|
|
140
|
+
setFilter(it => ({
|
|
141
|
+
...it,
|
|
142
|
+
values: it?.values?.includes(value)
|
|
143
|
+
? it.values.filter(v => v !== value)
|
|
144
|
+
: [...(it?.values ?? []), value],
|
|
145
|
+
notValues: it?.notValues?.filter(v => v !== value),
|
|
146
|
+
}))
|
|
147
|
+
return
|
|
148
|
+
case 'toggleExclude':
|
|
149
|
+
setFilter(it => ({
|
|
150
|
+
...it,
|
|
151
|
+
notValues: it?.notValues?.includes(value)
|
|
152
|
+
? it.notValues.filter(v => v !== value)
|
|
153
|
+
: [...(it?.notValues ?? []), value],
|
|
154
|
+
values: it?.values?.filter(v => v !== value),
|
|
155
|
+
}))
|
|
156
|
+
return
|
|
157
|
+
case 'include':
|
|
158
|
+
setFilter(it => ({
|
|
159
|
+
...it,
|
|
160
|
+
values: [...(it?.values ?? []), value],
|
|
161
|
+
notValues: it?.notValues?.filter(v => v !== value),
|
|
162
|
+
}))
|
|
163
|
+
return
|
|
164
|
+
case 'exclude':
|
|
165
|
+
setFilter(it => ({
|
|
166
|
+
...it,
|
|
167
|
+
notValues: [...(it?.notValues ?? []), value],
|
|
168
|
+
values: it?.values?.filter(v => v !== value),
|
|
169
|
+
}))
|
|
170
|
+
return
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return [current, set]
|
|
175
|
+
}, [filter?.values, filter?.notValues, setFilter])
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// ============================================================================
|
|
179
|
+
// Relation Filter Hook
|
|
180
|
+
// ============================================================================
|
|
181
|
+
|
|
182
|
+
export type DataViewSetRelationFilterAction = 'include' | 'exclude' | 'unset' | 'toggleInclude' | 'toggleExclude'
|
|
183
|
+
export type DataViewRelationFilterCurrent = 'include' | 'exclude' | 'none'
|
|
184
|
+
export type UseDataViewRelationFilterResult = [
|
|
185
|
+
current: DataViewRelationFilterCurrent,
|
|
186
|
+
set: (action: DataViewSetRelationFilterAction) => void,
|
|
187
|
+
]
|
|
188
|
+
|
|
189
|
+
export function useDataViewRelationFilter(name: string, entityId: string): UseDataViewRelationFilterResult {
|
|
190
|
+
const factory = useDataViewRelationFilterFactory(name)
|
|
191
|
+
return useMemo(() => factory(entityId), [factory, entityId])
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export function useDataViewRelationFilterFactory(name: string): (id: string) => UseDataViewRelationFilterResult {
|
|
195
|
+
const [filter, setFilter] = useDataViewFilter<RelationFilterArtifact>(name)
|
|
196
|
+
|
|
197
|
+
return useCallback((id: string): UseDataViewRelationFilterResult => {
|
|
198
|
+
const current: DataViewRelationFilterCurrent = (() => {
|
|
199
|
+
if (filter?.id?.includes(id)) return 'include'
|
|
200
|
+
if (filter?.notId?.includes(id)) return 'exclude'
|
|
201
|
+
return 'none'
|
|
202
|
+
})()
|
|
203
|
+
|
|
204
|
+
const set = (action: DataViewSetRelationFilterAction = 'include'): void => {
|
|
205
|
+
switch (action) {
|
|
206
|
+
case 'unset':
|
|
207
|
+
setFilter(it => ({
|
|
208
|
+
...it,
|
|
209
|
+
id: it?.id?.filter(v => v !== id),
|
|
210
|
+
notId: it?.notId?.filter(v => v !== id),
|
|
211
|
+
}))
|
|
212
|
+
return
|
|
213
|
+
case 'toggleInclude':
|
|
214
|
+
setFilter(it => ({
|
|
215
|
+
...it,
|
|
216
|
+
id: it?.id?.includes(id)
|
|
217
|
+
? it.id.filter(v => v !== id)
|
|
218
|
+
: [...(it?.id ?? []), id],
|
|
219
|
+
notId: it?.notId?.filter(v => v !== id),
|
|
220
|
+
}))
|
|
221
|
+
return
|
|
222
|
+
case 'toggleExclude':
|
|
223
|
+
setFilter(it => ({
|
|
224
|
+
...it,
|
|
225
|
+
notId: it?.notId?.includes(id)
|
|
226
|
+
? it.notId.filter(v => v !== id)
|
|
227
|
+
: [...(it?.notId ?? []), id],
|
|
228
|
+
id: it?.id?.filter(v => v !== id),
|
|
229
|
+
}))
|
|
230
|
+
return
|
|
231
|
+
case 'include':
|
|
232
|
+
setFilter(it => ({
|
|
233
|
+
...it,
|
|
234
|
+
id: [...(it?.id ?? []), id],
|
|
235
|
+
notId: it?.notId?.filter(v => v !== id),
|
|
236
|
+
}))
|
|
237
|
+
return
|
|
238
|
+
case 'exclude':
|
|
239
|
+
setFilter(it => ({
|
|
240
|
+
...it,
|
|
241
|
+
notId: [...(it?.notId ?? []), id],
|
|
242
|
+
id: it?.id?.filter(v => v !== id),
|
|
243
|
+
}))
|
|
244
|
+
return
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
return [current, set]
|
|
249
|
+
}, [filter?.id, filter?.notId, setFilter])
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// ============================================================================
|
|
253
|
+
// Text Filter Input Hook
|
|
254
|
+
// ============================================================================
|
|
255
|
+
|
|
256
|
+
export interface UseDataViewTextFilterInputResult {
|
|
257
|
+
value: string
|
|
258
|
+
onChange: (e: ChangeEvent<HTMLInputElement>) => void
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
export function useDataViewTextFilterInput({ name, debounceMs = 500 }: {
|
|
262
|
+
name: string
|
|
263
|
+
debounceMs?: number
|
|
264
|
+
}): UseDataViewTextFilterInputResult {
|
|
265
|
+
const [state, setFilter] = useDataViewFilter<TextFilterArtifact>(name)
|
|
266
|
+
const [value, setValue] = useState(state?.query ?? '')
|
|
267
|
+
const timerRef = useRef<ReturnType<typeof setTimeout> | undefined>(undefined)
|
|
268
|
+
|
|
269
|
+
useEffect(() => {
|
|
270
|
+
if (!timerRef.current) {
|
|
271
|
+
setValue(state?.query ?? '')
|
|
272
|
+
}
|
|
273
|
+
}, [state?.query])
|
|
274
|
+
|
|
275
|
+
const onChange = useCallback((e: ChangeEvent<HTMLInputElement>): void => {
|
|
276
|
+
const newValue = e.target.value
|
|
277
|
+
if (debounceMs && newValue) {
|
|
278
|
+
if (timerRef.current) clearTimeout(timerRef.current)
|
|
279
|
+
timerRef.current = setTimeout(() => {
|
|
280
|
+
timerRef.current = undefined
|
|
281
|
+
setFilter(it => ({ mode: 'contains' as const, ...it, query: newValue }))
|
|
282
|
+
}, debounceMs)
|
|
283
|
+
} else {
|
|
284
|
+
setFilter(it => ({ mode: 'contains' as const, ...it, query: newValue }))
|
|
285
|
+
}
|
|
286
|
+
setValue(newValue)
|
|
287
|
+
}, [debounceMs, setFilter])
|
|
288
|
+
|
|
289
|
+
return { value, onChange }
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// ============================================================================
|
|
293
|
+
// Text Filter Match Mode Hook
|
|
294
|
+
// ============================================================================
|
|
295
|
+
|
|
296
|
+
export type UseDataViewTextFilterMatchModeResult = [isCurrent: boolean, set: () => void]
|
|
297
|
+
|
|
298
|
+
export function useDataViewTextFilterMatchMode(
|
|
299
|
+
name: string,
|
|
300
|
+
mode: TextFilterArtifact['mode'],
|
|
301
|
+
): UseDataViewTextFilterMatchModeResult {
|
|
302
|
+
const [state, setFilter] = useDataViewFilter<TextFilterArtifact>(name)
|
|
303
|
+
|
|
304
|
+
const cb = useCallback((): void => {
|
|
305
|
+
setFilter(it => ({ query: '', ...it, mode }))
|
|
306
|
+
}, [setFilter, mode])
|
|
307
|
+
|
|
308
|
+
return [state?.mode === mode, cb]
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// ============================================================================
|
|
312
|
+
// Number Filter Input Hook
|
|
313
|
+
// ============================================================================
|
|
314
|
+
|
|
315
|
+
export interface UseDataViewNumberFilterInputResult {
|
|
316
|
+
value: string
|
|
317
|
+
onChange: (e: ChangeEvent<HTMLInputElement>) => void
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
export interface UseDataViewNumberFilterInputProps {
|
|
321
|
+
name: string
|
|
322
|
+
type: 'from' | 'to'
|
|
323
|
+
allowFloat?: boolean
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
export function useDataViewNumberFilterInput({ name, type, allowFloat }: UseDataViewNumberFilterInputProps): UseDataViewNumberFilterInputResult {
|
|
327
|
+
const [state, setFilter] = useDataViewFilter<NumberRangeFilterArtifact>(name)
|
|
328
|
+
|
|
329
|
+
const onChange = useCallback((e: ChangeEvent<HTMLInputElement>): void => {
|
|
330
|
+
const raw = e.target.value
|
|
331
|
+
const num = raw === '' ? null : (allowFloat ? parseFloat(raw) : parseInt(raw, 10))
|
|
332
|
+
const parsed = num !== null && isNaN(num) ? null : num
|
|
333
|
+
if (type === 'from') {
|
|
334
|
+
setFilter(it => ({ ...it, min: parsed, max: it?.max ?? null }))
|
|
335
|
+
} else {
|
|
336
|
+
setFilter(it => ({ ...it, min: it?.min ?? null, max: parsed }))
|
|
337
|
+
}
|
|
338
|
+
}, [type, allowFloat, setFilter])
|
|
339
|
+
|
|
340
|
+
const value = type === 'from' ? state?.min : state?.max
|
|
341
|
+
|
|
342
|
+
return { value: value?.toString() ?? '', onChange }
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// ============================================================================
|
|
346
|
+
// Null Filter Hook
|
|
347
|
+
// ============================================================================
|
|
348
|
+
|
|
349
|
+
export type DataViewSetNullFilterAction = 'include' | 'exclude' | 'unset' | 'toggleInclude' | 'toggleExclude'
|
|
350
|
+
export type DataViewNullFilterState = 'include' | 'exclude' | 'none'
|
|
351
|
+
export type UseDataViewNullFilterResult = [
|
|
352
|
+
state: DataViewNullFilterState,
|
|
353
|
+
set: (action: DataViewSetNullFilterAction) => void,
|
|
354
|
+
]
|
|
355
|
+
|
|
356
|
+
export function useDataViewNullFilter(name: string): UseDataViewNullFilterResult {
|
|
357
|
+
const [state, setFilter] = useDataViewFilter<{ nullCondition?: boolean } & FilterArtifact>(name)
|
|
358
|
+
|
|
359
|
+
const cb = useCallback((action: DataViewSetNullFilterAction): void => {
|
|
360
|
+
switch (action) {
|
|
361
|
+
case 'unset':
|
|
362
|
+
setFilter(it => ({ ...it, nullCondition: undefined }))
|
|
363
|
+
return
|
|
364
|
+
case 'toggleInclude':
|
|
365
|
+
setFilter(it => ({ ...it, nullCondition: it?.nullCondition === true ? undefined : true }))
|
|
366
|
+
return
|
|
367
|
+
case 'toggleExclude':
|
|
368
|
+
setFilter(it => ({ ...it, nullCondition: it?.nullCondition === false ? undefined : false }))
|
|
369
|
+
return
|
|
370
|
+
case 'include':
|
|
371
|
+
setFilter(it => ({ ...it, nullCondition: true }))
|
|
372
|
+
return
|
|
373
|
+
case 'exclude':
|
|
374
|
+
setFilter(it => ({ ...it, nullCondition: false }))
|
|
375
|
+
return
|
|
376
|
+
}
|
|
377
|
+
}, [setFilter])
|
|
378
|
+
|
|
379
|
+
const currentState: DataViewNullFilterState = state?.nullCondition === true
|
|
380
|
+
? 'include'
|
|
381
|
+
: state?.nullCondition === false
|
|
382
|
+
? 'exclude'
|
|
383
|
+
: 'none'
|
|
384
|
+
|
|
385
|
+
return [currentState, cb]
|
|
386
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Filter scope components — declarative filter registration and name scoping.
|
|
3
|
+
*
|
|
4
|
+
* Usage:
|
|
5
|
+
* ```tsx
|
|
6
|
+
* <DataViewFilterScope name="title">
|
|
7
|
+
* <DataViewTextFilterInput />
|
|
8
|
+
* </DataViewFilterScope>
|
|
9
|
+
*
|
|
10
|
+
* <DataViewFilter name="title" filterHandler={createTextFilterHandler('title')}>
|
|
11
|
+
* <DataViewTextFilterInput />
|
|
12
|
+
* </DataViewFilter>
|
|
13
|
+
*
|
|
14
|
+
* <DataViewTextFilter field="title">
|
|
15
|
+
* <DataViewTextFilterInput />
|
|
16
|
+
* </DataViewTextFilter>
|
|
17
|
+
*
|
|
18
|
+
* <DataViewHasFilterType name="title">
|
|
19
|
+
* <span>Title filter is available</span>
|
|
20
|
+
* </DataViewHasFilterType>
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import React, { type ReactElement } from 'react'
|
|
25
|
+
import { DataViewFilterNameProvider } from './filterContext.js'
|
|
26
|
+
import { useDataViewContext } from './DataViewContext.js'
|
|
27
|
+
import type { FilterHandler, FilterArtifact } from '@contember/bindx'
|
|
28
|
+
|
|
29
|
+
// ============================================================================
|
|
30
|
+
// DataViewFilterScope
|
|
31
|
+
// ============================================================================
|
|
32
|
+
|
|
33
|
+
export interface DataViewFilterScopeProps {
|
|
34
|
+
name: string
|
|
35
|
+
children: React.ReactNode
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Sets the filter name context for all children.
|
|
40
|
+
* Child filter triggers will infer their filter name from this scope.
|
|
41
|
+
*/
|
|
42
|
+
export function DataViewFilterScope({ name, children }: DataViewFilterScopeProps): ReactElement {
|
|
43
|
+
return (
|
|
44
|
+
<DataViewFilterNameProvider value={name}>
|
|
45
|
+
{children}
|
|
46
|
+
</DataViewFilterNameProvider>
|
|
47
|
+
)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// ============================================================================
|
|
51
|
+
// DataViewFilter
|
|
52
|
+
// ============================================================================
|
|
53
|
+
|
|
54
|
+
export interface DataViewFilterProps {
|
|
55
|
+
name: string
|
|
56
|
+
filterHandler: FilterHandler<FilterArtifact>
|
|
57
|
+
children?: React.ReactNode
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Registers a filter handler and sets the filter name context.
|
|
62
|
+
*
|
|
63
|
+
* Note: When used inside a DataGrid, filters are already registered
|
|
64
|
+
* from column definitions. This component then just provides the name scope.
|
|
65
|
+
*/
|
|
66
|
+
export function DataViewFilter({ name, children }: DataViewFilterProps): ReactElement {
|
|
67
|
+
return (
|
|
68
|
+
<DataViewFilterNameProvider value={name}>
|
|
69
|
+
{children}
|
|
70
|
+
</DataViewFilterNameProvider>
|
|
71
|
+
)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// ============================================================================
|
|
75
|
+
// DataViewHasFilterType
|
|
76
|
+
// ============================================================================
|
|
77
|
+
|
|
78
|
+
export interface DataViewHasFilterTypeProps {
|
|
79
|
+
name: string
|
|
80
|
+
children: React.ReactNode
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Renders children only if a filter with the given name is registered.
|
|
85
|
+
*/
|
|
86
|
+
export function DataViewHasFilterType({ name, children }: DataViewHasFilterTypeProps): ReactElement | null {
|
|
87
|
+
const { filtering } = useDataViewContext()
|
|
88
|
+
return filtering.filters.has(name) ? <>{children}</> : null
|
|
89
|
+
}
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Typed filter wrapper components — provide filter name scope for specific filter types.
|
|
3
|
+
*
|
|
4
|
+
* Each wrapper sets the filter name context so child trigger components
|
|
5
|
+
* can infer their filter name automatically.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* ```tsx
|
|
9
|
+
* <DataViewTextFilter field={it.title}>
|
|
10
|
+
* <DataViewTextFilterInput />
|
|
11
|
+
* <DataViewTextFilterMatchModeTrigger mode="contains">
|
|
12
|
+
* <button>Contains</button>
|
|
13
|
+
* </DataViewTextFilterMatchModeTrigger>
|
|
14
|
+
* </DataViewTextFilter>
|
|
15
|
+
*
|
|
16
|
+
* <DataViewBooleanFilter field={it.published}>
|
|
17
|
+
* <DataViewBooleanFilterTrigger value={true} action="toggle">
|
|
18
|
+
* <button>Published</button>
|
|
19
|
+
* </DataViewBooleanFilterTrigger>
|
|
20
|
+
* </DataViewBooleanFilter>
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import React, { type ReactElement } from 'react'
|
|
25
|
+
import type { FieldRefBase } from '@contember/bindx'
|
|
26
|
+
import { FIELD_REF_META } from '@contember/bindx'
|
|
27
|
+
import { DataViewFilterNameProvider } from './filterContext.js'
|
|
28
|
+
|
|
29
|
+
// ============================================================================
|
|
30
|
+
// Common props
|
|
31
|
+
// ============================================================================
|
|
32
|
+
|
|
33
|
+
interface FilterWrapperProps<T> {
|
|
34
|
+
field: FieldRefBase<T>
|
|
35
|
+
name?: string
|
|
36
|
+
children: React.ReactNode
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function resolveFilterName<T>(name: string | undefined, field: FieldRefBase<T>): string {
|
|
40
|
+
return name ?? field[FIELD_REF_META].fieldName
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// ============================================================================
|
|
44
|
+
// DataViewTextFilter
|
|
45
|
+
// ============================================================================
|
|
46
|
+
|
|
47
|
+
export interface DataViewTextFilterProps<T> extends FilterWrapperProps<T> {}
|
|
48
|
+
|
|
49
|
+
export function DataViewTextFilter<T>({ field, name, children }: DataViewTextFilterProps<T>): ReactElement {
|
|
50
|
+
return (
|
|
51
|
+
<DataViewFilterNameProvider value={resolveFilterName(name, field)}>
|
|
52
|
+
{children}
|
|
53
|
+
</DataViewFilterNameProvider>
|
|
54
|
+
)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// ============================================================================
|
|
58
|
+
// DataViewBooleanFilter
|
|
59
|
+
// ============================================================================
|
|
60
|
+
|
|
61
|
+
export interface DataViewBooleanFilterProps<T> extends FilterWrapperProps<T> {}
|
|
62
|
+
|
|
63
|
+
export function DataViewBooleanFilter<T>({ field, name, children }: DataViewBooleanFilterProps<T>): ReactElement {
|
|
64
|
+
return (
|
|
65
|
+
<DataViewFilterNameProvider value={resolveFilterName(name, field)}>
|
|
66
|
+
{children}
|
|
67
|
+
</DataViewFilterNameProvider>
|
|
68
|
+
)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// ============================================================================
|
|
72
|
+
// DataViewNumberFilter
|
|
73
|
+
// ============================================================================
|
|
74
|
+
|
|
75
|
+
export interface DataViewNumberFilterProps<T> extends FilterWrapperProps<T> {}
|
|
76
|
+
|
|
77
|
+
export function DataViewNumberFilter<T>({ field, name, children }: DataViewNumberFilterProps<T>): ReactElement {
|
|
78
|
+
return (
|
|
79
|
+
<DataViewFilterNameProvider value={resolveFilterName(name, field)}>
|
|
80
|
+
{children}
|
|
81
|
+
</DataViewFilterNameProvider>
|
|
82
|
+
)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// ============================================================================
|
|
86
|
+
// DataViewDateFilter
|
|
87
|
+
// ============================================================================
|
|
88
|
+
|
|
89
|
+
export interface DataViewDateFilterProps<T> extends FilterWrapperProps<T> {}
|
|
90
|
+
|
|
91
|
+
export function DataViewDateFilter<T>({ field, name, children }: DataViewDateFilterProps<T>): ReactElement {
|
|
92
|
+
return (
|
|
93
|
+
<DataViewFilterNameProvider value={resolveFilterName(name, field)}>
|
|
94
|
+
{children}
|
|
95
|
+
</DataViewFilterNameProvider>
|
|
96
|
+
)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// ============================================================================
|
|
100
|
+
// DataViewEnumFilter
|
|
101
|
+
// ============================================================================
|
|
102
|
+
|
|
103
|
+
export interface DataViewEnumFilterProps<T> extends FilterWrapperProps<T> {}
|
|
104
|
+
|
|
105
|
+
export function DataViewEnumFilter<T>({ field, name, children }: DataViewEnumFilterProps<T>): ReactElement {
|
|
106
|
+
return (
|
|
107
|
+
<DataViewFilterNameProvider value={resolveFilterName(name, field)}>
|
|
108
|
+
{children}
|
|
109
|
+
</DataViewFilterNameProvider>
|
|
110
|
+
)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// ============================================================================
|
|
114
|
+
// DataViewEnumListFilter
|
|
115
|
+
// ============================================================================
|
|
116
|
+
|
|
117
|
+
export interface DataViewEnumListFilterProps<T> extends FilterWrapperProps<T> {}
|
|
118
|
+
|
|
119
|
+
export function DataViewEnumListFilter<T>({ field, name, children }: DataViewEnumListFilterProps<T>): ReactElement {
|
|
120
|
+
return (
|
|
121
|
+
<DataViewFilterNameProvider value={resolveFilterName(name, field)}>
|
|
122
|
+
{children}
|
|
123
|
+
</DataViewFilterNameProvider>
|
|
124
|
+
)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// ============================================================================
|
|
128
|
+
// DataViewHasOneFilter
|
|
129
|
+
// ============================================================================
|
|
130
|
+
|
|
131
|
+
export interface DataViewHasOneFilterProps<T> extends FilterWrapperProps<T> {}
|
|
132
|
+
|
|
133
|
+
export function DataViewHasOneFilter<T>({ field, name, children }: DataViewHasOneFilterProps<T>): ReactElement {
|
|
134
|
+
return (
|
|
135
|
+
<DataViewFilterNameProvider value={resolveFilterName(name, field)}>
|
|
136
|
+
{children}
|
|
137
|
+
</DataViewFilterNameProvider>
|
|
138
|
+
)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// ============================================================================
|
|
142
|
+
// DataViewHasManyFilter
|
|
143
|
+
// ============================================================================
|
|
144
|
+
|
|
145
|
+
export interface DataViewHasManyFilterProps<T> extends FilterWrapperProps<T> {}
|
|
146
|
+
|
|
147
|
+
export function DataViewHasManyFilter<T>({ field, name, children }: DataViewHasManyFilterProps<T>): ReactElement {
|
|
148
|
+
return (
|
|
149
|
+
<DataViewFilterNameProvider value={resolveFilterName(name, field)}>
|
|
150
|
+
{children}
|
|
151
|
+
</DataViewFilterNameProvider>
|
|
152
|
+
)
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// ============================================================================
|
|
156
|
+
// DataViewIsDefinedFilter
|
|
157
|
+
// ============================================================================
|
|
158
|
+
|
|
159
|
+
export interface DataViewIsDefinedFilterProps<T> extends FilterWrapperProps<T> {}
|
|
160
|
+
|
|
161
|
+
export function DataViewIsDefinedFilter<T>({ field, name, children }: DataViewIsDefinedFilterProps<T>): ReactElement {
|
|
162
|
+
return (
|
|
163
|
+
<DataViewFilterNameProvider value={resolveFilterName(name, field)}>
|
|
164
|
+
{children}
|
|
165
|
+
</DataViewFilterNameProvider>
|
|
166
|
+
)
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// ============================================================================
|
|
170
|
+
// DataViewUnionTextFilter
|
|
171
|
+
// ============================================================================
|
|
172
|
+
|
|
173
|
+
export interface DataViewUnionTextFilterProps<T> {
|
|
174
|
+
/** Single field or array of fields to search across */
|
|
175
|
+
fields: FieldRefBase<T> | FieldRefBase<T>[]
|
|
176
|
+
/** Required unique filter name */
|
|
177
|
+
name: string
|
|
178
|
+
children: React.ReactNode
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export function DataViewUnionTextFilter<T>({ name, children }: DataViewUnionTextFilterProps<T>): ReactElement {
|
|
182
|
+
return (
|
|
183
|
+
<DataViewFilterNameProvider value={name}>
|
|
184
|
+
{children}
|
|
185
|
+
</DataViewFilterNameProvider>
|
|
186
|
+
)
|
|
187
|
+
}
|