diff --git a/CHANGELOG.md b/CHANGELOG.md index 27d6054..8abc0f4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,11 @@ ## Unreleased — 0.1.4 +- Fixed equipment creation from the Android active-session exercise editor. + The editor now reuses the catalogue creation route, returns to the same + stable occurrence with all draft fields intact, selects the explicitly + created equipment, and saves the edit in place rather than appending a + duplicate occurrence. - Fixed the Android active-session exercise editor for target-only continuous preparation entries. Opening an occurrence now renders its prefilled form, preserves its independent plan while editing performed values, updates only diff --git a/android/app/src/debug/java/com/labfytools/trainlog/ui/DraftUiTestActivity.kt b/android/app/src/debug/java/com/labfytools/trainlog/ui/DraftUiTestActivity.kt index 6b82aa5..a295c32 100644 --- a/android/app/src/debug/java/com/labfytools/trainlog/ui/DraftUiTestActivity.kt +++ b/android/app/src/debug/java/com/labfytools/trainlog/ui/DraftUiTestActivity.kt @@ -60,6 +60,7 @@ class DraftUiTestActivity : ComponentActivity() { sessionVisible = false }, onCreateExercise = {}, + onCreateEquipment = { _ -> }, /* CONTRACT: this host never writes shared export data. */ onSessionSaved = {}, ) diff --git a/android/app/src/main/java/com/labfytools/trainlog/ui/AppNavigation.kt b/android/app/src/main/java/com/labfytools/trainlog/ui/AppNavigation.kt index f7290f5..15eaa84 100644 --- a/android/app/src/main/java/com/labfytools/trainlog/ui/AppNavigation.kt +++ b/android/app/src/main/java/com/labfytools/trainlog/ui/AppNavigation.kt @@ -171,5 +171,11 @@ class TrainlogAppState : ViewModel() { val equipment = EquipmentScreenState() val exercise = ExerciseScreenState() val body = BodyScreenState() + /* WHY: the durable form intentionally does not serialize transient edit + * position, but a catalogue detour must still return to the same row. + * CONTRACT: this stable occurrence ID spans only that caller-aware detour; + * all actual form fields remain repository-owned and durable. + * INVARIANT: null means a new occurrence, never an inferred row. */ + var pendingSessionEquipmentEntryId: String? = null val navigationController = AppNavigationController(navigation, generator, equipment, exercise, body) } diff --git a/android/app/src/main/java/com/labfytools/trainlog/ui/SectionScreens.kt b/android/app/src/main/java/com/labfytools/trainlog/ui/SectionScreens.kt index 227deb1..d308599 100644 --- a/android/app/src/main/java/com/labfytools/trainlog/ui/SectionScreens.kt +++ b/android/app/src/main/java/com/labfytools/trainlog/ui/SectionScreens.kt @@ -423,17 +423,29 @@ fun EquipmentDetailScreen(repository: TrainlogRepository, equipmentId: String, o } @Composable -fun EquipmentCreateScreen(repository: TrainlogRepository, state: EquipmentScreenState, onCreated: () -> Unit) { +fun EquipmentCreateScreen( + repository: TrainlogRepository, + state: EquipmentScreenState, + onCreated: (EquipmentCatalogEntry) -> Unit, +) { val colors = LocalTrainlogColors.current val strings = localizedContext() TrainlogScreen(strings.getString(R.string.create_equipment), scrollKey = "equipment-create") { TrainlogFrame(strings.getString(R.string.personal_equipment)) { - TrainlogInputField(strings.getString(R.string.name), state.customName, { state.customName = it; state.message = null }) + TrainlogInputField( + strings.getString(R.string.name), + state.customName, + { state.customName = it; state.message = null }, + testTag = "equipment-create-name", + ) TrainlogPrimaryAction(strings.getString(R.string.create), strings.getString(R.string.create_machine_description)) { when (val result = repository.createCustomEquipment(state.customName)) { is CreateEquipmentResult.Created -> { state.selectedEquipmentId = result.equipment.equipmentId - state.customName = ""; state.message = null; state.revision++; onCreated() + state.customName = "" + state.message = null + state.revision++ + onCreated(result.equipment) } CreateEquipmentResult.Invalid -> state.message = strings.getString(R.string.equipment_name_invalid) CreateEquipmentResult.Conflict -> state.message = strings.getString(R.string.equipment_name_exists) diff --git a/android/app/src/main/java/com/labfytools/trainlog/ui/SessionScreen.kt b/android/app/src/main/java/com/labfytools/trainlog/ui/SessionScreen.kt index 8d31a50..a43074a 100644 --- a/android/app/src/main/java/com/labfytools/trainlog/ui/SessionScreen.kt +++ b/android/app/src/main/java/com/labfytools/trainlog/ui/SessionScreen.kt @@ -29,6 +29,7 @@ import androidx.compose.foundation.text.BasicText import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.key import androidx.compose.runtime.mutableStateOf @@ -48,7 +49,6 @@ import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.unit.dp import com.labfytools.trainlog.data.ActiveDraftLoadResult import com.labfytools.trainlog.data.ActiveDraftMutationResult -import com.labfytools.trainlog.data.CreateEquipmentResult import com.labfytools.trainlog.data.EquipmentLoadSemantics import com.labfytools.trainlog.data.FinalizeActiveDraftResult import com.labfytools.trainlog.data.ManualPercentMaxResult @@ -77,7 +77,10 @@ fun SessionScreen( catalogRevision: Int, onBack: () -> Unit, onCreateExercise: () -> Unit, + onCreateEquipment: (editingEntryId: String?) -> Unit, onSessionSaved: () -> Unit, + restoreEditingEntryId: String? = null, + onEditingContextRestored: () -> Unit = {}, ) { val colors = LocalTrainlogColors.current @@ -97,13 +100,27 @@ fun SessionScreen( var activeDraft by remember(catalogRevision) { + val loadedDraft = (initialLoad as? ActiveDraftLoadResult.Loaded)?.draft + val restoredIndex = restoreEditingEntryId?.let { entryId -> + loadedDraft?.exercises?.indexOfFirst { it.entryId == entryId } + ?.takeIf { it >= 0 } + } mutableStateOf( - (initialLoad as? - ActiveDraftLoadResult.Loaded) - ?.draft + if (restoredIndex == null) loadedDraft else loadedDraft?.let { draft -> + draft.copy( + form = draft.form.copy( + editingExerciseIndex = restoredIndex, + editingEntryId = restoreEditingEntryId, + ), + ) + } ) } + LaunchedEffect(restoreEditingEntryId) { + if (restoreEditingEntryId != null) onEditingContextRestored() + } + var message by remember(catalogRevision) { mutableStateOf< @@ -436,6 +453,9 @@ fun SessionScreen( initialPlan = currentDraft.form.editingExerciseIndex?.let { currentDraft.exercises.getOrNull(it)?.plan }, + onCreateEquipment = { + onCreateEquipment(currentDraft.form.editingEntryId) + }, onFormChanged = { form -> persistDraft( @@ -861,6 +881,7 @@ private fun SessionExerciseForm( sessionType: SessionType, initialForm: SessionDraftForm, initialPlan: SessionExercisePlan?, + onCreateEquipment: () -> Unit, onFormChanged: (SessionDraftForm) -> Unit, onCancel: () -> Unit, onAdd: @@ -935,13 +956,11 @@ private fun SessionExerciseForm( ) } - var equipmentRevision by remember(key) { mutableStateOf(0) } - val equipmentEntries = remember(equipmentRevision) { repository.listEquipment() } + val equipmentEntries = remember(key) { repository.listEquipment() } val fixedEquipmentId = remember(exercise.exerciseId) { repository.machineExerciseLegacyEquipmentId(exercise.exerciseId) } var equipmentSearch by remember(key) { mutableStateOf("") } - var customEquipmentName by remember(key) { mutableStateOf("") } var selectedEquipmentId by remember(key) { mutableStateOf(initialForm.selectedEquipmentId ?: fixedEquipmentId) } @@ -978,34 +997,18 @@ private fun SessionExerciseForm( value = equipmentSearch, onValueChange = { equipmentSearch = it }, ) - TrainlogInputField( - label = strings.getString(R.string.new_historical_context), - value = customEquipmentName, - onValueChange = { customEquipmentName = it }, - ) TrainlogAction( - label = strings.getString(R.string.create_context), - description = strings.getString(R.string.context_compatibility), + label = strings.getString(R.string.create_equipment), + description = strings.getString(R.string.create_equipment_from_session), accent = colors.success, - onClick = { - when (val result = repository.createCustomEquipment(customEquipmentName)) { - is CreateEquipmentResult.Created -> { - customEquipmentName = "" - equipmentRevision += 1 - selectedEquipmentId = result.equipment.equipmentId - onFormChanged(currentForm(exercise, setCountText, repsText, durationText, speedText, distanceText, selectedEquipmentId, weightText, maxWeightText)) - } - CreateEquipmentResult.Invalid -> error = strings.getString(R.string.context_name_invalid) - CreateEquipmentResult.Conflict -> error = strings.getString(R.string.context_exists) - is CreateEquipmentResult.DatabaseError -> error = strings.getString(R.string.context_create_error, localizedRepositoryMessage(strings, result.message)) - } - }, + modifier = Modifier.testTag("session-create-equipment"), + onClick = onCreateEquipment, ) } val selectedEquipment = equipmentEntries.firstOrNull { it.equipmentId == selectedEquipmentId } val percentResult = remember( exercise.exerciseId, selectedEquipmentId, targetPercentText, - targetChoice, equipmentRevision, + targetChoice, ) { if (targetChoice == ManualTargetChoice.PERCENT_MAX) repository.calculateManualPercentMaxTarget( diff --git a/android/app/src/main/java/com/labfytools/trainlog/ui/TrainlogApp.kt b/android/app/src/main/java/com/labfytools/trainlog/ui/TrainlogApp.kt index 14e31e8..6c595ca 100644 --- a/android/app/src/main/java/com/labfytools/trainlog/ui/TrainlogApp.kt +++ b/android/app/src/main/java/com/labfytools/trainlog/ui/TrainlogApp.kt @@ -105,7 +105,21 @@ fun TrainlogApp(repository: TrainlogRepository, exporter: SyncExporter, inbox: S ) AppRoute.Sessions -> SessionsHub(activeDraft, pendingAiDraftCount, { open(AppRoute.SessionEditor) }, ::openManualSession, { open(AppRoute.AiSessionDrafts) }, { open(AppRoute.CompletedSessions) }, { open(AppRoute.Programs) }) - AppRoute.SessionEditor -> SessionScreen(repository, catalogRevision, { draftRevision++; back() }, { open(AppRoute.ExerciseCreate(AppRoute.SessionEditor)) }, { exportSnapshot(); draftRevision++ }) + AppRoute.SessionEditor -> SessionScreen( + repository, + catalogRevision, + { draftRevision++; back() }, + { open(AppRoute.ExerciseCreate(AppRoute.SessionEditor)) }, + { editingEntryId -> + appState.pendingSessionEquipmentEntryId = editingEntryId + open(AppRoute.EquipmentCreate(AppRoute.SessionEditor)) + }, + { exportSnapshot(); draftRevision++ }, + restoreEditingEntryId = appState.pendingSessionEquipmentEntryId, + onEditingContextRestored = { + appState.pendingSessionEquipmentEntryId = null + }, + ) AppRoute.SessionGenerator -> SessionGeneratorScreen(repository, generatorState, { back() }, { generatorState.abandon(); draftRevision++; open(AppRoute.SessionEditor) }, { draftMessage = strings.getString(R.string.existing_draft_warning) draftRevision++ @@ -138,7 +152,45 @@ fun TrainlogApp(repository: TrainlogRepository, exporter: SyncExporter, inbox: S is AppRoute.ExerciseEdit -> ExerciseEditorRoute(repository, exerciseState, route.exerciseId, false) { exportSnapshot(); catalogRevision++; back() } AppRoute.Equipment -> EquipmentScreen(repository, equipmentState, { open(AppRoute.EquipmentCreate(AppRoute.Equipment)) }, { open(AppRoute.EquipmentDetail(it)) }) is AppRoute.EquipmentDetail -> EquipmentDetailScreen(repository, route.equipmentId) { open(AppRoute.ExerciseDetail(it)) } - is AppRoute.EquipmentCreate -> EquipmentCreateScreen(repository, equipmentState) { exportSnapshot(); catalogRevision++; back() } + is AppRoute.EquipmentCreate -> EquipmentCreateScreen(repository, equipmentState) { equipment -> + /* WHY: equipment creation temporarily leaves the active-session editor. + * CONTRACT: returning from that catalogue route preserves the durable + * form and selects the exact stable ID that was just created. + * INVARIANT: no equipment is synthesized; this runs only after the + * explicit EquipmentCreateScreen action succeeded. */ + if (route.caller == AppRoute.SessionEditor) { + when (val loaded = repository.loadActiveSessionDraft()) { + is ActiveDraftLoadResult.Loaded -> when ( + val saved = repository.saveActiveSessionDraft( + loaded.draft.copy( + form = loaded.draft.form.copy( + editingExerciseIndex = appState.pendingSessionEquipmentEntryId + ?.let { entryId -> + loaded.draft.exercises.indexOfFirst { + it.entryId == entryId + }.takeIf { it >= 0 } + }, + editingEntryId = appState.pendingSessionEquipmentEntryId, + selectedEquipmentId = equipment.equipmentId, + ), + ), + ) + ) { + ActiveDraftMutationResult.Saved -> draftRevision++ + is ActiveDraftMutationResult.Error -> { + draftMessage = localizedRepositoryMessage(localized, saved.message) + } + } + is ActiveDraftLoadResult.Error -> { + draftMessage = localizedRepositoryMessage(localized, loaded.message) + } + ActiveDraftLoadResult.None -> Unit + } + } + exportSnapshot() + catalogRevision++ + back() + } AppRoute.BodyMeasurements -> BodyScreen(repository, bodyState, { exportSnapshot() }, { back() }) AppRoute.SleepDiary -> SleepDiaryScreen(repository) AppRoute.LatestMaxima -> LatestMaximaScreen(repository) diff --git a/android/app/src/main/res/values-en/strings.xml b/android/app/src/main/res/values-en/strings.xml index 8dab40c..48593a1 100644 --- a/android/app/src/main/res/values-en/strings.xml +++ b/android/app/src/main/res/values-en/strings.xml @@ -417,7 +417,7 @@ Run again with the same inputs and reference instant.Create the normal draft without prefilling performed sets.Confirm the recent-exposure warning first. observed repeated doseobserved maximum without numeric prescriptionuser-selected MAX percentagecompatible MAX unavailableassistance load omittednumeric load absentmanually entered loadpreferred exerciserequested primary zonerequested secondary zonedistinct movementsame exercise observed recentlysimilar movement observed recently missing upper regionmissing lower regionmissing coremissing upper-body pushmissing upper-body pullmissing lower-body extensionmissing lower-body flexionfewer distinct exercises available - Create equipmentAdd a personal machine.Search by name, label, or alias + Create equipmentAdd a personal machine.Create it in the catalogue, then return to this exercise.Search by name, label, or alias Unknown reference · %1$sDefinitionPersonalProvidedLabel: %1$s %1$s confidenceVerified sources: %1$dPersonal equipmentCreate Create a personal machine with the current semantics.Enter a name of 1 to 120 characters.Equipment already has this name.Creation failed: %1$s. diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml index 70dc13c..1a11e82 100644 --- a/android/app/src/main/res/values/strings.xml +++ b/android/app/src/main/res/values/strings.xml @@ -417,7 +417,7 @@ Relancer les mêmes entrées et le même instant de référence.Créer le brouillon normal sans préremplir les séries réalisées.Confirmez d’abord l’avertissement d’exposition récente. dose répétée observéemaximum observé sans prescription numériquepourcentage de MAX choisi par l’utilisateurMAX compatible indisponiblecharge d’assistance omisecharge numérique absentecharge saisie manuellementexercice préférézone principale demandéezone secondaire demandéemouvement distinctmême exercice observé récemmentmouvement similaire observé récemment région supérieure manquanterégion inférieure manquantetronc manquantpoussée du haut du corps manquantetirage du haut du corps manquantextension du bas du corps manquanteflexion du bas du corps manquantemoins d’exercices distincts disponibles - Créer un équipementAjouter une machine personnelle.Rechercher par nom, étiquette ou alias + Créer un équipementAjouter une machine personnelle.Créer dans le catalogue puis revenir à cet exercice.Rechercher par nom, étiquette ou alias Référence inconnue · %1$sDéfinitionPersonnelFourniÉtiquette : %1$s Confiance %1$sSources vérifiées : %1$dÉquipement personnelCréer Créer une machine personnelle avec la sémantique actuelle.Saisissez un nom de 1 à 120 caractères.Un équipement porte déjà ce nom.Création impossible : %1$s. diff --git a/android/app/src/test/java/com/labfytools/trainlog/ui/AiSessionDraftUiWiringTest.kt b/android/app/src/test/java/com/labfytools/trainlog/ui/AiSessionDraftUiWiringTest.kt index 110ff76..9f34fdc 100644 --- a/android/app/src/test/java/com/labfytools/trainlog/ui/AiSessionDraftUiWiringTest.kt +++ b/android/app/src/test/java/com/labfytools/trainlog/ui/AiSessionDraftUiWiringTest.kt @@ -120,7 +120,7 @@ class AiSessionDraftUiWiringTest { assertEquals(ActiveDraftMutationResult.Saved, repository.startActiveSessionDraft()) var createCalls = 0 compose.setContent { TrainlogTheme { - SessionScreen(repository, 0, {}, { createCalls++ }, {}) + SessionScreen(repository, 0, {}, { createCalls++ }, { _ -> }, {}) } } val create = compose.onNodeWithTag("active-session-create-exercise") @@ -173,7 +173,7 @@ class AiSessionDraftUiWiringTest { repository.saveActiveSessionDraft(loaded), ) compose.setContent { - TrainlogTheme { SessionScreen(repository, 0, {}, {}, {}) } + TrainlogTheme { SessionScreen(repository, 0, {}, {}, { _ -> }, {}) } } compose.onNodeWithTag("edit-draft-exercise-${walkEntry.entryId}") diff --git a/android/app/src/test/java/com/labfytools/trainlog/ui/DraftFeedbackUiReadbackTest.kt b/android/app/src/test/java/com/labfytools/trainlog/ui/DraftFeedbackUiReadbackTest.kt index 49c878c..81782c2 100644 --- a/android/app/src/test/java/com/labfytools/trainlog/ui/DraftFeedbackUiReadbackTest.kt +++ b/android/app/src/test/java/com/labfytools/trainlog/ui/DraftFeedbackUiReadbackTest.kt @@ -67,7 +67,7 @@ class DraftFeedbackUiReadbackTest { val completedSessionId = mutableStateOf(null) compose.setContent { TrainlogTheme { completedSessionId.value?.let { SessionDetailScreen(repository, it, {}, {}) } - ?: SessionScreen(repository, 0, {}, {}, {}) + ?: SessionScreen(repository, 0, {}, {}, { _ -> }, {}) } } compose.onNodeWithTag("draft-feedback-${entry.entryId}").fetchSemanticsNode() compose.onNodeWithText("Ressenti · texte UI durable").fetchSemanticsNode() diff --git a/android/app/src/test/java/com/labfytools/trainlog/ui/TrainlogAppNavigationCallbackTest.kt b/android/app/src/test/java/com/labfytools/trainlog/ui/TrainlogAppNavigationCallbackTest.kt index c79f58c..1134af4 100644 --- a/android/app/src/test/java/com/labfytools/trainlog/ui/TrainlogAppNavigationCallbackTest.kt +++ b/android/app/src/test/java/com/labfytools/trainlog/ui/TrainlogAppNavigationCallbackTest.kt @@ -7,15 +7,21 @@ package com.labfytools.trainlog.ui import android.content.Context import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.test.assertIsDisplayed import androidx.compose.ui.test.hasClickAction import androidx.compose.ui.test.hasText import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.onNodeWithTag +import androidx.compose.ui.test.performTextInput import androidx.compose.ui.test.performSemanticsAction +import androidx.compose.ui.test.performScrollTo import androidx.compose.ui.semantics.SemanticsActions import androidx.test.core.app.ApplicationProvider import com.labfytools.trainlog.data.ActiveDraftMutationResult +import com.labfytools.trainlog.data.ActiveDraftLoadResult import com.labfytools.trainlog.data.BodyZoneRecentExposure import com.labfytools.trainlog.data.ExposureWindowSummary +import com.labfytools.trainlog.data.CreateExerciseResult import com.labfytools.trainlog.data.GenerationWarningLevel import com.labfytools.trainlog.data.SessionGenerationPreview import com.labfytools.trainlog.data.SessionGenerationPreviewExercise @@ -26,6 +32,13 @@ import com.labfytools.trainlog.data.SyncExporter import com.labfytools.trainlog.data.SyncRequestOutbox import com.labfytools.trainlog.data.TrainlogRepository import com.labfytools.trainlog.model.SessionExercisePlan +import com.labfytools.trainlog.model.ActiveSessionDraft +import com.labfytools.trainlog.model.NewExerciseProfile +import com.labfytools.trainlog.model.RecordingMode +import com.labfytools.trainlog.model.SessionExerciseDraft +import com.labfytools.trainlog.model.SessionLoadMode +import com.labfytools.trainlog.model.SessionSetDraft +import com.labfytools.trainlog.model.TrackingMode import com.labfytools.trainlog.ui.theme.TrainlogTheme import java.security.MessageDigest import java.util.UUID @@ -109,6 +122,83 @@ class TrainlogAppNavigationCallbackTest { assertArrayEquals(before, databaseDigest()) } + @Test fun activeExerciseEditorCreatesEquipmentReturnsSelectsAndSaves() { + val exercise = (repository.createExercise( + NewExerciseProfile( + "Goblet squat haltère", + RecordingMode.SETS, + TrackingMode.REPS, + 0, + ), + ) as CreateExerciseResult.Created).exercise + val occurrence = SessionExerciseDraft( + exercise = exercise, + sets = listOf(SessionSetDraft(reps = 10, weightKg = 8.0)), + plan = SessionExercisePlan( + sets = 1, + reps = 10, + weightKg = 8.0, + loadMode = SessionLoadMode.EXTERNAL, + ), + ) + assertEquals( + ActiveDraftMutationResult.Saved, + repository.saveActiveSessionDraft( + ActiveSessionDraft(exercises = listOf(occurrence)), + ), + ) + val editorState = TrainlogAppState().also { + it.navigation.open(AppRoute.SessionEditor) + } + compose.setContent { + TrainlogTheme { + TrainlogApp( + repository, + SyncExporter(context, repository), + SyncCatalogInbox(context, repository), + SyncRequestOutbox(context), + editorState, + ) + } + } + + compose.onNodeWithTag("edit-draft-exercise-${occurrence.entryId}") + .performSemanticsAction(SemanticsActions.OnClick) + compose.onNodeWithText("SAISIE — Goblet squat haltère") + .performScrollTo() + .assertIsDisplayed() + compose.onNodeWithTag("session-create-equipment") + .performSemanticsAction(SemanticsActions.OnClick) + compose.waitForIdle() + assertEquals(AppRoute.EquipmentCreate(AppRoute.SessionEditor), editorState.navigation.route) + assertEquals(occurrence.entryId, editorState.pendingSessionEquipmentEntryId) + + compose.onNodeWithTag("equipment-create-name").performTextInput("Haltère") + compose.onNodeWithText("Créer") + .performSemanticsAction(SemanticsActions.OnClick) + compose.waitForIdle() + + assertEquals(AppRoute.SessionEditor, editorState.navigation.route) + compose.onNodeWithText("SAISIE — Goblet squat haltère") + .performScrollTo() + .assertIsDisplayed() + compose.onNodeWithText("✓ Haltère").performScrollTo().assertIsDisplayed() + compose.onNodeWithTag("add-to-session") + .performSemanticsAction(SemanticsActions.OnClick) + compose.runOnIdle { + val loaded = repository.loadActiveSessionDraft() as ActiveDraftLoadResult.Loaded + assertEquals(1, loaded.draft.exercises.size) + assertEquals(occurrence.entryId, loaded.draft.exercises.single().entryId) + assertEquals(8.0, loaded.draft.exercises.single().plan?.weightKg) + assertEquals( + "Haltère", + repository.listEquipment().single { + it.equipmentId == loaded.draft.exercises.single().equipmentId + }.displayName, + ) + } + } + private fun databaseDigest(): ByteArray = MessageDigest.getInstance("SHA-256") .digest(context.getDatabasePath(databaseName).readBytes()) diff --git a/docs/android.md b/docs/android.md index 5276f8b..d7f1c0d 100644 --- a/docs/android.md +++ b/docs/android.md @@ -884,8 +884,11 @@ decimal input is accepted (`12,5`); one value applies to all sets or values may be separated with `;`. Assisted equipment is explicitly labelled `Assistance (kg)`. Empty load remains distinct from an entered zero. -`Nouvelle machine` in that same selector creates a persistent local custom -equipment entry with a generated stable `eq_…` ID and selects it immediately. +`Créer un équipement` in that selector opens the existing catalogue creation +route. An explicit successful creation returns automatically to the same +exercise editor, restores the stable occurrence being edited, and selects the +new persistent `eq_…` ID; the draft's other raw fields and target remain +unchanged. No equipment is synthesized merely by opening the route. The shared bundled catalogue uses reserved canonical IDs. User-created IDs are exchanged first by definitions V1, so a V2 association can resolve them on the receiving side; conflicts remain explicit and are never converted to null. diff --git a/docs/current_state.md b/docs/current_state.md index a4dcdb5..0977a1d 100644 --- a/docs/current_state.md +++ b/docs/current_state.md @@ -237,6 +237,9 @@ Android currently provides: - exactly one durable active session draft with identity-preserving continuous multi-position drag reorder, resume, confirmed discard, and atomic completion; +- caller-aware equipment creation from an active occurrence editor, with the + explicit catalogue-created ID selected on return and the occurrence identity + retained for in-place save; - completed-session consultation, transient drag reorder, and atomic factual or order correction while retaining session/occurrence/feedback identities for synchronization;