Repository: IgnisDa/ryot (language: rust)

Issue:
[Feature Request] - Add exercise to Fitness Template without sets having data
Problem: Currently when creating a template if you add an exercise that doesn't have a record yet you get the message "Only sets that have data will be added"

Request: If possible please add an option, maybe an environment variable, to make it possible to add exercises that do not have a record yet.

Purpose: I like to create/define my workout routines beforehand. With the current method it makes it a bit cumbersome to experiment with new exercises.


Discussion / hints:
makes sense. will see what I can do.


Fix patch:
diff --git a/apps/frontend/app/components/fitness.tsx b/apps/frontend/app/components/fitness.tsx
index aa0fea0a2f..876f3816c7 100644
--- a/apps/frontend/app/components/fitness.tsx
+++ b/apps/frontend/app/components/fitness.tsx
@@ -264,11 +264,18 @@ export const ExerciseHistory = (props: {
 		>
 			{exerciseDetails && workoutDetails && exercise ? (
 				<>
-					<Stack mb="xs" gap="xs" ref={parent}>
+					<Stack
+						gap="xs"
+						ref={parent}
+						mb={exercise.sets.length > 0 ? "xs" : undefined}
+					>
 						<Box>
 							<Group justify="space-between" wrap="nowrap">
 								<Anchor
+									fw="bold"
+									lineClamp={1}
 									component={Link}
+									style={{ scrollMargin: 20 }}
 									to={
 										props.hideExerciseDetails
 											? withFragment(
@@ -280,9 +287,6 @@ export const ExerciseHistory = (props: {
 												)
 											: getExerciseDetailsPath(exercise.id)
 									}
-									fw="bold"
-									lineClamp={1}
-									style={{ scrollMargin: 20 }}
 								>
 									{props.hideExerciseDetails
 										? workoutDetails.details.name
diff --git a/apps/frontend/app/lib/state/fitness.ts b/apps/frontend/app/lib/state/fitness.ts
index 5db3c404c1..f5615dbbf3 100644
--- a/apps/frontend/app/lib/state/fitness.ts
+++ b/apps/frontend/app/lib/state/fitness.ts
@@ -238,7 +238,7 @@ export const currentWorkoutToCreateWorkoutInput = (
 						: null,
 				});
 			}
-		if (sets.length === 0) continue;
+		if (!isCreatingTemplate && sets.length === 0) continue;
 		const notes = Array<string>();
 		for (const note of exercise.notes) if (note) notes.push(note);
 		const toAdd = {
diff --git a/apps/frontend/app/routes/_dashboard.fitness.$action.tsx b/apps/frontend/app/routes/_dashboard.fitness.$action.tsx
index 06452541d3..f9322fa37f 100644
--- a/apps/frontend/app/routes/_dashboard.fitness.$action.tsx
+++ b/apps/frontend/app/routes/_dashboard.fitness.$action.tsx
@@ -283,9 +283,21 @@ export default function Page() {
 	const promptForRestTimer = userPreferences.fitness.logging.promptForRestTimer;
 	const performTasksAfterSetConfirmed = usePerformTasksAfterSetConfirmed();
 	const isWorkoutPaused = isString(currentWorkout?.durations.at(-1)?.to);
-	const isAnySetConfirmed = currentWorkout?.exercises.some(
-		(_e, idx) => getProgressOfExercise(currentWorkout, idx) !== "not-started",
+
+	const numberOfExercises = currentWorkout?.exercises.length || 0;
+	const shouldDisplayWorkoutTimer = Boolean(
+		loaderData.action === FitnessAction.LogWorkout,
+	);
+	const shouldDisplayReorderButton = Boolean(numberOfExercises > 1);
+	const shouldDisplayFinishButton = Boolean(
+		loaderData.isCreatingTemplate
+			? numberOfExercises > 0
+			: currentWorkout?.exercises.some(
+					(_e, idx) =>
+						getProgressOfExercise(currentWorkout, idx) !== "not-started",
+				),
 	);
+	const shouldDisplayCancelButton = true;
 
 	useInterval(() => {
 		if (
@@ -415,12 +427,12 @@ export default function Page() {
 										name="Exercises"
 										value={
 											loaderData.isCreatingTemplate
-												? currentWorkout.exercises.length.toString()
+												? numberOfExercises.toString()
 												: `${
 														currentWorkout.exercises
 															.map((e) => e.sets.every((s) => s.confirmedAt))
 															.filter(Boolean).length
-													}/${currentWorkout.exercises.length}`
+													}/${numberOfExercises}`
 										}
 									/>
 									<StatDisplay
@@ -455,164 +467,157 @@ export default function Page() {
 								<Divider />
 								<SimpleGrid
 									cols={
-										2 +
-										(loaderData.isCreatingTemplate ||
-										loaderData.isUpdatingWorkout
-											? -1
-											: 0) +
-										Number(currentWorkout.exercises.length > 0) +
-										Number(isAnySetConfirmed)
+										Number(shouldDisplayWorkoutTimer) +
+										Number(shouldDisplayReorderButton) +
+										Number(shouldDisplayFinishButton) +
+										Number(shouldDisplayCancelButton)
 									}
 								>
-									<Button
-										radius="md"
-										color="orange"
-										variant="subtle"
-										size="compact-sm"
-										onClick={toggleTimerDrawer}
-										style={
-											loaderData.isCreatingTemplate ||
-											loaderData.isUpdatingWorkout
-												? { display: "none" }
-												: undefined
-										}
-									>
-										<RestTimer />
-									</Button>
-									{currentWorkout.exercises.length > 1 ? (
-										<>
-											<Button
-												radius="md"
-												color="blue"
-												variant="subtle"
-												size="compact-sm"
-												onClick={() => openReorderDrawer(null)}
-											>
-												Reorder
-											</Button>
-										</>
+									{shouldDisplayWorkoutTimer ? (
+										<Button
+											radius="md"
+											color="orange"
+											variant="subtle"
+											size="compact-sm"
+											onClick={toggleTimerDrawer}
+										>
+											<RestTimer />
+										</Button>
 									) : null}
-									{isAnySetConfirmed ? (
-										<>
-											<Button
-												radius="md"
-												color="green"
-												variant="subtle"
-												size="compact-sm"
-												loading={isSaveBtnLoading}
-												disabled={isWorkoutPaused}
-												onClick={() => {
-													if (!currentWorkout.name) {
-														notifications.show({
-															color: "red",
-															message: `Please give a name to the ${
-																loaderData.isCreatingTemplate
-																	? "template"
-																	: "workout"
-															}`,
-														});
-														return;
-													}
-													openConfirmationModal(
-														loaderData.isCreatingTemplate
-															? "Only sets that have data will added. Are you sure you want to save this template?"
-															: "Only sets marked as confirmed will be recorded. Are you sure you want to finish this workout?",
-														async () => {
-															setIsSaveBtnLoading(true);
-															await new Promise((r) => setTimeout(r, 1000));
-															const input = currentWorkoutToCreateWorkoutInput(
-																currentWorkout,
+									{shouldDisplayReorderButton ? (
+										<Button
+											radius="md"
+											color="blue"
+											variant="subtle"
+											size="compact-sm"
+											onClick={() => openReorderDrawer(null)}
+										>
+											Reorder
+										</Button>
+									) : null}
+									{shouldDisplayFinishButton ? (
+										<Button
+											radius="md"
+											color="green"
+											variant="subtle"
+											size="compact-sm"
+											loading={isSaveBtnLoading}
+											disabled={isWorkoutPaused}
+											onClick={() => {
+												if (!currentWorkout.name) {
+													notifications.show({
+														color: "red",
+														message: `Please give a name to the ${
+															loaderData.isCreatingTemplate
+																? "template"
+																: "workout"
+														}`,
+													});
+													return;
+												}
+												openConfirmationModal(
+													loaderData.isCreatingTemplate
+														? "Only sets that have data will added. Are you sure you want to save this template?"
+														: "Only sets marked as confirmed will be recorded. Are you sure you want to finish this workout?",
+													async () => {
+														setIsSaveBtnLoading(true);
+														await new Promise((r) => setTimeout(r, 1000));
+														const input = currentWorkoutToCreateWorkoutInput(
+															currentWorkout,
+															loaderData.isCreatingTemplate,
+														);
+														for (const exercise of currentWorkout.exercises) {
+															queryClient.removeQueries({
+																queryKey:
+																	queryFactory.fitness.userExerciseDetails(
+																		exercise.exerciseId,
+																	).queryKey,
+															});
+														}
+														stopTimer();
+														try {
+															const [entityId, fitnessEntity] = await match(
 																loaderData.isCreatingTemplate,
-															);
-															for (const exercise of currentWorkout.exercises) {
-																queryClient.removeQueries({
-																	queryKey:
-																		queryFactory.fitness.userExerciseDetails(
-																			exercise.exerciseId,
-																		).queryKey,
-																});
-															}
-															stopTimer();
-															try {
-																const [entityId, fitnessEntity] = await match(
-																	loaderData.isCreatingTemplate,
+															)
+																.with(true, () =>
+																	clientGqlService
+																		.request(
+																			CreateOrUpdateUserWorkoutTemplateDocument,
+																			input,
+																		)
+																		.then((c) => [
+																			c.createOrUpdateUserWorkoutTemplate,
+																			FitnessEntity.Templates,
+																		]),
 																)
-																	.with(true, () =>
-																		clientGqlService
-																			.request(
-																				CreateOrUpdateUserWorkoutTemplateDocument,
-																				input,
-																			)
-																			.then((c) => [
-																				c.createOrUpdateUserWorkoutTemplate,
-																				FitnessEntity.Templates,
-																			]),
-																	)
-																	.with(false, () =>
-																		clientGqlService
-																			.request(
-																				CreateOrUpdateUserWorkoutDocument,
-																				input,
-																			)
-																			.then((c) => [
-																				c.createOrUpdateUserWorkout,
-																				FitnessEntity.Workouts,
-																			]),
-																	)
-																	.exhaustive();
-																if (
-																	loaderData.action === FitnessAction.LogWorkout
+																.with(false, () =>
+																	clientGqlService
+																		.request(
+																			CreateOrUpdateUserWorkoutDocument,
+																			input,
+																		)
+																		.then((c) => [
+																			c.createOrUpdateUserWorkout,
+																			FitnessEntity.Workouts,
+																		]),
 																)
-																	events.createWorkout();
-																setCurrentWorkout(RESET);
-																navigate(
-																	$path("/fitness/:entity/:id", {
-																		id: entityId,
-																		entity: fitnessEntity,
-																	}),
-																);
-															} catch (e) {
-																notifications.show({
-																	color: "red",
-																	message: `Error while saving workout: ${JSON.stringify(e)}`,
-																});
-																setIsSaveBtnLoading(false);
-															}
-														},
-													);
-												}}
-											>
-												{loaderData.isCreatingTemplate ||
-												loaderData.isUpdatingWorkout
-													? "Save"
-													: "Finish"}
-											</Button>
-										</>
+																.exhaustive();
+															if (
+																loaderData.action === FitnessAction.LogWorkout
+															)
+																events.createWorkout();
+															setCurrentWorkout(RESET);
+															navigate(
+																$path("/fitness/:entity/:id", {
+																	id: entityId,
+																	entity: fitnessEntity,
+																}),
+															);
+														} catch (e) {
+															notifications.show({
+																color: "red",
+																message: `Error while saving workout: ${JSON.stringify(e)}`,
+															});
+															setIsSaveBtnLoading(false);
+														}
+													},
+												);
+											}}
+										>
+											{loaderData.isCreatingTemplate ||
+											loaderData.isUpdatingWorkout
+												? "Save"
+												: "Finish"}
+										</Button>
+									) : null}
+									{shouldDisplayCancelButton ? (
+										<Button
+											radius="md"
+											color="red"
+											variant="subtle"
+											size="compact-sm"
+											onClick={() => {
+												openConfirmationModal(
+													`Are you sure you want to cancel this ${
+														loaderData.isCreatingTemplate
+															? "template"
+															: "workout"
+													}?`,
+													() => {
+														for (const e of currentWorkout.exercises) {
+															const assets = [...e.images, ...e.videos];
+															for (const asset of assets)
+																deleteUploadedAsset(asset.key);
+														}
+														navigate($path("/"), { replace: true });
+														setCurrentWorkout(RESET);
+													},
+												);
+											}}
+										>
+											Cancel
+										</Button>
 									) : null}
-									<Button
-										color="red"
-										variant="subtle"
-										radius="md"
-										size="compact-sm"
-										onClick={() => {
-											openConfirmationModal(
-												`Are you sure you want to cancel this ${
-													loaderData.isCreatingTemplate ? "template" : "workout"
-												}?`,
-												() => {
-													for (const e of currentWorkout.exercises) {
-														const assets = [...e.images, ...e.videos];
-														for (const asset of assets)
-															deleteUploadedAsset(asset.key);
-													}
-													navigate($path("/"), { replace: true });
-													setCurrentWorkout(RESET);
-												},
-											);
-										}}
-									>
-										Cancel
-									</Button>
 								</SimpleGrid>
 								<Divider />
 								{currentWorkout.exercises.map((ex, idx) => (


Test patch: