commit cedfddc830eee593b31732900a421953551b70d2
parent 783bec010a56da3ca441605b034b29225180849a
Author: Asher Morgan <59518073+ashermorgan@users.noreply.github.com>
Date: Thu, 29 May 2025 20:02:57 -0700
Implement custom workout target names
Diffstat:
8 files changed, 141 insertions(+), 7 deletions(-)
diff --git a/src/components/TargetEditor.vue b/src/components/TargetEditor.vue
@@ -23,6 +23,11 @@
<tbody>
<tr v-for="(item, index) in internalValue.targets" :key="index">
<td>
+ <span v-if="setType === 'workout' && customWorkoutNames">
+ <input v-model="item.customName" :placeholder="workoutTargetToString(item)"
+ aria-label="Custom target name"/>:
+ </span>
+
<span v-if="setType === 'workout'">
<decimal-input v-model="item.splitValue" aria-label="Split distance value"
:min="0" :digits="2"/>
@@ -86,6 +91,7 @@ import { watch, ref } from 'vue';
import VueFeather from 'vue-feather';
+import { workoutTargetToString } from '@/utils/targets';
import { DISTANCE_UNITS, getDefaultDistanceUnit } from '@/utils/units';
import DecimalInput from '@/components/DecimalInput.vue';
@@ -104,9 +110,9 @@ const model = defineModel({
const props = defineProps({
/**
- * Whether the target set is a custom or default set
+ * Whether to allow custom names for workout targets
*/
- isCustomSet: {
+ customWorkoutNames: {
type: Boolean,
default: false,
},
@@ -120,6 +126,14 @@ const props = defineProps({
},
/**
+ * Whether the target set is a custom or default set
+ */
+ isCustomSet: {
+ type: Boolean,
+ default: false,
+ },
+
+ /**
* The target set type ('standard', 'split', or 'workout')
*/
setType: {
diff --git a/src/components/TargetSetSelector.vue b/src/components/TargetSetSelector.vue
@@ -13,7 +13,8 @@
<dialog ref="dialogElement" class="target-set-editor-dialog" aria-label="Edit target set">
<target-editor @close="sortTargetSet(); dialogElement.close()"
- @revert="revertTargetSet" :default-unit-system="defaultUnitSystem" :setType="setType"
+ @revert="revertTargetSet" :customWorkoutNames="customWorkoutNames"
+ :default-unit-system="defaultUnitSystem" :setType="setType"
v-model="targetSets[internalValue]" :isCustomSet="!internalValue.startsWith('_')"/>
</dialog>
</span>
@@ -46,6 +47,14 @@ const targetSets = defineModel('targetSets', {
defineProps({
/**
+ * Whether to allow custom names for workout targets
+ */
+ customWorkoutNames: {
+ type: Boolean,
+ default: false,
+ },
+
+ /**
* The unit system to use when creating distance targets
*/
defaultUnitSystem: {
diff --git a/src/utils/calculators.js b/src/utils/calculators.js
@@ -169,7 +169,7 @@ export function calculateWorkoutResults(input, target, options, preciseDurations
// Return result
return {
- key: workoutTargetToString(target),
+ key: target.customName || workoutTargetToString(target),
value: formatDuration(t3, 3, preciseDurations ? 2 : 0, true),
pace: '', // Pace not used in workout calculator
result: 'value',
diff --git a/src/views/BatchCalculator.vue b/src/views/BatchCalculator.vue
@@ -38,7 +38,8 @@
Target Set:
<target-set-selector v-model:selectedTargetSet="selectedTargetSet"
:setType="options.calculator === 'workout' ? 'workout' : 'standard'"
- v-model:targetSets="targetSets" :default-unit-system="defaultUnitSystem"/>
+ :customWorkoutNames="true" v-model:targetSets="targetSets"
+ :default-unit-system="defaultUnitSystem"/>
</div>
<race-options v-if="options.calculator !== 'pace'" v-model="advancedOptions"/>
</details>
diff --git a/src/views/WorkoutCalculator.vue b/src/views/WorkoutCalculator.vue
@@ -19,7 +19,8 @@
<div>
Target Set:
<target-set-selector v-model:selectedTargetSet="selectedTargetSet" setType="workout"
- v-model:targetSets="targetSets" :default-unit-system="defaultUnitSystem"/>
+ :customWorkoutNames="true" v-model:targetSets="targetSets"
+ :default-unit-system="defaultUnitSystem"/>
</div>
<race-options v-model="options"/>
</details>
diff --git a/tests/unit/components/TargetEditor.spec.js b/tests/unit/components/TargetEditor.spec.js
@@ -15,16 +15,20 @@ test('should correctly render standard target set', async () => {
],
},
setType: 'standard',
+ customWorkoutNames: true, // name input should not be rendered
},
});
// Assert target set correctly rendered
expect(wrapper.find('input').element.value).to.equal('My target set');
const rows = wrapper.findAll('tbody tr');
+ expect(rows[0].findAll('input').length).to.equal(0);
expect(rows[0].findComponent({ name: 'decimal-input' }).vm.modelValue).to.equal(1.61);
expect(rows[0].find('select').element.value).to.equal('kilometers');
+ expect(rows[1].findAll('input').length).to.equal(0);
expect(rows[1].findComponent({ name: 'decimal-input' }).vm.modelValue).to.equal(3.11);
expect(rows[1].find('select').element.value).to.equal('miles');
+ expect(rows[2].findAll('input').length).to.equal(0);
expect(rows[2].findComponent({ name: 'time-input' }).vm.modelValue).to.equal(600);
expect(rows.length).to.equal(3);
});
@@ -47,14 +51,16 @@ test('should correctly render split target set', async () => {
// Assert target set correctly rendered
expect(wrapper.find('input').element.value).to.equal('My target set');
const rows = wrapper.findAll('tbody tr');
+ expect(rows[0].findAll('input').length).to.equal(0);
expect(rows[0].findComponent({ name: 'decimal-input' }).vm.modelValue).to.equal(1.61);
expect(rows[0].find('select').element.value).to.equal('kilometers');
+ expect(rows[1].findAll('input').length).to.equal(0);
expect(rows[1].findComponent({ name: 'decimal-input' }).vm.modelValue).to.equal(3.11);
expect(rows[1].find('select').element.value).to.equal('miles');
expect(rows.length).to.equal(2);
});
-test('should correctly render workout target set', async () => {
+test('should correctly render workout target set without custom names', async () => {
// Initialize component
const wrapper = shallowMount(TargetEditor, {
propsData: {
@@ -85,13 +91,71 @@ test('should correctly render workout target set', async () => {
// Assert target set correctly rendered
expect(wrapper.find('input').element.value).to.equal('My target set');
const rows = wrapper.findAll('tbody tr');
+ expect(rows[0].findAll('input').length).to.equal(0);
expect(rows[0].findAllComponents({ name: 'decimal-input' })[0].vm.modelValue).to.equal(400);
expect(rows[0].findAll('select')[0].element.value).to.equal('meters');
expect(rows[0].findAllComponents({ name: 'decimal-input' })[1].vm.modelValue).to.equal(2);
expect(rows[0].findAll('select')[1].element.value).to.equal('miles');
+ expect(rows[1].findAll('input').length).to.equal(0);
expect(rows[1].findComponent({ name: 'decimal-input' }).vm.modelValue).to.equal(2);
expect(rows[1].find('select').element.value).to.equal('kilometers');
expect(rows[1].findComponent({ name: 'time-input' }).vm.modelValue).to.equal(6000);
+ expect(rows[2].findAll('input').length).to.equal(0);
+ expect(rows[2].findAllComponents({ name: 'decimal-input' })[0].vm.modelValue).to.equal(1);
+ expect(rows[2].findAll('select')[0].element.value).to.equal('miles');
+ expect(rows[2].findAllComponents({ name: 'decimal-input' })[1].vm.modelValue).to.equal(5);
+ expect(rows[2].findAll('select')[1].element.value).to.equal('kilometers');
+ expect(rows.length).to.equal(3);
+});
+
+test('should correctly render workout target set with custom names', async () => {
+ // Initialize component
+ const wrapper = shallowMount(TargetEditor, {
+ propsData: {
+ modelValue: {
+ name: 'My target set',
+ targets: [
+ {
+ // customName is undefined
+ distanceUnit: 'miles', distanceValue: 2,
+ splitUnit: 'meters', splitValue: 400,
+ type: 'distance',
+ },
+ {
+ customName: '',
+ time: 6000,
+ splitUnit: 'kilometers', splitValue: 2,
+ type: 'time',
+ },
+ {
+ customName: 'my custom name',
+ distanceUnit: 'kilometers', distanceValue: 5,
+ splitUnit: 'miles', splitValue: 1,
+ type: 'distance'
+ },
+ ],
+ },
+ setType: 'workout',
+ customWorkoutNames: true,
+ },
+ });
+
+ // Assert target set correctly rendered
+ expect(wrapper.find('input').element.value).to.equal('My target set');
+ const rows = wrapper.findAll('tbody tr');
+ expect(rows[0].find('input').element.value).to.equal('');
+ expect(rows[0].find('input').element.placeholder).to.equal('400 m @ 2 mi');
+ expect(rows[0].findAllComponents({ name: 'decimal-input' })[0].vm.modelValue).to.equal(400);
+ expect(rows[0].findAll('select')[0].element.value).to.equal('meters');
+ expect(rows[0].findAllComponents({ name: 'decimal-input' })[1].vm.modelValue).to.equal(2);
+ expect(rows[0].findAll('select')[1].element.value).to.equal('miles');
+ expect(rows[1].find('input').element.value).to.equal('');
+ expect(rows[1].find('input').element.placeholder).to.equal('2 km @ 1:40:00');
+ expect(rows[1].findComponent({ name: 'decimal-input' }).vm.modelValue).to.equal(2);
+ expect(rows[1].find('select').element.value).to.equal('kilometers');
+ expect(rows[1].findComponent({ name: 'time-input' }).vm.modelValue).to.equal(6000);
+ expect(rows[2].find('input').element.value).to.equal('my custom name');
+ expect(rows[2].find('input').element.placeholder).to.equal('1 mi @ 5 km');
expect(rows[2].findAllComponents({ name: 'decimal-input' })[0].vm.modelValue).to.equal(1);
expect(rows[2].findAll('select')[0].element.value).to.equal('miles');
expect(rows[2].findAllComponents({ name: 'decimal-input' })[1].vm.modelValue).to.equal(5);
diff --git a/tests/unit/components/TargetSetSelector.spec.js b/tests/unit/components/TargetSetSelector.spec.js
@@ -336,3 +336,20 @@ test('should correctly pass setType prop to TargetEditor', async () => {
// Assert target editor props are correct
expect(wrapper.findComponent({ name: 'target-editor' }).vm.setType).to.equal('foo');
});
+
+test('should correctly pass customWorkoutNames prop to TargetEditor', async () => {
+ const wrapper = shallowMount(TargetSetSelector, {
+ propsData: {
+ customWorkoutNames: false,
+ }
+ });
+
+ // Assert target editor props are correct
+ expect(wrapper.findComponent({ name: 'target-editor' }).vm.customWorkoutNames).to.equal(false);
+
+ // Update customWorkoutNames prop
+ await wrapper.setProps({ customWorkoutNames: true });
+
+ // Assert target editor props are correct
+ expect(wrapper.findComponent({ name: 'target-editor' }).vm.customWorkoutNames).to.equal(true);
+});
diff --git a/tests/unit/utils/calculators.spec.js b/tests/unit/utils/calculators.spec.js
@@ -174,6 +174,34 @@ test('should correctly calculate distance-based workouts according to race optio
expect(result.sort).to.be.closeTo(206.35, 0.01);
});
+test('should correctly calculate distance-based workouts with custom names', () => {
+ const input = {
+ distanceValue: 2,
+ distanceUnit: 'miles',
+ time: 630,
+ };
+ const target = {
+ distanceValue: 5,
+ distanceUnit: 'kilometers', // 5k split is ~17:11.77
+ splitValue: 1000,
+ splitUnit: 'meters',
+ type: 'distance',
+ customName: 'my custom name',
+ };
+ const options = {
+ model: 'RiegelModel',
+ riegelExponent: 1.12,
+ }
+
+ const result = calculatorUtils.calculateWorkoutResults(input, target, options);
+
+ expect(result.key).to.equal('my custom name');
+ expect(result.value).to.equal('3:26.35');
+ expect(result.pace).to.equal('');
+ expect(result.result).to.equal('value');
+ expect(result.sort).to.be.closeTo(206.35, 0.01);
+});
+
test('should correctly calculate time-based workouts', () => {
const input = {
distanceValue: 5,