From 761a07857d7e94870507761b2e7e03af834417e0 Mon Sep 17 00:00:00 2001 From: faithless11 <116338266+faithless11@users.noreply.github.com> Date: Mon, 5 Oct 2026 21:43:29 +0200 Subject: [PATCH 1/2] Link exercises in the routine detail view to their detail page The exercise names in the routine overview (and the dashboard routine card, which reuses SetConfigDataDetails) are now links to the exercise detail page, so users can look up how an exercise is performed directly from their plan. Co-Authored-By: Claude Opus 5.5 --- .../widgets/RoutineDetailsCard.test.tsx | 84 +++++++++++++++++++ .../Routines/widgets/RoutineDetailsCard.tsx | 27 +++++- 2 files changed, 108 insertions(+), 3 deletions(-) diff --git a/src/components/Routines/widgets/RoutineDetailsCard.test.tsx b/src/components/Routines/widgets/RoutineDetailsCard.test.tsx index f8c9b188f..057954079 100644 --- a/src/components/Routines/widgets/RoutineDetailsCard.test.tsx +++ b/src/components/Routines/widgets/RoutineDetailsCard.test.tsx @@ -5,6 +5,11 @@ import { RoutineDetailsCard } from "@/components/Routines/widgets/RoutineDetails import React from 'react'; import { MemoryRouter, Route, Routes } from "react-router-dom"; import { testRoutine1 } from "@/tests/workoutRoutinesTestData"; +import { testExerciseBenchPress } from "@/tests/exerciseTestdata"; +import { Day } from "@/components/Routines/models/Day"; +import { Routine } from "@/components/Routines/models/Routine"; +import { Slot } from "@/components/Routines/models/Slot"; +import { SlotEntry } from "@/components/Routines/models/SlotEntry"; import type { Mock } from 'vitest'; vi.mock("@/components/Exercises/api/language"); @@ -59,4 +64,83 @@ describe("Test the RoutineDetail component", () => { expect(screen.getByText('Pull day')).toBeInTheDocument(); expect(screen.getByText('routines.restDay')).toBeInTheDocument(); }); + + test('links the exercises to their detail page', async () => { + + // Arrange: a day the sequence has no data for, with an exercise in it + const pushDay = new Day({ + id: 7, + routineId: 1, + order: 4, + name: 'Push day', + description: '', + isRest: false, + needLogsToAdvance: false, + type: 'custom', + config: null, + slots: [ + new Slot({ + id: 3, + dayId: 7, + order: 1, + comment: '', + config: null, + entries: [ + new SlotEntry({ + id: 3, + slotId: 3, + exerciseId: 2, + exercise: testExerciseBenchPress, + repetitionUnitId: 1, + repetitionRounding: 1, + weightUnitId: 1, + weightRounding: 1, + order: 1, + comment: '', + type: 'normal', + config: null + }) + ] + }) + ] + }); + (useRoutineDetailQuery as Mock).mockImplementation(() => ({ + isSuccess: true, + isLoading: false, + data: new Routine({ + id: testRoutine1.id, + name: testRoutine1.name, + description: testRoutine1.description, + created: testRoutine1.created, + start: testRoutine1.start, + end: testRoutine1.end, + fitInWeek: testRoutine1.fitInWeek, + isTemplate: testRoutine1.isTemplate, + isPublic: testRoutine1.isPublic, + days: [...testRoutine1.days, pushDay], + dayData: testRoutine1.dayData, + }) + })); + + // Act + render( + + + + } /> + + + + ); + + // Assert: exercises from the sequence and from the structure-only days + expect(screen.getByRole('link', { name: 'Squats' })).toHaveAttribute( + 'href', + expect.stringMatching(/\/exercise\/345\/view\/squats$/) + ); + expect(screen.getByRole('link', { name: 'Benchpress' })).toHaveAttribute( + 'href', + expect.stringMatching(/\/exercise\/2\/view\/benchpress$/) + ); + }); }); diff --git a/src/components/Routines/widgets/RoutineDetailsCard.tsx b/src/components/Routines/widgets/RoutineDetailsCard.tsx index aea8af05f..b4a8faad8 100644 --- a/src/components/Routines/widgets/RoutineDetailsCard.tsx +++ b/src/components/Routines/widgets/RoutineDetailsCard.tsx @@ -1,4 +1,4 @@ -import { ExerciseImageAvatar, getLanguageByShortName, Language, useLanguageQuery } from "@/components/Exercises"; +import { Exercise, ExerciseImageAvatar, getLanguageByShortName, Language, useLanguageQuery } from "@/components/Exercises"; import { Day, getDayName } from "@/components/Routines/models/Day"; import { RoutineDayData } from "@/components/Routines/models/RoutineDayData"; import { SetConfigData } from "@/components/Routines/models/SetConfigData"; @@ -19,6 +19,7 @@ import { Container, Divider, IconButton, + Link, Stack, Typography, useTheme @@ -61,6 +62,26 @@ export const RoutineDetailsCard = () => { }; +/* + * Name of an exercise, linking to its detail page + */ +function ExerciseNameLink(props: { exercise: Exercise, language?: Language }) { + const { i18n } = useTranslation(); + const translation = props.exercise.getTranslation(props.language); + + return + {translation.name} + ; +} + + export function SetConfigDataDetails(props: { setConfigData: SetConfigData, rowHeight?: undefined | string, @@ -91,7 +112,7 @@ export function SetConfigDataDetails(props: { {props.showExercise && (props.setConfigData.exercise - ? props.setConfigData.exercise.getTranslation(language).name + ? : t('routines.exerciseNotAvailable'))}
@@ -200,7 +221,7 @@ function SlotEntryList(props: { slot: Slot }) { {props.slot.entries.map((entry) => {entry.exercise - ? entry.exercise.getTranslation(language).name + ? : t('routines.exerciseNotAvailable')} )} From f2c43ebda995617213dda0fbebc3e9bc63cbe090 Mon Sep 17 00:00:00 2001 From: Roland Geider Date: Wed, 7 Oct 2026 21:26:43 +0200 Subject: [PATCH 2/2] Simplify the exercise link tes --- .../widgets/RoutineDetailsCard.test.tsx | 67 ++----------------- 1 file changed, 7 insertions(+), 60 deletions(-) diff --git a/src/components/Routines/widgets/RoutineDetailsCard.test.tsx b/src/components/Routines/widgets/RoutineDetailsCard.test.tsx index 057954079..47b6d2ce1 100644 --- a/src/components/Routines/widgets/RoutineDetailsCard.test.tsx +++ b/src/components/Routines/widgets/RoutineDetailsCard.test.tsx @@ -5,11 +5,7 @@ import { RoutineDetailsCard } from "@/components/Routines/widgets/RoutineDetails import React from 'react'; import { MemoryRouter, Route, Routes } from "react-router-dom"; import { testRoutine1 } from "@/tests/workoutRoutinesTestData"; -import { testExerciseBenchPress } from "@/tests/exerciseTestdata"; -import { Day } from "@/components/Routines/models/Day"; import { Routine } from "@/components/Routines/models/Routine"; -import { Slot } from "@/components/Routines/models/Slot"; -import { SlotEntry } from "@/components/Routines/models/SlotEntry"; import type { Mock } from 'vitest'; vi.mock("@/components/Exercises/api/language"); @@ -65,61 +61,16 @@ describe("Test the RoutineDetail component", () => { expect(screen.getByText('routines.restDay')).toBeInTheDocument(); }); - test('links the exercises to their detail page', async () => { + test.each([ + ['with sequence data', testRoutine1], + ['without sequence data', new Routine({ ...testRoutine1, dayData: [] })], + ])('links the exercises to their detail page (%s)', async (_, routine) => { - // Arrange: a day the sequence has no data for, with an exercise in it - const pushDay = new Day({ - id: 7, - routineId: 1, - order: 4, - name: 'Push day', - description: '', - isRest: false, - needLogsToAdvance: false, - type: 'custom', - config: null, - slots: [ - new Slot({ - id: 3, - dayId: 7, - order: 1, - comment: '', - config: null, - entries: [ - new SlotEntry({ - id: 3, - slotId: 3, - exerciseId: 2, - exercise: testExerciseBenchPress, - repetitionUnitId: 1, - repetitionRounding: 1, - weightUnitId: 1, - weightRounding: 1, - order: 1, - comment: '', - type: 'normal', - config: null - }) - ] - }) - ] - }); + // Arrange (useRoutineDetailQuery as Mock).mockImplementation(() => ({ isSuccess: true, isLoading: false, - data: new Routine({ - id: testRoutine1.id, - name: testRoutine1.name, - description: testRoutine1.description, - created: testRoutine1.created, - start: testRoutine1.start, - end: testRoutine1.end, - fitInWeek: testRoutine1.fitInWeek, - isTemplate: testRoutine1.isTemplate, - isPublic: testRoutine1.isPublic, - days: [...testRoutine1.days, pushDay], - dayData: testRoutine1.dayData, - }) + data: routine })); // Act @@ -133,14 +84,10 @@ describe("Test the RoutineDetail component", () => { ); - // Assert: exercises from the sequence and from the structure-only days + // Assert expect(screen.getByRole('link', { name: 'Squats' })).toHaveAttribute( 'href', expect.stringMatching(/\/exercise\/345\/view\/squats$/) ); - expect(screen.getByRole('link', { name: 'Benchpress' })).toHaveAttribute( - 'href', - expect.stringMatching(/\/exercise\/2\/view\/benchpress$/) - ); }); });