feat: add URL deep-linking for DataGrids
- Add PageUrlState utility for URL ↔ state serialization - Add column `id` field for unique column identification in URLs - Update idSelector to return String for URL compatibility - All DataGrid pages now support URL params: search, sort, order, id - Browser URL updates via replaceState (no GoRouter rebuilds) - Add FilterPanelSemantics for filter panel semantic IDs - Add TESTING.md documentation 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.5
parent
a6c51f757a
commit
c494ace5d8
@@ -1,6 +1,10 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:tatlock_ui/core/api/api_client.dart';
|
||||
import 'package:tatlock_ui/routing/url_state.dart';
|
||||
import 'package:tatlock_ui/shared/components/data_grid/adapters/core_api_source.dart';
|
||||
import 'package:tatlock_ui/shared/components/data_grid/data_grid_exports.dart';
|
||||
|
||||
@@ -61,7 +65,10 @@ class UserData {
|
||||
|
||||
/// Users list page using the shared DataGrid component.
|
||||
class UsersListPage extends ConsumerStatefulWidget {
|
||||
const UsersListPage({super.key});
|
||||
const UsersListPage({super.key, this.routerState});
|
||||
|
||||
/// Router state for URL deep-linking.
|
||||
final GoRouterState? routerState;
|
||||
|
||||
@override
|
||||
ConsumerState<UsersListPage> createState() => _UsersListPageState();
|
||||
@@ -70,11 +77,19 @@ class UsersListPage extends ConsumerStatefulWidget {
|
||||
class _UsersListPageState extends ConsumerState<UsersListPage> {
|
||||
late final StateNotifierProvider<DataGridController<UserData>,
|
||||
DataGridState<UserData>> _gridProvider;
|
||||
late PageUrlState _urlState;
|
||||
Timer? _urlSyncTimer;
|
||||
bool _isSyncing = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
// Parse URL state
|
||||
_urlState = PageUrlState.fromQueryParams(
|
||||
widget.routerState?.uri.queryParameters ?? {},
|
||||
);
|
||||
|
||||
final dio = ref.read(coreApiClientProvider);
|
||||
final source = CoreApiDataSource<UserData>(
|
||||
dio: dio,
|
||||
@@ -82,13 +97,63 @@ class _UsersListPageState extends ConsumerState<UsersListPage> {
|
||||
fromJson: UserData.fromJson,
|
||||
);
|
||||
|
||||
// Initialize grid with URL state
|
||||
_gridProvider = dataGridProvider<UserData>(
|
||||
source: source,
|
||||
config: _buildConfig(),
|
||||
idSelector: (u) => u.id,
|
||||
initialSearch: _urlState.search,
|
||||
initialSortColumnIndex: _columnIndexForId(_urlState.sortColumn),
|
||||
initialSortDescending: _urlState.sortDescending,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_urlSyncTimer?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
/// Find column index by column ID.
|
||||
int? _columnIndexForId(String? columnId) {
|
||||
if (columnId == null) return null;
|
||||
final columns = _buildConfig().columns;
|
||||
for (var i = 0; i < columns.length; i++) {
|
||||
if (columns[i].id == columnId) return i;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Get column ID by index.
|
||||
String? _columnIdForIndex(int index) {
|
||||
final columns = _buildConfig().columns;
|
||||
if (index >= 0 && index < columns.length) {
|
||||
return columns[index].id;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Schedule URL sync with debounce.
|
||||
void _scheduleUrlSync() {
|
||||
_urlSyncTimer?.cancel();
|
||||
_urlSyncTimer = Timer(const Duration(milliseconds: 500), _syncUrlParams);
|
||||
}
|
||||
|
||||
/// Sync current state to URL.
|
||||
void _syncUrlParams() {
|
||||
final state = ref.read(_gridProvider);
|
||||
|
||||
final params = PageUrlState(
|
||||
search: state.searchQuery.isEmpty ? null : state.searchQuery,
|
||||
sortColumn: state.sortColumnIndex != null
|
||||
? _columnIdForIndex(state.sortColumnIndex!)
|
||||
: null,
|
||||
sortDescending: state.sortDescending,
|
||||
).toQueryParams();
|
||||
|
||||
updateBrowserUrlParams(params);
|
||||
}
|
||||
|
||||
Future<void> _syncFromAuthentik() async {
|
||||
if (_isSyncing) return;
|
||||
setState(() => _isSyncing = true);
|
||||
@@ -125,6 +190,7 @@ class _UsersListPageState extends ConsumerState<UsersListPage> {
|
||||
return DataGridConfig<UserData>(
|
||||
columns: [
|
||||
DataGridColumn<UserData>(
|
||||
id: 'user',
|
||||
header: 'User',
|
||||
valueBuilder: (u) => u.name,
|
||||
sortable: true,
|
||||
@@ -133,12 +199,14 @@ class _UsersListPageState extends ConsumerState<UsersListPage> {
|
||||
cellBuilder: (context, u) => _UserCell(user: u),
|
||||
),
|
||||
DataGridColumn<UserData>(
|
||||
id: 'roles',
|
||||
header: 'Roles',
|
||||
valueBuilder: (u) => u.roles.join(', '),
|
||||
width: const DataGridColumnWidth.flex(1),
|
||||
cellBuilder: (context, u) => _RolesCell(roles: u.roles),
|
||||
),
|
||||
DataGridColumn<UserData>(
|
||||
id: 'lastLogin',
|
||||
header: 'Last Login',
|
||||
valueBuilder: (u) => u.lastLoginFormatted,
|
||||
width: const DataGridColumnWidth.fixed(120),
|
||||
@@ -154,6 +222,15 @@ class _UsersListPageState extends ConsumerState<UsersListPage> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// Listen for grid state changes to sync URL
|
||||
ref.listen(_gridProvider, (previous, next) {
|
||||
if (previous?.searchQuery != next.searchQuery ||
|
||||
previous?.sortColumnIndex != next.sortColumnIndex ||
|
||||
previous?.sortDescending != next.sortDescending) {
|
||||
_scheduleUrlSync();
|
||||
}
|
||||
});
|
||||
|
||||
return DataGrid<UserData>(
|
||||
provider: _gridProvider,
|
||||
config: _buildConfig(),
|
||||
|
||||
Reference in New Issue
Block a user