From 35eae623d8261cd0808e82ff7cfff43c2c864f45 Mon Sep 17 00:00:00 2001 From: Marc Rejohn Castillano Date: Wed, 18 Feb 2026 21:21:45 +0800 Subject: [PATCH] Fixed unable to edit Team --- lib/screens/teams/teams_screen.dart | 77 +++++++++-------------------- lib/utils/supabase_response.dart | 34 +++++++++++++ test/supabase_response_test.dart | 34 +++++++++++++ test/teams_screen_test.dart | 36 ++++++++++++++ 4 files changed, 127 insertions(+), 54 deletions(-) create mode 100644 lib/utils/supabase_response.dart create mode 100644 test/supabase_response_test.dart diff --git a/lib/screens/teams/teams_screen.dart b/lib/screens/teams/teams_screen.dart index a7c86ea2..f1e81230 100644 --- a/lib/screens/teams/teams_screen.dart +++ b/lib/screens/teams/teams_screen.dart @@ -6,7 +6,8 @@ import '../../models/team.dart'; import '../../providers/teams_provider.dart'; import '../../providers/profile_provider.dart'; import '../../providers/tickets_provider.dart'; -import 'package:supabase_flutter/supabase_flutter.dart'; +import '../../providers/supabase_provider.dart'; +import '../../utils/supabase_response.dart'; import 'package:tasq/widgets/multi_select_picker.dart'; import '../../theme/app_surfaces.dart'; import '../../widgets/tasq_adaptive_list.dart'; @@ -395,7 +396,7 @@ class _TeamsScreenState extends ConsumerState { selectedMembers = [leaderId!, ...selectedMembers]; } - final client = Supabase.instance.client; + final client = ref.read(supabaseClientProvider); try { if (isEdit) { // update team row @@ -407,38 +408,24 @@ class _TeamsScreenState extends ConsumerState { 'office_ids': selectedOffices, }) .eq('id', team.id); - if (upRes['error'] != null) { - final err = upRes['error']; - throw Exception( - err is Map ? (err['message'] ?? err.toString()) : err.toString(), - ); - } + final upErr = extractSupabaseError(upRes); + if (upErr != null) throw Exception(upErr); // replace members for the team final delRes = await client .from('team_members') .delete() .eq('team_id', team.id); - if (delRes['error'] != null) { - final err = delRes['error']; - throw Exception( - err is Map ? (err['message'] ?? err.toString()) : err.toString(), - ); - } + final delErr = extractSupabaseError(delRes); + if (delErr != null) throw Exception(delErr); if (selectedMembers.isNotEmpty) { final rows = selectedMembers .map((u) => {'team_id': team.id, 'user_id': u}) .toList(); final memRes = await client.from('team_members').insert(rows); - if (memRes is Map && memRes['error'] != null) { - final err = memRes['error']; - throw Exception( - err is Map - ? (err['message'] ?? err.toString()) - : err.toString(), - ); - } + final memErr = extractSupabaseError(memRes); + if (memErr != null) throw Exception(memErr); // verify members persisted (handle Map or List response) final dynamic checkRes = await client @@ -471,18 +458,12 @@ class _TeamsScreenState extends ConsumerState { // normalize inserted row to extract id reliably across client response shapes dynamic insertedRow; - final insertResValue = insertRes; + final dynamic insertResValue = insertRes; if (insertResValue is List && insertResValue.isNotEmpty) { - insertedRow = (insertResValue as List).first; + insertedRow = insertResValue.first; } else { - if (insertResValue['error'] != null) { - final err = insertResValue['error']; - throw Exception( - err is Map - ? (err['message'] ?? err.toString()) - : err.toString(), - ); - } + final insertErr = extractSupabaseError(insertResValue); + if (insertErr != null) throw Exception(insertErr); final dataField = insertResValue['data']; if (dataField is List && dataField.isNotEmpty) { insertedRow = dataField.first; @@ -503,14 +484,8 @@ class _TeamsScreenState extends ConsumerState { .map((u) => {'team_id': teamId, 'user_id': u}) .toList(); final memRes = await client.from('team_members').insert(rows); - if (memRes is Map && memRes['error'] != null) { - final err = memRes['error']; - throw Exception( - err is Map - ? (err['message'] ?? err.toString()) - : err.toString(), - ); - } + final memErr2 = extractSupabaseError(memRes); + if (memErr2 != null) throw Exception(memErr2); // verify members persisted (handle Map or List response) final dynamic checkRes = await client @@ -656,27 +631,21 @@ class _TeamsScreenState extends ConsumerState { if (confirmed != true) return; try { - final delMembersRes = await Supabase.instance.client + final delMembersRes = await ref + .read(supabaseClientProvider) .from('team_members') .delete() .eq('team_id', teamId); - if (delMembersRes is Map && delMembersRes['error'] != null) { - final err = delMembersRes['error']; - throw Exception( - err is Map ? (err['message'] ?? err.toString()) : err.toString(), - ); - } + final delMembersErr = extractSupabaseError(delMembersRes); + if (delMembersErr != null) throw Exception(delMembersErr); - final delTeamRes = await Supabase.instance.client + final delTeamRes = await ref + .read(supabaseClientProvider) .from('teams') .delete() .eq('id', teamId); - if (delTeamRes is Map && delTeamRes['error'] != null) { - final err = delTeamRes['error']; - throw Exception( - err is Map ? (err['message'] ?? err.toString()) : err.toString(), - ); - } + final delTeamErr = extractSupabaseError(delTeamRes); + if (delTeamErr != null) throw Exception(delTeamErr); ref.invalidate(teamsProvider); ref.invalidate(teamMembersProvider); diff --git a/lib/utils/supabase_response.dart b/lib/utils/supabase_response.dart new file mode 100644 index 00000000..b2b9a44a --- /dev/null +++ b/lib/utils/supabase_response.dart @@ -0,0 +1,34 @@ +/// Utilities for normalizing Supabase/PostgREST response shapes. +/// +/// Supabase Dart responses can appear as Maps (legacy wrapper) or +/// PostgrestResponse-like objects (have `.error`, `.status`, `.statusText`). +/// Helpers here provide a single place to extract an error message safely so +/// callers don't accidentally call `[]` on non-Map objects. + +String? extractSupabaseError(dynamic res) { + if (res == null) return null; + if (res is Map) { + final err = res['error']; + if (err != null) { + return err is Map ? (err['message'] ?? err.toString()) : err.toString(); + } + if (res['status'] != null && res['status'] is int && res['status'] >= 400) { + return res['message']?.toString() ?? 'Request failed with status ${res['status']}'; + } + return null; + } + + // Try PostgrestResponse-like fields via dynamic access (safe within try/catch). + try { + final err = (res as dynamic).error; + if (err != null) return err is Map ? (err['message'] ?? err.toString()) : err.toString(); + } catch (_) {} + try { + final status = (res as dynamic).status; + if (status != null && status >= 400) { + final statusText = (res as dynamic).statusText; + return statusText ?? 'Request failed with status $status'; + } + } catch (_) {} + return null; +} diff --git a/test/supabase_response_test.dart b/test/supabase_response_test.dart new file mode 100644 index 00000000..439348f4 --- /dev/null +++ b/test/supabase_response_test.dart @@ -0,0 +1,34 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:tasq/utils/supabase_response.dart'; + +class _FakePostgrestLike { + final dynamic error; + final int? status; + final String? statusText; + _FakePostgrestLike({this.error, this.status, this.statusText}); +} + +void main() { + test('extractSupabaseError returns null for null/ok responses', () { + expect(extractSupabaseError(null), isNull); + expect(extractSupabaseError({'data': []}), isNull); + }); + + test('extractSupabaseError extracts message from Map-shaped error', () { + final res = { + 'error': {'message': 'boom'}, + }; + expect(extractSupabaseError(res), 'boom'); + + final res2 = {'error': 'simple-error'}; + expect(extractSupabaseError(res2), 'simple-error'); + }); + + test('extractSupabaseError extracts from Postgrest-like response', () { + final r1 = _FakePostgrestLike(error: {'message': 'bad'}); + expect(extractSupabaseError(r1), 'bad'); + + final r2 = _FakePostgrestLike(status: 500, statusText: 'server error'); + expect(extractSupabaseError(r2), 'server error'); + }); +} diff --git a/test/teams_screen_test.dart b/test/teams_screen_test.dart index a2df8186..b91a3237 100644 --- a/test/teams_screen_test.dart +++ b/test/teams_screen_test.dart @@ -136,4 +136,40 @@ void main() { expect(find.text('Team Name'), findsOneWidget); }, ); + + testWidgets('Edit Team dialog: Save button present and Cancel closes', ( + WidgetTester tester, + ) async { + await tester.binding.setSurfaceSize(const Size(1024, 800)); + addTearDown(() async => await tester.binding.setSurfaceSize(null)); + + final team = Team( + id: 'team-1', + name: 'Support', + leaderId: 'user-2', + officeIds: ['office-1'], + createdAt: DateTime.now(), + ); + + await tester.pumpWidget( + ProviderScope( + overrides: [ + ...baseOverrides(), + teamsProvider.overrideWith((ref) => Stream.value([team])), + ], + child: const MaterialApp(home: Scaffold(body: TeamsScreen())), + ), + ); + + // Tap edit and verify dialog shows Save button + await tester.tap(find.byIcon(Icons.edit)); + await tester.pumpAndSettle(); + + expect(find.text('Save'), findsOneWidget); + + // Cancel closes dialog + await tester.tap(find.text('Cancel')); + await tester.pumpAndSettle(); + expect(find.text('Save'), findsNothing); + }); }