| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -445,21 +445,18 @@ class ExtensionDriverSession { | |||
| 445 | 445 | required String tableName, | |
| 446 | 446 | }) async { | |
| 447 | 447 | final bridge = await ensureConnected(row); | |
| 448 | - try { | ||
| 449 | - final result = await bridge.sendRequest('db.getTableSchema', { | ||
| 450 | - 'connectionId': row.id, | ||
| 451 | - 'database': database, | ||
| 452 | - if (schema != null && schema.isNotEmpty) 'schema': schema, | ||
| 453 | - 'tableName': tableName, | ||
| 454 | - }); | ||
| 455 | - if (result is Map) { | ||
| 456 | - return TableSchemaMeta.fromJson(Map<String, dynamic>.from(result)); | ||
| 457 | - } | ||
| 458 | - return TableSchemaMeta(tableName: tableName, schema: schema); | ||
| 459 | - } catch (e) { | ||
| 460 | - debugPrint('ExtensionDriverSession getTableSchema fallback ($e)'); | ||
| 461 | - return TableSchemaMeta(tableName: tableName, schema: schema); | ||
| 448 | + final result = await bridge.sendRequest('db.getTableSchema', { | ||
| 449 | + 'connectionId': row.id, | ||
| 450 | + 'database': database, | ||
| 451 | + if (schema != null && schema.isNotEmpty) 'schema': schema, | ||
| 452 | + 'tableName': tableName, | ||
| 453 | + }); | ||
| 454 | + if (result is Map) { | ||
| 455 | + return TableSchemaMeta.fromJson(Map<String, dynamic>.from(result)); | ||
| 462 | 456 | } | |
| 457 | + throw StateError( | ||
| 458 | + 'Driver db.getTableSchema returned unexpected payload: $result', | ||
| 459 | + ); | ||
| 463 | 460 | } | |
| 464 | 461 | ||
| 465 | 462 | /// Executes batch data mutations (insert, update, delete) via `db.mutate`. | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -53,6 +53,9 @@ class _ExtensionTableViewState extends material.State<ExtensionTableView> { | |||
| 53 | 53 | ||
| 54 | 54 | ExtensionDriverCapabilities? _capabilities; | |
| 55 | 55 | DataGridStagingBuffer? _stagingBuffer; | |
| 56 | + bool _schemaLoaded = false; | ||
| 57 | + Object? _schemaError; | ||
| 58 | + List<String> _primaryKeys = const []; | ||
| 56 | 59 | bool _isSaving = false; | |
| 57 | 60 | ||
| 58 | 61 | bool _restartingDriver = false; | |
@@ -80,6 +83,12 @@ class _ExtensionTableViewState extends material.State<ExtensionTableView> { | |||
| 80 | 83 | ||
| 81 | 84 | Future<void> _restartDriver() async { | |
| 82 | 85 | if (_restartingDriver) return; | |
| 86 | + if (!await _confirmDiscardIfNeeded()) return; | ||
| 87 | + if (!mounted) return; | ||
| 88 | + _stagingBuffer?.dispose(); | ||
| 89 | + _stagingBuffer = null; | ||
| 90 | + _schemaLoaded = false; | ||
| 91 | + _schemaError = null; | ||
| 83 | 92 | setState(() { | |
| 84 | 93 | _restartingDriver = true; | |
| 85 | 94 | }); | |
@@ -122,13 +131,19 @@ class _ExtensionTableViewState extends material.State<ExtensionTableView> { | |||
| 122 | 131 | if (oldWidget.connectionRow.id != widget.connectionRow.id || | |
| 123 | 132 | oldWidget.database != widget.database || | |
| 124 | 133 | oldWidget.tableName != widget.tableName) { | |
| 125 | - _offset = 0; | ||
| 126 | - _totalRows = null; | ||
| 127 | - _filterController.clear(); | ||
| 128 | - _filterActive = false; | ||
| 129 | - _stagingBuffer?.dispose(); | ||
| 130 | - _stagingBuffer = null; | ||
| 131 | - unawaited(_loadPage(refreshCount: true)); | ||
| 134 | + unawaited(() async { | ||
| 135 | + if (!await _confirmDiscardIfNeeded()) return; | ||
| 136 | + if (!mounted) return; | ||
| 137 | + _stagingBuffer?.dispose(); | ||
| 138 | + _stagingBuffer = null; | ||
| 139 | + _schemaLoaded = false; | ||
| 140 | + _schemaError = null; | ||
| 141 | + _offset = 0; | ||
| 142 | + _totalRows = null; | ||
| 143 | + _filterController.clear(); | ||
| 144 | + _filterActive = false; | ||
| 145 | + await _loadPage(refreshCount: true); | ||
| 146 | + }()); | ||
| 132 | 147 | } | |
| 133 | 148 | } | |
| 134 | 149 | ||
@@ -140,16 +155,89 @@ class _ExtensionTableViewState extends material.State<ExtensionTableView> { | |||
| 140 | 155 | super.dispose(); | |
| 141 | 156 | } | |
| 142 | 157 | ||
| 158 | + String get _tableTitle => '${widget.database}.${widget.tableName}'; | ||
| 159 | + | ||
| 160 | + bool get _isDirty => _stagingBuffer?.isDirty == true; | ||
| 161 | + | ||
| 162 | + Future<bool> _confirmDiscardIfNeeded() { | ||
| 163 | + return confirmDiscardTableEditsIfDirty( | ||
| 164 | + context: context, | ||
| 165 | + buffer: _stagingBuffer, | ||
| 166 | + tableTitle: _tableTitle, | ||
| 167 | + ); | ||
| 168 | + } | ||
| 169 | + | ||
| 170 | + /// Guards navigation with a discard confirmation and then runs [action] | ||
| 171 | + /// before triggering a page load. | ||
| 172 | + Future<void> _navigateAndLoad(void Function() action) async { | ||
| 173 | + if (!await _confirmDiscardIfNeeded()) return; | ||
| 174 | + if (!mounted) return; | ||
| 175 | + _stagingBuffer?.dispose(); | ||
| 176 | + _stagingBuffer = null; | ||
| 177 | + action(); | ||
| 178 | + unawaited(_loadPage(refreshCount: true)); | ||
| 179 | + } | ||
| 180 | + | ||
| 181 | + Future<void> _onRefresh() async { | ||
| 182 | + if (!await _confirmDiscardIfNeeded()) return; | ||
| 183 | + if (!mounted) return; | ||
| 184 | + _stagingBuffer?.dispose(); | ||
| 185 | + _stagingBuffer = null; | ||
| 186 | + _schemaLoaded = false; | ||
| 187 | + _schemaError = null; | ||
| 188 | + await _loadPage(refreshCount: true); | ||
| 189 | + } | ||
| 190 | + | ||
| 191 | + Future<void> _ensureSchema() async { | ||
| 192 | + if (_schemaLoaded) return; | ||
| 193 | + if (widget.isView || _capabilities?.supportsMutations != true) { | ||
| 194 | + _schemaLoaded = true; | ||
| 195 | + _schemaError = null; | ||
| 196 | + _primaryKeys = const []; | ||
| 197 | + return; | ||
| 198 | + } | ||
| 199 | + final loaded = await loadTableViewSchema( | ||
| 200 | + () => ExtensionDriverSession.instance.getTableSchema( | ||
| 201 | + widget.connectionRow, | ||
| 202 | + database: widget.database, | ||
| 203 | + tableName: widget.tableName, | ||
| 204 | + ), | ||
| 205 | + ); | ||
| 206 | + final schema = loaded.schema; | ||
| 207 | + if (schema != null) { | ||
| 208 | + _primaryKeys = List<String>.from(schema.primaryKeys); | ||
| 209 | + _schemaError = null; | ||
| 210 | + } else { | ||
| 211 | + _primaryKeys = const []; | ||
| 212 | + _schemaError = loaded.error; | ||
| 213 | + } | ||
| 214 | + _schemaLoaded = true; | ||
| 215 | + } | ||
| 216 | + | ||
| 143 | 217 | void _updateStatusLine() { | |
| 144 | 218 | final total = _totalRows; | |
| 145 | 219 | final shownFrom = _rows.isEmpty ? 0 : _offset + 1; | |
| 146 | 220 | final shownTo = _offset + _rows.length; | |
| 221 | + final String pag; | ||
| 147 | 222 | if (total == null) { | |
| 148 | - _statusLine = _loading | ||
| 223 | + pag = _loading | ||
| 149 | 224 | ? 'Loading data...' | |
| 150 | 225 | : 'Showing $shownTo row(s) (Calculating count...).'; | |
| 151 | 226 | } else { | |
| 152 | - _statusLine = 'Rows $shownFrom–$shownTo of $total.'; | ||
| 227 | + pag = 'Rows $shownFrom–$shownTo of $total.'; | ||
| 228 | + } | ||
| 229 | + final reason = tableViewEditDisabledReason( | ||
| 230 | + isView: widget.isView, | ||
| 231 | + customSqlActive: false, | ||
| 232 | + hasPrimaryKey: _primaryKeys.isNotEmpty, | ||
| 233 | + schemaLoaded: _schemaLoaded, | ||
| 234 | + readOnly: _capabilities != null && !_capabilities!.supportsMutations, | ||
| 235 | + schemaError: _schemaError, | ||
| 236 | + ); | ||
| 237 | + if (reason != null) { | ||
| 238 | + _statusLine = '$pag · $reason'; | ||
| 239 | + } else { | ||
| 240 | + _statusLine = pag; | ||
| 153 | 241 | } | |
| 154 | 242 | } | |
| 155 | 243 | ||
@@ -208,18 +296,26 @@ class _ExtensionTableViewState extends material.State<ExtensionTableView> { | |||
| 208 | 296 | 'SELECT * FROM $_qualifiedName$_whereClause LIMIT ${widget.pageSize} OFFSET $_offset', | |
| 209 | 297 | ); | |
| 210 | 298 | ||
| 299 | + await _ensureSchema(); | ||
| 300 | + | ||
| 211 | 301 | if (!mounted) return; | |
| 212 | 302 | setState(() { | |
| 213 | 303 | _columns = dataResult.columns; | |
| 214 | 304 | _rows = dataResult.rows; | |
| 215 | 305 | _loading = false; | |
| 216 | - _stagingBuffer?.dispose(); | ||
| 217 | - if (!widget.isView && (_capabilities?.supportsMutations == true)) { | ||
| 218 | - _stagingBuffer = | ||
| 219 | - DataGridStagingBuffer(columns: _columns, rows: _rows); | ||
| 220 | - } else { | ||
| 221 | - _stagingBuffer = null; | ||
| 222 | - } | ||
| 306 | + final editingEnabled = tableViewEditingEnabled( | ||
| 307 | + isView: widget.isView, | ||
| 308 | + customSqlActive: false, | ||
| 309 | + hasPrimaryKey: _primaryKeys.isNotEmpty, | ||
| 310 | + readOnly: _capabilities?.supportsMutations != true, | ||
| 311 | + schemaError: _schemaError, | ||
| 312 | + ); | ||
| 313 | + _stagingBuffer = replaceTableViewStagingBuffer( | ||
| 314 | + previous: _stagingBuffer, | ||
| 315 | + columns: _columns, | ||
| 316 | + rows: _rows, | ||
| 317 | + enabled: editingEnabled, | ||
| 318 | + ); | ||
| 223 | 319 | _updateStatusLine(); | |
| 224 | 320 | }); | |
| 225 | 321 | ||
@@ -238,14 +334,22 @@ class _ExtensionTableViewState extends material.State<ExtensionTableView> { | |||
| 238 | 334 | final buffer = _stagingBuffer; | |
| 239 | 335 | if (buffer == null || !buffer.isDirty) return; | |
| 240 | 336 | ||
| 337 | + final primaryKeys = _primaryKeys; | ||
| 338 | + if (primaryKeys.isEmpty) { | ||
| 339 | + if (mounted) { | ||
| 340 | + showAppToast( | ||
| 341 | + context: context, | ||
| 342 | + message: | ||
| 343 | + 'Cannot save: no primary key is available for ${widget.tableName}. ' | ||
| 344 | + 'Edits would match all rows.', | ||
| 345 | + variant: AppToastVariant.error, | ||
| 346 | + ); | ||
| 347 | + } | ||
| 348 | + return; | ||
| 349 | + } | ||
| 350 | + | ||
| 241 | 351 | setState(() => _isSaving = true); | |
| 242 | 352 | try { | |
| 243 | - final schema = await ExtensionDriverSession.instance.getTableSchema( | ||
| 244 | - widget.connectionRow, | ||
| 245 | - database: widget.database, | ||
| 246 | - tableName: widget.tableName, | ||
| 247 | - ); | ||
| 248 | - | ||
| 249 | 353 | final mutations = <Map<String, dynamic>>[]; | |
| 250 | 354 | ||
| 251 | 355 | // 1. Updates | |
@@ -255,16 +359,10 @@ class _ExtensionTableViewState extends material.State<ExtensionTableView> { | |||
| 255 | 359 | final origRow = buffer.originalRows[rowIndex]; | |
| 256 | 360 | ||
| 257 | 361 | final whereMap = <String, dynamic>{}; | |
| 258 | - if (schema.primaryKeys.isNotEmpty) { | ||
| 259 | - for (final pk in schema.primaryKeys) { | ||
| 260 | - final idx = _columns.indexOf(pk); | ||
| 261 | - if (idx != -1 && idx < origRow.length) { | ||
| 262 | - whereMap[pk] = origRow[idx]; | ||
| 263 | - } | ||
| 264 | - } | ||
| 265 | - } else { | ||
| 266 | - for (var c = 0; c < _columns.length; c++) { | ||
| 267 | - whereMap[_columns[c]] = c < origRow.length ? origRow[c] : null; | ||
| 362 | + for (final pk in primaryKeys) { | ||
| 363 | + final idx = _columns.indexOf(pk); | ||
| 364 | + if (idx != -1 && idx < origRow.length) { | ||
| 365 | + whereMap[pk] = origRow[idx]; | ||
| 268 | 366 | } | |
| 269 | 367 | } | |
| 270 | 368 | ||
@@ -304,16 +402,10 @@ class _ExtensionTableViewState extends material.State<ExtensionTableView> { | |||
| 304 | 402 | for (final rowIndex in buffer.deletedRowIndices) { | |
| 305 | 403 | final origRow = buffer.originalRows[rowIndex]; | |
| 306 | 404 | final whereMap = <String, dynamic>{}; | |
| 307 | - if (schema.primaryKeys.isNotEmpty) { | ||
| 308 | - for (final pk in schema.primaryKeys) { | ||
| 309 | - final idx = _columns.indexOf(pk); | ||
| 310 | - if (idx != -1 && idx < origRow.length) { | ||
| 311 | - whereMap[pk] = origRow[idx]; | ||
| 312 | - } | ||
| 313 | - } | ||
| 314 | - } else { | ||
| 315 | - for (var c = 0; c < _columns.length; c++) { | ||
| 316 | - whereMap[_columns[c]] = c < origRow.length ? origRow[c] : null; | ||
| 405 | + for (final pk in primaryKeys) { | ||
| 406 | + final idx = _columns.indexOf(pk); | ||
| 407 | + if (idx != -1 && idx < origRow.length) { | ||
| 408 | + whereMap[pk] = origRow[idx]; | ||
| 317 | 409 | } | |
| 318 | 410 | } | |
| 319 | 411 | mutations.add({ | |
@@ -330,10 +422,17 @@ class _ExtensionTableViewState extends material.State<ExtensionTableView> { | |||
| 330 | 422 | mutations: mutations, | |
| 331 | 423 | ); | |
| 332 | 424 | if (!mounted) return; | |
| 333 | - final count = res['affectedRows'] ?? mutations.length; | ||
| 425 | + final affectedRows = res['affectedRows']; | ||
| 426 | + if (affectedRows is! int) { | ||
| 427 | + throw StateError( | ||
| 428 | + 'Save failed: driver did not return an affectedRows count.', | ||
| 429 | + ); | ||
| 430 | + } | ||
| 431 | + expectDmlMatchedRows(affectedRows); | ||
| 432 | + | ||
| 334 | 433 | showAppToast( | |
| 335 | 434 | context: context, | |
| 336 | - message: 'Successfully applied $count mutation(s).', | ||
| 435 | + message: 'Successfully applied $affectedRows mutation(s).', | ||
| 337 | 436 | variant: AppToastVariant.success, | |
| 338 | 437 | ); | |
| 339 | 438 | unawaited(_loadPage(refreshCount: true)); | |
@@ -351,16 +450,18 @@ class _ExtensionTableViewState extends material.State<ExtensionTableView> { | |||
| 351 | 450 | } | |
| 352 | 451 | ||
| 353 | 452 | void _applyFilter() { | |
| 354 | - _offset = 0; | ||
| 355 | - _totalRows = null; | ||
| 356 | - unawaited(_loadPage(refreshCount: true)); | ||
| 453 | + unawaited(_navigateAndLoad(() { | ||
| 454 | + _offset = 0; | ||
| 455 | + _totalRows = null; | ||
| 456 | + })); | ||
| 357 | 457 | } | |
| 358 | 458 | ||
| 359 | 459 | void _clearFilter() { | |
| 360 | 460 | _filterController.clear(); | |
| 361 | - _offset = 0; | ||
| 362 | - _totalRows = null; | ||
| 363 | - unawaited(_loadPage(refreshCount: true)); | ||
| 461 | + unawaited(_navigateAndLoad(() { | ||
| 462 | + _offset = 0; | ||
| 463 | + _totalRows = null; | ||
| 464 | + })); | ||
| 364 | 465 | } | |
| 365 | 466 | ||
| 366 | 467 | Future<void> _openDdlDialog() async { | |
@@ -437,24 +538,27 @@ class _ExtensionTableViewState extends material.State<ExtensionTableView> { | |||
| 437 | 538 | } | |
| 438 | 539 | } | |
| 439 | 540 | ||
| 440 | - bool get _canGoBack => _offset > 0; | ||
| 541 | + bool get _canGoBack => _offset > 0 && !_isDirty; | ||
| 441 | 542 | ||
| 442 | 543 | bool get _canGoForward { | |
| 544 | + if (_isDirty) return false; | ||
| 443 | 545 | final total = _totalRows; | |
| 444 | 546 | if (total == null) return _rows.length >= widget.pageSize; | |
| 445 | 547 | return _offset + widget.pageSize < total; | |
| 446 | 548 | } | |
| 447 | 549 | ||
| 448 | 550 | void _previousPage() { | |
| 449 | 551 | if (!_canGoBack || _loading) return; | |
| 450 | - _offset = (_offset - widget.pageSize).clamp(0, 1 << 30); | ||
| 451 | - unawaited(_loadPage()); | ||
| 552 | + unawaited(_navigateAndLoad(() { | ||
| 553 | + _offset = (_offset - widget.pageSize).clamp(0, 1 << 30); | ||
| 554 | + })); | ||
| 452 | 555 | } | |
| 453 | 556 | ||
| 454 | 557 | void _nextPage() { | |
| 455 | 558 | if (!_canGoForward || _loading) return; | |
| 456 | - _offset += widget.pageSize; | ||
| 457 | - unawaited(_loadPage()); | ||
| 559 | + unawaited(_navigateAndLoad(() { | ||
| 560 | + _offset += widget.pageSize; | ||
| 561 | + })); | ||
| 458 | 562 | } | |
| 459 | 563 | ||
| 460 | 564 | @override | |
@@ -473,7 +577,13 @@ class _ExtensionTableViewState extends material.State<ExtensionTableView> { | |||
| 473 | 577 | loading: _loading, | |
| 474 | 578 | canGoPrevious: _canGoBack && !_loading, | |
| 475 | 579 | canGoNext: _canGoForward && !_loading, | |
| 476 | - onNavigateHome: widget.onNavigateHome, | ||
| 580 | + onNavigateHome: widget.onNavigateHome != null | ||
| 581 | + ? () => unawaited(() async { | ||
| 582 | + if (!await _confirmDiscardIfNeeded()) return; | ||
| 583 | + if (!mounted) return; | ||
| 584 | + widget.onNavigateHome!(); | ||
| 585 | + }()) | ||
| 586 | + : null, | ||
| 477 | 587 | filterActive: _filterActive || _filterController.text.isNotEmpty, | |
| 478 | 588 | filterText: _filterController.text, | |
| 479 | 589 | onToggleFilter: () { | |
@@ -484,7 +594,7 @@ class _ExtensionTableViewState extends material.State<ExtensionTableView> { | |||
| 484 | 594 | onOpenDdl: _openDdlDialog, | |
| 485 | 595 | onGoPrevious: _previousPage, | |
| 486 | 596 | onGoNext: _nextPage, | |
| 487 | - onRefresh: () => _loadPage(refreshCount: true), | ||
| 597 | + onRefresh: () => unawaited(_onRefresh()), | ||
| 488 | 598 | onRestartDriver: () => unawaited(_restartDriver()), | |
| 489 | 599 | isRestarting: _restartingDriver, | |
| 490 | 600 | onCopyFormat: (format) { | |
| Back | FazBrowse Home | New Git URL |
0 commit comments