Skip to content

Commit b6112d7

Browse files
authored
Merge pull request #1143 from FilipCuper/fix/exercise-search-endpoint-1127
Fix #1127: Move exercise search to new endpoint
2 parents d0812b0 + de55e50 commit b6112d7

File tree

10 files changed

+135
-119
lines changed

10 files changed

+135
-119
lines changed

src/components/Exercises/Detail/Head/ExerciseDeleteDialog.tsx

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@ import React from "react";
2424
import { useTranslation } from "react-i18next";
2525
import { useNavigate } from "react-router-dom";
2626
import { deleteExercise, deleteExerciseTranslation, getExercise } from "services";
27-
import { ExerciseSearchResponse } from "services/responseType";
2827

2928
export function ExerciseDeleteDialog(props: {
3029
onClose: () => void,
@@ -91,10 +90,10 @@ export function ExerciseDeleteDialog(props: {
9190
<p>{t('exercises.replacementsSearch')}</p>
9291

9392
<NameAutocompleter
94-
callback={(exercise: ExerciseSearchResponse | null) => {
93+
callback={(exercise: Exercise | null) => {
9594
if (exercise !== null) {
96-
setReplacementId(exercise.data.base_id);
97-
loadCurrentReplacement(exercise.data.base_id);
95+
setReplacementId(exercise.id!);
96+
loadCurrentReplacement(exercise.id!);
9897
}
9998
}}
10099
/>

src/components/Exercises/ExerciseOverview.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import { useExercisesQuery } from "components/Exercises/queries";
1515
import React, { useContext, useMemo, useState } from "react";
1616
import { useTranslation } from "react-i18next";
1717
import { Link, useNavigate } from "react-router-dom";
18-
import { ExerciseSearchResponse } from "services/responseType";
18+
import { Exercise } from "components/Exercises/models/exercise";
1919
import { makeLink, WgerLink } from "utils/url";
2020
import { ExerciseFiltersContext } from './Filter/ExerciseFiltersContext';
2121
import { FilterDrawer } from './Filter/FilterDrawer';
@@ -135,12 +135,12 @@ export const ExerciseOverviewList = () => {
135135
page * ITEMS_PER_PAGE
136136
);
137137

138-
const exerciseAdded = (exerciseResponse: ExerciseSearchResponse | null) => {
139-
if (!exerciseResponse) {
138+
const exerciseAdded = (exercise: Exercise | null) => {
139+
if (!exercise) {
140140
return;
141141
}
142142

143-
navigate(makeLink(WgerLink.EXERCISE_DETAIL, i18n.language, { id: exerciseResponse.data.base_id }));
143+
navigate(makeLink(WgerLink.EXERCISE_DETAIL, i18n.language, { id: exercise.id }));
144144
};
145145

146146
return (

src/components/Exercises/Filter/NameAutcompleter.tsx

Lines changed: 41 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -12,25 +12,25 @@ import {
1212
Switch,
1313
TextField,
1414
} from "@mui/material";
15+
import { Exercise } from "components/Exercises/models/exercise";
1516
import { SERVER_URL } from "config";
1617
import debounce from "lodash/debounce";
1718
import * as React from "react";
1819
import { useState } from "react";
1920
import { useTranslation } from "react-i18next";
20-
import { getExercise, searchExerciseTranslations } from "services";
21-
import { ExerciseSearchResponse } from "services/responseType";
21+
import { searchExerciseTranslations } from "services";
2222
import { LANGUAGE_SHORT_ENGLISH } from "utils/consts";
2323

2424
type NameAutocompleterProps = {
25-
callback: (exerciseResponse: ExerciseSearchResponse | null) => void;
25+
callback: (exercise: Exercise | null) => void;
2626
loadExercise?: boolean;
2727
};
2828

2929
export function NameAutocompleter({ callback, loadExercise }: NameAutocompleterProps) {
30-
const [value, setValue] = React.useState<ExerciseSearchResponse | null>(null);
30+
const [value, setValue] = React.useState<Exercise | null>(null);
3131
const [inputValue, setInputValue] = React.useState("");
3232
const [searchEnglish, setSearchEnglish] = useState<boolean>(true);
33-
const [options, setOptions] = React.useState<readonly ExerciseSearchResponse[]>([]);
33+
const [options, setOptions] = React.useState<readonly Exercise[]>([]);
3434
const [t, i18n] = useTranslation();
3535

3636
loadExercise = loadExercise === undefined ? false : loadExercise;
@@ -61,7 +61,7 @@ export function NameAutocompleter({ callback, loadExercise }: NameAutocompleterP
6161
<>
6262
<Autocomplete
6363
id="exercise-name-autocomplete"
64-
getOptionLabel={(option) => option.value}
64+
getOptionLabel={(option) => option.getTranslation().name}
6565
data-testid="autocomplete"
6666
filterOptions={(x) => x}
6767
options={options}
@@ -70,13 +70,10 @@ export function NameAutocompleter({ callback, loadExercise }: NameAutocompleterP
7070
filterSelectedOptions
7171
value={value}
7272
noOptionsText={t("noResults")}
73-
isOptionEqualToValue={(option, value) => option.value === value.value}
74-
onChange={async (event: React.SyntheticEvent, newValue: ExerciseSearchResponse | null) => {
73+
isOptionEqualToValue={(option, value) => option.id === value.id}
74+
onChange={async (event: React.SyntheticEvent, newValue: Exercise | null) => {
7575
setOptions(newValue ? [newValue, ...options] : options);
7676
setValue(newValue);
77-
if (loadExercise && newValue !== null) {
78-
newValue.exercise = await getExercise(newValue.data.base_id);
79-
}
8077
callback(newValue);
8178
}}
8279
onInputChange={(event, newInputValue) => {
@@ -102,34 +99,39 @@ export function NameAutocompleter({ callback, loadExercise }: NameAutocompleterP
10299
}}
103100
/>
104101
)}
105-
renderOption={(props, option, state) => (
106-
<li
107-
{...props}
108-
key={`exercise-${state.index}-${option.data.id}`}
109-
data-testid={`autocompleter-result-${option.data.base_id}`}
110-
>
111-
<ListItem disablePadding component="div">
112-
<ListItemIcon>
113-
{option.data.image ? (
114-
<Avatar alt="" src={`${SERVER_URL}${option.data.image}`} variant="rounded" />
115-
) : (
116-
<PhotoIcon fontSize="large" />
117-
)}
118-
</ListItemIcon>
119-
<ListItemText
120-
primary={option.value}
121-
slotProps={{
122-
primary: {
123-
whiteSpace: "nowrap",
124-
overflow: "hidden",
125-
textOverflow: "ellipsis",
126-
},
127-
}}
128-
secondary={option.data.category}
129-
/>
130-
</ListItem>
131-
</li>
132-
)}
102+
renderOption={(props, option, state) => {
103+
const translation = option.getTranslation();
104+
const mainImage = option.mainImage;
105+
106+
return (
107+
<li
108+
{...props}
109+
key={`exercise-${state.index}-${option.id}`}
110+
data-testid={`autocompleter-result-${option.id}`}
111+
>
112+
<ListItem disablePadding component="div">
113+
<ListItemIcon>
114+
{mainImage ? (
115+
<Avatar alt="" src={`${SERVER_URL}${mainImage.url}`} variant="rounded" />
116+
) : (
117+
<PhotoIcon fontSize="large" />
118+
)}
119+
</ListItemIcon>
120+
<ListItemText
121+
primary={translation.name}
122+
slotProps={{
123+
primary: {
124+
whiteSpace: "nowrap",
125+
overflow: "hidden",
126+
textOverflow: "ellipsis",
127+
},
128+
}}
129+
secondary={option.category.name}
130+
/>
131+
</ListItem>
132+
</li>
133+
);
134+
}}
133135
/>
134136
{i18n.language !== LANGUAGE_SHORT_ENGLISH && (
135137
<FormGroup>

src/components/Exercises/Filter/NameAutocompleter.test.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { NameAutocompleter } from "components/Exercises/Filter/NameAutcompleter"
33
import React from 'react';
44
import { searchExerciseTranslations } from "services";
55
import { searchResponse } from "tests/exercises/searchResponse";
6+
import { Exercise } from "components/Exercises/models/exercise";
67

78
jest.mock("services");
89
const mockCallback = jest.fn();
@@ -71,6 +72,6 @@ describe("Test the NameAutocompleter component", () => {
7172
});
7273

7374
// Assert
74-
expect(mockCallback).toHaveBeenLastCalledWith(searchResponse[0]);
75+
expect(mockCallback).toHaveBeenCalledWith(expect.any(Exercise));
7576
});
7677
});

src/components/WorkoutRoutines/widgets/DayDetails.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import {
2424
import Grid from '@mui/material/Grid';
2525
import { LoadingPlaceholder, LoadingProgressIcon } from "components/Core/LoadingWidget/LoadingWidget";
2626
import { NameAutocompleter } from "components/Exercises/Filter/NameAutcompleter";
27+
import { Exercise } from "components/Exercises/models/exercise";
2728
import { useProfileQuery } from "components/User/queries/profile";
2829
import { Day } from "components/WorkoutRoutines/models/Day";
2930
import { Slot } from "components/WorkoutRoutines/models/Slot";
@@ -43,7 +44,6 @@ import { SlotDetails } from "components/WorkoutRoutines/widgets/SlotDetails";
4344
import React, { useState } from "react";
4445
import { useTranslation } from "react-i18next";
4546
import { Link } from "react-router-dom";
46-
import { ExerciseSearchResponse } from "services/responseType";
4747
import { SNACKBAR_AUTO_HIDE_DURATION, WEIGHT_UNIT_KG, WEIGHT_UNIT_LB } from "utils/consts";
4848
import { makeLink, WgerLink } from "utils/url";
4949

@@ -381,13 +381,13 @@ export const DayDetails = (props: {
381381
&& <Grid size={12}>
382382
<Box height={20} />
383383
<NameAutocompleter
384-
callback={(exercise: ExerciseSearchResponse | null) => {
384+
callback={(exercise: Exercise | null) => {
385385
if (exercise === null) {
386386
return;
387387
}
388388
addSlotEntryQuery.mutate(new SlotEntry({
389389
slotId: slot.id!,
390-
exerciseId: exercise.data.base_id,
390+
exerciseId: exercise.id!,
391391
type: 'normal',
392392
order: slot.entries.length + 1,
393393
weightUnitId: userProfileQuery.data!.useMetric ? WEIGHT_UNIT_KG : WEIGHT_UNIT_LB,

src/components/WorkoutRoutines/widgets/SlotDetails.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import EditOffIcon from '@mui/icons-material/EditOff';
44
import { Alert, AlertTitle, IconButton, Typography } from "@mui/material";
55
import Grid from '@mui/material/Grid';
66
import { NameAutocompleter } from "components/Exercises/Filter/NameAutcompleter";
7+
import { Exercise } from "components/Exercises/models/exercise";
78
import { useLanguageQuery } from "components/Exercises/queries";
89
import { BaseConfig } from "components/WorkoutRoutines/models/BaseConfig";
910
import { Slot } from "components/WorkoutRoutines/models/Slot";
@@ -21,7 +22,6 @@ import {
2122
import React, { useState } from "react";
2223
import { useTranslation } from "react-i18next";
2324
import { getLanguageByShortName } from "services";
24-
import { ExerciseSearchResponse } from "services/responseType";
2525

2626
/*
2727
* Converts a number to an alphabetic string, useful for counting
@@ -95,12 +95,12 @@ export const SlotEntryDetails = (props: {
9595

9696
const isPending = editSlotEntryQuery.isPending || deleteSlotEntryQuery.isPending;
9797

98-
const handleExerciseChange = (searchResponse: ExerciseSearchResponse | null) => {
99-
if (searchResponse === null) {
98+
const handleExerciseChange = (exercise: Exercise | null) => {
99+
if (exercise === null) {
100100
return;
101101
}
102102

103-
editSlotEntryQuery.mutate(SlotEntry.clone(props.slotEntry, { exerciseId: searchResponse.data.base_id }));
103+
editSlotEntryQuery.mutate(SlotEntry.clone(props.slotEntry, { exerciseId: exercise.id! }));
104104
setEditExercise(false);
105105
};
106106

src/components/WorkoutRoutines/widgets/forms/SessionLogsForm.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import Grid from '@mui/material/Grid';
66
import { WgerTextField } from "components/Common/forms/WgerTextField";
77
import { LoadingPlaceholder } from "components/Core/LoadingWidget/LoadingWidget";
88
import { NameAutocompleter } from "components/Exercises/Filter/NameAutcompleter";
9+
import { Exercise } from "components/Exercises/models/exercise";
910
import { useLanguageQuery } from "components/Exercises/queries";
1011
import { RIR_VALUES_SELECT } from "components/WorkoutRoutines/models/BaseConfig";
1112
import { LogEntryForm } from "components/WorkoutRoutines/models/WorkoutLog";
@@ -15,7 +16,6 @@ import { DateTime } from "luxon";
1516
import React, { useState } from 'react';
1617
import { useTranslation } from "react-i18next";
1718
import { getLanguageByShortName } from "services";
18-
import { ExerciseSearchResponse } from "services/responseType";
1919
import { REP_UNIT_REPETITIONS, SNACKBAR_AUTO_HIDE_DURATION } from "utils/consts";
2020
import * as yup from "yup";
2121

@@ -96,11 +96,11 @@ export const SessionLogsForm = ({ dayId, routineId, selectedDate }: SessionLogsF
9696
setSnackbarOpen(true);
9797
};
9898

99-
const handleCallback = async (exerciseResponse: ExerciseSearchResponse | null, formik: FormikProps<{
99+
const handleCallback = async (exercise: Exercise | null, formik: FormikProps<{
100100
logs: LogEntryForm[]
101101
}>) => {
102102

103-
if (exerciseResponse === null) {
103+
if (exercise === null) {
104104
return;
105105
}
106106

@@ -115,7 +115,7 @@ export const SessionLogsForm = ({ dayId, routineId, selectedDate }: SessionLogsF
115115
repetitionsTarget: '',
116116
rir: '',
117117
rirTarget: '',
118-
exercise: exerciseResponse.exercise!,
118+
exercise: exercise,
119119
};
120120
}
121121
return log;

src/services/exerciseTranslation.ts

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
import axios from 'axios';
2+
import { Exercise, ExerciseAdapter } from "components/Exercises/models/exercise";
23
import { Translation, TranslationAdapter } from "components/Exercises/models/translation";
34
import { ENGLISH_LANGUAGE_CODE, LANGUAGE_SHORT_ENGLISH } from "utils/consts";
45
import { makeHeader, makeUrl } from "utils/url";
5-
import { ExerciseSearchResponse, ExerciseSearchType, ResponseType } from "./responseType";
6+
import { ResponseType } from "./responseType";
67

78
export const EXERCISE_PATH = 'exercise';
89
export const EXERCISE_TRANSLATION_PATH = 'exercise-translation';
9-
export const EXERCISE_SEARCH_PATH = 'exercise/search';
1010

1111

1212
/*
@@ -15,7 +15,7 @@ export const EXERCISE_SEARCH_PATH = 'exercise/search';
1515
export const getExerciseTranslations = async (id: number): Promise<Translation[]> => {
1616
const url = makeUrl(EXERCISE_PATH, { query: { exercise: id } });
1717
// eslint-disable-next-line @typescript-eslint/no-explicit-any
18-
const { data } = await axios.get<ResponseType<any>>(url, {
18+
const { data } = await axios.get<ResponseType<Translation>>(url, {
1919
headers: makeHeader(),
2020
});
2121
const adapter = new TranslationAdapter();
@@ -24,19 +24,34 @@ export const getExerciseTranslations = async (id: number): Promise<Translation[]
2424

2525

2626
/*
27-
* Fetch all exercise translations for a given exercise base
27+
* Search for exercises by name using the exerciseinfo endpoint
2828
*/
29-
export const searchExerciseTranslations = async (name: string, languageCode: string = ENGLISH_LANGUAGE_CODE, searchEnglish: boolean = true,): Promise<ExerciseSearchResponse[]> => {
29+
export const searchExerciseTranslations = async (name: string,languageCode: string = ENGLISH_LANGUAGE_CODE,searchEnglish: boolean = true): Promise<Exercise[]> => {
3030
const languages = [languageCode];
3131
if (languageCode !== LANGUAGE_SHORT_ENGLISH && searchEnglish) {
3232
languages.push(LANGUAGE_SHORT_ENGLISH);
3333
}
3434

35+
const url = makeUrl('exerciseinfo', {
36+
query: {
37+
name__search: name,
38+
language__code: languages.join(','),
39+
limit: 50,
40+
}
41+
});
3542

36-
const url = makeUrl(EXERCISE_SEARCH_PATH, { query: { term: name, language: languages.join(',') } });
37-
38-
const { data } = await axios.get<ExerciseSearchType>(url);
39-
return data.suggestions;
43+
try {
44+
const { data } = await axios.get<ResponseType<Exercise>>(url);
45+
46+
if (!data || !data.results || !Array.isArray(data.results)) {
47+
return [];
48+
}
49+
50+
const adapter = new ExerciseAdapter();
51+
return data.results.map((item: unknown) => adapter.fromJson(item));
52+
} catch {
53+
return [];
54+
}
4055
};
4156

4257

src/services/responseType.ts

Lines changed: 3 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,9 @@
1-
import { Exercise } from "components/Exercises/models/exercise";
2-
1+
/*
2+
* Generic paginated response from the wger API
3+
*/
34
export interface ResponseType<T> {
45
count: number,
56
next: number | null,
67
previous: number | null,
78
results: T[]
89
}
9-
10-
export interface ExerciseSearchResponse {
11-
value: string,
12-
data: {
13-
id: number,
14-
base_id: number,
15-
name: string,
16-
category: string,
17-
image: string | null,
18-
image_thumbnail: string | null,
19-
},
20-
exercise?: Exercise
21-
}
22-
23-
export interface ExerciseSearchType {
24-
suggestions: ExerciseSearchResponse[];
25-
}
26-
27-
export interface IngredientSearchResponse {
28-
value: string,
29-
data: {
30-
id: number,
31-
name: string,
32-
image: string | null,
33-
image_thumbnail: string | null,
34-
}
35-
}

0 commit comments

Comments
 (0)