From 4cc8f39b20bef6336d3cf89ed3d3abd390f47cfc Mon Sep 17 00:00:00 2001 From: Rohan Chakraborty Date: Tue, 14 Jul 2026 21:26:47 +0530 Subject: [PATCH] chore: remove dataview beta --- .../{dataview-beta => dataview}/page.tsx | 2 +- .../data-view-beta/components/content.tsx | 407 ----------------- .../data-view-beta/components/custom.tsx | 44 -- .../components/display-access.tsx | 33 -- .../components/display-properties.tsx | 39 -- .../components/display-settings.tsx | 131 ------ .../data-view-beta/components/empty-state.tsx | 33 -- .../data-view-beta/components/filters.tsx | 166 ------- .../data-view-beta/components/grouping.tsx | 62 --- .../data-view-beta/components/list.tsx | 425 ----------------- .../data-view-beta/components/ordering.tsx | 76 ---- .../data-view-beta/components/renderer.tsx | 28 -- .../data-view-beta/components/search.tsx | 64 --- .../data-view-beta/components/table.tsx | 16 - .../data-view-beta/components/toolbar.tsx | 50 -- .../components/view-switcher.tsx | 44 -- .../components/virtualized-content.tsx | 428 ------------------ .../data-view-beta/components/zero-state.tsx | 32 -- .../components/data-view-beta/context.tsx | 9 - .../data-view-beta/data-view.module.css | 333 -------------- .../components/data-view-beta/data-view.tsx | 337 -------------- .../data-view-beta/data-view.types.tsx | 281 ------------ .../data-view-beta/hooks/useDataView.tsx | 13 - .../data-view-beta/hooks/useFilters.tsx | 89 ---- .../components/data-view-beta/index.ts | 24 - .../utils/filter-operations.tsx | 292 ------------ .../components/data-view-beta/utils/index.tsx | 360 --------------- 27 files changed, 1 insertion(+), 3817 deletions(-) rename apps/www/src/app/examples/{dataview-beta => dataview}/page.tsx (99%) delete mode 100644 packages/raystack/components/data-view-beta/components/content.tsx delete mode 100644 packages/raystack/components/data-view-beta/components/custom.tsx delete mode 100644 packages/raystack/components/data-view-beta/components/display-access.tsx delete mode 100644 packages/raystack/components/data-view-beta/components/display-properties.tsx delete mode 100644 packages/raystack/components/data-view-beta/components/display-settings.tsx delete mode 100644 packages/raystack/components/data-view-beta/components/empty-state.tsx delete mode 100644 packages/raystack/components/data-view-beta/components/filters.tsx delete mode 100644 packages/raystack/components/data-view-beta/components/grouping.tsx delete mode 100644 packages/raystack/components/data-view-beta/components/list.tsx delete mode 100644 packages/raystack/components/data-view-beta/components/ordering.tsx delete mode 100644 packages/raystack/components/data-view-beta/components/renderer.tsx delete mode 100644 packages/raystack/components/data-view-beta/components/search.tsx delete mode 100644 packages/raystack/components/data-view-beta/components/table.tsx delete mode 100644 packages/raystack/components/data-view-beta/components/toolbar.tsx delete mode 100644 packages/raystack/components/data-view-beta/components/view-switcher.tsx delete mode 100644 packages/raystack/components/data-view-beta/components/virtualized-content.tsx delete mode 100644 packages/raystack/components/data-view-beta/components/zero-state.tsx delete mode 100644 packages/raystack/components/data-view-beta/context.tsx delete mode 100644 packages/raystack/components/data-view-beta/data-view.module.css delete mode 100644 packages/raystack/components/data-view-beta/data-view.tsx delete mode 100644 packages/raystack/components/data-view-beta/data-view.types.tsx delete mode 100644 packages/raystack/components/data-view-beta/hooks/useDataView.tsx delete mode 100644 packages/raystack/components/data-view-beta/hooks/useFilters.tsx delete mode 100644 packages/raystack/components/data-view-beta/index.ts delete mode 100644 packages/raystack/components/data-view-beta/utils/filter-operations.tsx delete mode 100644 packages/raystack/components/data-view-beta/utils/index.tsx diff --git a/apps/www/src/app/examples/dataview-beta/page.tsx b/apps/www/src/app/examples/dataview/page.tsx similarity index 99% rename from apps/www/src/app/examples/dataview-beta/page.tsx rename to apps/www/src/app/examples/dataview/page.tsx index fcb7b6723..32c79b21c 100644 --- a/apps/www/src/app/examples/dataview-beta/page.tsx +++ b/apps/www/src/app/examples/dataview/page.tsx @@ -501,7 +501,7 @@ const Page = () => { } active > diff --git a/packages/raystack/components/data-view-beta/components/content.tsx b/packages/raystack/components/data-view-beta/components/content.tsx deleted file mode 100644 index 0e972e237..000000000 --- a/packages/raystack/components/data-view-beta/components/content.tsx +++ /dev/null @@ -1,407 +0,0 @@ -'use client'; - -import { Cross2Icon, TableIcon } from '@radix-ui/react-icons'; -import type { Header, Row } from '@tanstack/react-table'; -import { flexRender } from '@tanstack/react-table'; -import { cx } from 'class-variance-authority'; -import { useCallback, useEffect, useMemo, useRef } from 'react'; - -import { Badge } from '../../badge'; -import { Button } from '../../button'; -import { EmptyState } from '../../empty-state'; -import { Flex } from '../../flex'; -import { Skeleton } from '../../skeleton'; -import { Table } from '../../table'; -import styles from '../data-view.module.css'; -import { - DataViewContentClassNames, - DataViewTableColumn, - GroupedData -} from '../data-view.types'; -import { useDataView } from '../hooks/useDataView'; -import { - countLeafRows, - getClientHiddenLeafRowCount, - hasActiveQuery, - hasActiveTableFiltering -} from '../utils'; - -export interface ContentProps { - columns: DataViewTableColumn[]; - emptyState?: React.ReactNode; - zeroState?: React.ReactNode; - classNames?: DataViewContentClassNames; - stickyGroupHeader?: boolean; - loadingRowCount?: number; -} - -interface HeadersProps { - headers: Header[]; - columnMap: Map>; - className?: string; -} - -function Headers({ - headers, - columnMap, - className -}: HeadersProps) { - return ( - - - {headers.map(header => { - const spec = columnMap.get(header.column.id); - const content = - spec?.header !== undefined - ? flexRender(spec.header, header.getContext()) - : flexRender(header.column.columnDef.header, header.getContext()); - return ( - - {content} - - ); - })} - - - ); -} - -function LoaderRows({ - rowCount, - columnCount -}: { - rowCount: number; - columnCount: number; -}) { - const rows = Array.from({ length: rowCount }); - return rows.map((_, rowIndex) => { - const columns = Array.from({ length: columnCount }); - return ( - - {columns.map((_, colIndex) => ( - - - - ))} - - ); - }); -} - -function GroupHeader({ - colSpan, - data, - stickySectionHeader -}: { - colSpan: number; - data: GroupedData; - stickySectionHeader?: boolean; -}) { - return ( - - - {data?.label} - {data.showGroupCount ? ( - {data?.count} - ) : null} - - - ); -} - -interface RowsProps { - rows: Row[]; - renderedAccessors: string[]; - columnMap: Map>; - onRowClick?: (row: TData) => void; - classNames?: { row?: string }; - lastRowRef?: React.RefObject; - stickyGroupHeader?: boolean; -} - -function Rows({ - rows, - renderedAccessors, - columnMap, - onRowClick, - classNames, - lastRowRef, - stickyGroupHeader = false -}: RowsProps) { - return rows.map((row, idx) => { - const isSelected = row.getIsSelected(); - const cells = row.getVisibleCells() || []; - const isGroupHeader = row.subRows && row.subRows.length > 0; - const isLastRow = idx === rows.length - 1; - - if (isGroupHeader) { - return ( - } - stickySectionHeader={stickyGroupHeader} - /> - ); - } - - return ( - onRowClick?.(row.original)} - > - {renderedAccessors.map(accessor => { - const spec = columnMap.get(accessor); - const cell = cells.find(c => c.column.id === accessor); - if (!cell) { - return ( - - ); - } - return ( - - {spec?.cell - ? flexRender(spec.cell, cell.getContext()) - : ((cell.getValue() as React.ReactNode) ?? null)} - - ); - })} - - ); - }); -} - -const DefaultEmptyComponent = () => ( - } heading='No Data' /> -); - -export function Content({ - columns, - emptyState, - zeroState, - classNames = {}, - stickyGroupHeader = false, - loadingRowCount -}: ContentProps) { - const { - onRowClick, - table, - mode, - totalRowCount, - isLoading, - loadMoreData, - loadingRowCount: ctxLoadingRowCount = 3, - tableQuery, - defaultSort, - updateTableQuery - } = useDataView(); - - const effectiveLoadingRowCount = loadingRowCount ?? ctxLoadingRowCount; - - const columnMap = useMemo(() => { - const map = new Map>(); - columns.forEach(c => map.set(c.accessorKey, c)); - return map; - }, [columns]); - - const visibleLeafColumns = table.getVisibleLeafColumns(); - - // Render order is taken from `columns` prop, filtered by TanStack visibility. - const renderedAccessors = useMemo(() => { - const visibleSet = new Set(visibleLeafColumns.map(c => c.id)); - return columns.map(c => c.accessorKey).filter(k => visibleSet.has(k)); - }, [columns, visibleLeafColumns]); - - const headerGroups = table?.getHeaderGroups() ?? []; - const lastHeaderGroup = headerGroups[headerGroups.length - 1]; - const headersInOrder = useMemo(() => { - if (!lastHeaderGroup) return [] as Header[]; - return renderedAccessors - .map( - accessor => - lastHeaderGroup.headers.find(h => h.column.id === accessor) as - | Header - | undefined - ) - .filter((h): h is Header => Boolean(h)); - }, [lastHeaderGroup, renderedAccessors]); - - const rowModel = table?.getRowModel(); - const { rows = [] } = rowModel || {}; - - const lastRowRef = useRef(null); - const observerRef = useRef(null); - - /* Refs keep callback stable so observer is only recreated when mode/rows.length change; */ - const loadMoreDataRef = useRef(loadMoreData); - const isLoadingRef = useRef(isLoading); - loadMoreDataRef.current = loadMoreData; - isLoadingRef.current = isLoading; - - const handleObserver = useCallback((entries: IntersectionObserverEntry[]) => { - const target = entries[0]; - if (!target?.isIntersecting) return; - if (isLoadingRef.current) return; - const loadMore = loadMoreDataRef.current; - if (loadMore) loadMore(); - }, []); - - useEffect(() => { - if (mode !== 'server') return; - - if (observerRef.current) { - observerRef.current.disconnect(); - observerRef.current = null; - } - - const lastRow = lastRowRef.current; - if (!lastRow) return; - - observerRef.current = new IntersectionObserver(handleObserver, { - threshold: 0.1 - }); - observerRef.current.observe(lastRow); - - return () => { - observerRef.current?.disconnect(); - observerRef.current = null; - }; - }, [mode, rows.length, handleObserver]); - - const visibleColumnsLength = renderedAccessors.length; - - const hasData = rows?.length > 0 || isLoading; - - const hasChanges = hasActiveQuery(tableQuery || {}, defaultSort); - - const isZeroState = !hasData && !hasChanges; - const isEmptyState = !hasData && hasChanges; - - const stateToShow: React.ReactNode = isZeroState - ? (zeroState ?? emptyState ?? ) - : isEmptyState - ? (emptyState ?? ) - : null; - - const hiddenLeafRowCount = - mode === 'client' - ? getClientHiddenLeafRowCount(table) - : totalRowCount !== undefined - ? Math.max(0, totalRowCount - countLeafRows(rows)) - : null; - const hasActiveFiltering = !isLoading && hasActiveTableFiltering(table); - const showFilterSummary = - hasActiveFiltering && - (mode === 'server' || - (typeof hiddenLeafRowCount === 'number' && hiddenLeafRowCount > 0)); - - const handleClearFilters = useCallback(() => { - updateTableQuery(prev => ({ - ...prev, - filters: [], - search: '' - })); - }, [updateTableQuery]); - - return ( -
- - {hasData && ( - - )} - - {hasData ? ( - <> - - {isLoading ? ( - - ) : null} - - ) : ( - - - {stateToShow} - - - )} - -
- {showFilterSummary ? ( - - {mode === 'server' && hiddenLeafRowCount === null ? ( - - Some items might be hidden by filters - - ) : ( - - - {hiddenLeafRowCount} - - - items hidden by filters - - - )} - - - ) : null} -
- ); -} - -Content.displayName = 'DataView.Content'; diff --git a/packages/raystack/components/data-view-beta/components/custom.tsx b/packages/raystack/components/data-view-beta/components/custom.tsx deleted file mode 100644 index 28ce9a1a6..000000000 --- a/packages/raystack/components/data-view-beta/components/custom.tsx +++ /dev/null @@ -1,44 +0,0 @@ -'use client'; - -import { ReactNode, useEffect } from 'react'; -import { DataViewContextType, DataViewField } from '../data-view.types'; -import { useDataView } from '../hooks/useDataView'; - -export interface DataViewCustomProps { - /** Multi-view name. When set, the renderer gates itself on the active view. */ - name?: string; - /** Optional view-scoped field override. Full replacement of root `fields` for this view's active session. */ - fields?: DataViewField[]; - /** - * Render prop that receives the full DataView context (table, fields, - * tableQuery, hasData, isEmptyState, etc.) and returns the rendered view. - * Pair with `` to keep field visibility in sync with - * the single Display Properties toggle. - */ - children: (context: DataViewContextType) => ReactNode; -} - -/** - * Escape-hatch renderer for free-form views (cards, kanban, map, etc.). - * Consumes the DataView context and hands it to a render prop. - */ -export function DataViewCustom({ - name, - fields: fieldsOverride, - children -}: DataViewCustomProps) { - const ctx = useDataView(); - const { activeView, registerFieldsForView } = ctx; - - useEffect(() => { - if (!name || !fieldsOverride) return; - return registerFieldsForView(name, fieldsOverride); - }, [name, fieldsOverride, registerFieldsForView]); - - const isActive = !name || activeView === undefined || activeView === name; - if (!isActive) return null; - - return <>{children(ctx)}; -} - -DataViewCustom.displayName = 'DataView.Custom'; diff --git a/packages/raystack/components/data-view-beta/components/display-access.tsx b/packages/raystack/components/data-view-beta/components/display-access.tsx deleted file mode 100644 index cf1f5ff23..000000000 --- a/packages/raystack/components/data-view-beta/components/display-access.tsx +++ /dev/null @@ -1,33 +0,0 @@ -'use client'; - -import { ReactNode } from 'react'; -import { useDataView } from '../hooks/useDataView'; - -export interface DataViewDisplayAccessProps { - /** Field (column) accessor key. Gates rendering on the column's current visibility state. */ - accessorKey: string; - children: ReactNode; - /** Rendered when the referenced field is currently hidden. Defaults to null. */ - fallback?: ReactNode; -} - -/** - * Gates children on the current column visibility state from DataView context. - * Use inside free-form renderers (Timeline bars, custom renderers, cell overrides) - * so the single DisplayControls toggle reaches the same visibility story that - * Table/List rows get through their column specs. - */ -export function DisplayAccess({ - accessorKey, - children, - fallback = null -}: DataViewDisplayAccessProps) { - const { table } = useDataView(); - const column = table?.getColumn(accessorKey); - // If the column doesn't exist, default to visible so consumers can wrap JSX - // in DisplayAccess without worrying about typos silently breaking the render. - const isVisible = column ? column.getIsVisible() : true; - return <>{isVisible ? children : fallback}; -} - -DisplayAccess.displayName = 'DataView.DisplayAccess'; diff --git a/packages/raystack/components/data-view-beta/components/display-properties.tsx b/packages/raystack/components/data-view-beta/components/display-properties.tsx deleted file mode 100644 index 3a67229b2..000000000 --- a/packages/raystack/components/data-view-beta/components/display-properties.tsx +++ /dev/null @@ -1,39 +0,0 @@ -'use client'; - -import { Chip } from '../../chip'; -import { Flex } from '../../flex'; -import { Text } from '../../text'; -import { DataViewField } from '../data-view.types'; -import { useDataView } from '../hooks/useDataView'; - -export function DisplayProperties({ - fields -}: { - fields: DataViewField[]; -}) { - const { table } = useDataView(); - const hidableFields = fields?.filter(f => f.hideable) ?? []; - - return ( - - Display Properties - - {hidableFields.map(field => { - const column = table.getColumn(field.accessorKey); - const isVisible = column ? column.getIsVisible() : true; - return ( - column?.toggleVisibility()} - > - {field.label} - - ); - })} - - - ); -} diff --git a/packages/raystack/components/data-view-beta/components/display-settings.tsx b/packages/raystack/components/data-view-beta/components/display-settings.tsx deleted file mode 100644 index f1f1fa249..000000000 --- a/packages/raystack/components/data-view-beta/components/display-settings.tsx +++ /dev/null @@ -1,131 +0,0 @@ -'use client'; - -import { MixerHorizontalIcon } from '@radix-ui/react-icons'; - -import { isValidElement, ReactNode } from 'react'; -import { Button } from '../../button'; -import { Flex } from '../../flex'; -import { Popover } from '../../popover'; -import styles from '../data-view.module.css'; -import { defaultGroupOption, SortOrdersValues } from '../data-view.types'; -import { useDataView } from '../hooks/useDataView'; -import { DisplayProperties } from './display-properties'; -import { Grouping } from './grouping'; -import { Ordering } from './ordering'; -import { ViewSwitcher } from './view-switcher'; - -interface DisplaySettingsProps { - trigger?: ReactNode; -} - -export function DisplaySettings({ - trigger = ( - - ) -}: DisplaySettingsProps) { - const { - fields, - updateTableQuery, - tableQuery, - defaultSort, - onDisplaySettingsReset, - views - } = useDataView(); - - const sortableColumns = (fields ?? []) - .filter(f => f.sortable) - .map(f => ({ - label: f.label, - id: f.accessorKey - })); - - function onSortChange(columnId: string, order: SortOrdersValues) { - updateTableQuery(query => { - return { - ...query, - sort: [{ name: columnId, order }] - }; - }); - } - - function onGroupChange(columnId: string) { - updateTableQuery(query => { - return { - ...query, - group_by: [columnId] - }; - }); - } - - function onGroupRemove() { - updateTableQuery(query => { - return { - ...query, - group_by: [] - }; - }); - } - - function onReset() { - onDisplaySettingsReset(); - } - - const showViewSwitcher = (views?.length ?? 0) > 1; - - return ( - - {trigger}} - /> - - - {showViewSwitcher ? ( - - - - ) : null} - - - - - - - - - - - - - - ); -} - -DisplaySettings.displayName = 'DataView.DisplayControls'; diff --git a/packages/raystack/components/data-view-beta/components/empty-state.tsx b/packages/raystack/components/data-view-beta/components/empty-state.tsx deleted file mode 100644 index 672e7e7a9..000000000 --- a/packages/raystack/components/data-view-beta/components/empty-state.tsx +++ /dev/null @@ -1,33 +0,0 @@ -'use client'; - -import { cx } from 'class-variance-authority'; -import { ReactNode } from 'react'; -import styles from '../data-view.module.css'; -import { useDataView } from '../hooks/useDataView'; - -export interface DataViewEmptyStateProps { - /** Restrict to a specific view's `name`. When set, the EmptyState only renders if both `isEmptyState` is true AND the active view matches. */ - forView?: string; - className?: string; - children: ReactNode; -} - -/** - * Renders its children when the current data + query result in an empty state - * (i.e., a query is active but no rows match). Reads `isEmptyState` from - * DataView context, so the empty/zero distinction is computed in one place. - */ -export function DataViewEmptyState({ - forView, - className, - children -}: DataViewEmptyStateProps) { - const { isEmptyState, activeView } = useDataView(); - if (!isEmptyState) return null; - if (forView && activeView !== forView) return null; - return ( -
{children}
- ); -} - -DataViewEmptyState.displayName = 'DataView.EmptyState'; diff --git a/packages/raystack/components/data-view-beta/components/filters.tsx b/packages/raystack/components/data-view-beta/components/filters.tsx deleted file mode 100644 index ac957e27b..000000000 --- a/packages/raystack/components/data-view-beta/components/filters.tsx +++ /dev/null @@ -1,166 +0,0 @@ -'use client'; - -import { cx } from 'class-variance-authority'; -import { isValidElement, ReactNode, useMemo } from 'react'; -import { FilterIcon } from '~/icons'; -import { FilterOperatorTypes, FilterType } from '~/types/filters'; -import { Button } from '../../button'; -import { FilterChip } from '../../filter-chip'; -import { Flex } from '../../flex'; -import { IconButton } from '../../icon-button'; -import { Menu } from '../../menu'; -import styles from '../data-view.module.css'; -import { DataViewField } from '../data-view.types'; -import { useDataView } from '../hooks/useDataView'; -import { useFilters } from '../hooks/useFilters'; - -type Trigger = - | ReactNode - | (({ - availableFilters, - appliedFilters - }: { - availableFilters: DataViewField[]; - appliedFilters: Set; - }) => ReactNode); - -interface AddFilterProps { - fieldList: DataViewField[]; - appliedFiltersSet: Set; - onAddFilter: (field: DataViewField) => void; - children?: Trigger; -} - -function AddFilter({ - fieldList = [], - appliedFiltersSet, - onAddFilter, - children -}: AddFilterProps) { - const availableFilters = fieldList?.filter( - f => !appliedFiltersSet.has(f.accessorKey) - ); - - const trigger = useMemo(() => { - if (typeof children === 'function') - return children({ availableFilters, appliedFilters: appliedFiltersSet }); - else if (children) return children; - else if (appliedFiltersSet.size > 0) - return ( - - - - ); - else - return ( - - ); - }, [children, appliedFiltersSet, availableFilters]); - - return availableFilters.length > 0 ? ( - - {trigger}} - /> - - {availableFilters?.map(field => ( - onAddFilter(field)}> - {field.label} - - ))} - - - ) : null; -} - -export function Filters({ - classNames, - className, - trigger -}: { - classNames?: { - container?: string; - filterChips?: string; - addFilter?: string; - }; - className?: string; - trigger?: Trigger; -}) { - const { fields, tableQuery } = useDataView(); - - const { - onAddFilter, - handleRemoveFilter, - handleFilterValueChange, - handleFilterOperationChange - } = useFilters(); - - const filterableFields = fields?.filter(f => f.filterable) ?? []; - - const appliedFiltersSet = new Set( - tableQuery?.filters?.map(filter => filter.name) - ); - - const appliedFilters = - tableQuery?.filters?.map(filter => { - const field = fields?.find(f => f.accessorKey === filter.name); - return { - filterType: field?.filterType || FilterType.string, - label: field?.label || '', - options: field?.filterOptions || [], - selectProps: field?.filterProps?.select, - calendarProps: field?.filterProps?.calendar, - ...filter - }; - }) || []; - - return ( - - {appliedFilters.length > 0 && ( - - {appliedFilters.map(filter => ( - handleRemoveFilter(filter.name)} - onValueChange={value => - handleFilterValueChange(filter.name, value) - } - onOperationChange={operator => - handleFilterOperationChange( - filter.name, - operator as FilterOperatorTypes - ) - } - columnType={filter.filterType} - options={filter.options} - selectProps={filter.selectProps} - calendarProps={filter.calendarProps} - className={classNames?.filterChips} - /> - ))} - - )} - - {trigger} - - - ); -} - -Filters.displayName = 'DataView.Filters'; diff --git a/packages/raystack/components/data-view-beta/components/grouping.tsx b/packages/raystack/components/data-view-beta/components/grouping.tsx deleted file mode 100644 index 5d68c533a..000000000 --- a/packages/raystack/components/data-view-beta/components/grouping.tsx +++ /dev/null @@ -1,62 +0,0 @@ -'use client'; - -import { Flex } from '../../flex'; -import { Select } from '../../select'; -import { Text } from '../../text'; -import styles from '../data-view.module.css'; -import { DataViewField, defaultGroupOption } from '../data-view.types'; - -interface GroupingProps { - fields: DataViewField[]; - onChange: (fieldAccessor: string) => void; - onRemove: () => void; - value: string; -} - -export function Grouping({ - fields = [], - onChange, - onRemove, - value -}: GroupingProps) { - const groupableFields = fields.filter(f => f.groupable); - - const handleGroupChange = (fieldAccessor: string) => { - if (fieldAccessor === defaultGroupOption.id) { - onRemove(); - return; - } - const field = fields.find(f => f.accessorKey === fieldAccessor); - if (field) { - onChange(field.accessorKey); - } - }; - - return ( - - - Grouping - - - - - - ); -} diff --git a/packages/raystack/components/data-view-beta/components/list.tsx b/packages/raystack/components/data-view-beta/components/list.tsx deleted file mode 100644 index b233cb1fa..000000000 --- a/packages/raystack/components/data-view-beta/components/list.tsx +++ /dev/null @@ -1,425 +0,0 @@ -'use client'; - -import { Cross2Icon } from '@radix-ui/react-icons'; -import type { Header, Row } from '@tanstack/react-table'; -import { flexRender } from '@tanstack/react-table'; -import { useVirtualizer } from '@tanstack/react-virtual'; -import { cx } from 'class-variance-authority'; -import { CSSProperties, useCallback, useEffect, useMemo, useRef } from 'react'; - -import { Badge } from '../../badge'; -import { Button } from '../../button'; -import { Flex } from '../../flex'; -import styles from '../data-view.module.css'; -import { - DataViewListColumn, - DataViewListProps, - GroupedData -} from '../data-view.types'; -import { useDataView } from '../hooks/useDataView'; -import { - countLeafRows, - getClientHiddenLeafRowCount, - hasActiveTableFiltering -} from '../utils'; - -function formatGridWidth(width: string | number | undefined) { - if (width === undefined) return '1fr'; - if (typeof width === 'number') return `${width}px`; - return width; -} - -export function DataViewList({ - name, - variant = 'list', - showHeaders, - role, - fields: fieldsOverride, - columns, - rowHeight, - virtualized = false, - overscan = 8, - showDividers, - showGroupHeaders = true, - stickyGroupHeader = false, - loadMoreOffset = 100, - classNames = {} -}: DataViewListProps) { - const { - table, - mode, - onRowClick, - isLoading, - loadMoreData, - tableQuery, - totalRowCount, - updateTableQuery, - activeView, - registerFieldsForView, - hasData - } = useDataView(); - - // Register per-view field override so the toolbar's effectiveFields reflects - // this renderer's metadata while it's the active view. - useEffect(() => { - if (!name || !fieldsOverride) return; - return registerFieldsForView(name, fieldsOverride); - }, [name, fieldsOverride, registerFieldsForView]); - - // Multi-view gate. When `name` is set, render only when this is the active - // view. When `name` is unset (single-renderer mode), always render. - const isActive = !name || activeView === undefined || activeView === name; - - const isTableVariant = variant === 'table'; - const headersVisible = showHeaders ?? isTableVariant; - const ariaRole = role ?? (isTableVariant ? 'table' : 'list'); - const dividers = showDividers ?? isTableVariant; - const effectiveRowHeight = rowHeight ?? (isTableVariant ? 40 : 56); - - const visibleLeafColumns = table.getVisibleLeafColumns(); - - const columnMap = useMemo(() => { - const map = new Map>(); - columns.forEach(c => map.set(c.accessorKey, c)); - return map; - }, [columns]); - - // Render order from `columns`, filtered by current TanStack visibility. - const renderedAccessors = useMemo(() => { - const visibleSet = new Set(visibleLeafColumns.map(c => c.id)); - return columns.map(c => c.accessorKey).filter(k => visibleSet.has(k)); - }, [columns, visibleLeafColumns]); - - const gridTemplateColumns = useMemo(() => { - return renderedAccessors - .map(accessor => formatGridWidth(columnMap.get(accessor)?.width)) - .join(' '); - }, [renderedAccessors, columnMap]); - - const headerGroups = table?.getHeaderGroups() ?? []; - const lastHeaderGroup = headerGroups[headerGroups.length - 1]; - const headersInOrder = useMemo(() => { - if (!lastHeaderGroup) return [] as Header[]; - return renderedAccessors - .map( - accessor => - lastHeaderGroup.headers.find(h => h.column.id === accessor) as - | Header - | undefined - ) - .filter((h): h is Header => Boolean(h)); - }, [lastHeaderGroup, renderedAccessors]); - - const rowModel = table?.getRowModel(); - const { rows = [] } = rowModel || {}; - - const scrollRef = useRef(null); - const observerRef = useRef(null); - const lastRowRef = useRef(null); - - const loadMoreDataRef = useRef(loadMoreData); - const isLoadingRef = useRef(isLoading); - loadMoreDataRef.current = loadMoreData; - isLoadingRef.current = isLoading; - - const handleObserver = useCallback((entries: IntersectionObserverEntry[]) => { - const target = entries[0]; - if (!target?.isIntersecting) return; - if (isLoadingRef.current) return; - const loadMore = loadMoreDataRef.current; - if (loadMore) loadMore(); - }, []); - - // Non-virtualized load-more via last-row IntersectionObserver - useEffect(() => { - if (!isActive) return; - if (mode !== 'server' || virtualized) return; - - if (observerRef.current) { - observerRef.current.disconnect(); - observerRef.current = null; - } - - const lastRow = lastRowRef.current; - if (!lastRow) return; - - observerRef.current = new IntersectionObserver(handleObserver, { - threshold: 0.1 - }); - observerRef.current.observe(lastRow); - - return () => { - observerRef.current?.disconnect(); - observerRef.current = null; - }; - }, [isActive, mode, virtualized, rows.length, handleObserver]); - - const virtualizer = useVirtualizer({ - count: rows.length, - getScrollElement: () => scrollRef.current, - estimateSize: i => { - const row = rows[i]; - const isGroupHeader = row?.subRows && row.subRows.length > 0; - return isGroupHeader ? 36 : effectiveRowHeight; - }, - overscan - }); - - const hiddenLeafRowCount = - mode === 'client' - ? getClientHiddenLeafRowCount(table) - : totalRowCount !== undefined - ? Math.max(0, totalRowCount - countLeafRows(rows)) - : null; - const hasActiveFiltering = !isLoading && hasActiveTableFiltering(table); - const showFilterSummary = - hasActiveFiltering && - (mode === 'server' || - (typeof hiddenLeafRowCount === 'number' && hiddenLeafRowCount > 0)); - - const handleClearFilters = useCallback(() => { - updateTableQuery(prev => ({ - ...prev, - filters: [], - search: '' - })); - }, [updateTableQuery]); - - const handleVirtualScroll = useCallback(() => { - if (!virtualized) return; - const el = scrollRef.current; - if (!el) return; - if (isLoading) return; - const { scrollTop, scrollHeight, clientHeight } = el; - if (scrollHeight - scrollTop - clientHeight < loadMoreOffset!) { - loadMoreData(); - } - }, [virtualized, isLoading, loadMoreData, loadMoreOffset]); - - if (!isActive) return null; - if (!hasData) return null; - - const renderHeaderRow = () => { - if (!headersVisible) return null; - return ( -
-
- {headersInOrder.map(header => { - const spec = columnMap.get(header.column.id); - const content = - spec?.header !== undefined - ? flexRender(spec.header, header.getContext()) - : flexRender( - header.column.columnDef.header, - header.getContext() - ); - return ( -
- {content} -
- ); - })} -
-
- ); - }; - - const renderRowCells = (row: Row) => - renderedAccessors.map(accessor => { - const spec = columnMap.get(accessor); - const cell = row.getVisibleCells().find(c => c.column.id === accessor); - if (!cell) { - return ( -
- ); - } - return ( -
- {spec?.cell - ? flexRender(spec.cell, cell.getContext()) - : ((cell.getValue() as React.ReactNode) ?? null)} -
- ); - }); - - const renderGroupHeader = ( - row: Row, - style?: CSSProperties, - key?: string - ) => { - const data = row.original as GroupedData; - return ( -
- - {data?.label} - {data?.showGroupCount ? ( - {data?.count} - ) : null} - -
- ); - }; - - const renderDataRow = ( - row: Row, - style?: CSSProperties, - key?: string, - refCb?: (el: HTMLDivElement | null) => void - ) => ( -
onRowClick?.(row.original)} - > - {renderRowCells(row)} -
- ); - - return ( -
-
- {renderHeaderRow()} - {virtualized ? ( -
- {virtualizer.getVirtualItems().map(item => { - const row = rows[item.index]; - if (!row) return null; - const isGroupHeader = row.subRows && row.subRows.length > 0; - const positionStyle: CSSProperties = { - position: 'absolute', - top: item.start, - left: 0, - right: 0, - height: item.size - }; - if (isGroupHeader) { - return showGroupHeaders - ? renderGroupHeader( - row, - positionStyle, - row.id + '-' + item.index - ) - : null; - } - return renderDataRow( - row, - positionStyle, - row.id + '-' + item.index - ); - })} -
- ) : ( - rows.map((row, idx) => { - const isGroupHeader = row.subRows && row.subRows.length > 0; - const isLast = idx === rows.length - 1; - if (isGroupHeader) { - return showGroupHeaders ? renderGroupHeader(row) : null; - } - return renderDataRow(row, undefined, undefined, el => { - if (isLast) lastRowRef.current = el; - }); - }) - )} -
- {showFilterSummary ? ( - - {mode === 'server' && hiddenLeafRowCount === null ? ( - - Some items might be hidden by filters - - ) : ( - - - {hiddenLeafRowCount} - - - items hidden by filters - - - )} - - - ) : null} -
- ); -} - -DataViewList.displayName = 'DataView.List'; diff --git a/packages/raystack/components/data-view-beta/components/ordering.tsx b/packages/raystack/components/data-view-beta/components/ordering.tsx deleted file mode 100644 index 073d74a92..000000000 --- a/packages/raystack/components/data-view-beta/components/ordering.tsx +++ /dev/null @@ -1,76 +0,0 @@ -'use client'; - -import { TextAlignBottomIcon, TextAlignTopIcon } from '@radix-ui/react-icons'; - -import { Flex } from '../../flex'; -import { IconButton } from '../../icon-button'; -import { Select } from '../../select'; -import { Text } from '../../text'; -import styles from '../data-view.module.css'; -import { - ColumnData, - DataViewSort, - SortOrders, - SortOrdersValues -} from '../data-view.types'; - -export interface OrderingProps { - columnList: ColumnData[]; - onChange: (columnId: string, order: SortOrdersValues) => void; - value: DataViewSort; -} - -export function Ordering({ columnList, onChange, value }: OrderingProps) { - function handleColumnChange(columnId: string) { - onChange(columnId, value.order); - } - - function handleOrderChange() { - const newOrder = - value.order === SortOrders.ASC ? SortOrders.DESC : SortOrders.ASC; - onChange(value.name, newOrder); - } - - return ( - - - Ordering - - - - - {value.order === SortOrders?.ASC ? ( - - ) : ( - - )} - - - - ); -} diff --git a/packages/raystack/components/data-view-beta/components/renderer.tsx b/packages/raystack/components/data-view-beta/components/renderer.tsx deleted file mode 100644 index 3d7e78627..000000000 --- a/packages/raystack/components/data-view-beta/components/renderer.tsx +++ /dev/null @@ -1,28 +0,0 @@ -'use client'; - -import { ReactNode } from 'react'; -import { DataViewContextType } from '../data-view.types'; -import { useDataView } from '../hooks/useDataView'; - -export interface DataViewRendererProps { - /** - * Render prop that receives the full DataView context (table, fields, - * tableQuery, etc.) and returns the rendered view. Use together with - * `` to keep field visibility in sync with the - * single Display Properties toggle. - */ - children: (context: DataViewContextType) => ReactNode; -} - -/** - * Escape-hatch renderer for free-form views (cards, kanban, map, etc.). - * Consumes the DataView context and hands it to a render prop. - */ -export function DataViewRenderer({ - children -}: DataViewRendererProps) { - const ctx = useDataView(); - return <>{children(ctx)}; -} - -DataViewRenderer.displayName = 'DataView.Renderer'; diff --git a/packages/raystack/components/data-view-beta/components/search.tsx b/packages/raystack/components/data-view-beta/components/search.tsx deleted file mode 100644 index 546fd57e3..000000000 --- a/packages/raystack/components/data-view-beta/components/search.tsx +++ /dev/null @@ -1,64 +0,0 @@ -'use client'; - -import { ChangeEvent, type ComponentProps } from 'react'; -import { Search } from '../../search'; -import { useDataView } from '../hooks/useDataView'; - -export interface DataViewSearchProps extends ComponentProps { - /** - * Automatically disable search in zero state (when no data and no filters/search applied). - * @defaultValue true - */ - autoDisableInZeroState?: boolean; -} - -export function DataViewSearch({ - autoDisableInZeroState = true, - disabled, - ...props -}: DataViewSearchProps) { - const { - updateTableQuery, - tableQuery, - shouldShowFilters = false - } = useDataView(); - - const handleSearch = (e: ChangeEvent) => { - const value = e.target.value; - updateTableQuery(query => { - return { - ...query, - search: value - }; - }); - }; - - const handleClear = () => { - updateTableQuery(query => { - return { - ...query, - search: '' - }; - }); - }; - - // Auto-disable in zero state if enabled, but allow manual override - // Once search is applied, keep it enabled (even if shouldShowFilters is false) - const hasSearch = Boolean( - tableQuery?.search && tableQuery.search.trim() !== '' - ); - const isDisabled = - disabled ?? (autoDisableInZeroState && !shouldShowFilters && !hasSearch); - - return ( - - ); -} - -DataViewSearch.displayName = 'DataView.Search'; diff --git a/packages/raystack/components/data-view-beta/components/table.tsx b/packages/raystack/components/data-view-beta/components/table.tsx deleted file mode 100644 index ea268d3af..000000000 --- a/packages/raystack/components/data-view-beta/components/table.tsx +++ /dev/null @@ -1,16 +0,0 @@ -'use client'; - -import { DataViewTableProps } from '../data-view.types'; -import { DataViewList } from './list'; - -/** - * @deprecated Prefer ``. Kept as a thin alias - * during the multi-renderer migration. - */ -export function DataViewTable( - props: DataViewTableProps -) { - return {...props} variant='table' />; -} - -DataViewTable.displayName = 'DataView.Table'; diff --git a/packages/raystack/components/data-view-beta/components/toolbar.tsx b/packages/raystack/components/data-view-beta/components/toolbar.tsx deleted file mode 100644 index 696dc5e00..000000000 --- a/packages/raystack/components/data-view-beta/components/toolbar.tsx +++ /dev/null @@ -1,50 +0,0 @@ -'use client'; - -import { cx } from 'class-variance-authority'; -import { PropsWithChildren } from 'react'; -import { Flex } from '../../flex'; -import styles from '../data-view.module.css'; -import { useDataView } from '../hooks/useDataView'; -import { DisplaySettings } from './display-settings'; -import { Filters } from './filters'; - -interface ToolbarProps { - className?: string; -} - -export function Toolbar({ - className, - children -}: PropsWithChildren) { - const { shouldShowFilters = false } = useDataView(); - - if (!shouldShowFilters) { - return null; - } - - // If children are provided, render them so consumers can compose Search / Filters / DisplayControls. - if (children) { - return ( - - {children} - - ); - } - - return ( - - /> - /> - - ); -} - -Toolbar.displayName = 'DataView.Toolbar'; diff --git a/packages/raystack/components/data-view-beta/components/view-switcher.tsx b/packages/raystack/components/data-view-beta/components/view-switcher.tsx deleted file mode 100644 index 833931712..000000000 --- a/packages/raystack/components/data-view-beta/components/view-switcher.tsx +++ /dev/null @@ -1,44 +0,0 @@ -'use client'; - -import { cx } from 'class-variance-authority'; -import { Tabs } from '../../tabs'; -import styles from '../data-view.module.css'; -import { useDataView } from '../hooks/useDataView'; - -export interface DataViewViewSwitcherProps { - className?: string; - size?: 'small' | 'medium' | 'large'; -} - -/** - * Renders a tab-based switcher for the configured `views`. Reads `views` and - * `activeView` from DataView context and writes through `setActiveView`. Renders - * nothing when `views` is unset or contains a single entry. - */ -export function ViewSwitcher({ - className, - size = 'small' -}: DataViewViewSwitcherProps) { - const { views, activeView, setActiveView } = useDataView(); - - if (!views || views.length < 2) return null; - - return ( - setActiveView(v)} - size={size} - className={cx(styles.viewSwitcher, className)} - > - - {views.map(v => ( - - {v.label} - - ))} - - - ); -} - -ViewSwitcher.displayName = 'DataView.ViewSwitcher'; diff --git a/packages/raystack/components/data-view-beta/components/virtualized-content.tsx b/packages/raystack/components/data-view-beta/components/virtualized-content.tsx deleted file mode 100644 index 231b31e91..000000000 --- a/packages/raystack/components/data-view-beta/components/virtualized-content.tsx +++ /dev/null @@ -1,428 +0,0 @@ -'use client'; - -import { TableIcon } from '@radix-ui/react-icons'; -import type { Header, Row } from '@tanstack/react-table'; -import { flexRender } from '@tanstack/react-table'; -import { useVirtualizer } from '@tanstack/react-virtual'; -import { cx } from 'class-variance-authority'; -import { useCallback, useLayoutEffect, useMemo, useRef, useState } from 'react'; -import tableStyles from '~/components/table/table.module.css'; -import { Badge } from '../../badge'; -import { EmptyState } from '../../empty-state'; -import { Flex } from '../../flex'; -import { Skeleton } from '../../skeleton'; -import styles from '../data-view.module.css'; -import { - DataViewContentClassNames, - DataViewTableColumn, - defaultGroupOption, - GroupedData -} from '../data-view.types'; -import { useDataView } from '../hooks/useDataView'; -import { hasActiveQuery } from '../utils'; - -export interface VirtualizedContentProps { - columns: DataViewTableColumn[]; - emptyState?: React.ReactNode; - zeroState?: React.ReactNode; - classNames?: DataViewContentClassNames; - rowHeight?: number; - groupHeaderHeight?: number; - overscan?: number; - loadMoreOffset?: number; - stickyGroupHeader?: boolean; -} - -function VirtualHeaders({ - headers, - columnMap, - className -}: { - headers: Header[]; - columnMap: Map>; - className?: string; -}) { - return ( -
-
- {headers.map(header => { - const spec = columnMap.get(header.column.id); - const content = - spec?.header !== undefined - ? flexRender(spec.header, header.getContext()) - : flexRender(header.column.columnDef.header, header.getContext()); - return ( -
- {content} -
- ); - })} -
-
- ); -} - -function VirtualGroupHeader({ - data, - style -}: { - data: GroupedData; - style?: React.CSSProperties; -}) { - return ( -
- - {data?.label} - {data.showGroupCount ? ( - {data?.count} - ) : null} - -
- ); -} - -function VirtualRows({ - rows, - virtualizer, - renderedAccessors, - columnMap, - onRowClick, - classNames -}: { - rows: Row[]; - virtualizer: ReturnType; - renderedAccessors: string[]; - columnMap: Map>; - onRowClick?: (row: TData) => void; - classNames?: { row?: string }; -}) { - const items = virtualizer.getVirtualItems(); - - return items.map(item => { - const row = rows[item.index]; - if (!row) return null; - - const isSelected = row.getIsSelected(); - const cells = row.getVisibleCells() || []; - const isGroupHeader = row.subRows && row.subRows.length > 0; - const rowKey = row.id + '-' + item.index; - - const positionStyle: React.CSSProperties = { - height: item.size, - top: item.start - }; - - if (isGroupHeader) { - return ( - } - style={positionStyle} - /> - ); - } - - return ( -
onRowClick?.(row.original)} - > - {renderedAccessors.map(accessor => { - const spec = columnMap.get(accessor); - const cell = cells.find(c => c.column.id === accessor); - if (!cell) { - return ( -
- ); - } - return ( -
- {spec?.cell - ? flexRender(spec.cell, cell.getContext()) - : ((cell.getValue() as React.ReactNode) ?? null)} -
- ); - })} -
- ); - }); -} - -function VirtualLoaderRows({ - renderedAccessors, - columnMap, - count -}: { - renderedAccessors: string[]; - columnMap: Map>; - count: number; -}) { - return ( -
- {Array.from({ length: count }).map((_, rowIndex) => ( -
- {renderedAccessors.map(accessor => { - const spec = columnMap.get(accessor); - return ( -
- -
- ); - })} -
- ))} -
- ); -} - -const DefaultEmptyComponent = () => ( - } heading='No Data' /> -); - -export function VirtualizedContent({ - columns, - rowHeight = 40, - groupHeaderHeight, - overscan = 5, - loadMoreOffset = 100, - emptyState, - zeroState, - classNames = {}, - stickyGroupHeader = false -}: VirtualizedContentProps) { - const { - onRowClick, - table, - isLoading, - loadMoreData, - tableQuery, - defaultSort, - loadingRowCount = 3 - } = useDataView(); - - const columnMap = useMemo(() => { - const map = new Map>(); - columns.forEach(c => map.set(c.accessorKey, c)); - return map; - }, [columns]); - - const visibleLeafColumns = table.getVisibleLeafColumns(); - - const renderedAccessors = useMemo(() => { - const visibleSet = new Set(visibleLeafColumns.map(c => c.id)); - return columns.map(c => c.accessorKey).filter(k => visibleSet.has(k)); - }, [columns, visibleLeafColumns]); - - const headerGroups = table?.getHeaderGroups() ?? []; - const lastHeaderGroup = headerGroups[headerGroups.length - 1]; - const headersInOrder = useMemo(() => { - if (!lastHeaderGroup) return [] as Header[]; - return renderedAccessors - .map( - accessor => - lastHeaderGroup.headers.find(h => h.column.id === accessor) as - | Header - | undefined - ) - .filter((h): h is Header => Boolean(h)); - }, [lastHeaderGroup, renderedAccessors]); - - const rowModel = table?.getRowModel(); - const { rows = [] } = rowModel || {}; - - const scrollContainerRef = useRef(null); - const headerRef = useRef(null); - const [stickyGroup, setStickyGroup] = useState | null>( - null - ); - const [headerHeight, setHeaderHeight] = useState(40); - - const groupBy = tableQuery?.group_by?.[0]; - const isGrouped = Boolean(groupBy) && groupBy !== defaultGroupOption.id; - - const groupHeaderList = useMemo(() => { - const list: { index: number; data: GroupedData }[] = []; - rows.forEach((row, i) => { - if (row.subRows && row.subRows.length > 0) { - list.push({ index: i, data: row.original as GroupedData }); - } - }); - return list; - }, [rows]); - - const showLoaderRows = isLoading; - - const virtualizer = useVirtualizer({ - count: rows.length, - getScrollElement: () => scrollContainerRef.current, - estimateSize: index => { - const row = rows[index]; - const isGroupHeader = row?.subRows && row.subRows.length > 0; - return isGroupHeader ? (groupHeaderHeight ?? rowHeight) : rowHeight; - }, - overscan - }); - - const updateStickyGroup = useCallback(() => { - if (!stickyGroupHeader || !isGrouped || groupHeaderList.length === 0) { - setStickyGroup(null); - return; - } - const items = virtualizer.getVirtualItems(); - const firstIndex = items[0]?.index ?? 0; - const current = groupHeaderList - .filter(g => g.index <= firstIndex) - .pop()?.data; - setStickyGroup(current ?? null); - }, [stickyGroupHeader, isGrouped, groupHeaderList, virtualizer]); - - const handleVirtualScroll = useCallback(() => { - const el = scrollContainerRef.current; - if (!el) return; - if (stickyGroupHeader) updateStickyGroup(); - if (isLoading) return; - const { scrollTop, scrollHeight, clientHeight } = el; - if (scrollHeight - scrollTop - clientHeight < loadMoreOffset!) { - loadMoreData(); - } - }, [ - stickyGroupHeader, - isLoading, - loadMoreData, - loadMoreOffset, - updateStickyGroup - ]); - - const totalHeight = virtualizer.getTotalSize(); - - useLayoutEffect(() => { - if (headerRef.current) { - setHeaderHeight(headerRef.current.getBoundingClientRect().height); - } - }, [headersInOrder]); - - useLayoutEffect(() => { - if (stickyGroupHeader) updateStickyGroup(); - }, [stickyGroupHeader, updateStickyGroup, groupHeaderList, isGrouped]); - - const hasData = rows?.length > 0 || isLoading; - - const hasChanges = hasActiveQuery(tableQuery || {}, defaultSort); - - const isZeroState = !hasData && !hasChanges; - const isEmptyState = !hasData && hasChanges; - - const stateToShow: React.ReactNode = isZeroState - ? (zeroState ?? emptyState ?? ) - : isEmptyState - ? (emptyState ?? ) - : null; - - if (!hasData) { - return
{stateToShow}
; - } - - return ( -
-
-
- -
- {stickyGroupHeader && isGrouped && stickyGroup && ( -
- - {stickyGroup.label} - {stickyGroup.showGroupCount ? ( - {stickyGroup.count} - ) : null} - -
- )} -
- -
-
- {showLoaderRows && ( - - )} -
- ); -} - -VirtualizedContent.displayName = 'DataView.VirtualizedContent'; diff --git a/packages/raystack/components/data-view-beta/components/zero-state.tsx b/packages/raystack/components/data-view-beta/components/zero-state.tsx deleted file mode 100644 index c5dc7a274..000000000 --- a/packages/raystack/components/data-view-beta/components/zero-state.tsx +++ /dev/null @@ -1,32 +0,0 @@ -'use client'; - -import { cx } from 'class-variance-authority'; -import { ReactNode } from 'react'; -import styles from '../data-view.module.css'; -import { useDataView } from '../hooks/useDataView'; - -export interface DataViewZeroStateProps { - /** Restrict to a specific view's `name`. When set, the ZeroState only renders if both `isZeroState` is true AND the active view matches. */ - forView?: string; - className?: string; - children: ReactNode; -} - -/** - * Renders its children when there is no data and no active query (the "first - * use" state). Reads `isZeroState` from DataView context. - */ -export function DataViewZeroState({ - forView, - className, - children -}: DataViewZeroStateProps) { - const { isZeroState, activeView } = useDataView(); - if (!isZeroState) return null; - if (forView && activeView !== forView) return null; - return ( -
{children}
- ); -} - -DataViewZeroState.displayName = 'DataView.ZeroState'; diff --git a/packages/raystack/components/data-view-beta/context.tsx b/packages/raystack/components/data-view-beta/context.tsx deleted file mode 100644 index bfd4da0fa..000000000 --- a/packages/raystack/components/data-view-beta/context.tsx +++ /dev/null @@ -1,9 +0,0 @@ -'use client'; - -import { createContext } from 'react'; - -import { DataViewContextType } from './data-view.types'; - -export const DataViewContext = createContext | null>( - null -); diff --git a/packages/raystack/components/data-view-beta/data-view.module.css b/packages/raystack/components/data-view-beta/data-view.module.css deleted file mode 100644 index 12078f2f7..000000000 --- a/packages/raystack/components/data-view-beta/data-view.module.css +++ /dev/null @@ -1,333 +0,0 @@ -.toolbar { - padding: var(--rs-space-3) 0; - align-self: stretch; - - border-bottom: 0.5px solid var(--rs-color-border-base-primary); - background: var(--rs-color-background-base-primary); -} - -/* Fill the toolbar and let the applied FilterChips wrap/shrink to the panel - * instead of overflowing in a single row. */ -.filters { - flex: 1; - min-width: 0; - flex-wrap: wrap; -} - -.appliedFilters { - flex-wrap: wrap; - min-width: 0; -} - -.display-popover-content { - padding: 0px; - /* Todo: var does not exist for 300px */ - min-width: 300px; -} - -.display-popover-properties-container { - /* Todo: var does not exist for 160px */ - --select-width: 160px; - padding: var(--rs-space-5); - border-bottom: 1px solid var(--rs-color-border-base-primary); -} - -.display-popover-properties-select { - width: var(--select-width); -} - -.display-popover-properties-select[with-icon-button] { - /* Reduce Icon button with "--rs-space-7" and flex gap "--rs-space-2" */ - width: calc(var(--select-width) - var(--rs-space-7) - var(--rs-space-2)); -} - -.display-popover-properties-select > span { - overflow: hidden; - white-space: nowrap; - text-overflow: ellipsis; -} - -.display-popover-reset-container { - padding: var(--rs-space-3) var(--rs-space-5); -} - -.display-popover-sort-icon { - height: var(--rs-space-6); - width: var(--rs-space-6); -} - -.flex-1 { - flex: 1; -} - -.filterSummaryFooter { - display: flex; - justify-content: center; - align-items: center; - flex-wrap: wrap; - gap: var(--rs-space-4); - width: 100%; - padding: var(--rs-space-9) 0; - box-sizing: border-box; -} - -.filterSummaryCount { - color: var(--rs-color-foreground-base-primary); - font-family: var(--rs-font-body); - font-size: var(--rs-font-size-small); - font-style: normal; - font-weight: var(--rs-font-weight-medium); - line-height: var(--rs-line-height-small); - letter-spacing: var(--rs-letter-spacing-small); -} - -.filterSummaryLabel { - color: var(--rs-color-foreground-base-secondary); - font-family: var(--rs-font-body); - font-size: var(--rs-font-size-small); - font-style: normal; - font-weight: var(--rs-font-weight-regular); - line-height: var(--rs-line-height-small); - letter-spacing: var(--rs-letter-spacing-small); -} - -.contentRoot { - height: 100%; - overflow: auto; -} - -.row { - background: var(--rs-color-background-base-primary); -} - -.row:hover { - background: var(--rs-color-background-base-primary-hover); -} - -.clickable { - cursor: pointer; -} - -.head { - position: sticky; - top: 0; - z-index: 1; -} - -.cell { - background: inherit; -} - -.emptyStateCell { - border-bottom: none; -} - -/* Virtualization styles */ -.scrollContainer { - overflow-y: auto; - overflow-x: auto; - position: relative; - height: 100%; - overscroll-behavior: auto; -} - -.stickyHeader { - position: sticky; - top: 0; - z-index: 1; - background: var(--rs-color-background-base-primary); -} - -/* Virtual table (div-based) styles */ -.virtualTable { - display: flex; - flex-direction: column; - width: 100%; -} - -.virtualHeaderGroup { - display: flex; - flex-direction: column; -} - -.virtualHeaderRow { - display: flex; -} - -.virtualBodyGroup { - position: relative; -} - -.virtualRow { - display: flex; - position: absolute; - width: 100%; - left: 0; -} - -.virtualHead { - flex: 1 1 0; - min-width: 0; - display: flex; - align-items: center; -} - -.virtualCell { - flex: 1 1 0; - min-width: 0; - display: flex; - align-items: center; - box-sizing: border-box; -} - -.virtualSectionHeader { - position: absolute; - width: 100%; - left: 0; - display: flex; - align-items: center; - background: var(--rs-color-background-base-secondary); - font-weight: var(--rs-font-weight-medium); - padding: var(--rs-space-3); -} - -/* Sticky group anchor: shows current group label while scrolling (virtualized) */ -.stickyGroupAnchor { - position: sticky; - z-index: 1; - display: flex; - align-items: center; - background: var(--rs-color-background-base-secondary); - font-weight: var(--rs-font-weight-medium); - padding: var(--rs-space-3); - border-bottom: 0.5px solid var(--rs-color-border-base-primary); - box-shadow: 0 1px 0 0 var(--rs-color-border-base-primary); -} - -.stickyLoaderContainer { - position: sticky; - bottom: 0; - z-index: 1; - background: var(--rs-color-background-base-primary); -} - -.loaderRow { - position: relative; -} - -/* Non-virtualized: sticky section header under table header */ -.stickySectionHeader { - position: sticky; - top: var(--rs-space-10); - z-index: 1; - background: var(--rs-color-background-base-secondary); - box-shadow: 0 1px 0 0 var(--rs-color-border-base-primary); -} - -/* List renderer styles (handles both variant="table" and variant="list") */ -.listRoot { - width: 100%; - height: 100%; - overflow: auto; - background: var(--rs-color-background-base-primary); -} - -.listGrid { - display: grid; - width: 100%; - align-items: stretch; -} - -.listHeader { - grid-column: 1 / -1; - display: grid; - grid-template-columns: subgrid; - position: sticky; - top: 0; - z-index: 1; - background: var(--rs-color-background-base-primary); - border-bottom: 0.5px solid var(--rs-color-border-base-primary); -} - -.listHeaderRow { - grid-column: 1 / -1; - display: grid; - grid-template-columns: subgrid; -} - -.listHeaderCell { - padding: var(--rs-space-3) var(--rs-space-4); - min-width: 0; - display: flex; - align-items: center; - font-weight: var(--rs-font-weight-medium); - font-size: var(--rs-font-size-small); - color: var(--rs-color-foreground-base-secondary); - box-sizing: border-box; -} - -.listRow { - grid-column: 1 / -1; - display: grid; - grid-template-columns: subgrid; - align-items: center; - background: var(--rs-color-background-base-primary); - box-sizing: border-box; -} - -.listRow:hover { - background: var(--rs-color-background-base-primary-hover); -} - -.listRowDivider { - border-bottom: 0.5px solid var(--rs-color-border-base-primary); -} - -.listCell { - padding: var(--rs-space-3) var(--rs-space-4); - min-width: 0; - display: flex; - align-items: center; - box-sizing: border-box; -} - -.listGroupHeader { - grid-column: 1 / -1; - display: flex; - align-items: center; - gap: var(--rs-space-3); - background: var(--rs-color-background-base-secondary); - font-weight: var(--rs-font-weight-medium); - padding: var(--rs-space-3) var(--rs-space-4); - box-sizing: border-box; -} - -.listGroupHeaderSticky { - position: sticky; - top: var(--rs-space-10); - z-index: 1; -} - -.listEmptyState { - display: flex; - justify-content: center; - align-items: center; - padding: var(--rs-space-9) 0; - width: 100%; - box-sizing: border-box; -} - -/* Empty / Zero state sibling container */ -.dataStateContainer { - display: flex; - justify-content: center; - align-items: center; - padding: var(--rs-space-9) 0; - width: 100%; - box-sizing: border-box; -} - -/* Multi-view switcher inside DisplayControls */ -.viewSwitcher { - flex-shrink: 0; -} diff --git a/packages/raystack/components/data-view-beta/data-view.tsx b/packages/raystack/components/data-view-beta/data-view.tsx deleted file mode 100644 index 02b284dc4..000000000 --- a/packages/raystack/components/data-view-beta/data-view.tsx +++ /dev/null @@ -1,337 +0,0 @@ -'use client'; - -import { - getCoreRowModel, - getExpandedRowModel, - getFilteredRowModel, - getSortedRowModel, - Updater, - useReactTable, - VisibilityState -} from '@tanstack/react-table'; -import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; -import { Content } from './components/content'; -import { DataViewCustom } from './components/custom'; -import { DisplayAccess } from './components/display-access'; -import { DisplaySettings } from './components/display-settings'; -import { DataViewEmptyState } from './components/empty-state'; -import { Filters } from './components/filters'; -import { DataViewList } from './components/list'; -import { DataViewSearch } from './components/search'; -import { DataViewTable } from './components/table'; -import { Toolbar } from './components/toolbar'; -import { ViewSwitcher } from './components/view-switcher'; -import { VirtualizedContent } from './components/virtualized-content'; -import { DataViewZeroState } from './components/zero-state'; -import { DataViewContext } from './context'; -import { - DataViewContextType, - DataViewField, - DataViewProps, - defaultGroupOption, - GroupedData, - InternalQuery, - TableQueryUpdateFn -} from './data-view.types'; -import { - hasActiveQuery as computeHasActiveQuery, - fieldsToColumnDefs, - getDefaultTableQuery, - getInitialColumnVisibility, - groupData, - hasQueryChanged, - queryToTableState, - transformToDataViewQuery -} from './utils'; - -function DataViewRoot({ - data = [], - fields, - query, - mode = 'client', - isLoading = false, - totalRowCount, - loadingRowCount = 3, - defaultSort, - children, - onTableQueryChange, - onLoadMore, - onRowClick, - onColumnVisibilityChange, - getRowId, - views, - defaultView, - view, - onViewChange -}: React.PropsWithChildren>) { - const defaultTableQuery = useMemo( - () => getDefaultTableQuery(defaultSort, query), - [defaultSort, query] - ); - - // Active view (controlled / uncontrolled) - const isViewControlled = view !== undefined; - const [internalActiveView, setInternalActiveView] = useState< - string | undefined - >(defaultView ?? views?.[0]?.value); - const activeView = isViewControlled ? view : internalActiveView; - const setActiveView = useCallback( - (next: string) => { - if (!isViewControlled) setInternalActiveView(next); - onViewChange?.(next); - }, - [isViewControlled, onViewChange] - ); - - // Per-view field overrides registered by mounted renderers. - const [fieldsByView, setFieldsByView] = useState< - Record[]> - >({}); - - const registerFieldsForView = useCallback( - (name: string, override: DataViewField[]) => { - setFieldsByView(prev => { - if (prev[name] === override) return prev; - return { ...prev, [name]: override }; - }); - return () => { - setFieldsByView(prev => { - if (!(name in prev)) return prev; - const next = { ...prev }; - delete next[name]; - return next; - }); - }; - }, - [] - ); - - const effectiveFields = useMemo(() => { - if (activeView && fieldsByView[activeView]) return fieldsByView[activeView]; - return fields; - }, [activeView, fieldsByView, fields]); - - const initialColumnVisibility = useMemo( - () => getInitialColumnVisibility(fields), - [fields] - ); - - const [columnVisibility, setColumnVisibility] = useState( - initialColumnVisibility - ); - const handleColumnVisibilityChange = useCallback( - (value: Updater) => { - setColumnVisibility(prev => { - const newValue = typeof value === 'function' ? value(prev) : value; - onColumnVisibilityChange?.(newValue); - return newValue; - }); - }, - [onColumnVisibilityChange] - ); - - const [tableQuery, setTableQuery] = - useState(defaultTableQuery); - - const oldQueryRef = useRef(null); - - const reactTableState = useMemo( - () => queryToTableState(tableQuery), - [tableQuery] - ); - - const onDisplaySettingsReset = useCallback(() => { - setTableQuery(prev => ({ - ...prev, - ...defaultTableQuery, - sort: [defaultSort], - group_by: [defaultGroupOption.id] - })); - handleColumnVisibilityChange(initialColumnVisibility); - }, [ - defaultSort, - defaultTableQuery, - initialColumnVisibility, - handleColumnVisibilityChange - ]); - - const group_by = tableQuery.group_by?.[0]; - - // Build column defs from the effective (active-view) fields so toolbar + - // headless filter/sort/visibility track per-view metadata overrides. - const columnDefs = useMemo( - () => fieldsToColumnDefs(effectiveFields, tableQuery.filters), - [effectiveFields, tableQuery.filters] - ); - - const groupedData = useMemo( - () => groupData(data, group_by, effectiveFields), - [data, group_by, effectiveFields] - ); - - useEffect(() => { - if ( - tableQuery && - onTableQueryChange && - hasQueryChanged(oldQueryRef.current, tableQuery) && - mode === 'server' - ) { - onTableQueryChange(transformToDataViewQuery(tableQuery)); - oldQueryRef.current = tableQuery; - } - }, [tableQuery, onTableQueryChange]); - - const table = useReactTable({ - data: groupedData as unknown as TData[], - columns: columnDefs, - getRowId, - getCoreRowModel: getCoreRowModel(), - getExpandedRowModel: getExpandedRowModel(), - getSubRows: row => (row as unknown as GroupedData)?.subRows || [], - getSortedRowModel: mode === 'server' ? undefined : getSortedRowModel(), - getFilteredRowModel: mode === 'server' ? undefined : getFilteredRowModel(), - manualSorting: mode === 'server', - manualFiltering: mode === 'server', - onColumnVisibilityChange: handleColumnVisibilityChange, - globalFilterFn: mode === 'server' ? undefined : 'auto', - initialState: { - columnVisibility: initialColumnVisibility - }, - filterFromLeafRows: true, - state: { - ...reactTableState, - columnVisibility: columnVisibility, - expanded: - group_by && group_by !== defaultGroupOption.id ? true : undefined - } - }); - - function updateTableQuery(fn: TableQueryUpdateFn) { - setTableQuery(prev => fn(prev)); - } - - const loadMoreData = useCallback(() => { - if (mode === 'server' && onLoadMore) { - onLoadMore(); - } - }, [mode, onLoadMore]); - - const searchQuery = query?.search; - useEffect(() => { - if (searchQuery) { - updateTableQuery(prev => ({ - ...prev, - search: searchQuery - })); - } - }, [searchQuery]); - - const rowCount = (() => { - try { - return table.getRowModel().rows.length; - } catch { - return data.length; - } - })(); - - const hasData = rowCount > 0 || isLoading; - - const hasActiveQueryFlag = useMemo( - () => computeHasActiveQuery(tableQuery, defaultSort), - [tableQuery, defaultSort] - ); - - const isZeroState = !hasData && !hasActiveQueryFlag; - const isEmptyState = !hasData && hasActiveQueryFlag; - - // Filters bar: visible when there's data OR an active filter, but hidden - // for the pure zero-state (no data + no query) so the empty surface stays clean. - const shouldShowFilters = hasData || hasActiveQueryFlag; - - const contextValue: DataViewContextType = useMemo(() => { - return { - table, - fields: effectiveFields, - rootFields: fields, - mode, - isLoading, - loadMoreData, - tableQuery, - updateTableQuery, - onDisplaySettingsReset, - defaultSort, - totalRowCount, - loadingRowCount, - onRowClick, - shouldShowFilters, - views, - activeView, - setActiveView, - registerFieldsForView, - hasData, - hasActiveQuery: hasActiveQueryFlag, - isZeroState, - isEmptyState - }; - }, [ - table, - effectiveFields, - fields, - mode, - isLoading, - loadMoreData, - tableQuery, - onDisplaySettingsReset, - defaultSort, - totalRowCount, - loadingRowCount, - onRowClick, - shouldShowFilters, - views, - activeView, - setActiveView, - registerFieldsForView, - hasData, - hasActiveQueryFlag, - isZeroState, - isEmptyState - ]); - - return {children}; -} - -DataViewRoot.displayName = 'DataView'; - -/** - * @preview - * `DataView` is a preview component. Its API is not yet stable and - * **will have breaking changes** before the 1.0 release — prop names, - * sub-component shapes, and context surface may all change without - * following semver. Pin to exact versions if depending on it. - */ -// biome-ignore lint/suspicious/noShadowRestrictedNames: public component name intentionally matches the package export -export const DataView = Object.assign(DataViewRoot, { - // Renderers - List: DataViewList, - /** @deprecated Use `` */ - Table: DataViewTable, - // Escape hatch — render prop receives the full DataView context. - Custom: DataViewCustom, - /** @deprecated Renamed to `DataView.Custom`. */ - Renderer: DataViewCustom, - // Legacy sub-renderer exports (used by consumers that imported inner pieces). - Content: Content, - VirtualizedContent: VirtualizedContent, - // Visibility primitive for free-form renderers. - DisplayAccess: DisplayAccess, - // Empty / zero state siblings (driven by context). - EmptyState: DataViewEmptyState, - ZeroState: DataViewZeroState, - // View switching primitive (used by DisplayControls; can be placed standalone). - ViewSwitcher: ViewSwitcher, - // Toolbar primitives - Toolbar: Toolbar, - Search: DataViewSearch, - Filters: Filters, - DisplayControls: DisplaySettings -}); diff --git a/packages/raystack/components/data-view-beta/data-view.types.tsx b/packages/raystack/components/data-view-beta/data-view.types.tsx deleted file mode 100644 index 455bd7e0d..000000000 --- a/packages/raystack/components/data-view-beta/data-view.types.tsx +++ /dev/null @@ -1,281 +0,0 @@ -import type { ColumnDef, Table, VisibilityState } from '@tanstack/table-core'; -import type { - DataTableFilterOperatorTypes, - FilterOperatorTypes, - FilterSelectOption, - FilterTypes, - FilterValueType -} from '~/types/filters'; -import type { FilterChipCalendarProps } from '../filter-chip'; -import type { BaseSelectProps } from '../select/select-root'; - -export type DataViewMode = 'client' | 'server'; - -export const SortOrders = { - ASC: 'asc', - DESC: 'desc' -} as const; - -type SortOrdersKeys = keyof typeof SortOrders; -export type SortOrdersValues = (typeof SortOrders)[SortOrdersKeys]; - -export interface DataViewSort { - name: string; - order: SortOrdersValues; -} - -export interface DataViewFilterValues { - value: any; - // Only one of these value fields should be present at a time - boolValue?: boolean; - stringValue?: string; - numberValue?: number; -} - -// Internal filter with UI operators and metadata -export interface InternalFilter extends DataViewFilterValues { - _type?: FilterTypes; - _dataType?: FilterValueType; - name: string; - operator: FilterOperatorTypes; -} - -// DataView filter for backend API (no internal fields) -export interface DataViewFilter extends DataViewFilterValues { - name: string; - operator: DataTableFilterOperatorTypes; -} - -// Internal query with UI operators and metadata -export interface InternalQuery { - filters?: InternalFilter[]; - sort?: DataViewSort[]; - group_by?: string[]; - offset?: number; - limit?: number; - search?: string; -} - -// DataView query for backend API (clean, no internal fields) -export interface DataViewQuery extends Omit { - filters?: DataViewFilter[]; -} - -/** - * Renderer-agnostic field metadata. One entry per logical column of the data - * model. Declared once on ``; drives filters, sort, group, visibility - * across every renderer. Cell/header rendering belongs on the renderer's own - * column spec, not here. - */ -export interface DataViewField { - accessorKey: string; - /** Human-readable label shown in filter chips, Display controls, and the default Table header. */ - label: string; - icon?: React.ReactNode; - - // filter capability - filterable?: boolean; - filterType?: FilterTypes; - dataType?: FilterValueType; - filterOptions?: FilterSelectOption[]; - defaultFilterValue?: unknown; - filterProps?: { - select?: BaseSelectProps; - calendar?: FilterChipCalendarProps; - }; - - // ordering / grouping / visibility capability - sortable?: boolean; - groupable?: boolean; - hideable?: boolean; - defaultHidden?: boolean; - - // group-header presentation (used by any renderer that groups) - showGroupCount?: boolean; - groupCountMap?: Record; - groupLabelsMap?: Record; -} - -/** - * Unified column spec for DataView.List. Same shape used for both - * `variant="table"` and `variant="list"`. The `header` slot is only rendered - * when headers are visible (default for `variant="table"`). - */ -export interface DataViewListColumn { - accessorKey: string; - /** TanStack-style cell renderer. */ - cell?: ColumnDef['cell']; - /** TanStack-style header renderer. Overrides the field's `label`. */ - header?: ColumnDef['header']; - /** CSS grid track width. `1fr`, `auto`, `'200px'`, `'minmax(80px, 1fr)'`, or a number (pixels). Defaults to `1fr`. */ - width?: string | number; - classNames?: { cell?: string; header?: string }; - styles?: { cell?: React.CSSProperties; header?: React.CSSProperties }; -} - -/** - * @deprecated Use `DataViewListColumn` with ``. - * Kept as a type alias for backwards compatibility. - */ -export type DataViewTableColumn = DataViewListColumn< - TData, - TValue ->; - -/** - * One entry in the multi-view configuration. `value` matches the `name` prop on - * a renderer; `label` shows in the view switcher. - */ -export interface ViewSpec { - value: string; - label: string; - icon?: React.ReactNode; -} - -export interface DataViewProps { - data: TData[]; - /** Renderer-agnostic field metadata. Drives filter/sort/group/visibility. */ - fields: DataViewField[]; - query?: DataViewQuery; // Initial query (will be transformed to internal format) - mode?: DataViewMode; - isLoading?: boolean; - totalRowCount?: number; - loadingRowCount?: number; - onTableQueryChange?: (query: DataViewQuery) => void; - defaultSort: DataViewSort; - onLoadMore?: () => Promise; - onRowClick?: (row: TData) => void; - onColumnVisibilityChange?: (columnVisibility: VisibilityState) => void; - /** Return a stable unique id for each row (used as React key). Use for sortable/filterable tables. */ - getRowId?: (row: TData, index: number) => string; - /** Multi-view configuration. When set, the toolbar's DisplayControls renders a view switcher and renderers gate themselves on the active view via their `name` prop. */ - views?: ViewSpec[]; - /** Default active view (uncontrolled). Should match a `views[].value`. */ - defaultView?: string; - /** Active view (controlled). */ - view?: string; - /** Called when the active view changes. */ - onViewChange?: (view: string) => void; -} - -export type DataViewContentClassNames = { - root?: string; - table?: string; - header?: string; - body?: string; - row?: string; -}; - -export type DataViewListClassNames = { - root?: string; - header?: string; - headerCell?: string; - row?: string; - cell?: string; - groupHeader?: string; -}; - -export interface DataViewListProps { - /** Multi-view name. When set, the renderer gates itself on the active view. */ - name?: string; - /** Visual variant. `table` renders headers and uses `role="table"`; `list` renders no headers and uses `role="list"`. Default `list`. */ - variant?: 'table' | 'list'; - /** Override the header row visibility. Defaults to `variant === 'table'`. */ - showHeaders?: boolean; - /** Override the ARIA role applied to the renderer root. Defaults to derived from `variant`. */ - role?: 'table' | 'list'; - /** Optional view-scoped field override. Full replacement of root `fields` for this view's active session. */ - fields?: DataViewField[]; - - /** Column render specs (cell/header/width/styles). */ - columns: DataViewListColumn[]; - /** Row height in px. Default 40 for `variant="table"`, 56 for `variant="list"`. */ - rowHeight?: number; - /** When true, virtualizes rows. */ - virtualized?: boolean; - /** Number of rows to render outside the viewport when virtualized. */ - overscan?: number; - /** Render thin dividers between rows. Defaults to true for `variant="table"`. */ - showDividers?: boolean; - /** Show group section headers when grouping is active. Default true. */ - showGroupHeaders?: boolean; - /** When true, group headers stick under the table header while scrolling. Default false. */ - stickyGroupHeader?: boolean; - /** Distance in pixels from bottom to trigger load more. */ - loadMoreOffset?: number; - classNames?: DataViewListClassNames; -} - -/** - * @deprecated Pass these to `DataView.List` with `variant="table"` instead. - */ -export type DataViewTableBaseProps = Omit< - DataViewListProps, - 'variant' ->; - -/** - * @deprecated Use `DataViewListProps` with `variant="table"`. - */ -export type DataViewTableProps< - TData, - TValue = unknown -> = DataViewTableBaseProps; - -export type TableQueryUpdateFn = (query: InternalQuery) => InternalQuery; - -export type DataViewContextType = { - table: Table; - /** Effective fields for the active view (= override fields if registered, else root fields). */ - fields: DataViewField[]; - /** Root-declared fields, unchanged by view overrides. */ - rootFields: DataViewField[]; - isLoading?: boolean; - loadMoreData: () => void; - mode: DataViewMode; - defaultSort: DataViewSort; - tableQuery?: InternalQuery; - totalRowCount?: number; - loadingRowCount?: number; - onDisplaySettingsReset: () => void; - updateTableQuery: (fn: TableQueryUpdateFn) => void; - onRowClick?: (row: TData) => void; - shouldShowFilters?: boolean; - - // multi-view - views?: ViewSpec[]; - activeView?: string; - setActiveView: (view: string) => void; - /** Called by each renderer on mount to register its `fields` override for its `name`. Returns a cleanup function. */ - registerFieldsForView: ( - name: string, - fields: DataViewField[] - ) => () => void; - - // global derived state — shared across all renderers and sibling components - hasData: boolean; - hasActiveQuery: boolean; - isZeroState: boolean; - isEmptyState: boolean; -}; - -export interface ColumnData { - label: string; - id: string; - isVisible?: boolean; -} - -interface SubRows<_T> {} - -export interface GroupedData extends SubRows { - label: string; - group_key: string; - subRows: T[]; - count?: number; - showGroupCount?: boolean; -} - -export const defaultGroupOption = { - id: '--', - label: 'No grouping' -}; diff --git a/packages/raystack/components/data-view-beta/hooks/useDataView.tsx b/packages/raystack/components/data-view-beta/hooks/useDataView.tsx deleted file mode 100644 index 2ae93e9ed..000000000 --- a/packages/raystack/components/data-view-beta/hooks/useDataView.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import { useContext } from 'react'; - -import { DataViewContext } from '../context'; -import { DataViewContextType } from '../data-view.types'; - -export const useDataView = (): DataViewContextType => { - const ctx = useContext(DataViewContext); - if (ctx === null) { - throw new Error('useDataView must be used inside of a DataView.Provider'); - } - - return ctx as DataViewContextType; -}; diff --git a/packages/raystack/components/data-view-beta/hooks/useFilters.tsx b/packages/raystack/components/data-view-beta/hooks/useFilters.tsx deleted file mode 100644 index 31530833c..000000000 --- a/packages/raystack/components/data-view-beta/hooks/useFilters.tsx +++ /dev/null @@ -1,89 +0,0 @@ -import { - FilterOperatorTypes, - FilterType, - filterOperators -} from '~/types/filters'; -import { DataViewField } from '../data-view.types'; -import { getDataType } from '../utils/filter-operations'; -import { useDataView } from './useDataView'; - -export function useFilters() { - const { updateTableQuery } = useDataView(); - - function onAddFilter(field: DataViewField) { - const options = field.filterOptions || []; - const filterType = field.filterType || FilterType.string; - const dataType = getDataType({ filterType, dataType: field.dataType }); - const defaultFilter = filterOperators[filterType][0]; - const defaultValue = - field.defaultFilterValue ?? - (filterType === FilterType.date - ? new Date() - : filterType === FilterType.select - ? options[0]?.value - : ''); - - updateTableQuery(query => { - return { - ...query, - filters: [ - ...(query.filters || []), - { - _dataType: dataType, - _type: filterType, - name: field.accessorKey, - value: defaultValue, - operator: defaultFilter.value - } - ] - }; - }); - } - - function handleRemoveFilter(fieldAccessor: string) { - updateTableQuery(query => { - return { - ...query, - filters: query.filters?.filter(filter => filter.name !== fieldAccessor) - }; - }); - } - - function handleFilterValueChange(fieldAccessor: string, value: any) { - updateTableQuery(query => { - return { - ...query, - filters: query.filters?.map(filter => { - if (filter.name === fieldAccessor) { - return { ...filter, value }; - } - return filter; - }) - }; - }); - } - - function handleFilterOperationChange( - fieldAccessor: string, - operator: FilterOperatorTypes - ) { - updateTableQuery(query => { - return { - ...query, - filters: query.filters?.map(filter => { - if (filter.name === fieldAccessor) { - return { ...filter, operator }; - } - return filter; - }) - }; - }); - } - - return { - onAddFilter, - handleRemoveFilter, - handleFilterValueChange, - handleFilterOperationChange - }; -} diff --git a/packages/raystack/components/data-view-beta/index.ts b/packages/raystack/components/data-view-beta/index.ts deleted file mode 100644 index 60f37f6a5..000000000 --- a/packages/raystack/components/data-view-beta/index.ts +++ /dev/null @@ -1,24 +0,0 @@ -export { EmptyFilterValue } from '~/types/filters'; -export type { DataViewCustomProps } from './components/custom'; -export type { DataViewDisplayAccessProps } from './components/display-access'; -export type { DataViewEmptyStateProps } from './components/empty-state'; -export type { DataViewSearchProps } from './components/search'; -export type { DataViewViewSwitcherProps } from './components/view-switcher'; -export type { DataViewZeroStateProps } from './components/zero-state'; -export { DataView } from './data-view'; -export { - DataViewField, - DataViewFilter, - DataViewListClassNames, - DataViewListColumn, - DataViewListProps, - DataViewProps, - DataViewQuery, - DataViewSort, - DataViewTableColumn, - DataViewTableProps, - InternalFilter, - InternalQuery, - ViewSpec -} from './data-view.types'; -export { useDataView } from './hooks/useDataView'; diff --git a/packages/raystack/components/data-view-beta/utils/filter-operations.tsx b/packages/raystack/components/data-view-beta/utils/filter-operations.tsx deleted file mode 100644 index dcaa28db8..000000000 --- a/packages/raystack/components/data-view-beta/utils/filter-operations.tsx +++ /dev/null @@ -1,292 +0,0 @@ -import type { FilterFn } from '@tanstack/table-core'; -import dayjs from 'dayjs'; -import isSameOrAfter from 'dayjs/plugin/isSameOrAfter'; -import isSameOrBefore from 'dayjs/plugin/isSameOrBefore'; - -import { - DataTableFilterOperatorTypes, - DateFilterOperatorType, - EmptyFilterValue, - FilterOperatorTypes, - FilterType, - FilterTypes, - FilterValue, - FilterValueType, - MultiSelectFilterOperatorType, - NumberFilterOperatorType, - SelectFilterOperatorType, - StringFilterOperatorType -} from '~/types/filters'; -import { DataViewFilterValues } from '../data-view.types'; - -dayjs.extend(isSameOrAfter); -dayjs.extend(isSameOrBefore); - -export type FilterFunctionsMap = { - number: Record>; - string: Record>; - date: Record>; - select: Record>; - multiselect: Record>; -}; - -export const filterOperationsMap: FilterFunctionsMap = { - number: { - eq: (row, columnId, filterValue: FilterValue, _addMeta) => { - return Number(row.getValue(columnId)) === Number(filterValue.value); - }, - neq: (row, columnId, filterValue: FilterValue, _addMeta) => { - return Number(row.getValue(columnId)) !== Number(filterValue.value); - }, - lt: (row, columnId, filterValue: FilterValue, _addMeta) => { - return Number(row.getValue(columnId)) < Number(filterValue.value); - }, - lte: (row, columnId, filterValue: FilterValue, _addMeta) => { - return Number(row.getValue(columnId)) <= Number(filterValue.value); - }, - gt: (row, columnId, filterValue: FilterValue, _addMeta) => { - return Number(row.getValue(columnId)) > Number(filterValue.value); - }, - gte: (row, columnId, filterValue: FilterValue, _addMeta) => { - return Number(row.getValue(columnId)) >= Number(filterValue.value); - } - }, - string: { - eq: (row, columnId, filterValue: FilterValue, _addMeta) => { - return ( - String(row.getValue(columnId)).toLowerCase() === - String(filterValue.value).toLowerCase() - ); - }, - neq: (row, columnId, filterValue: FilterValue, _addMeta) => { - return ( - String(row.getValue(columnId)).toLowerCase() !== - String(filterValue.value).toLowerCase() - ); - }, - contains: (row, columnId, filterValue: FilterValue, _addMeta) => { - const columnValue = String(row.getValue(columnId)).toLowerCase(); - const filterStr = String(filterValue.value).toLowerCase(); - return columnValue.includes(filterStr); - }, - starts_with: (row, columnId, filterValue: FilterValue, _addMeta) => { - const columnValue = String(row.getValue(columnId)).toLowerCase(); - const filterStr = String(filterValue.value).toLowerCase(); - return columnValue.startsWith(filterStr); - }, - ends_with: (row, columnId, filterValue: FilterValue, _addMeta) => { - const columnValue = String(row.getValue(columnId)).toLowerCase(); - const filterStr = String(filterValue.value).toLowerCase(); - return columnValue.endsWith(filterStr); - } - }, - date: { - eq: (row, columnId, filterValue: FilterValue, _addMeta) => { - return dayjs(row.getValue(columnId)).isSame( - dayjs(filterValue.date), - 'day' - ); - }, - neq: (row, columnId, filterValue: FilterValue, _addMeta) => { - return !dayjs(row.getValue(columnId)).isSame( - dayjs(filterValue.date), - 'day' - ); - }, - lt: (row, columnId, filterValue: FilterValue, _addMeta) => { - return dayjs(row.getValue(columnId)).isBefore( - dayjs(filterValue.date), - 'day' - ); - }, - lte: (row, columnId, filterValue: FilterValue, _addMeta) => { - return dayjs(row.getValue(columnId)).isSameOrBefore( - dayjs(filterValue.date), - 'day' - ); - }, - gt: (row, columnId, filterValue: FilterValue, _addMeta) => { - return dayjs(row.getValue(columnId)).isAfter( - dayjs(filterValue.date), - 'day' - ); - }, - gte: (row, columnId, filterValue: FilterValue, _addMeta) => { - return dayjs(row.getValue(columnId)).isSameOrAfter( - dayjs(filterValue.date), - 'day' - ); - } - }, - select: { - eq: (row, columnId, filterValue: FilterValue, _addMeta) => { - if (String(filterValue.value) === EmptyFilterValue) { - return row.getValue(columnId) === ''; - } - // Select only supports string values - return String(row.getValue(columnId)) === String(filterValue.value); - }, - neq: (row, columnId, filterValue: FilterValue, _addMeta) => { - if (String(filterValue.value) === EmptyFilterValue) { - return row.getValue(columnId) !== ''; - } - // Select only supports string values - return String(row.getValue(columnId)) !== String(filterValue.value); - } - }, - multiselect: { - in: (row, columnId, filterValue: FilterValue, _addMeta) => { - if (!Array.isArray(filterValue.value)) return false; - - return filterValue.value - .map(value => (value === EmptyFilterValue ? '' : String(value))) - .includes(String(row.getValue(columnId))); - }, - notin: (row, columnId, filterValue: FilterValue, _addMeta) => { - if (!Array.isArray(filterValue.value)) return false; - - return !filterValue.value - .map(value => (value === EmptyFilterValue ? '' : String(value))) - .includes(String(row.getValue(columnId))); - } - } -} as const; - -export function getFilterFn( - type: T, - operator: FilterOperatorTypes -) { - // @ts-expect-error FilterOperatorTypes is union of all possible operators - return filterOperationsMap[type][operator]; -} - -const handleStringBasedTypes = ( - filterType: FilterTypes, - value: any, - operator?: FilterOperatorTypes | DataTableFilterOperatorTypes -): DataViewFilterValues => { - switch (filterType) { - case FilterType.date: { - const dateValue = dayjs(value); - let stringValue = ''; - if (dateValue.isValid()) { - try { - stringValue = dateValue.toISOString(); - } catch { - stringValue = ''; - } - } - return { - value, - stringValue - }; - } - case FilterType.select: - return { - stringValue: value === EmptyFilterValue ? '' : value, - value - }; - case FilterType.multiselect: - return { - value, - stringValue: value - .map((value: any) => - value === EmptyFilterValue ? '' : String(value) - ) - .join() - }; - case FilterType.string: { - // Apply wildcards for ilike operations - let processedValue = value; - // Check if we need to apply wildcards (operator could be UI type or already converted to 'ilike') - if (operator === 'contains') { - processedValue = `%${value}%`; - } else if (operator === 'starts_with') { - processedValue = `${value}%`; - } else if (operator === 'ends_with') { - processedValue = `%${value}`; - } else if (operator === 'ilike') { - // If already converted to ilike, assume it needs contains-style wildcards - // unless the value already has wildcards - if (!value.includes('%')) { - processedValue = `%${value}%`; - } - } - return { - stringValue: processedValue, - value - }; - } - default: - return { - stringValue: value, - value - }; - } -}; - -export const getFilterOperator = ({ - value, - filterType, - operator -}: { - value: any; - filterType?: FilterTypes; - operator: FilterOperatorTypes; -}): DataTableFilterOperatorTypes => { - if (value === EmptyFilterValue && filterType === FilterType.select) { - return 'empty'; - } - - // Map string filter operators to ilike for DataViewFilter - if ( - filterType === FilterType.string && - (operator === 'contains' || - operator === 'starts_with' || - operator === 'ends_with') - ) { - return 'ilike'; - } - - return operator as DataTableFilterOperatorTypes; -}; - -export const getFilterValue = ({ - value, - dataType = 'string', - filterType = FilterType.string, - operator -}: { - value: any; - dataType?: FilterValueType; - filterType?: FilterTypes; - operator?: FilterOperatorTypes | DataTableFilterOperatorTypes; -}): DataViewFilterValues => { - if (dataType === 'boolean') { - return { boolValue: value, value }; - } - if (dataType === 'number') { - return { numberValue: value, value }; - } - - // Handle string-based types - return handleStringBasedTypes(filterType, value, operator); -}; - -export const getDataType = ({ - filterType = FilterType.string, - dataType = 'string' -}: { - dataType?: FilterValueType; - filterType?: FilterTypes; -}): FilterValueType => { - switch (filterType) { - case FilterType.multiselect: - case FilterType.select: - return dataType; - case FilterType.date: - return 'string'; - default: - return filterType; - } -}; diff --git a/packages/raystack/components/data-view-beta/utils/index.tsx b/packages/raystack/components/data-view-beta/utils/index.tsx deleted file mode 100644 index b0adfba9f..000000000 --- a/packages/raystack/components/data-view-beta/utils/index.tsx +++ /dev/null @@ -1,360 +0,0 @@ -import type { ColumnDef, Row, Table } from '@tanstack/react-table'; -import { TableState } from '@tanstack/table-core'; -import dayjs from 'dayjs'; - -import { FilterOperatorTypes, FilterType } from '~/types/filters'; -import { - DataViewField, - DataViewQuery, - DataViewSort, - defaultGroupOption, - GroupedData, - InternalFilter, - InternalQuery, - SortOrders -} from '../data-view.types'; -import { - getFilterFn, - getFilterOperator, - getFilterValue -} from './filter-operations'; - -export function queryToTableState(query: InternalQuery): Partial { - const columnFilters = - query.filters - ?.filter(data => { - if (data._type === FilterType.date) return dayjs(data.value).isValid(); - if (data.value !== '') return true; - return false; - }) - ?.map(data => { - const valueObject = - data._type === FilterType.date - ? { date: data.value } - : { value: data.value }; - return { - value: valueObject, - id: data?.name - }; - }) || []; - - const sorting = query.sort?.map(data => ({ - id: data?.name, - desc: data?.order === SortOrders.DESC - })); - return { - columnFilters: columnFilters, - sorting: sorting, - globalFilter: query.search - }; -} - -/** - * Convert field metadata to TanStack ColumnDefs. These carry filter/sort/group/ - * visibility capability and the filter predicate, but no `cell` renderer — - * cell/header rendering is installed by each renderer from its own column spec. - * `header` is set to `field.label` so any renderer that falls back to the - * TanStack header string sees the right label. - */ -export function fieldsToColumnDefs( - fields: DataViewField[] = [], - filters: InternalFilter[] = [] -): ColumnDef[] { - return fields.map(field => { - const colFilter = filters?.find(f => f.name === field.accessorKey); - const filterFn = colFilter?.operator - ? getFilterFn(field.filterType || FilterType.string, colFilter.operator) - : undefined; - - return { - id: field.accessorKey, - accessorKey: field.accessorKey, - header: field.label, - enableColumnFilter: field.filterable ?? false, - enableSorting: field.sortable ?? false, - enableGrouping: field.groupable ?? false, - enableHiding: field.hideable ?? false, - filterFn - } as ColumnDef; - }); -} - -export function groupData( - data: TData[], - group_by?: string, - fields: DataViewField[] = [] -): GroupedData[] { - if (!data) return []; - if (!group_by || group_by === defaultGroupOption.id) - return data as GroupedData[]; - - const groupMap = new Map(); - data.forEach((currentData: TData) => { - const item = currentData as Record; - const keyValue = item[group_by]; - if (!groupMap.has(keyValue)) { - groupMap.set(keyValue, []); - } - groupMap.get(keyValue)?.push(item as TData); - }); - - const field = fields.find(f => f.accessorKey === group_by); - const showGroupCount = field?.showGroupCount || false; - const groupLablesMap = field?.groupLabelsMap || {}; - const groupCountMap = field?.groupCountMap || {}; - const groupedData: GroupedData[] = []; - - groupMap.forEach((value, key) => { - groupedData.push({ - label: groupLablesMap[key] || key, - group_key: key, - subRows: value, - count: groupCountMap[key] ?? value.length, - showGroupCount - }); - }); - - return groupedData; -} - -const generateFilterMap = ( - filters: InternalFilter[] = [] -): Map => { - return new Map( - filters - ?.filter(data => data._type === FilterType.select || data.value !== '') - .map(({ name, operator, value }) => [`${name}-${operator}`, value]) - ); -}; - -const generateSortMap = (sort: DataViewSort[] = []): Map => { - return new Map(sort.map(({ name, order }) => [name, order])); -}; - -const isFilterChanged = ( - oldFilters: InternalFilter[] = [], - newFilters: InternalFilter[] = [] -): boolean => { - const oldFilterMap = generateFilterMap(oldFilters); - const newFilterMap = generateFilterMap(newFilters); - - if (oldFilterMap.size !== newFilterMap.size) return true; - - return [...newFilterMap].some( - ([key, value]) => oldFilterMap.get(key) !== value - ); -}; - -const isSortChanged = ( - oldSort: DataViewSort[] = [], - newSort: DataViewSort[] = [] -): boolean => { - if (oldSort.length !== newSort.length) return true; - - const oldSortMap = generateSortMap(oldSort); - const newSortMap = generateSortMap(newSort); - - return [...newSortMap].some(([key, order]) => oldSortMap.get(key) !== order); -}; - -const isGroupChanged = ( - oldGroupBy: string[] = [], - newGroupBy: string[] = [] -): boolean => { - if (oldGroupBy.length !== newGroupBy.length) return true; - - const oldGroupSet = new Set(oldGroupBy); - return newGroupBy.some(item => !oldGroupSet.has(item)); -}; - -const isSearchChanged = (oldSearch?: string, newSearch?: string): boolean => { - return oldSearch !== newSearch; -}; - -/** - * Checks if there is an active filter, search, or updated sort/grouping - * compared to the defaults. Used to distinguish zero state from empty state. - */ -export const hasActiveQuery = ( - tableQuery: InternalQuery, - defaultSort: DataViewSort -): boolean => { - const hasFilters = - (tableQuery?.filters && tableQuery.filters.length > 0) || false; - const hasSearch = Boolean( - tableQuery?.search && tableQuery.search.trim() !== '' - ); - const sortChanged = isSortChanged([defaultSort], tableQuery.sort || []); - const groupChanged = isGroupChanged( - [defaultGroupOption.id], - tableQuery.group_by || [] - ); - return hasFilters || hasSearch || sortChanged || groupChanged; -}; - -export const hasQueryChanged = ( - oldQuery: InternalQuery | null, - newQuery: InternalQuery -): boolean => { - if (!oldQuery) return true; - return ( - isFilterChanged(oldQuery.filters, newQuery.filters) || - isSortChanged(oldQuery.sort, newQuery.sort) || - isGroupChanged(oldQuery.group_by, newQuery.group_by) || - isSearchChanged(oldQuery.search, newQuery.search) - ); -}; - -export function getInitialColumnVisibility( - fields: DataViewField[] = [] -): Record { - return fields.reduce>((acc, field) => { - acc[field.accessorKey] = field.defaultHidden ? false : true; - return acc; - }, {}); -} - -export function transformToDataViewQuery(query: InternalQuery): DataViewQuery { - const { group_by = [], filters = [], sort = [], ...rest } = query; - const sanitizedGroupBy = group_by?.filter( - key => key !== defaultGroupOption.id - ); - - const sanitizedFilters = - filters - ?.filter(data => { - if (data._type === FilterType.select) return true; - if (data._type === FilterType.date) return dayjs(data.value).isValid(); - if (data.value !== '') return true; - return false; - }) - ?.map(data => ({ - name: data.name, - operator: getFilterOperator({ - operator: data.operator, - value: data.value, - filterType: data._type - }), - ...getFilterValue({ - value: data.value, - filterType: data._type, - dataType: data._dataType, - operator: data.operator - }) - })) || []; - - return { - ...rest, - sort: sort, - group_by: sanitizedGroupBy, - filters: sanitizedFilters - }; -} - -// Transform DataViewQuery to InternalQuery -// This reverses the transformation done by transformToDataViewQuery -export function dataViewQueryToInternal(query: DataViewQuery): InternalQuery { - const { filters, ...rest } = query; - - if (!filters) { - return rest; - } - - // Convert DataViewFilter[] to InternalFilter[] - const internalFilters: InternalFilter[] = filters.map(filter => { - const { - operator, - value, - stringValue, - numberValue, - boolValue, - ...filterRest - } = filter; - - // Reverse the operator mapping and wildcard transformation - let transformedFilter = { - operator: operator as FilterOperatorTypes, - value: value, - ...(stringValue !== undefined && { stringValue }), - ...(numberValue !== undefined && { numberValue }), - ...(boolValue !== undefined && { boolValue }) - }; - - // If operator is 'ilike', determine the original operator based on wildcards - if (operator === 'ilike' && stringValue) { - if (stringValue.startsWith('%') && stringValue.endsWith('%')) { - transformedFilter = { - operator: 'contains', - value: stringValue.slice(1, -1) // Remove % from both ends - }; - } else if (stringValue.endsWith('%')) { - transformedFilter = { - operator: 'starts_with', - value: stringValue.slice(0, -1) // Remove % from end - }; - } else if (stringValue.startsWith('%')) { - transformedFilter = { - operator: 'ends_with', - value: stringValue.slice(1) // Remove % from start - }; - } else { - // Default to contains if no wildcards (shouldn't happen normally) - transformedFilter = { - operator: 'contains', - value: stringValue - }; - } - } - - return { - ...filterRest, - ...transformedFilter, - // We don't have type information, so leave it undefined - // The UI will need to infer or set these based on column definitions - _type: undefined, - _dataType: undefined - } as InternalFilter; - }); - - return { - ...rest, - filters: internalFilters - }; -} - -/** Leaf count from the row tree. Do not use `model.flatRows` here: with `filterFromLeafRows`, TanStack's filtered model leaves `flatRows` empty while `rows` is correct. */ -export function countLeafRows(rows: Row[]): number { - return rows.reduce( - (n, row) => n + (row.subRows?.length ? countLeafRows(row.subRows) : 1), - 0 - ); -} - -/** Difference between pre- and post-filter leaf rows (client mode only). */ -export function getClientHiddenLeafRowCount(table: Table): number { - const pre = table.getPreFilteredRowModel(); - const post = table.getFilteredRowModel(); - return Math.max(0, countLeafRows(pre.rows) - countLeafRows(post.rows)); -} - -export function hasActiveTableFiltering(table: Table): boolean { - const state = table.getState(); - if (state.columnFilters?.length > 0) return true; - const gf = state.globalFilter; - if (gf === undefined || gf === null) return false; - return String(gf).trim() !== ''; -} - -export function getDefaultTableQuery( - defaultSort: DataViewSort, - oldQuery: DataViewQuery = {} -): InternalQuery { - // Convert DataViewQuery to InternalQuery - const internalQuery = dataViewQueryToInternal(oldQuery); - - return { - sort: [defaultSort], - group_by: [defaultGroupOption.id], - ...internalQuery - }; -}