Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion packages/bindx-dataview/src/DataGrid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {
import { useDataViewKey } from './DataViewKeyProvider.js'
import { DataViewProvider, type DataViewContextValue, type DataViewLoaderState } from './DataViewContext.js'
import { useDataGridSetup, QUERY_FILTER_NAME } from './useDataGridSetup.js'
import { useListFetchAllData } from './useListFetchAllData.js'

export { QUERY_FILTER_NAME }

Expand Down Expand Up @@ -152,6 +153,14 @@ function DataGridImpl<TRoleMap extends Record<string, object>>({
setHighlightIndex(null)
}, [items])

// ---- Unpaged fetch of the same list the grid loads ----
const fetchAllData = useListFetchAllData({
entityType,
filter: setup.combinedFilter,
orderBy: setup.sorting.resolvedOrderBy,
selection: setup.selection,
})

const contextValue = useMemo((): DataViewContextValue => ({
filtering: setup.filtering,
sorting: setup.sorting,
Expand All @@ -166,10 +175,11 @@ function DataGridImpl<TRoleMap extends Record<string, object>>({
highlightIndex,
setHighlightIndex,
selectionMeta: setup.selection,
fetchAllData,
toolbarContent: setup.toolbarContent,
layoutRenders: setup.layoutRenders,
layoutElements: setup.layoutElements,
}), [setup.filtering, setup.sorting, setup.paging, setup.selectionState, setup.columns, entityType, items, itemCount, loaderState, reload, highlightIndex, setup.selection, setup.toolbarContent, setup.layoutRenders, setup.layoutElements])
}), [setup.filtering, setup.sorting, setup.paging, setup.selectionState, setup.columns, entityType, items, itemCount, loaderState, reload, highlightIndex, setup.selection, fetchAllData, setup.toolbarContent, setup.layoutRenders, setup.layoutElements])

return (
<DataViewProvider value={contextValue}>
Expand Down
5 changes: 5 additions & 0 deletions packages/bindx-dataview/src/DataViewContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ export interface DataViewElementData {
readonly fallback?: boolean
}

/** Fetches every row in the view's scope, unpaged. Resolves to null when the query returned an unexpected result. */
export type DataViewFetchAllData = () => Promise<readonly Record<string, unknown>[] | null>

export interface DataViewContextValue {
readonly filtering: FilteringState
readonly sorting: SortingStateResult
Expand All @@ -32,6 +35,8 @@ export interface DataViewContextValue {
readonly highlightIndex: number | null
readonly setHighlightIndex: (index: number | null) => void
readonly selectionMeta: SelectionMeta
/** Owned by each view, so the query keeps the scope the view loads with (static filter, parent relation). */
readonly fetchAllData: DataViewFetchAllData
readonly toolbarContent?: React.ReactNode
/** Named layout render callbacks — analyzed during collection, called per item at runtime */
readonly layoutRenders: ReadonlyMap<string, (item: DataViewItem) => React.ReactNode>
Expand Down
88 changes: 38 additions & 50 deletions packages/bindx-dataview/src/HasManyDataGrid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@ import type {
import { FIELD_REF_META } from '@contember/bindx'
import {
buildQueryFromSelection,
generateHasManyAlias,
EntityHandle,
setEntityData,
} from '@contember/bindx'
Expand All @@ -40,8 +39,9 @@ import {
useBindxContext,
} from '@contember/bindx-react'
import { useDataViewKey } from './DataViewKeyProvider.js'
import { DataViewProvider, type DataViewContextValue, type DataViewLoaderState } from './DataViewContext.js'
import { DataViewProvider, type DataViewContextValue, type DataViewFetchAllData, type DataViewLoaderState } from './DataViewContext.js'
import { useDataGridSetup } from './useDataGridSetup.js'
import { buildHasManyRelationQuery, extractHasManyRelationRows } from './hasManyRelationQuery.js'

// ============================================================================
// Props
Expand Down Expand Up @@ -133,17 +133,6 @@ function HasManyDataGridImpl<TEntity extends object>({
const [loaderState, setLoaderState] = useState<DataViewLoaderState>('initial')
const [listState, setListState] = useState<ListState>(INITIAL_LIST_STATE)

// ---- Build relation query spec ----
const alias = useMemo(
() => generateHasManyAlias(fieldName, {
filter: setup.combinedFilter,
orderBy: setup.sorting.resolvedOrderBy as unknown[],
limit: setup.paging.queryLimit,
offset: setup.paging.queryOffset,
}),
[fieldName, setup.combinedFilter, setup.sorting.resolvedOrderBy, setup.paging.queryLimit, setup.paging.queryOffset],
)

const optionsKey = useMemo(
() => JSON.stringify({
filter: setup.combinedFilter ?? {},
Expand All @@ -161,40 +150,25 @@ function HasManyDataGridImpl<TEntity extends object>({

const fetchData = async (): Promise<void> => {
try {
const targetSpec = buildQueryFromSelection(setup.selection)
const currentOptions = JSON.parse(optionsKey) as {
filter: Record<string, unknown>
orderBy: readonly Record<string, unknown>[]
limit?: number
offset?: number
}

const parentSpec = {
fields: [
{ name: 'id', sourcePath: ['id'] },
{
name: alias,
sourcePath: [fieldName],
isArray: true as const,
totalCount: true,
filter: Object.keys(currentOptions.filter).length > 0 ? currentOptions.filter : undefined,
orderBy: currentOptions.orderBy.length > 0 ? currentOptions.orderBy : undefined,
limit: currentOptions.limit,
offset: currentOptions.offset,
nested: targetSpec,
},
],
}
const { alias, query } = buildHasManyRelationQuery({
parentEntityType,
parentEntityId,
fieldName,
filter: currentOptions.filter,
orderBy: currentOptions.orderBy,
limit: currentOptions.limit,
offset: currentOptions.offset,
targetSpec: buildQueryFromSelection(setup.selection),
})

const result = await batcher.enqueue(
{
type: 'get',
entityType: parentEntityType,
by: { id: parentEntityId },
spec: parentSpec,
},
{ signal: abortController.signal },
)
const result = await batcher.enqueue(query, { signal: abortController.signal })

if (abortController.signal.aborted) return

Expand All @@ -203,25 +177,21 @@ function HasManyDataGridImpl<TEntity extends object>({
return
}

const relationData = (result.data[alias] ?? result.data[fieldName]) as Array<Record<string, unknown>> | undefined
const totalCount = Array.isArray(relationData) && 'totalCount' in relationData
? (relationData as Array<Record<string, unknown>> & { totalCount: number }).totalCount
: undefined

if (!Array.isArray(relationData)) {
setListState({ status: 'ready', items: [], totalCount })
const relation = extractHasManyRelationRows(result.data, { alias, fieldName })
if (!relation) {
setListState({ status: 'ready', items: [] })
return
}

const items = relationData.map((data: Record<string, unknown>) => {
const items = relation.rows.map((data: Record<string, unknown>) => {
const id = data['id'] as string
dispatcher.dispatch(
setEntityData(targetEntityType, id, data, true),
)
return { id, data: data as object }
})

setListState({ status: 'ready', items, totalCount })
setListState({ status: 'ready', items, totalCount: relation.totalCount })
} catch (error) {
if (abortController.signal.aborted) return
setListState({ status: 'error', items: [] })
Expand All @@ -233,7 +203,7 @@ function HasManyDataGridImpl<TEntity extends object>({
return () => {
abortController.abort()
}
}, [parentEntityType, parentEntityId, fieldName, targetEntityType, alias, optionsKey, setup.selection, batcher, dispatcher, store])
}, [parentEntityType, parentEntityId, fieldName, targetEntityType, optionsKey, setup.selection, batcher, dispatcher, store])

// ---- Build items from state ----
const items = useMemo((): EntityAccessor<TEntity>[] => {
Expand Down Expand Up @@ -285,6 +255,23 @@ function HasManyDataGridImpl<TEntity extends object>({
setHighlightIndex(null)
}, [items])

// ---- Unpaged read of the same parent-scoped relation ----
const fetchAllData = useCallback<DataViewFetchAllData>(async () => {
const { alias, query } = buildHasManyRelationQuery({
parentEntityType,
parentEntityId,
fieldName,
filter: setup.combinedFilter,
orderBy: setup.sorting.resolvedOrderBy,
targetSpec: buildQueryFromSelection(setup.selection),
})

const result = await batcher.enqueue(query)
if (result.type !== 'get' || !result.data) return null

return extractHasManyRelationRows(result.data, { alias, fieldName })?.rows ?? null
}, [parentEntityType, parentEntityId, fieldName, setup.combinedFilter, setup.sorting.resolvedOrderBy, setup.selection, batcher])

const contextValue = useMemo((): DataViewContextValue => ({
filtering: setup.filtering,
sorting: setup.sorting,
Expand All @@ -299,10 +286,11 @@ function HasManyDataGridImpl<TEntity extends object>({
highlightIndex,
setHighlightIndex,
selectionMeta: setup.selection,
fetchAllData,
toolbarContent: setup.toolbarContent,
layoutRenders: setup.layoutRenders,
layoutElements: setup.layoutElements,
}), [setup.filtering, setup.sorting, setup.paging, setup.selectionState, setup.columns, targetEntityType, items, itemCount, loaderState, reload, highlightIndex, setup.selection, setup.toolbarContent, setup.layoutRenders, setup.layoutElements])
}), [setup.filtering, setup.sorting, setup.paging, setup.selectionState, setup.columns, targetEntityType, items, itemCount, loaderState, reload, highlightIndex, setup.selection, fetchAllData, setup.toolbarContent, setup.layoutRenders, setup.layoutElements])

return (
<DataViewProvider value={contextValue}>
Expand Down
50 changes: 10 additions & 40 deletions packages/bindx-dataview/src/export.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,6 @@ import React, { forwardRef, type ReactElement, useCallback, useState } from 'rea
import { Slot } from '@radix-ui/react-slot'
import { composeEventHandlers } from '@radix-ui/primitive'
import { useDataViewContext } from './DataViewContext.js'
import { useBindxContext } from '@contember/bindx-react'
import { buildQueryFromSelection } from '@contember/bindx'
import type { ListQuery, ListQueryResult } from '@contember/bindx'

// ============================================================================
// Export Factory Interface
Expand Down Expand Up @@ -117,32 +114,20 @@ export interface DataViewExportTriggerProps {

export const DataViewExportTrigger = forwardRef<HTMLButtonElement, DataViewExportTriggerProps>(
({ baseName, exportFactory = defaultExportFactory, onlyVisible = false, ...props }, ref) => {
const { columns, entityType, filtering, selection, selectionMeta } = useDataViewContext()
const { adapter } = useBindxContext()
const { columns, entityType, fetchAllData, selection } = useDataViewContext()
const [isExporting, setIsExporting] = useState(false)

const handleExport = useCallback(async (): Promise<void> => {
if (!adapter || isExporting) return
if (isExporting) return
setIsExporting(true)

try {
const visibleColumns = onlyVisible
? columns.filter((c, i) => selection.isVisible(c.fieldName ?? `col-${i}`))
: columns

const listQuery: ListQuery = {
type: 'list',
entityType,
filter: filtering.resolvedWhere,
orderBy: undefined,
limit: undefined,
offset: undefined,
spec: buildQueryFromSelection(selectionMeta),
}

const results = await adapter.query([listQuery])
const result = results[0]
if (!result || result.type !== 'list') {
const data = await fetchAllData()
if (!data) {
console.error('Export failed: unexpected result')
return
}
Expand All @@ -152,7 +137,7 @@ export const DataViewExportTrigger = forwardRef<HTMLButtonElement, DataViewExpor
.map(c => ({ name: String(c.header ?? c.fieldName ?? ''), fieldName: c.fieldName }))

const { blob, extension } = exportFactory.create({
data: (result as ListQueryResult).data,
data,
columns: exportColumns,
})

Expand All @@ -169,7 +154,7 @@ export const DataViewExportTrigger = forwardRef<HTMLButtonElement, DataViewExpor
} finally {
setIsExporting(false)
}
}, [adapter, columns, entityType, filtering.resolvedWhere, selectionMeta, exportFactory, baseName, isExporting, onlyVisible, selection])
}, [columns, entityType, fetchAllData, exportFactory, baseName, isExporting, onlyVisible, selection])

const { onClick, ...otherProps } = props as React.ButtonHTMLAttributes<HTMLButtonElement>

Expand All @@ -195,25 +180,10 @@ export interface FetchAllDataResult {
}

export function useDataViewFetchAllData(): () => Promise<FetchAllDataResult | null> {
const { entityType, filtering, selectionMeta } = useDataViewContext()
const { adapter } = useBindxContext()
const { fetchAllData } = useDataViewContext()

return useCallback(async (): Promise<FetchAllDataResult | null> => {
if (!adapter) return null

const listQuery: ListQuery = {
type: 'list',
entityType,
filter: filtering.resolvedWhere,
orderBy: undefined,
limit: undefined,
offset: undefined,
spec: buildQueryFromSelection(selectionMeta),
}

const results = await adapter.query([listQuery])
const result = results[0]
if (!result || result.type !== 'list') return null
return { data: (result as ListQueryResult).data }
}, [adapter, entityType, filtering.resolvedWhere, selectionMeta])
const data = await fetchAllData()
return data ? { data } : null
}, [fetchAllData])
}
Loading