@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,239 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Loader state, reload trigger, and conditional rendering components using Radix Slot.
|
|
3
|
+
*
|
|
4
|
+
* Usage:
|
|
5
|
+
* ```tsx
|
|
6
|
+
* <DataViewReloadTrigger>
|
|
7
|
+
* <button>Refresh</button>
|
|
8
|
+
* </DataViewReloadTrigger>
|
|
9
|
+
*
|
|
10
|
+
* <DataViewLoaderState loaded={<Table />} refreshing={<Table />} initial={<Loading />} failed={<Error />} />
|
|
11
|
+
*
|
|
12
|
+
* <DataViewEmpty>No results found</DataViewEmpty>
|
|
13
|
+
* <DataViewNonEmpty><Table /></DataViewNonEmpty>
|
|
14
|
+
*
|
|
15
|
+
* <DataViewKeyboardEventHandler>
|
|
16
|
+
* <div tabIndex={0}>...</div>
|
|
17
|
+
* </DataViewKeyboardEventHandler>
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import React, { forwardRef, type ReactElement, useCallback } from 'react'
|
|
22
|
+
import { Slot } from '@radix-ui/react-slot'
|
|
23
|
+
import { composeEventHandlers } from '@radix-ui/primitive'
|
|
24
|
+
import { useDataViewContext, type DataViewItem } from './DataViewContext.js'
|
|
25
|
+
import type { DataViewLoaderState as DataViewLoaderStateType } from './DataViewContext.js'
|
|
26
|
+
|
|
27
|
+
// ============================================================================
|
|
28
|
+
// Reload Trigger
|
|
29
|
+
// ============================================================================
|
|
30
|
+
|
|
31
|
+
export interface DataViewReloadTriggerProps {
|
|
32
|
+
children: ReactElement
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export const DataViewReloadTrigger = forwardRef<HTMLButtonElement, DataViewReloadTriggerProps>(
|
|
36
|
+
(props, ref) => {
|
|
37
|
+
const { reload, loaderState } = useDataViewContext()
|
|
38
|
+
const { onClick, ...otherProps } = props as React.ButtonHTMLAttributes<HTMLButtonElement> & { children: ReactElement }
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<Slot
|
|
42
|
+
ref={ref}
|
|
43
|
+
onClick={composeEventHandlers(onClick, reload)}
|
|
44
|
+
data-state={loaderState}
|
|
45
|
+
{...otherProps}
|
|
46
|
+
/>
|
|
47
|
+
)
|
|
48
|
+
},
|
|
49
|
+
)
|
|
50
|
+
DataViewReloadTrigger.displayName = 'DataViewReloadTrigger'
|
|
51
|
+
|
|
52
|
+
// ============================================================================
|
|
53
|
+
// Loader State Switch (Contember: DataViewLoaderState)
|
|
54
|
+
// ============================================================================
|
|
55
|
+
|
|
56
|
+
// ---- DataViewLoaderState (Contember-matching API) ----
|
|
57
|
+
// Boolean-prop conditional wrapper: renders children when state matches any true flag
|
|
58
|
+
|
|
59
|
+
export interface DataViewLoaderStateProps {
|
|
60
|
+
children: React.ReactNode
|
|
61
|
+
/** Render children when data is loaded */
|
|
62
|
+
loaded?: boolean
|
|
63
|
+
/** Render children when refreshing (after initial load) */
|
|
64
|
+
refreshing?: boolean
|
|
65
|
+
/** Render children during initial load */
|
|
66
|
+
initial?: boolean
|
|
67
|
+
/** Render children on failure */
|
|
68
|
+
failed?: boolean
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function DataViewLoaderState({
|
|
72
|
+
children,
|
|
73
|
+
...props
|
|
74
|
+
}: DataViewLoaderStateProps): ReactElement | null {
|
|
75
|
+
const { loaderState } = useDataViewContext()
|
|
76
|
+
return (props as Record<string, boolean | undefined>)[loaderState] ? <>{children}</> : null
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// ---- DataViewLoaderStateSwitch (bindx convenience API) ----
|
|
80
|
+
// Switch pattern: separate ReactNode per state
|
|
81
|
+
|
|
82
|
+
export interface DataViewLoaderStateSwitchProps {
|
|
83
|
+
/** Render when data is loaded */
|
|
84
|
+
loaded?: React.ReactNode
|
|
85
|
+
/** Render when refreshing (after initial load) */
|
|
86
|
+
refreshing?: React.ReactNode
|
|
87
|
+
/** Render during initial load */
|
|
88
|
+
initial?: React.ReactNode
|
|
89
|
+
/** Render on failure */
|
|
90
|
+
failed?: React.ReactNode
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function DataViewLoaderStateSwitch({
|
|
94
|
+
loaded,
|
|
95
|
+
refreshing,
|
|
96
|
+
initial,
|
|
97
|
+
failed,
|
|
98
|
+
}: DataViewLoaderStateSwitchProps): ReactElement {
|
|
99
|
+
const { loaderState } = useDataViewContext()
|
|
100
|
+
|
|
101
|
+
switch (loaderState) {
|
|
102
|
+
case 'loaded':
|
|
103
|
+
return <>{loaded ?? null}</>
|
|
104
|
+
case 'refreshing':
|
|
105
|
+
return <>{refreshing ?? loaded ?? null}</>
|
|
106
|
+
case 'initial':
|
|
107
|
+
return <>{initial ?? null}</>
|
|
108
|
+
case 'failed':
|
|
109
|
+
return <>{failed ?? null}</>
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// ============================================================================
|
|
114
|
+
// Hooks
|
|
115
|
+
// ============================================================================
|
|
116
|
+
|
|
117
|
+
export function useDataViewLoaderState(): DataViewLoaderStateType {
|
|
118
|
+
return useDataViewContext().loaderState
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function useDataViewReload(): () => void {
|
|
122
|
+
return useDataViewContext().reload
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function useDataViewHighlightIndex(): number | null {
|
|
126
|
+
return useDataViewContext().highlightIndex
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// ============================================================================
|
|
130
|
+
// Empty / Non-Empty
|
|
131
|
+
// ============================================================================
|
|
132
|
+
|
|
133
|
+
export interface DataViewEmptyProps {
|
|
134
|
+
children: React.ReactNode
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function DataViewEmpty({ children }: DataViewEmptyProps): ReactElement {
|
|
138
|
+
const { loaderState, itemCount } = useDataViewContext()
|
|
139
|
+
if (loaderState !== 'loaded' || itemCount > 0) return <>{null}</>
|
|
140
|
+
return <>{children}</>
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export interface DataViewNonEmptyProps {
|
|
144
|
+
children: React.ReactNode
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export function DataViewNonEmpty({ children }: DataViewNonEmptyProps): ReactElement {
|
|
148
|
+
const { loaderState, itemCount } = useDataViewContext()
|
|
149
|
+
if (loaderState !== 'loaded' || itemCount === 0) return <>{null}</>
|
|
150
|
+
return <>{children}</>
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// ============================================================================
|
|
154
|
+
// Each Row
|
|
155
|
+
// ============================================================================
|
|
156
|
+
|
|
157
|
+
export interface DataViewEachRowProps {
|
|
158
|
+
children: (item: DataViewItem, index: number) => React.ReactNode
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export function DataViewEachRow({ children }: DataViewEachRowProps): ReactElement {
|
|
162
|
+
const { items } = useDataViewContext()
|
|
163
|
+
return <>{items.map((item, index) => children(item, index))}</>
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// ============================================================================
|
|
167
|
+
// Highlight Row — Slot-based
|
|
168
|
+
// ============================================================================
|
|
169
|
+
|
|
170
|
+
export interface DataViewHighlightRowProps {
|
|
171
|
+
index: number
|
|
172
|
+
children: ReactElement
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export const DataViewHighlightRow = forwardRef<HTMLElement, DataViewHighlightRowProps>(
|
|
176
|
+
({ index, ...props }, ref) => {
|
|
177
|
+
const { highlightIndex, setHighlightIndex } = useDataViewContext()
|
|
178
|
+
const isHighlighted = highlightIndex === index
|
|
179
|
+
|
|
180
|
+
const handleClick = useCallback((): void => {
|
|
181
|
+
setHighlightIndex(index)
|
|
182
|
+
}, [setHighlightIndex, index])
|
|
183
|
+
|
|
184
|
+
const { onClick, ...otherProps } = props as React.HTMLAttributes<HTMLElement> & { children: ReactElement }
|
|
185
|
+
|
|
186
|
+
return (
|
|
187
|
+
<Slot
|
|
188
|
+
ref={ref}
|
|
189
|
+
onClick={composeEventHandlers(onClick, handleClick)}
|
|
190
|
+
data-highlighted={isHighlighted ? '' : undefined}
|
|
191
|
+
{...otherProps}
|
|
192
|
+
/>
|
|
193
|
+
)
|
|
194
|
+
},
|
|
195
|
+
)
|
|
196
|
+
DataViewHighlightRow.displayName = 'DataViewHighlightRow'
|
|
197
|
+
|
|
198
|
+
// ============================================================================
|
|
199
|
+
// Keyboard Event Handler — Slot-based
|
|
200
|
+
// ============================================================================
|
|
201
|
+
|
|
202
|
+
export interface DataViewKeyboardEventHandlerProps {
|
|
203
|
+
children: ReactElement
|
|
204
|
+
onSelectHighlighted?: (item: DataViewItem) => void
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export const DataViewKeyboardEventHandler = forwardRef<HTMLElement, DataViewKeyboardEventHandlerProps>(
|
|
208
|
+
({ onSelectHighlighted, ...props }, ref) => {
|
|
209
|
+
const { items, highlightIndex, setHighlightIndex } = useDataViewContext()
|
|
210
|
+
|
|
211
|
+
const handleKeyDown = useCallback((e: React.KeyboardEvent): void => {
|
|
212
|
+
if (items.length === 0) return
|
|
213
|
+
|
|
214
|
+
if (e.key === 'ArrowDown') {
|
|
215
|
+
e.preventDefault()
|
|
216
|
+
setHighlightIndex(Math.min((highlightIndex ?? -1) + 1, items.length - 1))
|
|
217
|
+
} else if (e.key === 'ArrowUp') {
|
|
218
|
+
e.preventDefault()
|
|
219
|
+
setHighlightIndex(Math.max((highlightIndex ?? 1) - 1, 0))
|
|
220
|
+
} else if (e.key === 'Escape') {
|
|
221
|
+
setHighlightIndex(null)
|
|
222
|
+
} else if (e.key === 'Enter' && highlightIndex !== null && onSelectHighlighted) {
|
|
223
|
+
const item = items[highlightIndex]
|
|
224
|
+
if (item) onSelectHighlighted(item)
|
|
225
|
+
}
|
|
226
|
+
}, [items, highlightIndex, setHighlightIndex, onSelectHighlighted])
|
|
227
|
+
|
|
228
|
+
const { onKeyDown, ...otherProps } = props as React.HTMLAttributes<HTMLElement> & { children: ReactElement }
|
|
229
|
+
|
|
230
|
+
return (
|
|
231
|
+
<Slot
|
|
232
|
+
ref={ref}
|
|
233
|
+
onKeyDown={composeEventHandlers(onKeyDown, handleKeyDown)}
|
|
234
|
+
{...otherProps}
|
|
235
|
+
/>
|
|
236
|
+
)
|
|
237
|
+
},
|
|
238
|
+
)
|
|
239
|
+
DataViewKeyboardEventHandler.displayName = 'DataViewKeyboardEventHandler'
|
package/src/markers.ts
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Marker components for DataGrid children analysis.
|
|
3
|
+
*
|
|
4
|
+
* These are metadata components — they return null at runtime.
|
|
5
|
+
* Their props are extracted by `analyzeChildren()` during the DataGrid's
|
|
6
|
+
* collection phase and stored in DataViewContext for layout components to consume.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type React from 'react'
|
|
10
|
+
import type { ReactNode } from 'react'
|
|
11
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
12
|
+
import type { EntityAccessor } from '@contember/bindx'
|
|
13
|
+
|
|
14
|
+
// ============================================================================
|
|
15
|
+
// Toolbar Content Marker
|
|
16
|
+
// ============================================================================
|
|
17
|
+
|
|
18
|
+
export interface DataGridToolbarContentProps {
|
|
19
|
+
readonly children: React.ReactNode
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Marker for toolbar content inside a DataGrid children render function.
|
|
24
|
+
* At analysis time, the children ReactNode is extracted and stored in context.
|
|
25
|
+
* At runtime, returns null (content is rendered by toolbar layout components).
|
|
26
|
+
*/
|
|
27
|
+
export function DataGridToolbarContent(_props: DataGridToolbarContentProps): null {
|
|
28
|
+
return null
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// ============================================================================
|
|
32
|
+
// Layout Marker (replaces DataGridTileLayout)
|
|
33
|
+
// ============================================================================
|
|
34
|
+
|
|
35
|
+
export interface DataGridLayoutProps<T = never> {
|
|
36
|
+
/** Unique name for this layout (e.g. "grid", "rows", "cards") */
|
|
37
|
+
readonly name: string
|
|
38
|
+
/** Label shown in the layout switcher UI (supports ReactNode for icons) */
|
|
39
|
+
readonly label?: ReactNode
|
|
40
|
+
/** Pass the DataGrid's `it` parameter for type inference on the children callback. */
|
|
41
|
+
readonly item?: T
|
|
42
|
+
readonly children: (item: T) => ReactNode
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Marker for a named layout inside a DataGrid children render function.
|
|
47
|
+
* Multiple DataGridLayout markers can coexist with different names.
|
|
48
|
+
* At analysis time, the callback is extracted and called with the collector proxy
|
|
49
|
+
* to discover field selections. At runtime, returns null (callback is stored in
|
|
50
|
+
* context and invoked per-item by layout rendering components).
|
|
51
|
+
*/
|
|
52
|
+
export function DataGridLayout<T>(_props: DataGridLayoutProps<T>): null {
|
|
53
|
+
return null
|
|
54
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Composable paging UI components using Radix Slot pattern.
|
|
3
|
+
*
|
|
4
|
+
* Usage:
|
|
5
|
+
* ```tsx
|
|
6
|
+
* <DataViewChangePageTrigger page="previous">
|
|
7
|
+
* <button>Previous</button>
|
|
8
|
+
* </DataViewChangePageTrigger>
|
|
9
|
+
*
|
|
10
|
+
* <DataViewChangePageTrigger page="next">
|
|
11
|
+
* <button>Next</button>
|
|
12
|
+
* </DataViewChangePageTrigger>
|
|
13
|
+
*
|
|
14
|
+
* <DataViewSetItemsPerPageTrigger value={25}>
|
|
15
|
+
* <button>25 per page</button>
|
|
16
|
+
* </DataViewSetItemsPerPageTrigger>
|
|
17
|
+
*
|
|
18
|
+
* <DataViewPagingStateView>
|
|
19
|
+
* {({ pageIndex, totalPages }) => <span>Page {pageIndex + 1} of {totalPages}</span>}
|
|
20
|
+
* </DataViewPagingStateView>
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import React, { forwardRef, type ReactElement, type ReactNode, useCallback } from 'react'
|
|
25
|
+
import { Slot } from '@radix-ui/react-slot'
|
|
26
|
+
import { composeEventHandlers } from '@radix-ui/primitive'
|
|
27
|
+
import { useDataViewContext } from './DataViewContext.js'
|
|
28
|
+
import { dataAttribute } from './dataAttribute.js'
|
|
29
|
+
|
|
30
|
+
// ============================================================================
|
|
31
|
+
// DataViewChangePageTrigger
|
|
32
|
+
// ============================================================================
|
|
33
|
+
|
|
34
|
+
export interface DataViewChangePageTriggerProps {
|
|
35
|
+
/** Target page: named shortcut or zero-based page index */
|
|
36
|
+
page: 'first' | 'last' | 'next' | 'previous' | number
|
|
37
|
+
/** Button element */
|
|
38
|
+
children: ReactElement
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export const DataViewChangePageTrigger = forwardRef<HTMLButtonElement, DataViewChangePageTriggerProps>(
|
|
42
|
+
({ page, ...props }, ref) => {
|
|
43
|
+
const { paging } = useDataViewContext()
|
|
44
|
+
|
|
45
|
+
const isDisabled = resolveIsDisabled(page, paging.hasPrevious, paging.hasNext, paging.state.pageIndex, paging.info.totalPages)
|
|
46
|
+
const isActive = typeof page === 'number' && page === paging.state.pageIndex
|
|
47
|
+
|
|
48
|
+
const handleClick = useCallback((): void => {
|
|
49
|
+
if (page === 'first') paging.first()
|
|
50
|
+
else if (page === 'last') paging.last()
|
|
51
|
+
else if (page === 'next') paging.next()
|
|
52
|
+
else if (page === 'previous') paging.previous()
|
|
53
|
+
else paging.goTo(page)
|
|
54
|
+
}, [paging, page])
|
|
55
|
+
|
|
56
|
+
const { onClick, ...otherProps } = props as React.ButtonHTMLAttributes<HTMLButtonElement>
|
|
57
|
+
|
|
58
|
+
return (
|
|
59
|
+
<Slot
|
|
60
|
+
ref={ref}
|
|
61
|
+
onClick={composeEventHandlers(onClick, handleClick)}
|
|
62
|
+
disabled={isDisabled}
|
|
63
|
+
data-active={dataAttribute(isActive)}
|
|
64
|
+
data-current={String(paging.state.pageIndex)}
|
|
65
|
+
{...otherProps}
|
|
66
|
+
/>
|
|
67
|
+
)
|
|
68
|
+
},
|
|
69
|
+
)
|
|
70
|
+
DataViewChangePageTrigger.displayName = 'DataViewChangePageTrigger'
|
|
71
|
+
|
|
72
|
+
function resolveIsDisabled(
|
|
73
|
+
page: DataViewChangePageTriggerProps['page'],
|
|
74
|
+
hasPrevious: boolean,
|
|
75
|
+
hasNext: boolean,
|
|
76
|
+
currentPageIndex: number,
|
|
77
|
+
totalPages: number | null,
|
|
78
|
+
): boolean {
|
|
79
|
+
if (page === 'first' || page === 'previous') return !hasPrevious
|
|
80
|
+
if (page === 'next') return !hasNext
|
|
81
|
+
if (page === 'last') return totalPages === null || currentPageIndex >= totalPages - 1
|
|
82
|
+
return page === currentPageIndex
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// ============================================================================
|
|
86
|
+
// DataViewSetItemsPerPageTrigger
|
|
87
|
+
// ============================================================================
|
|
88
|
+
|
|
89
|
+
export interface DataViewSetItemsPerPageTriggerProps {
|
|
90
|
+
/** Items per page to set. null = show all. */
|
|
91
|
+
value: number | null
|
|
92
|
+
/** Button element */
|
|
93
|
+
children: ReactElement
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export const DataViewSetItemsPerPageTrigger = forwardRef<HTMLButtonElement, DataViewSetItemsPerPageTriggerProps>(
|
|
97
|
+
({ value, ...props }, ref) => {
|
|
98
|
+
const { paging } = useDataViewContext()
|
|
99
|
+
const currentItemsPerPage = paging.state.itemsPerPage === 0 ? null : paging.state.itemsPerPage
|
|
100
|
+
const isActive = currentItemsPerPage === value
|
|
101
|
+
|
|
102
|
+
const handleClick = useCallback((): void => {
|
|
103
|
+
paging.setItemsPerPage(value)
|
|
104
|
+
}, [paging, value])
|
|
105
|
+
|
|
106
|
+
const { onClick, ...otherProps } = props as React.ButtonHTMLAttributes<HTMLButtonElement>
|
|
107
|
+
|
|
108
|
+
return (
|
|
109
|
+
<Slot
|
|
110
|
+
ref={ref}
|
|
111
|
+
onClick={composeEventHandlers(onClick, handleClick)}
|
|
112
|
+
data-active={dataAttribute(isActive)}
|
|
113
|
+
{...otherProps}
|
|
114
|
+
/>
|
|
115
|
+
)
|
|
116
|
+
},
|
|
117
|
+
)
|
|
118
|
+
DataViewSetItemsPerPageTrigger.displayName = 'DataViewSetItemsPerPageTrigger'
|
|
119
|
+
|
|
120
|
+
// ============================================================================
|
|
121
|
+
// DataViewPagingStateView (Contember name) / DataViewPagingInfo (alias)
|
|
122
|
+
// ============================================================================
|
|
123
|
+
|
|
124
|
+
export interface DataViewPagingInfoProps {
|
|
125
|
+
children: (info: {
|
|
126
|
+
pageIndex: number
|
|
127
|
+
totalPages: number | null
|
|
128
|
+
totalCount: number | null
|
|
129
|
+
itemsPerPage: number
|
|
130
|
+
}) => ReactNode
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function DataViewPagingStateView({
|
|
134
|
+
children,
|
|
135
|
+
}: DataViewPagingInfoProps): ReactElement {
|
|
136
|
+
const { paging } = useDataViewContext()
|
|
137
|
+
|
|
138
|
+
return <>{children({
|
|
139
|
+
pageIndex: paging.state.pageIndex,
|
|
140
|
+
totalPages: paging.info.totalPages,
|
|
141
|
+
totalCount: paging.info.totalCount,
|
|
142
|
+
itemsPerPage: paging.state.itemsPerPage,
|
|
143
|
+
})}</>
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** Alias for DataViewPagingStateView */
|
|
147
|
+
export const DataViewPagingInfo = DataViewPagingStateView
|
|
148
|
+
|
|
149
|
+
export type DataViewPagingStateViewProps = DataViewPagingInfoProps
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MultiSelect — headless multi-select component for has-many relations.
|
|
3
|
+
*
|
|
4
|
+
* Manages selection state for a has-many relation field and provides
|
|
5
|
+
* context for descendant components (SelectDataView, SelectOption, etc.).
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* ```tsx
|
|
9
|
+
* <MultiSelect relation={article.tags} options={schema.Tag}>
|
|
10
|
+
* <SelectDataView queryField={['name']}>
|
|
11
|
+
* {it => <SelectOption><SelectItemTrigger><Field field={it.name} /></SelectItemTrigger></SelectOption>}
|
|
12
|
+
* </SelectDataView>
|
|
13
|
+
* </MultiSelect>
|
|
14
|
+
* ```
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { type ReactNode, useCallback, useMemo } from 'react'
|
|
18
|
+
import type { EntityAccessor, EntityDef, HasManyRef } from '@contember/bindx'
|
|
19
|
+
import {
|
|
20
|
+
SelectCurrentEntitiesContext,
|
|
21
|
+
SelectHandleSelectContext,
|
|
22
|
+
SelectIsSelectedContext,
|
|
23
|
+
SelectOptionsContext,
|
|
24
|
+
type SelectAction,
|
|
25
|
+
type SelectEvents,
|
|
26
|
+
type SelectHandler,
|
|
27
|
+
} from './selectContext.js'
|
|
28
|
+
|
|
29
|
+
export interface MultiSelectProps extends SelectEvents {
|
|
30
|
+
/** Has-many relation ref from parent entity */
|
|
31
|
+
relation: HasManyRef<object>
|
|
32
|
+
/** Entity definition for the options list */
|
|
33
|
+
options: EntityDef
|
|
34
|
+
children: ReactNode
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function MultiSelect({
|
|
38
|
+
relation,
|
|
39
|
+
options,
|
|
40
|
+
children,
|
|
41
|
+
onSelect,
|
|
42
|
+
onUnselect,
|
|
43
|
+
}: MultiSelectProps): ReactNode {
|
|
44
|
+
const items = relation.items
|
|
45
|
+
const selectedIds = useMemo(
|
|
46
|
+
() => new Set(items.map(it => it.id)),
|
|
47
|
+
[items],
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
const entitiesArr = useMemo(
|
|
51
|
+
(): readonly EntityAccessor<object>[] => items as EntityAccessor<object>[],
|
|
52
|
+
[items],
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
const handleSelect = useCallback<SelectHandler>(
|
|
56
|
+
(entity: EntityAccessor<object>, action: SelectAction = 'toggle') => {
|
|
57
|
+
const isCurrentlySelected = selectedIds.has(entity.id)
|
|
58
|
+
if (action === 'toggle') {
|
|
59
|
+
action = isCurrentlySelected ? 'unselect' : 'select'
|
|
60
|
+
}
|
|
61
|
+
if (action === 'unselect' && isCurrentlySelected) {
|
|
62
|
+
relation.disconnect(entity.id)
|
|
63
|
+
onUnselect?.(entity)
|
|
64
|
+
} else if (action === 'select' && !isCurrentlySelected) {
|
|
65
|
+
relation.connect(entity.id)
|
|
66
|
+
onSelect?.(entity)
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
[relation, selectedIds, onSelect, onUnselect],
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
const isSelected = useCallback(
|
|
73
|
+
(entity: EntityAccessor<object>): boolean => selectedIds.has(entity.id),
|
|
74
|
+
[selectedIds],
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
return (
|
|
78
|
+
<SelectCurrentEntitiesContext.Provider value={entitiesArr}>
|
|
79
|
+
<SelectIsSelectedContext.Provider value={isSelected}>
|
|
80
|
+
<SelectHandleSelectContext.Provider value={handleSelect}>
|
|
81
|
+
<SelectOptionsContext.Provider value={options}>
|
|
82
|
+
{children}
|
|
83
|
+
</SelectOptionsContext.Provider>
|
|
84
|
+
</SelectHandleSelectContext.Provider>
|
|
85
|
+
</SelectIsSelectedContext.Provider>
|
|
86
|
+
</SelectCurrentEntitiesContext.Provider>
|
|
87
|
+
)
|
|
88
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Select — headless single-select component for has-one relations.
|
|
3
|
+
*
|
|
4
|
+
* Manages selection state for a has-one relation field and provides
|
|
5
|
+
* context for descendant components (SelectDataView, SelectOption, etc.).
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* ```tsx
|
|
9
|
+
* <Select relation={article.category} options={schema.Category}>
|
|
10
|
+
* <SelectDataView queryField={['name']}>
|
|
11
|
+
* ...options rendering...
|
|
12
|
+
* </SelectDataView>
|
|
13
|
+
* </Select>
|
|
14
|
+
* ```
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { type ReactNode, useCallback, useMemo } from 'react'
|
|
18
|
+
import type { EntityAccessor, EntityDef, HasOneRef } from '@contember/bindx'
|
|
19
|
+
import { isPlaceholderId } from '@contember/bindx'
|
|
20
|
+
import {
|
|
21
|
+
SelectCurrentEntitiesContext,
|
|
22
|
+
SelectHandleSelectContext,
|
|
23
|
+
SelectIsSelectedContext,
|
|
24
|
+
SelectOptionsContext,
|
|
25
|
+
type SelectAction,
|
|
26
|
+
type SelectEvents,
|
|
27
|
+
type SelectHandler,
|
|
28
|
+
} from './selectContext.js'
|
|
29
|
+
|
|
30
|
+
export interface SelectProps extends SelectEvents {
|
|
31
|
+
/** Has-one relation ref from parent entity */
|
|
32
|
+
relation: HasOneRef<object>
|
|
33
|
+
/** Entity definition for the options list */
|
|
34
|
+
options: EntityDef
|
|
35
|
+
children: ReactNode
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function Select({
|
|
39
|
+
relation,
|
|
40
|
+
options,
|
|
41
|
+
children,
|
|
42
|
+
onSelect,
|
|
43
|
+
onUnselect,
|
|
44
|
+
}: SelectProps): ReactNode {
|
|
45
|
+
const entity = relation.$entity
|
|
46
|
+
const entityId = entity.id
|
|
47
|
+
const isConnected = !isPlaceholderId(entityId)
|
|
48
|
+
const currentId = isConnected ? entityId : null
|
|
49
|
+
|
|
50
|
+
const entitiesArr = useMemo(
|
|
51
|
+
(): readonly EntityAccessor<object>[] =>
|
|
52
|
+
isConnected ? [entity as EntityAccessor<object>] : [],
|
|
53
|
+
[isConnected, entity],
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
const handleSelect = useCallback<SelectHandler>(
|
|
57
|
+
(selected: EntityAccessor<object>, action: SelectAction = 'toggle') => {
|
|
58
|
+
if (action === 'toggle') {
|
|
59
|
+
action = selected.id === currentId ? 'unselect' : 'select'
|
|
60
|
+
}
|
|
61
|
+
if (action === 'unselect') {
|
|
62
|
+
relation.$disconnect()
|
|
63
|
+
onUnselect?.(selected)
|
|
64
|
+
} else if (action === 'select') {
|
|
65
|
+
relation.$connect(selected.id)
|
|
66
|
+
onSelect?.(selected)
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
[relation, currentId, onSelect, onUnselect],
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
const isSelected = useCallback(
|
|
73
|
+
(entity: EntityAccessor<object>): boolean => entity.id === currentId,
|
|
74
|
+
[currentId],
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
return (
|
|
78
|
+
<SelectCurrentEntitiesContext.Provider value={entitiesArr}>
|
|
79
|
+
<SelectIsSelectedContext.Provider value={isSelected}>
|
|
80
|
+
<SelectHandleSelectContext.Provider value={handleSelect}>
|
|
81
|
+
<SelectOptionsContext.Provider value={options}>
|
|
82
|
+
{children}
|
|
83
|
+
</SelectOptionsContext.Provider>
|
|
84
|
+
</SelectHandleSelectContext.Provider>
|
|
85
|
+
</SelectIsSelectedContext.Provider>
|
|
86
|
+
</SelectCurrentEntitiesContext.Provider>
|
|
87
|
+
)
|
|
88
|
+
}
|