Skip to content

feat: Add clone routine functionality - #46

Open
Devasy wants to merge 1 commit into
mainfrom
feat/clone-routine-13229627913334241519
Open

feat: Add clone routine functionality#46
Devasy wants to merge 1 commit into
mainfrom
feat/clone-routine-13229627913334241519

Conversation

@Devasy

@Devasy Devasy commented May 4, 2026

Copy link
Copy Markdown
Owner

This PR introduces a "Clone Routine" feature to the Routines screen. Users can long-press a routine and select "Clone Routine". This prompts the user with an alert dialog asking for a new routine name, pre-filled with the original routine's name as a hint. If the user submits an empty name, it defaults to the original name. The routine is cloned with the exact same exercises and added to the routine list. Appropriate widget tests have been added to verify this new functionality.


PR created automatically by Jules for task 13229627913334241519 started by @Devasy23

Summary by CodeRabbit

Release Notes

  • New Features
    • Added "Clone Routine" feature allowing users to duplicate existing routines with a custom name. If no custom name is provided, the cloned routine automatically uses the original name while preserving all associated exercises.

- Add 'Clone Routine' option in the long-press options menu.
- Show an input dialog with the original routine's name as a hint.
- Create a copy of the routine using the entered name (or the original if empty).
- Include widget tests in `routines_screen_test.dart` to verify cloning.

Co-authored-by: Devasy23 <110348311+Devasy23@users.noreply.github.com>
@google-labs-jules

Copy link
Copy Markdown
Contributor

👋 Jules, reporting for duty! I'm here to lend a hand with this pull request.

When you start a review, I'll add a 👀 emoji to each comment to let you know I've read it. I'll focus on feedback directed at me and will do my best to stay out of conversations between you and other bots or reviewers to keep the noise down.

I'll push a commit with your requested changes shortly after. Please note there might be a delay between these steps, but rest assured I'm on the job!

For more direct control, you can switch me to Reactive Mode. When this mode is on, I will only act on comments where you specifically mention me with @jules. You can find this option in the Pull Request section of your global Jules UI settings. You can always switch back!

New to Jules? Learn more at jules.google/docs.


For security, I will only act on instructions from the user who triggered this task.

@coderabbitai

coderabbitai Bot commented May 4, 2026

Copy link
Copy Markdown
Contributor

Walkthrough

This PR adds a "Clone Routine" feature to the routines screen. Users can long-press a routine to open a bottom sheet, select "Clone Routine", enter a new name (or accept the default), and create a duplicate routine with the same exercises. Comprehensive tests verify both custom-name and default-name cloning scenarios.

Changes

Routine Cloning Feature

Layer / File(s) Summary
UI Bottom Sheet Enhancement
workout-logger/lib/screens/routines_screen.dart (lines 259–269)
Added "Clone Routine" ListTile to the routine options bottom sheet that triggers the clone dialog when tapped.
Clone Dialog Implementation
workout-logger/lib/screens/routines_screen.dart (lines 287–322)
Added _showCloneDialog method that displays a text input dialog, validates and sanitizes the new routine name (falling back to the original if blank), and invokes provider.createRoutine with the new name and existing exercise IDs.
Clone Feature Tests
workout-logger/test/screens/routines_screen_test.dart
Added full widget test suite with setup of MockStorageService and WorkoutProvider, and two test cases: one verifying clone with a custom name and one verifying clone with a default name; both assert provider state changes and UI behavior.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: Add clone routine functionality' directly and accurately summarizes the main change: adding a clone routine feature to the workout logger application.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
Review rate limit: 0/1 reviews remaining, refill in 60 minutes.

Comment @coderabbitai help to get the list of available commands and usage tips.

@codecov

codecov Bot commented May 4, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 95.23810% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 33.40%. Comparing base (2dd9145) to head (e9a2a02).
⚠️ Report is 3 commits behind head on main.

Files with missing lines Patch % Lines
workout-logger/lib/screens/routines_screen.dart 95.23% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main      #46      +/-   ##
==========================================
+ Coverage   29.13%   33.40%   +4.27%     
==========================================
  Files          33       36       +3     
  Lines        5999     6208     +209     
==========================================
+ Hits         1748     2074     +326     
+ Misses       4251     4134     -117     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@workout-logger/lib/screens/routines_screen.dart`:
- Around line 308-315: The onPressed handler for the FilledButton calls the
async provider.createRoutine(...) without awaiting it, then immediately calls
Navigator.pop(context), causing the dialog to close before the routine is
saved/added (and swallowing storage errors); change the handler to await
provider.createRoutine(newName, routine.exerciseIds) and only call
Navigator.pop(context) after that await, wrapping the await in try/catch to
surface or log any errors (mirror the pattern used in _saveRoutine) so the UI
only closes after a successful save and errors are handled.
- Around line 287-289: _showCloneDialog currently allocates a
TextEditingController but never disposes it, leaking resources each time the
dialog is opened; fix by creating the TextEditingController before calling
showDialog and then disposing it in the Future completion handler (e.g., in the
.then(...) or await finally) so it is always disposed whether the dialog is
dismissed or accepted; locate _showCloneDialog and ensure the controller is
disposed after the dialog Future completes rather than relying on widget
disposal (since _RoutineCard is StatelessWidget).

In `@workout-logger/test/screens/routines_screen_test.dart`:
- Around line 12-25: Add a tearDown that calls dispose() on the WorkoutProvider
instance created in setUp so the ChangeNotifier is properly cleaned up after
each test; locate the setUp block that constructs WorkoutProvider (and uses
ChangeNotifierProvider.value in tests) and add a matching tearDown that calls
provider.dispose() (or provider.close() if applicable) to prevent listener leaks
and leftover state between tests.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 0a561695-4060-4628-90bd-d1cc44ef7cf3

📥 Commits

Reviewing files that changed from the base of the PR and between 36c519c and e9a2a02.

📒 Files selected for processing (2)
  • workout-logger/lib/screens/routines_screen.dart
  • workout-logger/test/screens/routines_screen_test.dart

Comment on lines +287 to +289
void _showCloneDialog(BuildContext context) {
final TextEditingController nameController = TextEditingController();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

TextEditingController is never disposed — resource leak

The Flutter SDK requires calling dispose() on a TextEditingController when it is no longer needed, to ensure any resources used by the object are discarded. Each time a user opens the clone dialog, a new controller is allocated but never freed. Since _RoutineCard is a StatelessWidget, attach disposal to the dialog's returned Future:

🛠️ Proposed fix
  void _showCloneDialog(BuildContext context) {
    final TextEditingController nameController = TextEditingController();

-   showDialog(
+   showDialog<void>(
      context: context,
      builder: (context) => AlertDialog(
        ...
      ),
-   );
+   ).then((_) => nameController.dispose());
  }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@workout-logger/lib/screens/routines_screen.dart` around lines 287 - 289,
_showCloneDialog currently allocates a TextEditingController but never disposes
it, leaking resources each time the dialog is opened; fix by creating the
TextEditingController before calling showDialog and then disposing it in the
Future completion handler (e.g., in the .then(...) or await finally) so it is
always disposed whether the dialog is dismissed or accepted; locate
_showCloneDialog and ensure the controller is disposed after the dialog Future
completes rather than relying on widget disposal (since _RoutineCard is
StatelessWidget).

Comment on lines +308 to +315
FilledButton(
onPressed: () {
final newName = nameController.text.trim().isEmpty
? routine.name
: nameController.text.trim();
provider.createRoutine(newName, routine.exerciseIds);
Navigator.pop(context);
},

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

createRoutine must be awaited — dialog may close before the clone appears in the list

provider.createRoutine is async; internally it does await _storage.saveRoutine(routine) before calling _routines.add(routine) and notifyListeners(). Because the call is fire-and-forget here, Navigator.pop fires before the routine is actually inserted and the widget tree rebuilds — producing a brief but visible race where the dialog closes but the cloned routine is absent from the list. Storage errors are also silently swallowed. The analogous _saveRoutine method (line 707) already awaits the same call correctly.

🐛 Proposed fix
-            onPressed: () {
+            onPressed: () async {
               final newName = nameController.text.trim().isEmpty
                   ? routine.name
                   : nameController.text.trim();
-              provider.createRoutine(newName, routine.exerciseIds);
-              Navigator.pop(context);
+              await provider.createRoutine(newName, routine.exerciseIds);
+              if (context.mounted) Navigator.pop(context);
             },
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
FilledButton(
onPressed: () {
final newName = nameController.text.trim().isEmpty
? routine.name
: nameController.text.trim();
provider.createRoutine(newName, routine.exerciseIds);
Navigator.pop(context);
},
FilledButton(
onPressed: () async {
final newName = nameController.text.trim().isEmpty
? routine.name
: nameController.text.trim();
await provider.createRoutine(newName, routine.exerciseIds);
if (context.mounted) Navigator.pop(context);
},
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@workout-logger/lib/screens/routines_screen.dart` around lines 308 - 315, The
onPressed handler for the FilledButton calls the async
provider.createRoutine(...) without awaiting it, then immediately calls
Navigator.pop(context), causing the dialog to close before the routine is
saved/added (and swallowing storage errors); change the handler to await
provider.createRoutine(newName, routine.exerciseIds) and only call
Navigator.pop(context) after that await, wrapping the await in try/catch to
surface or log any errors (mirror the pattern used in _saveRoutine) so the UI
only closes after a successful save and errors are handled.

Comment on lines +12 to +25
late MockStorageService mockStorage;
late WorkoutProvider provider;

setUp(() async {
mockStorage = MockStorageService();
provider = WorkoutProvider(
mockStorage,
programManager: ProgramManager(mockStorage),
);
await provider.init();

// Setup a pre-existing routine
await provider.createRoutine('Leg Day', ['squat', 'leg_press']);
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

Add a tearDown to dispose WorkoutProvider

ChangeNotifierProvider.value does not take ownership of the provided instance — it never calls dispose(). Without a tearDown, WorkoutProvider (a ChangeNotifier) is never disposed after each test, which can trigger debug-mode warnings about listener leaks and leave residual state if the framework defers cleanup.

♻️ Proposed fix
     setUp(() async {
       mockStorage = MockStorageService();
       provider = WorkoutProvider(
         mockStorage,
         programManager: ProgramManager(mockStorage),
       );
       await provider.init();

       // Setup a pre-existing routine
       await provider.createRoutine('Leg Day', ['squat', 'leg_press']);
     });
+
+    tearDown(() {
+      provider.dispose();
+    });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@workout-logger/test/screens/routines_screen_test.dart` around lines 12 - 25,
Add a tearDown that calls dispose() on the WorkoutProvider instance created in
setUp so the ChangeNotifier is properly cleaned up after each test; locate the
setUp block that constructs WorkoutProvider (and uses
ChangeNotifierProvider.value in tests) and add a matching tearDown that calls
provider.dispose() (or provider.close() if applicable) to prevent listener leaks
and leftover state between tests.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant