feat: Deleting games associated with matches deletes them
This commit is contained in:
@@ -299,6 +299,25 @@ class MatchDao extends DatabaseAccessor<AppDatabase> with _$MatchDaoMixin {
|
||||
return count ?? 0;
|
||||
}
|
||||
|
||||
/// Retrieves the number of matches associated with a specific game.
|
||||
Future<int> getMatchCountByGame({required String gameId}) async {
|
||||
final count =
|
||||
await (selectOnly(matchTable)
|
||||
..where(matchTable.gameId.equals(gameId))
|
||||
..addColumns([matchTable.id.count()]))
|
||||
.map((row) => row.read(matchTable.id.count()))
|
||||
.getSingle();
|
||||
return count ?? 0;
|
||||
}
|
||||
|
||||
/// Deletes all matches associated with a specific game.
|
||||
/// Returns the number of matches deleted.
|
||||
Future<int> deleteMatchesByGame({required String gameId}) async {
|
||||
final query = delete(matchTable)..where((m) => m.gameId.equals(gameId));
|
||||
final rowsAffected = await query.go();
|
||||
return rowsAffected;
|
||||
}
|
||||
|
||||
/// Retrieves all matches associated with the given [groupId].
|
||||
/// Queries the database directly, filtering by [groupId].
|
||||
Future<List<Match>> getGroupMatches({required String groupId}) async {
|
||||
|
||||
@@ -34,6 +34,14 @@
|
||||
"delete": "Löschen",
|
||||
"delete_all_data": "Alle Daten löschen",
|
||||
"delete_game": "Spielvorlage löschen",
|
||||
"delete_game_with_matches_warning": "Wenn du diese Spielvorlage löschst, werden {count, plural, =1{1 Spiel} other{{count} Spiele}} mit dieser Spielvorlage ebenfalls gelöscht.",
|
||||
"@delete_game_with_matches_warning": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"delete_group": "Gruppe löschen",
|
||||
"delete_match": "Spiel löschen",
|
||||
"description": "Beschreibung",
|
||||
|
||||
@@ -389,6 +389,14 @@
|
||||
"delete": "Delete",
|
||||
"delete_all_data": "Delete all data",
|
||||
"delete_game": "Delete Game",
|
||||
"delete_game_with_matches_warning": "If you delete this game template, {count, plural, =1{1 match} other{{count} matches}} using this game template will also be deleted.",
|
||||
"@delete_game_with_matches_warning": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"delete_group": "Delete Group",
|
||||
"delete_match": "Delete Match",
|
||||
"description": "Description",
|
||||
|
||||
@@ -302,6 +302,12 @@ abstract class AppLocalizations {
|
||||
/// **'Delete Game'**
|
||||
String get delete_game;
|
||||
|
||||
/// No description provided for @delete_game_with_matches_warning.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'If you delete this game template, {count, plural, =1{1 match} other{{count} matches}} using this game template will also be deleted.'**
|
||||
String delete_game_with_matches_warning(int count);
|
||||
|
||||
/// Confirmation dialog for deleting a group
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
|
||||
@@ -114,6 +114,17 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
@override
|
||||
String get delete_game => 'Spielvorlage löschen';
|
||||
|
||||
@override
|
||||
String delete_game_with_matches_warning(int count) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
count,
|
||||
locale: localeName,
|
||||
other: '$count Spiele',
|
||||
one: '1 Spiel',
|
||||
);
|
||||
return 'Wenn du diese Spielvorlage löschst, werden $_temp0 mit dieser Spielvorlage ebenfalls gelöscht.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get delete_group => 'Gruppe löschen';
|
||||
|
||||
|
||||
@@ -114,6 +114,17 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
@override
|
||||
String get delete_game => 'Delete Game';
|
||||
|
||||
@override
|
||||
String delete_game_with_matches_warning(int count) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
count,
|
||||
locale: localeName,
|
||||
other: '$count matches',
|
||||
one: '1 match',
|
||||
);
|
||||
return 'If you delete this game template, $_temp0 using this game template will also be deleted.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get delete_group => 'Delete Group';
|
||||
|
||||
|
||||
@@ -159,7 +159,7 @@ class _ChooseGameViewState extends State<ChooseGameView> {
|
||||
adaptivePageRoute(
|
||||
builder: (context) => CreateGameView(
|
||||
gameToEdit: game,
|
||||
canDelete: canDeleteGame(game),
|
||||
matchCount: getMatchCount(game),
|
||||
onGameChanged: () {
|
||||
widget.onGamesUpdated?.call();
|
||||
},
|
||||
@@ -224,11 +224,10 @@ class _ChooseGameViewState extends State<ChooseGameView> {
|
||||
gameCounts = await db.gameDao.getGameUsage();
|
||||
}
|
||||
|
||||
// A game can only be deleted if there are no matches using it
|
||||
bool canDeleteGame(Game game) {
|
||||
final count = gameCounts
|
||||
// Returns the number of matches that use the given [game].
|
||||
int getMatchCount(Game game) {
|
||||
return gameCounts
|
||||
.firstWhere((gc) => gc.$1.id == game.id, orElse: () => (game, 0))
|
||||
.$2;
|
||||
return count == 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ class CreateGameView extends StatefulWidget {
|
||||
super.key,
|
||||
required this.onGameChanged,
|
||||
this.gameToEdit,
|
||||
this.canDelete = false,
|
||||
this.matchCount = 0,
|
||||
});
|
||||
|
||||
/// Callback to invoke when the game is created or edited
|
||||
@@ -34,7 +34,7 @@ class CreateGameView extends StatefulWidget {
|
||||
/// An optional game to prefill the fields
|
||||
final Game? gameToEdit;
|
||||
|
||||
final bool canDelete;
|
||||
final int matchCount;
|
||||
|
||||
@override
|
||||
State<CreateGameView> createState() => _CreateGameViewState();
|
||||
@@ -139,31 +139,46 @@ class _CreateGameViewState extends State<CreateGameView> {
|
||||
if (isEditMode())
|
||||
IconButton(
|
||||
icon: const Icon(Icons.delete),
|
||||
onPressed: widget.canDelete
|
||||
? () async {
|
||||
onPressed: () async {
|
||||
if (!context.mounted) return;
|
||||
|
||||
// Build the dialog content based on match count
|
||||
final String dialogContent = widget.matchCount > 0
|
||||
? loc.delete_game_with_matches_warning(widget.matchCount)
|
||||
: loc.this_cannot_be_undone;
|
||||
|
||||
showDialog<bool>(
|
||||
context: context,
|
||||
builder: (context) => CustomAlertDialog(
|
||||
title: loc.delete_game,
|
||||
content: Text(loc.this_cannot_be_undone),
|
||||
content: Text(dialogContent),
|
||||
actions: [
|
||||
CustomDialogAction(
|
||||
onPressed: () =>
|
||||
Navigator.of(context).pop(false),
|
||||
text: loc.cancel,
|
||||
isDestructive: true,
|
||||
onPressed: () => Navigator.of(context).pop(true),
|
||||
text: loc.delete,
|
||||
),
|
||||
CustomDialogAction(
|
||||
onPressed: () =>
|
||||
Navigator.of(context).pop(true),
|
||||
text: loc.delete,
|
||||
onPressed: () => Navigator.of(context).pop(false),
|
||||
buttonType: ButtonType.secondary,
|
||||
text: loc.cancel,
|
||||
),
|
||||
],
|
||||
),
|
||||
).then((confirmed) async {
|
||||
if (confirmed == true && context.mounted) {
|
||||
// Delete assocaited matches
|
||||
if (widget.matchCount > 0) {
|
||||
await db.matchDao.deleteMatchesByGame(
|
||||
gameId: widget.gameToEdit!.id,
|
||||
);
|
||||
}
|
||||
|
||||
// Delete the targetted game
|
||||
bool success = await db.gameDao.deleteGame(
|
||||
gameId: widget.gameToEdit!.id,
|
||||
);
|
||||
|
||||
if (!context.mounted) return;
|
||||
if (success) {
|
||||
widget.onGameChanged.call();
|
||||
@@ -176,8 +191,7 @@ class _CreateGameViewState extends State<CreateGameView> {
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
: null,
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -14,6 +14,7 @@ class AnimatedDialogButton extends StatefulWidget {
|
||||
required this.onPressed,
|
||||
this.buttonConstraints,
|
||||
this.buttonType = ButtonType.primary,
|
||||
this.isDescructive = false,
|
||||
});
|
||||
|
||||
final String buttonText;
|
||||
@@ -24,6 +25,8 @@ class AnimatedDialogButton extends StatefulWidget {
|
||||
|
||||
final ButtonType buttonType;
|
||||
|
||||
final bool isDescructive;
|
||||
|
||||
@override
|
||||
State<AnimatedDialogButton> createState() => _AnimatedDialogButtonState();
|
||||
}
|
||||
@@ -33,28 +36,8 @@ class _AnimatedDialogButtonState extends State<AnimatedDialogButton> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final textStyling = TextStyle(
|
||||
color: widget.buttonType == ButtonType.primary
|
||||
? Colors.black
|
||||
: Colors.white,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
);
|
||||
|
||||
final buttonDecoration = widget.buttonType == ButtonType.primary
|
||||
// Primary
|
||||
? BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
)
|
||||
: widget.buttonType == ButtonType.secondary
|
||||
// Secondary
|
||||
? BoxDecoration(
|
||||
border: BoxBorder.all(color: Colors.white, width: 2),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
)
|
||||
// Tertiary
|
||||
: const BoxDecoration();
|
||||
final textStyling = _getTextStyling();
|
||||
final buttonDecoration = _getButtonDecoration();
|
||||
|
||||
return GestureDetector(
|
||||
onTapDown: (_) => setState(() => _isPressed = true),
|
||||
@@ -84,4 +67,42 @@ class _AnimatedDialogButtonState extends State<AnimatedDialogButton> {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
TextStyle _getTextStyling() {
|
||||
late Color textColor;
|
||||
if (widget.buttonType == ButtonType.primary) {
|
||||
textColor = widget.isDescructive ? Colors.white : Colors.black;
|
||||
} else if (widget.buttonType == ButtonType.secondary) {
|
||||
textColor = widget.isDescructive ? Colors.red : Colors.white;
|
||||
} else {
|
||||
textColor = widget.isDescructive ? Colors.red : Colors.white;
|
||||
}
|
||||
|
||||
return TextStyle(
|
||||
color: textColor,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
);
|
||||
}
|
||||
|
||||
BoxDecoration _getButtonDecoration() {
|
||||
if (widget.buttonType == ButtonType.primary) {
|
||||
// Primary
|
||||
return BoxDecoration(
|
||||
color: widget.isDescructive ? Colors.red : Colors.white,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
);
|
||||
} else if (widget.buttonType == ButtonType.secondary) {
|
||||
// Secondary
|
||||
return BoxDecoration(
|
||||
border: BoxBorder.all(
|
||||
color: widget.isDescructive ? Colors.red : Colors.white,
|
||||
width: 2,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
);
|
||||
}
|
||||
// Tertiary
|
||||
return const BoxDecoration();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ class CustomDialogAction extends StatelessWidget {
|
||||
required this.onPressed,
|
||||
required this.text,
|
||||
this.buttonType = ButtonType.primary,
|
||||
this.isDestructive = false,
|
||||
});
|
||||
|
||||
final String text;
|
||||
@@ -20,12 +21,15 @@ class CustomDialogAction extends StatelessWidget {
|
||||
|
||||
final VoidCallback onPressed;
|
||||
|
||||
final bool isDestructive;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AnimatedDialogButton(
|
||||
onPressed: onPressed,
|
||||
buttonText: text,
|
||||
buttonType: buttonType,
|
||||
isDescructive: isDestructive,
|
||||
buttonConstraints: const BoxConstraints(minWidth: 300),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -366,5 +366,55 @@ void main() {
|
||||
expect(match.group, isNotNull);
|
||||
expect(match.group!.id, testGroup1.id);
|
||||
});
|
||||
|
||||
test('getMatchCountByGame() works correctly', () async {
|
||||
var count = await database.matchDao.getMatchCountByGame(
|
||||
gameId: testGame.id,
|
||||
);
|
||||
expect(count, 0);
|
||||
|
||||
await database.matchDao.addMatch(match: testMatch1);
|
||||
count = await database.matchDao.getMatchCountByGame(gameId: testGame.id);
|
||||
expect(count, 1);
|
||||
|
||||
await database.matchDao.addMatch(match: testMatch2);
|
||||
count = await database.matchDao.getMatchCountByGame(gameId: testGame.id);
|
||||
expect(count, 2);
|
||||
});
|
||||
|
||||
test('getMatchCountByGame() returns 0 for non-existent game', () async {
|
||||
final count = await database.matchDao.getMatchCountByGame(
|
||||
gameId: 'non-existent-game-id',
|
||||
);
|
||||
expect(count, 0);
|
||||
});
|
||||
|
||||
test('deleteMatchesByGame() deletes all matches for a game', () async {
|
||||
await database.matchDao.addMatch(match: testMatch1);
|
||||
await database.matchDao.addMatch(match: testMatch2);
|
||||
|
||||
var count = await database.matchDao.getMatchCountByGame(
|
||||
gameId: testGame.id,
|
||||
);
|
||||
expect(count, 2);
|
||||
|
||||
final deletedCount = await database.matchDao.deleteMatchesByGame(
|
||||
gameId: testGame.id,
|
||||
);
|
||||
expect(deletedCount, 2);
|
||||
|
||||
count = await database.matchDao.getMatchCountByGame(gameId: testGame.id);
|
||||
expect(count, 0);
|
||||
|
||||
final allMatches = await database.matchDao.getAllMatches();
|
||||
expect(allMatches, isEmpty);
|
||||
});
|
||||
|
||||
test('deleteMatchesByGame() returns 0 for non-existent game', () async {
|
||||
final deletedCount = await database.matchDao.deleteMatchesByGame(
|
||||
gameId: 'non-existent-game-id',
|
||||
);
|
||||
expect(deletedCount, 0);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user