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;