Fixed unable to edit Team

This commit is contained in:
Marc Rejohn Castillano 2026-02-18 21:21:45 +08:00
parent 15ce7b7a10
commit 35eae623d8
4 changed files with 127 additions and 54 deletions

View File

@ -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<TeamsScreen> {
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<TeamsScreen> {
'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<TeamsScreen> {
// 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<TeamsScreen> {
.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<TeamsScreen> {
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);

View File

@ -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;
}

View File

@ -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');
});
}

View File

@ -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);
});
}