From dc5434991aab1e1628b8113a381e5b1b419cf859 Mon Sep 17 00:00:00 2001 From: Brandon Presley Date: Mon, 14 Aug 2023 13:14:34 +1200 Subject: [PATCH] Pause adding multi-edit to workouts Got up to the point where i'm find/replacing the old names with new names, and I got confused about the purpose of this feature. --- EditWorkout.tsx | 24 ++-- EditWorkouts.tsx | 223 +++++++++++++++++++++++++++++++++++++ ListMenu.tsx | 18 +-- SetItem.tsx | 1 - SetList.tsx | 3 +- WorkoutItem.tsx | 104 ++++++----------- WorkoutList.tsx | 39 ++++++- WorkoutsPage.tsx | 5 +- tests/EditWorkout.test.tsx | 50 ++++----- 9 files changed, 346 insertions(+), 121 deletions(-) create mode 100644 EditWorkouts.tsx diff --git a/EditWorkout.tsx b/EditWorkout.tsx index 383f880..e4e5987 100644 --- a/EditWorkout.tsx +++ b/EditWorkout.tsx @@ -23,16 +23,16 @@ export default function EditWorkout() { const { params } = useRoute>(); const [removeImage, setRemoveImage] = useState(false); const [showRemove, setShowRemove] = useState(false); - const [name, setName] = useState(params.value.name); - const [steps, setSteps] = useState(params.value.steps); - const [uri, setUri] = useState(params.value.image); + const [name, setName] = useState(params.gymSet.name); + const [steps, setSteps] = useState(params.gymSet.steps); + const [uri, setUri] = useState(params.gymSet.image); const [minutes, setMinutes] = useState( - params.value.minutes?.toString() ?? "3" + params.gymSet.minutes?.toString() ?? "3" ); const [seconds, setSeconds] = useState( - params.value.seconds?.toString() ?? "30" + params.gymSet.seconds?.toString() ?? "30" ); - const [sets, setSets] = useState(params.value.sets?.toString() ?? "3"); + const [sets, setSets] = useState(params.gymSet.sets?.toString() ?? "3"); const navigation = useNavigation(); const setsRef = useRef(null); const stepsRef = useRef(null); @@ -48,9 +48,9 @@ export default function EditWorkout() { const update = async () => { await setRepo.update( - { name: params.value.name }, + { name: params.gymSet.name }, { - name: name || params.value.name, + name: name || params.gymSet.name, sets: Number(sets), minutes: +minutes, seconds: +seconds, @@ -62,7 +62,7 @@ export default function EditWorkout() { `UPDATE plans SET workouts = REPLACE(workouts, $1, $2) WHERE workouts LIKE $3`, - [params.value.name, name, `%${params.value.name}%`] + [params.gymSet.name, name, `%${params.gymSet.name}%`] ); navigation.goBack(); }; @@ -84,7 +84,7 @@ export default function EditWorkout() { }; const save = async () => { - if (params.value.name) return update(); + if (params.gymSet.name) return update(); return add(); }; @@ -109,7 +109,9 @@ export default function EditWorkout() { return ( <> - + >(); + const [removeImage, setRemoveImage] = useState(false); + const [showRemove, setShowRemove] = useState(false); + const [name, setName] = useState(""); + const [oldNames, setOldNames] = useState(params.names.join(", ")); + const [steps, setSteps] = useState(""); + const [oldSteps, setOldSteps] = useState(""); + const [uri, setUri] = useState(""); + const [oldUri, setOldUri] = useState(""); + const [minutes, setMinutes] = useState(""); + const [oldMinutes, setOldMinutes] = useState(""); + const [seconds, setSeconds] = useState(""); + const [oldSeconds, setOldSeconds] = useState(""); + const [sets, setSets] = useState(""); + const [oldSets, setOldSets] = useState(""); + const navigation = useNavigation(); + const setsRef = useRef(null); + const stepsRef = useRef(null); + const minutesRef = useRef(null); + const secondsRef = useRef(null); + const [settings, setSettings] = useState(); + + useFocusEffect( + useCallback(() => { + settingsRepo.findOne({ where: {} }).then(setSettings); + setRepo + .createQueryBuilder() + .select() + .where("name IN (:...names)", { names: params.names }) + .groupBy("name") + .getMany() + .then((gymSets) => { + console.log({ gymSets }); + setOldNames(gymSets.map((set) => set.name).join(", ")); + setOldSteps(gymSets.map((set) => set.steps).join(", ")); + setOldUri(gymSets.map((set) => set.steps).join(", ")); + }); + }, [params.names]) + ); + + const update = async () => { + await setRepo.update( + { name: In(params.names) }, + { + name: name || undefined, + sets: sets ? Number(sets) : undefined, + minutes: minutes ? Number(minutes) : undefined, + seconds: seconds ? Number(seconds) : undefined, + steps: steps || undefined, + image: removeImage ? "" : uri, + } + ); + await planRepo + .createQueryBuilder() + .update() + .set({ + workouts: () => `REPLACE(workouts, '${params.gymSet.name}', '${name}')`, + }) + .where("workouts LIKE :name", { name: `%${params.gymSet.name}%` }) + .execute(); + navigation.goBack(); + }; + + const add = async () => { + const now = await getNow(); + await setRepo.save({ + ...defaultSet, + name, + hidden: true, + image: uri, + minutes: minutes ? +minutes : 3, + seconds: seconds ? +seconds : 30, + sets: sets ? +sets : 3, + steps, + created: now, + }); + navigation.goBack(); + }; + + const save = async () => { + if (params.gymSet.name) return update(); + return add(); + }; + + const changeImage = useCallback(async () => { + const { fileCopyUri } = await DocumentPicker.pickSingle({ + type: DocumentPicker.types.images, + copyTo: "documentDirectory", + }); + if (fileCopyUri) setUri(fileCopyUri); + }, []); + + const handleRemove = useCallback(async () => { + setUri(""); + setRemoveImage(true); + setShowRemove(false); + }, []); + + const submitName = () => { + if (settings.steps) stepsRef.current?.focus(); + else setsRef.current?.focus(); + }; + + return ( + <> + + + + + {settings?.steps && ( + setsRef.current?.focus()} + /> + )} + { + const fixed = fixNumeric(newSets); + setSets(fixed); + if (fixed.length !== newSets.length) + toast("Sets must be a number"); + }} + label="Sets per workout" + keyboardType="numeric" + onSubmitEditing={() => minutesRef.current?.focus()} + /> + {settings?.alarm && ( + <> + secondsRef.current?.focus()} + value={minutes} + onChangeText={(newMinutes) => { + const fixed = fixNumeric(newMinutes); + setMinutes(fixed); + if (fixed.length !== newMinutes.length) + toast("Reps must be a number"); + }} + label="Rest minutes" + keyboardType="numeric" + /> + + + )} + {settings?.images && uri && ( + setShowRemove(true)} + > + + + )} + {settings?.images && !uri && ( + + )} + + + + Are you sure you want to remove the image? + + + + ); +} diff --git a/ListMenu.tsx b/ListMenu.tsx index 5c22841..76b21f7 100644 --- a/ListMenu.tsx +++ b/ListMenu.tsx @@ -11,11 +11,11 @@ export default function ListMenu({ ids, }: { onEdit: () => void; - onCopy: () => void; + onCopy?: () => void; onClear: () => void; onDelete: () => void; onSelect: () => void; - ids?: number[]; + ids?: unknown[]; }) { const [showMenu, setShowMenu] = useState(false); const [showRemove, setShowRemove] = useState(false); @@ -64,12 +64,14 @@ export default function ListMenu({ onPress={edit} disabled={ids?.length === 0} /> - + {onCopy && ( + + )} void; settings: Settings; ids: number[]; setIds: (value: number[]) => void; diff --git a/SetList.tsx b/SetList.tsx index b1123d9..178801f 100644 --- a/SetList.tsx +++ b/SetList.tsx @@ -52,12 +52,11 @@ export default function SetList() { settings={settings} item={item} key={item.id} - onRemove={() => refresh(term)} ids={ids} setIds={setIds} /> ), - [refresh, term, settings, ids] + [settings, ids] ); const next = useCallback(async () => { diff --git a/WorkoutItem.tsx b/WorkoutItem.tsx index 891c2fc..8cd18cd 100644 --- a/WorkoutItem.tsx +++ b/WorkoutItem.tsx @@ -1,39 +1,26 @@ import { NavigationProp, useNavigation } from "@react-navigation/native"; -import { useCallback, useMemo, useState } from "react"; -import { GestureResponderEvent, Image } from "react-native"; -import { List, Menu, Text } from "react-native-paper"; -import ConfirmDialog from "./ConfirmDialog"; -import { setRepo } from "./db"; +import { useCallback, useMemo } from "react"; +import { Image } from "react-native"; +import { List } from "react-native-paper"; +import { DARK_RIPPLE } from "./constants"; +import { LIGHT_RIPPLE } from "./constants"; import GymSet from "./gym-set"; +import useDark from "./use-dark"; import { WorkoutsPageParams } from "./WorkoutsPage"; export default function WorkoutItem({ item, - onRemove, + setNames, + names, images, }: { item: GymSet; - onRemove: () => void; images: boolean; + setNames: (value: string[]) => void; + names: string[]; }) { - const [showMenu, setShowMenu] = useState(false); - const [anchor, setAnchor] = useState({ x: 0, y: 0 }); - const [showRemove, setShowRemove] = useState(""); const navigation = useNavigation>(); - - const remove = useCallback(async () => { - await setRepo.delete({ name: item.name }); - setShowMenu(false); - onRemove(); - }, [setShowMenu, onRemove, item.name]); - - const longPress = useCallback( - (e: GestureResponderEvent) => { - setAnchor({ x: e.nativeEvent.pageX, y: e.nativeEvent.pageY }); - setShowMenu(true); - }, - [setShowMenu, setAnchor] - ); + const dark = useDark(); const description = useMemo(() => { const seconds = item.seconds?.toString().padStart(2, "0"); @@ -47,50 +34,33 @@ export default function WorkoutItem({ ); }, [item.image, images]); - const right = useCallback(() => { - return ( - - setShowMenu(false)} - > - { - setShowRemove(item.name); - setShowMenu(false); - }} - title="Delete" - /> - - - ); - }, [anchor, showMenu, item.name]); + const long = useCallback(() => { + if (names.length > 0) return; + setNames([item.name]); + }, [names.length, item.name, setNames]); + + const backgroundColor = useMemo(() => { + if (!names.includes(item.name)) return; + if (dark) return DARK_RIPPLE; + return LIGHT_RIPPLE; + }, [dark, names, item.name]); + + const press = useCallback(() => { + if (names.length === 0) + return navigation.navigate("EditWorkout", { gymSet: item }); + const removing = names.find((name) => name === item.name); + if (removing) setNames(names.filter((name) => name !== item.name)); + else setNames([...names, item.name]); + }, [names, item, navigation, setNames]); return ( - <> - navigation.navigate("EditWorkout", { value: item })} - title={item.name} - description={description} - onLongPress={longPress} - left={left} - right={right} - /> - (show ? null : setShowRemove(""))} - onOk={remove} - > - This irreversibly deletes ALL sets related to this workout. Are you - sure? - - + ); } diff --git a/WorkoutList.tsx b/WorkoutList.tsx index 46c0dad..51deab3 100644 --- a/WorkoutList.tsx +++ b/WorkoutList.tsx @@ -10,6 +10,7 @@ import { LIMIT } from "./constants"; import { setRepo, settingsRepo } from "./db"; import DrawerHeader from "./DrawerHeader"; import GymSet from "./gym-set"; +import ListMenu from "./ListMenu"; import Page from "./Page"; import SetList from "./SetList"; import Settings from "./settings"; @@ -22,6 +23,7 @@ export default function WorkoutList() { const [term, setTerm] = useState(""); const [end, setEnd] = useState(false); const [settings, setSettings] = useState(); + const [names, setNames] = useState([]); const navigation = useNavigation>(); const refresh = useCallback(async (value: string) => { @@ -52,13 +54,14 @@ export default function WorkoutList() { images={settings?.images} item={item} key={item.name} - onRemove={() => refresh(term)} + names={names} + setNames={setNames} /> ), - [refresh, term, settings?.images] + [settings?.images, names] ); - const next = useCallback(async () => { + const next = async () => { if (end) return; const newOffset = offset + LIMIT; console.log(`${SetList.name}.next:`, { @@ -81,11 +84,11 @@ export default function WorkoutList() { setWorkouts([...workouts, ...newWorkouts]); if (newWorkouts.length < LIMIT) return setEnd(true); setOffset(newOffset); - }, [term, end, offset, workouts]); + }; const onAdd = useCallback(async () => { navigation.navigate("EditWorkout", { - value: new GymSet(), + gymSet: new GymSet(), }); }, [navigation]); @@ -97,9 +100,33 @@ export default function WorkoutList() { [refresh] ); + const clear = useCallback(() => { + setNames([]); + }, []); + + const remove = async () => { + setNames([]); + await setRepo.delete(names.length > 0 ? names : {}); + await refresh(term); + }; + + const select = () => { + setNames(workouts.map((workout) => workout.name)); + }; + + const edit = useCallback(() => {}, []); + return ( <> - + + + {workouts?.length === 0 ? ( ({ +jest.mock("../db.ts", () => ({ settingsRepo: { findOne: () => Promise.resolve({ @@ -16,28 +16,28 @@ jest.mock('../db.ts', () => ({ alarm: true, } as Settings), }, -})) +})); -test('renders correctly', async () => { - const Stack = createStackNavigator() +test("renders correctly", async () => { + const Stack = createStackNavigator(); const { getByText, getAllByText } = render( - , - ) - const title = await waitFor(() => getByText(/Edit workout/i)) - expect(title).toBeDefined() - expect(getAllByText(/Name/i).length).toBeGreaterThan(0) - expect(getAllByText(/Sets/i).length).toBeGreaterThan(0) - expect(getAllByText(/Minutes/i).length).toBeGreaterThan(0) - expect(getAllByText(/Seconds/i).length).toBeGreaterThan(0) - expect(getAllByText(/Save/i).length).toBeGreaterThan(0) -}) + + ); + const title = await waitFor(() => getByText(/Edit workout/i)); + expect(title).toBeDefined(); + expect(getAllByText(/Name/i).length).toBeGreaterThan(0); + expect(getAllByText(/Sets/i).length).toBeGreaterThan(0); + expect(getAllByText(/Minutes/i).length).toBeGreaterThan(0); + expect(getAllByText(/Seconds/i).length).toBeGreaterThan(0); + expect(getAllByText(/Save/i).length).toBeGreaterThan(0); +});