# DataGrid Component System This document specifies the reusable DataGrid component for consistent table UIs across all features. ## Overview The DataGrid system provides a declarative, type-safe way to display tabular data with: - Configurable columns with custom renderers - Row selection (single and multi-select) - Sorting and searching - Per-row and bulk actions - Pagination and infinite scroll - Integration with Core API Inspired by: [fframe ListGrid](https://github.com/postmeridiem/fframe/tree/main/fframe/lib/screens/listgrid_screen) ## Quick Start ```dart DataGrid( config: DataGridConfig( columns: [ DataGridColumn( header: 'Name', valueBuilder: (c) => c.name, sortable: true, sortField: 'name', ), DataGridColumn( header: 'Status', valueBuilder: (c) => c.status.name, cellBuilder: (context, c) => ContainerStatusBadge(status: c.status), ), DataGridColumn( header: 'Image', valueBuilder: (c) => c.image, width: DataGridColumnWidth.flex(2), ), ], actions: [ DataGridAction( icon: Icons.play_arrow, label: 'Start', onTap: (c) => ref.read(containerActionsProvider).start(c.id), showWhen: (c) => c.status == ContainerStatus.stopped, ), DataGridAction( icon: Icons.stop, label: 'Stop', onTap: (c) => ref.read(containerActionsProvider).stop(c.id), showWhen: (c) => c.status == ContainerStatus.running, destructive: true, requiresConfirmation: true, ), ], rowsSelectable: true, enableSearch: true, onRowTap: (c) => context.push('/containers/${c.id}'), ), source: ContainersDataSource(ref.watch(apiClientProvider)), ) ``` --- ## Configuration Classes ### DataGridConfig Main configuration for a data grid instance. ```dart class DataGridConfig { const DataGridConfig({ required this.columns, this.actions = const [], this.bulkActions = const [], this.rowsSelectable = false, this.showHeader = true, this.showFooter = true, this.enableSearch = false, this.searchableColumns = const [], this.defaultSortColumn, this.defaultSortDescending = false, this.emptyStateBuilder, this.loadingBuilder, this.errorBuilder, this.onRowTap, this.cellPadding = const EdgeInsets.symmetric(horizontal: 12, vertical: 8), this.headerHeight = 48.0, this.rowHeight, this.dataMode = const DataGridDataMode.all(), }); /// Column definitions final List> columns; /// Per-row actions (shown in action menu) final List> actions; /// Bulk actions (shown when rows selected) final List> bulkActions; /// Enable row selection checkboxes final bool rowsSelectable; /// Show header row with column names final bool showHeader; /// Show footer with row count/pagination final bool showFooter; /// Enable search bar final bool enableSearch; /// Column indices to search (empty = all searchable columns) final List searchableColumns; /// Default sort column index final int? defaultSortColumn; /// Default sort direction final bool defaultSortDescending; /// Custom empty state widget final Widget Function(BuildContext)? emptyStateBuilder; /// Custom loading widget final Widget Function(BuildContext)? loadingBuilder; /// Custom error widget final Widget Function(BuildContext, Object error)? errorBuilder; /// Row tap callback final void Function(T item)? onRowTap; /// Cell padding final EdgeInsetsGeometry cellPadding; /// Header row height final double headerHeight; /// Data row height (null = intrinsic) final double? rowHeight; /// Data loading mode final DataGridDataMode dataMode; } ``` ### DataGridColumn Column definition with rendering options. ```dart class DataGridColumn { const DataGridColumn({ required this.header, required this.valueBuilder, this.cellBuilder, this.cellControlsBuilder, this.width = const DataGridColumnWidth.flex(1), this.alignment = DataGridColumnAlignment.start, this.sortable = false, this.sortField, this.searchable = false, this.visible = true, this.tooltip, }); /// Column header text final String header; /// Extract string value from item (for sorting/searching) final String Function(T item) valueBuilder; /// Custom cell widget builder (overrides default text) final Widget Function(BuildContext context, T item)? cellBuilder; /// Additional controls to show on hover/focus final Widget Function(BuildContext context, T item)? cellControlsBuilder; /// Column width specification final DataGridColumnWidth width; /// Cell content alignment final DataGridColumnAlignment alignment; /// Enable sorting on this column final bool sortable; /// API field name for sorting (defaults to column index) final String? sortField; /// Include in search final bool searchable; /// Column visibility final bool visible; /// Tooltip builder for cell final String Function(T item)? tooltip; } ``` ### DataGridColumnWidth Column width specification using sealed classes. ```dart sealed class DataGridColumnWidth { const DataGridColumnWidth._(); /// Fixed pixel width const factory DataGridColumnWidth.fixed(double width) = _FixedWidth; /// Flex factor (like Expanded) const factory DataGridColumnWidth.flex(int flex) = _FlexWidth; /// Fraction of available width (0.0 - 1.0) const factory DataGridColumnWidth.fraction(double fraction) = _FractionWidth; } // Usage: DataGridColumnWidth.fixed(100) // Always 100px DataGridColumnWidth.flex(2) // 2x flex factor DataGridColumnWidth.fraction(0.3) // 30% of available width ``` ### DataGridColumnAlignment ```dart enum DataGridColumnAlignment { start, center, end, } ``` ### DataGridAction Per-row action definition. ```dart class DataGridAction { const DataGridAction({ required this.icon, required this.label, required this.onTap, this.showWhen, this.destructive = false, this.requiresConfirmation = false, this.confirmationMessage, }); /// Action icon final IconData icon; /// Action label (shown in menu) final String label; /// Action callback final Future Function(T item) onTap; /// Conditional visibility final bool Function(T item)? showWhen; /// Show in red (destructive action) final bool destructive; /// Show confirmation dialog before executing final bool requiresConfirmation; /// Custom confirmation message final String? confirmationMessage; } ``` ### DataGridBulkAction Action on multiple selected rows. ```dart class DataGridBulkAction { const DataGridBulkAction({ required this.icon, required this.label, required this.onTap, this.minSelected = 1, this.maxSelected, this.destructive = false, this.requiresConfirmation = false, }); /// Action icon final IconData icon; /// Action label final String label; /// Action callback with selected items final Future Function(List items) onTap; /// Minimum items required final int minSelected; /// Maximum items allowed (null = unlimited) final int? maxSelected; /// Show in red final bool destructive; /// Show confirmation dialog final bool requiresConfirmation; } ``` ### DataGridDataMode Data loading strategy. ```dart sealed class DataGridDataMode { const DataGridDataMode._(); /// Load all data at once const factory DataGridDataMode.all() = _AllDataMode; /// Traditional pagination const factory DataGridDataMode.paginated({ int pageSize, }) = _PaginatedDataMode; /// Infinite scroll const factory DataGridDataMode.infinite({ int initialLoad, int loadMoreThreshold, }) = _InfiniteDataMode; } ``` --- ## Data Source ### DataGridSource Interface ```dart abstract class DataGridSource { /// Fetch data with optional filtering/sorting Future> fetch({ String? searchQuery, String? sortField, bool sortDescending = false, int? offset, int? limit, }); /// Get total count (for pagination) Future count({String? searchQuery}); } ``` ### DataGridResult ```dart class DataGridResult { const DataGridResult({ required this.items, required this.totalCount, this.hasMore = false, }); final List items; final int totalCount; final bool hasMore; } ``` ### CoreApiDataSource Pre-built adapter for Core API endpoints. ```dart class CoreApiDataSource extends DataGridSource { CoreApiDataSource({ required this.client, required this.endpoint, required this.fromJson, this.searchParam = 'search', this.sortParam = 'sort', this.orderParam = 'order', this.offsetParam = 'offset', this.limitParam = 'limit', }); final ApiClient client; final String endpoint; final T Function(Map) fromJson; final String searchParam; final String sortParam; final String orderParam; final String offsetParam; final String limitParam; @override Future> fetch({ String? searchQuery, String? sortField, bool sortDescending = false, int? offset, int? limit, }) async { final params = {}; if (searchQuery != null && searchQuery.isNotEmpty) { params[searchParam] = searchQuery; } if (sortField != null) { params[sortParam] = sortField; params[orderParam] = sortDescending ? 'desc' : 'asc'; } if (offset != null) params[offsetParam] = offset; if (limit != null) params[limitParam] = limit; final response = await client.get(endpoint, queryParameters: params); final items = (response.data as List).map((j) => fromJson(j)).toList(); return DataGridResult( items: items, totalCount: items.length, // Or from response headers hasMore: items.length == limit, ); } } ``` --- ## State Management ### DataGridNotifier Riverpod notifier for grid state. ```dart @riverpod class DataGridNotifier extends _$DataGridNotifier { @override DataGridState build(DataGridSource source, DataGridConfig config) { _loadData(); return DataGridState.loading(); } Future _loadData() async { state = DataGridState.loading(); try { final result = await source.fetch( searchQuery: _searchQuery, sortField: _sortField, sortDescending: _sortDescending, ); state = DataGridState.loaded( items: result.items, totalCount: result.totalCount, ); } catch (e) { state = DataGridState.error(e); } } void search(String query) { ... } void sort(String field, bool descending) { ... } void selectRow(T item) { ... } void selectAll() { ... } void clearSelection() { ... } Future refresh() { ... } } ``` ### DataGridState ```dart @freezed class DataGridState with _$DataGridState { const factory DataGridState.loading() = _Loading; const factory DataGridState.loaded({ required List items, required int totalCount, @Default({}) Set selectedItems, String? searchQuery, String? sortField, @Default(false) bool sortDescending, }) = _Loaded; const factory DataGridState.error(Object error) = _Error; } ``` --- ## Widget Structure ``` lib/shared/components/data_grid/ ├── data_grid.dart # Main widget ├── data_grid_config.dart # Configuration classes ├── data_grid_column.dart # Column definition ├── data_grid_action.dart # Action definitions ├── data_grid_source.dart # Data source interface ├── data_grid_provider.dart # Riverpod state ├── data_grid_state.dart # Freezed state class ├── widgets/ │ ├── data_grid_header.dart # Header row with sort indicators │ ├── data_grid_row.dart # Data row │ ├── data_grid_cell.dart # Cell wrapper │ ├── data_grid_checkbox.dart # Selection checkbox │ ├── data_grid_actions_menu.dart # Row actions popup │ ├── data_grid_bulk_actions.dart # Bulk action bar │ ├── data_grid_search_bar.dart # Search input │ ├── data_grid_footer.dart # Footer with count/pagination │ └── data_grid_empty_state.dart # Empty state display └── adapters/ └── core_api_source.dart # Core API adapter ``` --- ## Usage Examples ### Basic Table ```dart DataGrid( config: DataGridConfig( columns: [ DataGridColumn(header: 'Name', valueBuilder: (u) => u.name), DataGridColumn(header: 'Email', valueBuilder: (u) => u.email), ], ), source: UsersDataSource(), ) ``` ### With Actions and Selection ```dart DataGrid( config: DataGridConfig( columns: [...], rowsSelectable: true, actions: [ DataGridAction( icon: Icons.restart_alt, label: 'Restart', onTap: (c) async => await restartContainer(c.id), requiresConfirmation: true, ), ], bulkActions: [ DataGridBulkAction( icon: Icons.delete, label: 'Delete Selected', onTap: (items) async => await deleteContainers(items), destructive: true, requiresConfirmation: true, ), ], ), source: ContainersDataSource(), ) ``` ### With Custom Cell Rendering ```dart DataGridColumn( header: 'State', valueBuilder: (d) => d.state, cellBuilder: (context, device) => Switch( value: device.state == 'on', onChanged: (v) => toggleDevice(device.entityId, v), ), ) ``` ### With Pagination ```dart DataGrid( config: DataGridConfig( columns: [...], dataMode: DataGridDataMode.paginated(pageSize: 50), showFooter: true, ), source: LogsDataSource(), ) ```