@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,617 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Composable filter UI components using Radix Slot pattern.
|
|
3
|
+
*
|
|
4
|
+
* All trigger components use `@radix-ui/react-slot` for proper prop merging
|
|
5
|
+
* and `forwardRef` for ref forwarding. Filter names can be inferred from
|
|
6
|
+
* context (via DataViewTextFilter, DataViewFilterScope, etc.) or passed explicitly.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* ```tsx
|
|
10
|
+
* // With explicit name
|
|
11
|
+
* <DataViewTextFilterInput name="title">
|
|
12
|
+
* <input placeholder="Search..." />
|
|
13
|
+
* </DataViewTextFilterInput>
|
|
14
|
+
*
|
|
15
|
+
* // With context-inferred name
|
|
16
|
+
* <DataViewTextFilter field="title">
|
|
17
|
+
* <DataViewTextFilterInput>
|
|
18
|
+
* <input placeholder="Search..." />
|
|
19
|
+
* </DataViewTextFilterInput>
|
|
20
|
+
* <DataViewTextFilterMatchModeTrigger mode="contains">
|
|
21
|
+
* <button>Contains</button>
|
|
22
|
+
* </DataViewTextFilterMatchModeTrigger>
|
|
23
|
+
* </DataViewTextFilter>
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import React, { forwardRef, type ReactElement, type ReactNode, useCallback } from 'react'
|
|
28
|
+
import { Slot } from '@radix-ui/react-slot'
|
|
29
|
+
import { composeEventHandlers } from '@radix-ui/primitive'
|
|
30
|
+
import { useOptionalDataViewFilterName } from './filterContext.js'
|
|
31
|
+
import { dataAttribute } from './dataAttribute.js'
|
|
32
|
+
import {
|
|
33
|
+
useDataViewFilter,
|
|
34
|
+
useDataViewBooleanFilter,
|
|
35
|
+
useDataViewEnumFilter,
|
|
36
|
+
useDataViewRelationFilter,
|
|
37
|
+
useDataViewTextFilterInput,
|
|
38
|
+
useDataViewTextFilterMatchMode,
|
|
39
|
+
useDataViewNullFilter,
|
|
40
|
+
type UseDataViewFilterResult,
|
|
41
|
+
type DataViewSetBooleanFilterAction,
|
|
42
|
+
type DataViewBooleanFilterCurrent,
|
|
43
|
+
type DataViewSetEnumFilterAction,
|
|
44
|
+
type DataViewEnumFilterCurrent,
|
|
45
|
+
type DataViewSetRelationFilterAction,
|
|
46
|
+
type DataViewRelationFilterCurrent,
|
|
47
|
+
type DataViewSetNullFilterAction,
|
|
48
|
+
type DataViewNullFilterState,
|
|
49
|
+
} from './filterHooks.js'
|
|
50
|
+
import type {
|
|
51
|
+
FilterArtifact,
|
|
52
|
+
TextFilterArtifact,
|
|
53
|
+
NumberRangeFilterArtifact,
|
|
54
|
+
DateFilterArtifact,
|
|
55
|
+
EnumFilterArtifact,
|
|
56
|
+
} from '@contember/bindx'
|
|
57
|
+
import { useDataViewContext } from './DataViewContext.js'
|
|
58
|
+
|
|
59
|
+
// Re-export the hook and types from filterHooks (backwards compat)
|
|
60
|
+
export {
|
|
61
|
+
useDataViewFilter,
|
|
62
|
+
} from './filterHooks.js'
|
|
63
|
+
export type { UseDataViewFilterResult as DataViewFilterState } from './filterHooks.js'
|
|
64
|
+
|
|
65
|
+
function resolveFilterName(name: string | undefined): string {
|
|
66
|
+
if (name !== undefined) return name
|
|
67
|
+
// eslint-disable-next-line react-hooks/rules-of-hooks
|
|
68
|
+
const contextName = useOptionalDataViewFilterName()
|
|
69
|
+
if (contextName !== null) return contextName
|
|
70
|
+
throw new Error('Filter name must be provided via `name` prop or filter scope context')
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// ============================================================================
|
|
74
|
+
// Text Filter Input — Slot-based
|
|
75
|
+
// ============================================================================
|
|
76
|
+
|
|
77
|
+
const SlotInput = Slot as React.ForwardRefExoticComponent<
|
|
78
|
+
React.InputHTMLAttributes<HTMLInputElement> & React.RefAttributes<HTMLInputElement>
|
|
79
|
+
>
|
|
80
|
+
|
|
81
|
+
export interface DataViewTextFilterInputProps {
|
|
82
|
+
name?: string
|
|
83
|
+
debounceMs?: number
|
|
84
|
+
children: ReactElement
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export const DataViewTextFilterInput = forwardRef<HTMLInputElement, DataViewTextFilterInputProps>(
|
|
88
|
+
({ name, debounceMs, ...props }, ref) => {
|
|
89
|
+
name = resolveFilterName(name)
|
|
90
|
+
return (
|
|
91
|
+
<SlotInput
|
|
92
|
+
{...useDataViewTextFilterInput({ name, debounceMs })}
|
|
93
|
+
{...props}
|
|
94
|
+
ref={ref}
|
|
95
|
+
/>
|
|
96
|
+
)
|
|
97
|
+
},
|
|
98
|
+
)
|
|
99
|
+
DataViewTextFilterInput.displayName = 'DataViewTextFilterInput'
|
|
100
|
+
|
|
101
|
+
// ============================================================================
|
|
102
|
+
// Text Filter Match Mode Trigger — Slot-based
|
|
103
|
+
// ============================================================================
|
|
104
|
+
|
|
105
|
+
export interface DataViewTextFilterMatchModeTriggerProps {
|
|
106
|
+
name?: string
|
|
107
|
+
mode: TextFilterArtifact['mode']
|
|
108
|
+
children: ReactElement
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export const DataViewTextFilterMatchModeTrigger = forwardRef<HTMLButtonElement, DataViewTextFilterMatchModeTriggerProps>(
|
|
112
|
+
({ name, mode, ...props }, ref) => {
|
|
113
|
+
name = resolveFilterName(name)
|
|
114
|
+
const [active, cb] = useDataViewTextFilterMatchMode(name, mode)
|
|
115
|
+
const { onClick, ...otherProps } = props as React.ButtonHTMLAttributes<HTMLButtonElement>
|
|
116
|
+
|
|
117
|
+
return (
|
|
118
|
+
<Slot
|
|
119
|
+
ref={ref}
|
|
120
|
+
onClick={composeEventHandlers(onClick, cb)}
|
|
121
|
+
data-active={dataAttribute(active)}
|
|
122
|
+
{...otherProps}
|
|
123
|
+
/>
|
|
124
|
+
)
|
|
125
|
+
},
|
|
126
|
+
)
|
|
127
|
+
DataViewTextFilterMatchModeTrigger.displayName = 'DataViewTextFilterMatchModeTrigger'
|
|
128
|
+
|
|
129
|
+
// ============================================================================
|
|
130
|
+
// Text Filter Match Mode Label
|
|
131
|
+
// ============================================================================
|
|
132
|
+
|
|
133
|
+
export interface DataViewTextFilterMatchModeLabelProps {
|
|
134
|
+
name?: string
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const TEXT_MODE_LABELS: Record<TextFilterArtifact['mode'], string> = {
|
|
138
|
+
contains: 'Contains',
|
|
139
|
+
startsWith: 'Starts with',
|
|
140
|
+
endsWith: 'Ends with',
|
|
141
|
+
equals: 'Equals',
|
|
142
|
+
notContains: 'Not contains',
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export function DataViewTextFilterMatchModeLabel({
|
|
146
|
+
name,
|
|
147
|
+
}: DataViewTextFilterMatchModeLabelProps): ReactElement {
|
|
148
|
+
name = resolveFilterName(name)
|
|
149
|
+
const [state] = useDataViewFilter<TextFilterArtifact>(name)
|
|
150
|
+
const mode = state?.mode ?? 'contains'
|
|
151
|
+
return <>{TEXT_MODE_LABELS[mode]}</>
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// ============================================================================
|
|
155
|
+
// Text Filter Reset Trigger
|
|
156
|
+
// ============================================================================
|
|
157
|
+
|
|
158
|
+
export interface DataViewTextFilterResetTriggerProps {
|
|
159
|
+
name?: string
|
|
160
|
+
children: ReactElement
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export const DataViewTextFilterResetTrigger = forwardRef<HTMLButtonElement, DataViewTextFilterResetTriggerProps>(
|
|
164
|
+
({ name, ...props }, ref) => {
|
|
165
|
+
name = resolveFilterName(name)
|
|
166
|
+
const [state, setFilter] = useDataViewFilter<TextFilterArtifact>(name)
|
|
167
|
+
const hasQuery = (state?.query?.length ?? 0) > 0
|
|
168
|
+
|
|
169
|
+
const handleReset = useCallback((): void => {
|
|
170
|
+
setFilter(it => ({ mode: 'contains' as const, ...it, query: '' }))
|
|
171
|
+
}, [setFilter])
|
|
172
|
+
|
|
173
|
+
if (!hasQuery) return null
|
|
174
|
+
|
|
175
|
+
const { onClick, ...otherProps } = props as React.ButtonHTMLAttributes<HTMLButtonElement>
|
|
176
|
+
|
|
177
|
+
return (
|
|
178
|
+
<Slot
|
|
179
|
+
ref={ref}
|
|
180
|
+
onClick={composeEventHandlers(onClick, handleReset)}
|
|
181
|
+
{...otherProps}
|
|
182
|
+
/>
|
|
183
|
+
)
|
|
184
|
+
},
|
|
185
|
+
)
|
|
186
|
+
DataViewTextFilterResetTrigger.displayName = 'DataViewTextFilterResetTrigger'
|
|
187
|
+
|
|
188
|
+
// ============================================================================
|
|
189
|
+
// Number Filter Input — Slot-based
|
|
190
|
+
// ============================================================================
|
|
191
|
+
|
|
192
|
+
const SlotNumberInput = Slot as React.ForwardRefExoticComponent<
|
|
193
|
+
React.InputHTMLAttributes<HTMLInputElement> & React.RefAttributes<HTMLInputElement>
|
|
194
|
+
>
|
|
195
|
+
|
|
196
|
+
export interface DataViewNumberFilterInputProps {
|
|
197
|
+
name?: string
|
|
198
|
+
type: 'from' | 'to'
|
|
199
|
+
allowFloat?: boolean
|
|
200
|
+
children: ReactElement
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export const DataViewNumberFilterInput = forwardRef<HTMLInputElement, DataViewNumberFilterInputProps>(
|
|
204
|
+
({ name, type, allowFloat, ...props }, ref) => {
|
|
205
|
+
name = resolveFilterName(name)
|
|
206
|
+
const [artifact, setFilter] = useDataViewFilter<NumberRangeFilterArtifact>(name)
|
|
207
|
+
const min = artifact?.min ?? null
|
|
208
|
+
const max = artifact?.max ?? null
|
|
209
|
+
const value = type === 'from' ? min : max
|
|
210
|
+
|
|
211
|
+
const onChange = useCallback((e: React.ChangeEvent<HTMLInputElement>): void => {
|
|
212
|
+
const raw = e.target.value
|
|
213
|
+
const num = raw === '' ? null : (allowFloat ? parseFloat(raw) : parseInt(raw, 10))
|
|
214
|
+
const parsed = num !== null && isNaN(num) ? null : num
|
|
215
|
+
if (type === 'from') {
|
|
216
|
+
setFilter(it => ({ ...it, min: parsed, max: it?.max ?? null }))
|
|
217
|
+
} else {
|
|
218
|
+
setFilter(it => ({ ...it, min: it?.min ?? null, max: parsed }))
|
|
219
|
+
}
|
|
220
|
+
}, [type, allowFloat, setFilter])
|
|
221
|
+
|
|
222
|
+
return (
|
|
223
|
+
<SlotNumberInput
|
|
224
|
+
type="number"
|
|
225
|
+
value={value ?? ''}
|
|
226
|
+
onChange={onChange}
|
|
227
|
+
{...props}
|
|
228
|
+
ref={ref}
|
|
229
|
+
/>
|
|
230
|
+
)
|
|
231
|
+
},
|
|
232
|
+
)
|
|
233
|
+
DataViewNumberFilterInput.displayName = 'DataViewNumberFilterInput'
|
|
234
|
+
|
|
235
|
+
// ============================================================================
|
|
236
|
+
// Date Filter Input — Slot-based
|
|
237
|
+
// ============================================================================
|
|
238
|
+
|
|
239
|
+
const SlotDateInput = Slot as React.ForwardRefExoticComponent<
|
|
240
|
+
React.InputHTMLAttributes<HTMLInputElement> & React.RefAttributes<HTMLInputElement>
|
|
241
|
+
>
|
|
242
|
+
|
|
243
|
+
export interface DataViewDateFilterInputProps {
|
|
244
|
+
name?: string
|
|
245
|
+
type: 'start' | 'end'
|
|
246
|
+
children: ReactElement
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
export const DataViewDateFilterInput = forwardRef<HTMLInputElement, DataViewDateFilterInputProps>(
|
|
250
|
+
({ name, type, ...props }, ref) => {
|
|
251
|
+
name = resolveFilterName(name)
|
|
252
|
+
const [artifact, setFilter] = useDataViewFilter<import('@contember/bindx').DateFilterArtifact>(name)
|
|
253
|
+
const start = artifact?.start ?? null
|
|
254
|
+
const end = artifact?.end ?? null
|
|
255
|
+
const value = type === 'start' ? start : end
|
|
256
|
+
|
|
257
|
+
const onChange = useCallback((e: React.ChangeEvent<HTMLInputElement>): void => {
|
|
258
|
+
const val = e.target.value || null
|
|
259
|
+
if (type === 'start') {
|
|
260
|
+
setFilter(it => ({ ...it, start: val, end: it?.end ?? null }))
|
|
261
|
+
} else {
|
|
262
|
+
setFilter(it => ({ ...it, start: it?.start ?? null, end: val }))
|
|
263
|
+
}
|
|
264
|
+
}, [type, setFilter])
|
|
265
|
+
|
|
266
|
+
return (
|
|
267
|
+
<SlotDateInput
|
|
268
|
+
type="date"
|
|
269
|
+
value={value ?? ''}
|
|
270
|
+
onChange={onChange}
|
|
271
|
+
{...props}
|
|
272
|
+
ref={ref}
|
|
273
|
+
/>
|
|
274
|
+
)
|
|
275
|
+
},
|
|
276
|
+
)
|
|
277
|
+
DataViewDateFilterInput.displayName = 'DataViewDateFilterInput'
|
|
278
|
+
|
|
279
|
+
// ============================================================================
|
|
280
|
+
// Boolean Filter Trigger — Slot-based with action
|
|
281
|
+
// ============================================================================
|
|
282
|
+
|
|
283
|
+
export interface DataViewBooleanFilterTriggerProps {
|
|
284
|
+
name?: string
|
|
285
|
+
value: boolean
|
|
286
|
+
action?: DataViewSetBooleanFilterAction
|
|
287
|
+
children: ReactElement
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
export const DataViewBooleanFilterTrigger = forwardRef<HTMLButtonElement, DataViewBooleanFilterTriggerProps>(
|
|
291
|
+
({ name, action = 'include', value, ...props }, ref) => {
|
|
292
|
+
name = resolveFilterName(name)
|
|
293
|
+
const [current, setFilter] = useDataViewBooleanFilter(name, value)
|
|
294
|
+
|
|
295
|
+
const toggleFilter = useCallback((): void => {
|
|
296
|
+
setFilter(action)
|
|
297
|
+
}, [action, setFilter])
|
|
298
|
+
|
|
299
|
+
const active = current === actionToStateBool[action]
|
|
300
|
+
const { onClick, ...otherProps } = props as React.ButtonHTMLAttributes<HTMLButtonElement>
|
|
301
|
+
|
|
302
|
+
return (
|
|
303
|
+
<Slot
|
|
304
|
+
ref={ref}
|
|
305
|
+
onClick={composeEventHandlers(onClick, toggleFilter)}
|
|
306
|
+
data-active={dataAttribute(active)}
|
|
307
|
+
data-current={current}
|
|
308
|
+
{...otherProps}
|
|
309
|
+
/>
|
|
310
|
+
)
|
|
311
|
+
},
|
|
312
|
+
)
|
|
313
|
+
DataViewBooleanFilterTrigger.displayName = 'DataViewBooleanFilterTrigger'
|
|
314
|
+
|
|
315
|
+
const actionToStateBool: Record<DataViewSetBooleanFilterAction, DataViewBooleanFilterCurrent> = {
|
|
316
|
+
include: 'include',
|
|
317
|
+
unset: 'none',
|
|
318
|
+
toggle: 'include',
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// ============================================================================
|
|
322
|
+
// Enum Filter Trigger — Slot-based with action
|
|
323
|
+
// ============================================================================
|
|
324
|
+
|
|
325
|
+
export interface DataViewEnumFilterTriggerProps {
|
|
326
|
+
name?: string
|
|
327
|
+
value: string
|
|
328
|
+
action?: DataViewSetEnumFilterAction
|
|
329
|
+
children: ReactElement
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
export const DataViewEnumFilterTrigger = forwardRef<HTMLButtonElement, DataViewEnumFilterTriggerProps>(
|
|
333
|
+
({ name, action = 'include', value, ...props }, ref) => {
|
|
334
|
+
name = resolveFilterName(name)
|
|
335
|
+
const [current, setFilter] = useDataViewEnumFilter(name, value)
|
|
336
|
+
|
|
337
|
+
const toggleFilter = useCallback((): void => {
|
|
338
|
+
setFilter(action)
|
|
339
|
+
}, [action, setFilter])
|
|
340
|
+
|
|
341
|
+
const active = current === actionToStateEnum[action]
|
|
342
|
+
const { onClick, ...otherProps } = props as React.ButtonHTMLAttributes<HTMLButtonElement>
|
|
343
|
+
|
|
344
|
+
return (
|
|
345
|
+
<Slot
|
|
346
|
+
ref={ref}
|
|
347
|
+
onClick={composeEventHandlers(onClick, toggleFilter)}
|
|
348
|
+
data-active={dataAttribute(active)}
|
|
349
|
+
data-current={current}
|
|
350
|
+
{...otherProps}
|
|
351
|
+
/>
|
|
352
|
+
)
|
|
353
|
+
},
|
|
354
|
+
)
|
|
355
|
+
DataViewEnumFilterTrigger.displayName = 'DataViewEnumFilterTrigger'
|
|
356
|
+
|
|
357
|
+
const actionToStateEnum: Record<DataViewSetEnumFilterAction, DataViewEnumFilterCurrent> = {
|
|
358
|
+
exclude: 'exclude',
|
|
359
|
+
include: 'include',
|
|
360
|
+
unset: 'none',
|
|
361
|
+
toggleInclude: 'include',
|
|
362
|
+
toggleExclude: 'exclude',
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// ============================================================================
|
|
366
|
+
// Enum Filter State — render prop
|
|
367
|
+
// ============================================================================
|
|
368
|
+
|
|
369
|
+
export interface DataViewEnumFilterStateProps {
|
|
370
|
+
name?: string
|
|
371
|
+
value: string
|
|
372
|
+
children: (state: { isIncluded: boolean; isExcluded: boolean }) => ReactNode
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
export function DataViewEnumFilterState({
|
|
376
|
+
name,
|
|
377
|
+
value,
|
|
378
|
+
children,
|
|
379
|
+
}: DataViewEnumFilterStateProps): ReactElement {
|
|
380
|
+
name = resolveFilterName(name)
|
|
381
|
+
const [artifact] = useDataViewFilter<EnumFilterArtifact>(name)
|
|
382
|
+
const includedValues = artifact?.values ?? []
|
|
383
|
+
const excludedValues = artifact?.notValues ?? []
|
|
384
|
+
|
|
385
|
+
return <>{children({
|
|
386
|
+
isIncluded: includedValues.includes(value),
|
|
387
|
+
isExcluded: excludedValues.includes(value),
|
|
388
|
+
})}</>
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// ============================================================================
|
|
392
|
+
// Relation Filter Trigger — Slot-based with action
|
|
393
|
+
// ============================================================================
|
|
394
|
+
|
|
395
|
+
export interface DataViewRelationFilterTriggerProps {
|
|
396
|
+
name?: string
|
|
397
|
+
id: string
|
|
398
|
+
action?: DataViewSetRelationFilterAction
|
|
399
|
+
children: ReactElement
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
export const DataViewRelationFilterTrigger = forwardRef<HTMLButtonElement, DataViewRelationFilterTriggerProps>(
|
|
403
|
+
({ name, action = 'include', id, ...props }, ref) => {
|
|
404
|
+
name = resolveFilterName(name)
|
|
405
|
+
const [current, setFilter] = useDataViewRelationFilter(name, id)
|
|
406
|
+
|
|
407
|
+
const toggleFilter = useCallback((): void => {
|
|
408
|
+
setFilter(action)
|
|
409
|
+
}, [action, setFilter])
|
|
410
|
+
|
|
411
|
+
const active = current === actionToStateRelation[action]
|
|
412
|
+
const { onClick, ...otherProps } = props as React.ButtonHTMLAttributes<HTMLButtonElement>
|
|
413
|
+
|
|
414
|
+
return (
|
|
415
|
+
<Slot
|
|
416
|
+
ref={ref}
|
|
417
|
+
onClick={composeEventHandlers(onClick, toggleFilter)}
|
|
418
|
+
data-active={dataAttribute(active)}
|
|
419
|
+
data-current={current}
|
|
420
|
+
{...otherProps}
|
|
421
|
+
/>
|
|
422
|
+
)
|
|
423
|
+
},
|
|
424
|
+
)
|
|
425
|
+
DataViewRelationFilterTrigger.displayName = 'DataViewRelationFilterTrigger'
|
|
426
|
+
|
|
427
|
+
const actionToStateRelation: Record<DataViewSetRelationFilterAction, DataViewRelationFilterCurrent> = {
|
|
428
|
+
exclude: 'exclude',
|
|
429
|
+
include: 'include',
|
|
430
|
+
unset: 'none',
|
|
431
|
+
toggleInclude: 'include',
|
|
432
|
+
toggleExclude: 'exclude',
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// ============================================================================
|
|
436
|
+
// Relation Filter State — render prop
|
|
437
|
+
// ============================================================================
|
|
438
|
+
|
|
439
|
+
export interface DataViewRelationFilterStateProps {
|
|
440
|
+
name?: string
|
|
441
|
+
id: string
|
|
442
|
+
children: (state: { isIncluded: boolean; isExcluded: boolean }) => ReactNode
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
export function DataViewRelationFilterState({
|
|
446
|
+
name,
|
|
447
|
+
id,
|
|
448
|
+
children,
|
|
449
|
+
}: DataViewRelationFilterStateProps): ReactElement {
|
|
450
|
+
name = resolveFilterName(name)
|
|
451
|
+
const [artifact] = useDataViewFilter<import('@contember/bindx').RelationFilterArtifact>(name)
|
|
452
|
+
const includedIds = artifact?.id ?? []
|
|
453
|
+
const excludedIds = artifact?.notId ?? []
|
|
454
|
+
|
|
455
|
+
return <>{children({
|
|
456
|
+
isIncluded: includedIds.includes(id),
|
|
457
|
+
isExcluded: excludedIds.includes(id),
|
|
458
|
+
})}</>
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// ============================================================================
|
|
462
|
+
// Null Filter Trigger — Slot-based with action
|
|
463
|
+
// ============================================================================
|
|
464
|
+
|
|
465
|
+
export interface DataViewNullFilterTriggerProps {
|
|
466
|
+
name?: string
|
|
467
|
+
action: DataViewSetNullFilterAction
|
|
468
|
+
children: ReactElement
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
export const DataViewNullFilterTrigger = forwardRef<HTMLButtonElement, DataViewNullFilterTriggerProps>(
|
|
472
|
+
({ name, action, ...props }, ref) => {
|
|
473
|
+
name = resolveFilterName(name)
|
|
474
|
+
const [current, setFilter] = useDataViewNullFilter(name)
|
|
475
|
+
|
|
476
|
+
const toggleFilter = useCallback((): void => {
|
|
477
|
+
setFilter(action)
|
|
478
|
+
}, [action, setFilter])
|
|
479
|
+
|
|
480
|
+
const active = current === actionToStateNull[action]
|
|
481
|
+
const { onClick, ...otherProps } = props as React.ButtonHTMLAttributes<HTMLButtonElement>
|
|
482
|
+
|
|
483
|
+
return (
|
|
484
|
+
<Slot
|
|
485
|
+
ref={ref}
|
|
486
|
+
onClick={composeEventHandlers(onClick, toggleFilter)}
|
|
487
|
+
data-active={dataAttribute(active)}
|
|
488
|
+
data-current={current}
|
|
489
|
+
{...otherProps}
|
|
490
|
+
/>
|
|
491
|
+
)
|
|
492
|
+
},
|
|
493
|
+
)
|
|
494
|
+
DataViewNullFilterTrigger.displayName = 'DataViewNullFilterTrigger'
|
|
495
|
+
|
|
496
|
+
const actionToStateNull: Record<DataViewSetNullFilterAction, DataViewNullFilterState> = {
|
|
497
|
+
exclude: 'exclude',
|
|
498
|
+
include: 'include',
|
|
499
|
+
unset: 'none',
|
|
500
|
+
toggleInclude: 'include',
|
|
501
|
+
toggleExclude: 'exclude',
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// ============================================================================
|
|
505
|
+
// Generic Filter Reset — Slot-based
|
|
506
|
+
// ============================================================================
|
|
507
|
+
|
|
508
|
+
export interface DataViewFilterResetTriggerProps {
|
|
509
|
+
name?: string
|
|
510
|
+
children: ReactElement
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
export const DataViewFilterResetTrigger = forwardRef<HTMLButtonElement, DataViewFilterResetTriggerProps>(
|
|
514
|
+
({ name, ...props }, ref) => {
|
|
515
|
+
name = resolveFilterName(name)
|
|
516
|
+
const { filtering } = useDataViewContext()
|
|
517
|
+
const filter = filtering.filters.get(name)
|
|
518
|
+
const isActive = filter ? filter.handler.isActive(filter.artifact) : false
|
|
519
|
+
|
|
520
|
+
const handleReset = useCallback((): void => {
|
|
521
|
+
filtering.resetFilter(name!)
|
|
522
|
+
}, [filtering, name])
|
|
523
|
+
|
|
524
|
+
const { onClick, ...otherProps } = props as React.ButtonHTMLAttributes<HTMLButtonElement>
|
|
525
|
+
|
|
526
|
+
return (
|
|
527
|
+
<Slot
|
|
528
|
+
ref={ref}
|
|
529
|
+
onClick={composeEventHandlers(onClick, handleReset)}
|
|
530
|
+
disabled={!isActive}
|
|
531
|
+
data-active={dataAttribute(isActive)}
|
|
532
|
+
{...otherProps}
|
|
533
|
+
/>
|
|
534
|
+
)
|
|
535
|
+
},
|
|
536
|
+
)
|
|
537
|
+
DataViewFilterResetTrigger.displayName = 'DataViewFilterResetTrigger'
|
|
538
|
+
|
|
539
|
+
// ============================================================================
|
|
540
|
+
// Date Filter Reset Trigger — Slot-based
|
|
541
|
+
// ============================================================================
|
|
542
|
+
|
|
543
|
+
export interface DataViewDateFilterResetTriggerProps {
|
|
544
|
+
name?: string
|
|
545
|
+
/** Which part to reset: 'start', 'end', or undefined (both) */
|
|
546
|
+
type?: 'start' | 'end'
|
|
547
|
+
children: ReactElement
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
export const DataViewDateFilterResetTrigger = forwardRef<HTMLButtonElement, DataViewDateFilterResetTriggerProps>(
|
|
551
|
+
({ name, type, ...props }, ref) => {
|
|
552
|
+
name = resolveFilterName(name)
|
|
553
|
+
const [state, setFilter] = useDataViewFilter<import('@contember/bindx').DateFilterArtifact>(name)
|
|
554
|
+
const hasValue = type === 'start'
|
|
555
|
+
? (state?.start ?? null) !== null
|
|
556
|
+
: type === 'end'
|
|
557
|
+
? (state?.end ?? null) !== null
|
|
558
|
+
: (state?.start ?? null) !== null || (state?.end ?? null) !== null
|
|
559
|
+
|
|
560
|
+
const handleReset = useCallback((): void => {
|
|
561
|
+
if (type === 'start') {
|
|
562
|
+
setFilter(it => it ? { ...it, start: null } : undefined)
|
|
563
|
+
} else if (type === 'end') {
|
|
564
|
+
setFilter(it => it ? { ...it, end: null } : undefined)
|
|
565
|
+
} else {
|
|
566
|
+
setFilter(undefined)
|
|
567
|
+
}
|
|
568
|
+
}, [setFilter, type])
|
|
569
|
+
|
|
570
|
+
if (!hasValue) return null
|
|
571
|
+
|
|
572
|
+
const { onClick, ...otherProps } = props as React.ButtonHTMLAttributes<HTMLButtonElement>
|
|
573
|
+
|
|
574
|
+
return (
|
|
575
|
+
<Slot
|
|
576
|
+
ref={ref}
|
|
577
|
+
onClick={composeEventHandlers(onClick, handleReset)}
|
|
578
|
+
{...otherProps}
|
|
579
|
+
/>
|
|
580
|
+
)
|
|
581
|
+
},
|
|
582
|
+
)
|
|
583
|
+
DataViewDateFilterResetTrigger.displayName = 'DataViewDateFilterResetTrigger'
|
|
584
|
+
|
|
585
|
+
// ============================================================================
|
|
586
|
+
// Number Filter Reset Trigger — Slot-based
|
|
587
|
+
// ============================================================================
|
|
588
|
+
|
|
589
|
+
export interface DataViewNumberFilterResetTriggerProps {
|
|
590
|
+
name?: string
|
|
591
|
+
children: ReactElement
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
export const DataViewNumberFilterResetTrigger = forwardRef<HTMLButtonElement, DataViewNumberFilterResetTriggerProps>(
|
|
595
|
+
({ name, ...props }, ref) => {
|
|
596
|
+
name = resolveFilterName(name)
|
|
597
|
+
const [state, setFilter] = useDataViewFilter<NumberRangeFilterArtifact>(name)
|
|
598
|
+
const hasValue = (state?.min ?? null) !== null || (state?.max ?? null) !== null
|
|
599
|
+
|
|
600
|
+
const handleReset = useCallback((): void => {
|
|
601
|
+
setFilter(undefined)
|
|
602
|
+
}, [setFilter])
|
|
603
|
+
|
|
604
|
+
if (!hasValue) return null
|
|
605
|
+
|
|
606
|
+
const { onClick, ...otherProps } = props as React.ButtonHTMLAttributes<HTMLButtonElement>
|
|
607
|
+
|
|
608
|
+
return (
|
|
609
|
+
<Slot
|
|
610
|
+
ref={ref}
|
|
611
|
+
onClick={composeEventHandlers(onClick, handleReset)}
|
|
612
|
+
{...otherProps}
|
|
613
|
+
/>
|
|
614
|
+
)
|
|
615
|
+
},
|
|
616
|
+
)
|
|
617
|
+
DataViewNumberFilterResetTrigger.displayName = 'DataViewNumberFilterResetTrigger'
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Filter name context — provides the current filter name to descendant components.
|
|
3
|
+
*
|
|
4
|
+
* Used by DataViewTextFilter, DataViewBooleanFilter, etc. to scope
|
|
5
|
+
* trigger components so they don't need an explicit `name` prop.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { createContext, useContext } from 'react'
|
|
9
|
+
|
|
10
|
+
const DataViewFilterNameContext = createContext<string | null>(null)
|
|
11
|
+
|
|
12
|
+
export function useDataViewFilterName(): string {
|
|
13
|
+
const name = useContext(DataViewFilterNameContext)
|
|
14
|
+
if (name === null) {
|
|
15
|
+
throw new Error(
|
|
16
|
+
'useDataViewFilterName: no filter name in context. ' +
|
|
17
|
+
'Wrap this component in a DataViewTextFilter, DataViewBooleanFilter, DataViewFilterScope, etc.',
|
|
18
|
+
)
|
|
19
|
+
}
|
|
20
|
+
return name
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function useOptionalDataViewFilterName(): string | null {
|
|
24
|
+
return useContext(DataViewFilterNameContext)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export const DataViewFilterNameProvider = DataViewFilterNameContext.Provider
|