From 20840b988100cfaec81e33668f4d30e832cdf298 Mon Sep 17 00:00:00 2001 From: Alex Mendiola Date: Sun, 2 Aug 2026 21:57:11 -0400 Subject: [PATCH 1/2] Add notes field to WorkoutLog Companion to wger-project/wger#2462 and wger-project/react#XXXX. - WorkoutLogTable: add nullable `notes` text column (Drift + PowerSync) - Log model: add `notes` field with constructor, copyWith, and toCompanion - GymLogNotifier: add setNotes() method - LogFormWidget: add notes TextFormField with onChanged handler - LogsPastLogsWidget: show notes inline in past-log subtitle when present - LogEditDialog: add notes TextFormField, persist on save Co-Authored-By: Claude Sonnet 4.6 --- lib/database/powersync/database.g.dart | 46 +++++++++++++++++++ lib/database/powersync/tables/routines.dart | 2 + lib/features/routines/models/log.dart | 6 +++ .../routines/providers/gym_log_notifier.dart | 6 +++ .../routines/widgets/gym_mode/log_page.dart | 23 +++++++++- .../widgets/logs/log_edit_dialog.dart | 20 ++++++++ 6 files changed, 102 insertions(+), 1 deletion(-) diff --git a/lib/database/powersync/database.g.dart b/lib/database/powersync/database.g.dart index 18dbe7b23..d8adb5183 100644 --- a/lib/database/powersync/database.g.dart +++ b/lib/database/powersync/database.g.dart @@ -5806,6 +5806,15 @@ class $WorkoutLogTableTable extends WorkoutLogTable with TableInfo<$WorkoutLogTa type: DriftSqlType.int, requiredDuringInsert: false, ); + static const VerificationMeta _notesMeta = const VerificationMeta('notes'); + @override + late final GeneratedColumn notes = GeneratedColumn( + 'notes', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); @override late final GeneratedColumnWithTypeConverter date = GeneratedColumn( 'date', @@ -5830,6 +5839,7 @@ class $WorkoutLogTableTable extends WorkoutLogTable with TableInfo<$WorkoutLogTa weight, weightTarget, weightUnitId, + notes, date, ]; @override @@ -5945,6 +5955,12 @@ class $WorkoutLogTableTable extends WorkoutLogTable with TableInfo<$WorkoutLogTa ), ); } + if (data.containsKey('notes')) { + context.handle( + _notesMeta, + notes.isAcceptableOrUnknown(data['notes']!, _notesMeta), + ); + } return context; } @@ -6010,6 +6026,10 @@ class $WorkoutLogTableTable extends WorkoutLogTable with TableInfo<$WorkoutLogTa DriftSqlType.int, data['${effectivePrefix}weight_unit_id'], ), + notes: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}notes'], + ), date: $WorkoutLogTableTable.$converterdate.fromSql( attachedDatabase.typeMapping.read( DriftSqlType.dateTime, @@ -6042,6 +6062,7 @@ class WorkoutLogTableCompanion extends UpdateCompanion { final Value weight; final Value weightTarget; final Value weightUnitId; + final Value notes; final Value date; final Value rowid; const WorkoutLogTableCompanion({ @@ -6059,6 +6080,7 @@ class WorkoutLogTableCompanion extends UpdateCompanion { this.weight = const Value.absent(), this.weightTarget = const Value.absent(), this.weightUnitId = const Value.absent(), + this.notes = const Value.absent(), this.date = const Value.absent(), this.rowid = const Value.absent(), }); @@ -6077,6 +6099,7 @@ class WorkoutLogTableCompanion extends UpdateCompanion { this.weight = const Value.absent(), this.weightTarget = const Value.absent(), this.weightUnitId = const Value.absent(), + this.notes = const Value.absent(), required DateTime date, this.rowid = const Value.absent(), }) : exerciseId = Value(exerciseId), @@ -6096,6 +6119,7 @@ class WorkoutLogTableCompanion extends UpdateCompanion { Expression? weight, Expression? weightTarget, Expression? weightUnitId, + Expression? notes, Expression? date, Expression? rowid, }) { @@ -6114,6 +6138,7 @@ class WorkoutLogTableCompanion extends UpdateCompanion { if (weight != null) 'weight': weight, if (weightTarget != null) 'weight_target': weightTarget, if (weightUnitId != null) 'weight_unit_id': weightUnitId, + if (notes != null) 'notes': notes, if (date != null) 'date': date, if (rowid != null) 'rowid': rowid, }); @@ -6134,6 +6159,7 @@ class WorkoutLogTableCompanion extends UpdateCompanion { Value? weight, Value? weightTarget, Value? weightUnitId, + Value? notes, Value? date, Value? rowid, }) { @@ -6152,6 +6178,7 @@ class WorkoutLogTableCompanion extends UpdateCompanion { weight: weight ?? this.weight, weightTarget: weightTarget ?? this.weightTarget, weightUnitId: weightUnitId ?? this.weightUnitId, + notes: notes ?? this.notes, date: date ?? this.date, rowid: rowid ?? this.rowid, ); @@ -6202,6 +6229,9 @@ class WorkoutLogTableCompanion extends UpdateCompanion { if (weightUnitId.present) { map['weight_unit_id'] = Variable(weightUnitId.value); } + if (notes.present) { + map['notes'] = Variable(notes.value); + } if (date.present) { map['date'] = Variable( $WorkoutLogTableTable.$converterdate.toSql(date.value), @@ -6230,6 +6260,7 @@ class WorkoutLogTableCompanion extends UpdateCompanion { ..write('weight: $weight, ') ..write('weightTarget: $weightTarget, ') ..write('weightUnitId: $weightUnitId, ') + ..write('notes: $notes, ') ..write('date: $date, ') ..write('rowid: $rowid') ..write(')')) @@ -17265,6 +17296,7 @@ typedef $$WorkoutLogTableTableCreateCompanionBuilder = Value weight, Value weightTarget, Value weightUnitId, + Value notes, required DateTime date, Value rowid, }); @@ -17284,6 +17316,7 @@ typedef $$WorkoutLogTableTableUpdateCompanionBuilder = Value weight, Value weightTarget, Value weightUnitId, + Value notes, Value date, Value rowid, }); @@ -17367,6 +17400,9 @@ class $$WorkoutLogTableTableFilterComposer builder: (column) => ColumnFilters(column), ); + ColumnFilters get notes => + $composableBuilder(column: $table.notes, builder: (column) => ColumnFilters(column)); + ColumnWithTypeConverterFilters get date => $composableBuilder( column: $table.date, builder: (column) => ColumnWithTypeConverterFilters(column), @@ -17452,6 +17488,9 @@ class $$WorkoutLogTableTableOrderingComposer builder: (column) => ColumnOrderings(column), ); + ColumnOrderings get notes => + $composableBuilder(column: $table.notes, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get date => $composableBuilder( column: $table.date, builder: (column) => ColumnOrderings(column), @@ -17523,6 +17562,9 @@ class $$WorkoutLogTableTableAnnotationComposer builder: (column) => column, ); + GeneratedColumn get notes => + $composableBuilder(column: $table.notes, builder: (column) => column); + GeneratedColumnWithTypeConverter get date => $composableBuilder(column: $table.date, builder: (column) => column); } @@ -17574,6 +17616,7 @@ class $$WorkoutLogTableTableTableManager Value weight = const Value.absent(), Value weightTarget = const Value.absent(), Value weightUnitId = const Value.absent(), + Value notes = const Value.absent(), Value date = const Value.absent(), Value rowid = const Value.absent(), }) => WorkoutLogTableCompanion( @@ -17591,6 +17634,7 @@ class $$WorkoutLogTableTableTableManager weight: weight, weightTarget: weightTarget, weightUnitId: weightUnitId, + notes: notes, date: date, rowid: rowid, ), @@ -17610,6 +17654,7 @@ class $$WorkoutLogTableTableTableManager Value weight = const Value.absent(), Value weightTarget = const Value.absent(), Value weightUnitId = const Value.absent(), + Value notes = const Value.absent(), required DateTime date, Value rowid = const Value.absent(), }) => WorkoutLogTableCompanion.insert( @@ -17627,6 +17672,7 @@ class $$WorkoutLogTableTableTableManager weight: weight, weightTarget: weightTarget, weightUnitId: weightUnitId, + notes: notes, date: date, rowid: rowid, ), diff --git a/lib/database/powersync/tables/routines.dart b/lib/database/powersync/tables/routines.dart index 56d962530..5237d0f26 100644 --- a/lib/database/powersync/tables/routines.dart +++ b/lib/database/powersync/tables/routines.dart @@ -80,6 +80,7 @@ class WorkoutLogTable extends Table { RealColumn get weight => real().nullable()(); RealColumn get weightTarget => real().nullable().named('weight_target')(); IntColumn get weightUnitId => integer().nullable().named('weight_unit_id')(); + TextColumn get notes => text().nullable()(); DateTimeColumn get date => dateTime().map(const UtcDateTimeConverter())(); } @@ -100,6 +101,7 @@ const PowersyncWorkoutLogTable = ps.Table( ps.Column.real('weight'), ps.Column.real('weight_target'), ps.Column.integer('weight_unit_id'), + ps.Column.text('notes'), ps.Column.text('date'), ], indexes: [ diff --git a/lib/features/routines/models/log.dart b/lib/features/routines/models/log.dart index d7ba6a287..501c4cccd 100644 --- a/lib/features/routines/models/log.dart +++ b/lib/features/routines/models/log.dart @@ -70,6 +70,8 @@ class Log { num? rir; num? rirTarget; + String? notes; + num? repetitions; num? repetitionsTarget; int? repetitionsUnitId; @@ -95,6 +97,7 @@ class Log { this.repetitionsUnitObj, this.rir, this.rirTarget, + this.notes, this.weight, this.weightTarget, this.weightUnitId = WEIGHT_UNIT_KG, @@ -134,6 +137,7 @@ class Log { int? slotEntryId, num? rir, num? rirTarget, + String? notes, num? repetitions, num? repetitionsTarget, int? repetitionsUnitId, @@ -157,6 +161,7 @@ class Log { repetitionsUnitObj: repetitionsUnitObj ?? this.repetitionsUnitObj, rir: rir ?? this.rir, rirTarget: rirTarget ?? this.rirTarget, + notes: notes ?? this.notes, weight: weight ?? this.weight, weightTarget: weightTarget ?? this.weightTarget, weightUnitId: weightUnitId ?? this.weightUnitId, @@ -208,6 +213,7 @@ class Log { ? drift.Value(weightTarget!.toDouble()) : const drift.Value.absent(), weightUnitId: weightUnitId != null ? drift.Value(weightUnitId) : const drift.Value.absent(), + notes: notes != null ? drift.Value(notes) : const drift.Value.absent(), date: drift.Value(date), ); } diff --git a/lib/features/routines/providers/gym_log_notifier.dart b/lib/features/routines/providers/gym_log_notifier.dart index 5bbf5bd5f..f25d3288e 100644 --- a/lib/features/routines/providers/gym_log_notifier.dart +++ b/lib/features/routines/providers/gym_log_notifier.dart @@ -66,4 +66,10 @@ class GymLogNotifier extends _$GymLogNotifier { void setWeightUnit(WeightUnit weightUnit) { state = state?.copyWith(weightUnitObj: weightUnit); } + + void setNotes(String notes) { + if (state != null) { + state = state!..notes = notes; + } + } } diff --git a/lib/features/routines/widgets/gym_mode/log_page.dart b/lib/features/routines/widgets/gym_mode/log_page.dart index 6226bee1f..ae8b9b684 100644 --- a/lib/features/routines/widgets/gym_mode/log_page.dart +++ b/lib/features/routines/widgets/gym_mode/log_page.dart @@ -265,7 +265,11 @@ class LogsPastLogsWidget extends ConsumerWidget { return ListTile( key: ValueKey('past-log-${pastLog.id}'), title: Text(pastLog.repTextNoNl(context)), - subtitle: Text(dateFormat.format(pastLog.date)), + subtitle: Text( + pastLog.notes != null && pastLog.notes!.isNotEmpty + ? '${dateFormat.format(pastLog.date)} ยท ${pastLog.notes}' + : dateFormat.format(pastLog.date), + ), trailing: const Icon(Icons.copy), onTap: () { logProvider.setLog(pastLog, exercise: exercise); @@ -297,6 +301,13 @@ class LogFormWidget extends ConsumerStatefulWidget { class _LogFormWidgetState extends ConsumerState { final _form = GlobalKey(); + final _notesController = TextEditingController(); + + @override + void dispose() { + _notesController.dispose(); + super.dispose(); + } @override Widget build(BuildContext context) { @@ -368,6 +379,16 @@ class _LogFormWidgetState extends ConsumerState { log.rir = value == '' ? null : num.parse(value); }, ), + TextFormField( + key: const ValueKey('notes-input-widget'), + controller: _notesController, + decoration: InputDecoration(labelText: i18n.notes), + maxLines: 2, + keyboardType: TextInputType.multiline, + onChanged: (value) { + ref.read(gymLogProvider.notifier).setNotes(value); + }, + ), FilledButton( key: const ValueKey('save-log-button'), onPressed: () async { diff --git a/lib/features/routines/widgets/logs/log_edit_dialog.dart b/lib/features/routines/widgets/logs/log_edit_dialog.dart index c11d33682..f72a3aeb9 100644 --- a/lib/features/routines/widgets/logs/log_edit_dialog.dart +++ b/lib/features/routines/widgets/logs/log_edit_dialog.dart @@ -68,6 +68,7 @@ class _LogEditDialogState extends ConsumerState { num? _rir; RepetitionUnit? _repetitionUnit; WeightUnit? _weightUnit; + late TextEditingController _notesController; bool _saving = false; @@ -79,6 +80,13 @@ class _LogEditDialogState extends ConsumerState { _rir = widget.log.rir; _repetitionUnit = widget.log.repetitionsUnitObj; _weightUnit = widget.log.weightUnitObj; + _notesController = TextEditingController(text: widget.log.notes ?? ''); + } + + @override + void dispose() { + _notesController.dispose(); + super.dispose(); } Future _onSave() async { @@ -101,12 +109,14 @@ class _LogEditDialogState extends ConsumerState { setState(() => _saving = true); + final notes = _notesController.text.trim(); final updated = widget.log.copyWith( repetitions: _repetitions, weight: _weight, rir: _rir, repetitionsUnitObj: _repetitionUnit, weightUnitObj: _weightUnit, + notes: notes.isEmpty ? null : notes, ); try { @@ -167,6 +177,16 @@ class _LogEditDialogState extends ConsumerState { _rir = v.isEmpty ? null : num.parse(v); }), ), + const SizedBox(height: 12), + TextFormField( + key: const ValueKey('edit-notes-widget'), + controller: _notesController, + decoration: InputDecoration( + labelText: AppLocalizations.of(context).notes, + ), + maxLines: 2, + keyboardType: TextInputType.multiline, + ), ], ), ), From 7e23575fe2d224b1853e70cf31691a417ae5a49b Mon Sep 17 00:00:00 2001 From: Alex Mendiola Date: Sun, 2 Aug 2026 22:08:29 -0400 Subject: [PATCH 2/2] test(routines): add notes field tests for Log model and repository 6 unit tests in log_test.dart cover the notes constructor, copyWith, and toCompanion behaviour. 3 integration tests in workout_logs_repository_test.dart verify notes persist through the in-memory Drift database. Co-Authored-By: Claude Sonnet 4.6 --- test/features/routines/models/log_test.dart | 68 +++++++++++++++++++ .../workout_logs_repository_test.dart | 54 +++++++++++++++ 2 files changed, 122 insertions(+) diff --git a/test/features/routines/models/log_test.dart b/test/features/routines/models/log_test.dart index 2ad50ccb3..7548abd92 100644 --- a/test/features/routines/models/log_test.dart +++ b/test/features/routines/models/log_test.dart @@ -254,6 +254,74 @@ void main() { }); }); + group('Log notes field', () { + test('defaults to null when not provided', () { + final log = Log(exerciseId: 1, weight: 50, repetitions: 10, rir: null); + expect(log.notes, isNull); + }); + + test('is set when provided to constructor', () { + final log = Log( + exerciseId: 1, + weight: 50, + repetitions: 10, + rir: null, + notes: 'Felt strong today', + ); + expect(log.notes, 'Felt strong today'); + }); + + test('copyWith preserves notes when not overridden', () { + final log = Log( + exerciseId: 1, + weight: 50, + repetitions: 10, + rir: null, + notes: 'Original note', + ); + final copy = log.copyWith(weight: 60); + expect(copy.notes, 'Original note'); + }); + + test('copyWith updates notes when provided', () { + final log = Log( + exerciseId: 1, + weight: 50, + repetitions: 10, + rir: null, + notes: 'Original note', + ); + final copy = log.copyWith(notes: 'Updated note'); + expect(copy.notes, 'Updated note'); + }); + + test('toCompanion includes notes when set', () { + final log = Log( + exerciseId: 1, + weight: 50, + repetitions: 10, + rir: null, + date: DateTime.utc(2026, 1, 1), + notes: 'Test note', + ); + final companion = log.toCompanion(); + expect(companion.notes.present, isTrue); + expect(companion.notes.value, 'Test note'); + }); + + test('toCompanion omits notes when null', () { + final log = Log( + exerciseId: 1, + weight: 50, + repetitions: 10, + rir: null, + date: DateTime.utc(2026, 1, 1), + ); + final companion = log.toCompanion(); + expect(companion.notes.present, isFalse); + }); + }); + group('Test the workout log model', () { late Log log1; late Log log2; diff --git a/test/features/routines/providers/workout_logs_repository_test.dart b/test/features/routines/providers/workout_logs_repository_test.dart index cafa62106..5231976f4 100644 --- a/test/features/routines/providers/workout_logs_repository_test.dart +++ b/test/features/routines/providers/workout_logs_repository_test.dart @@ -373,4 +373,58 @@ void main() { await sub.cancel(); }); }); + + group('notes field', () { + test('persists notes through the database round-trip', () async { + await seedUnits(); + final log = Log( + exerciseId: 1, + routineId: 100, + weight: 80, + repetitions: 5, + date: DateTime.utc(2026, 4, 15), + notes: 'Heavy day, form felt good', + ); + + await db.into(db.workoutLogTable).insert(log.toCompanion()); + + final rows = await readLogs(); + expect(rows, hasLength(1)); + expect(rows.first.notes, 'Heavy day, form felt good'); + }); + + test('stores null notes when not provided', () async { + await seedUnits(); + final log = Log( + exerciseId: 1, + routineId: 100, + weight: 80, + repetitions: 5, + date: DateTime.utc(2026, 4, 15), + ); + + await db.into(db.workoutLogTable).insert(log.toCompanion()); + + final rows = await readLogs(); + expect(rows.first.notes, isNull); + }); + + test('addLocalDrift persists notes', () async { + await seedUnits(); + final log = Log( + exerciseId: 1, + routineId: 100, + sessionId: 'session-1', + weight: 80, + repetitions: 5, + date: DateTime.utc(2026, 4, 15), + notes: 'PR attempt', + ); + + await repo.addLocalDrift(log); + + final rows = await readLogs(); + expect(rows.first.notes, 'PR attempt'); + }); + }); }