fix: resolve stations editor reactivity issue in award admin
Use Svelte stores with value/on:input instead of bind:value for stations array to properly track nested property changes. Add invisible reactivity triggers to force Svelte to track station callsign/points properties. Also update award sorting to handle numeric prefixes in numerical order (44 before 73) and update RS-44 satellite award category. Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -30,8 +30,7 @@ function loadAwardDefinitions() {
|
||||
try {
|
||||
// Auto-discover all JSON files in the award-definitions directory
|
||||
const files = readdirSync(AWARD_DEFINITIONS_DIR)
|
||||
.filter(f => f.endsWith('.json'))
|
||||
.sort();
|
||||
.filter(f => f.endsWith('.json'));
|
||||
|
||||
for (const file of files) {
|
||||
try {
|
||||
@@ -47,6 +46,32 @@ function loadAwardDefinitions() {
|
||||
logger.error('Error loading award definitions', { error: error.message });
|
||||
}
|
||||
|
||||
// Sort by award name with numeric prefixes in numerical order
|
||||
definitions.sort((a, b) => {
|
||||
const nameA = a.name || '';
|
||||
const nameB = b.name || '';
|
||||
|
||||
// Extract leading numbers if present
|
||||
const matchA = nameA.match(/^(\d+)/);
|
||||
const matchB = nameB.match(/^(\d+)/);
|
||||
|
||||
// If both start with numbers, compare numerically first
|
||||
if (matchA && matchB) {
|
||||
const numA = parseInt(matchA[1], 10);
|
||||
const numB = parseInt(matchB[1], 10);
|
||||
if (numA !== numB) {
|
||||
return numA - numB;
|
||||
}
|
||||
// If numbers are equal, fall through to alphabetical
|
||||
}
|
||||
// If one starts with a number, it comes first
|
||||
else if (matchA) return -1;
|
||||
else if (matchB) return 1;
|
||||
|
||||
// Otherwise, alphabetical comparison (case-insensitive)
|
||||
return nameA.toLowerCase().localeCompare(nameB.toLowerCase());
|
||||
});
|
||||
|
||||
// Cache the definitions for future calls
|
||||
cachedAwardDefinitions = definitions;
|
||||
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
<script>
|
||||
import { onMount } from 'svelte';
|
||||
import { browser } from '$app/environment';
|
||||
import { writable, derived } from 'svelte/store';
|
||||
import { awardsAdminAPI } from '$lib/api.js';
|
||||
import FilterBuilder from '../components/FilterBuilder.svelte';
|
||||
import TestAwardModal from '../components/TestAwardModal.svelte';
|
||||
|
||||
// Create a store for stations to ensure reactivity
|
||||
const stationsStore = writable([]);
|
||||
|
||||
let loading = true;
|
||||
let saving = false;
|
||||
let error = null;
|
||||
@@ -27,6 +31,11 @@
|
||||
}
|
||||
};
|
||||
|
||||
// Update stations store when formData changes
|
||||
$: if (formData.rules?.stations) {
|
||||
stationsStore.set(formData.rules.stations.map(s => ({...s})));
|
||||
}
|
||||
|
||||
// Available bands and modes
|
||||
const ALL_BANDS = ['160m', '80m', '60m', '40m', '30m', '20m', '17m', '15m', '12m', '10m', '6m', '2m', '70cm', '23cm', '13cm', '9cm', '6cm', '3cm'];
|
||||
const ALL_MODES = ['CW', 'SSB', 'AM', 'FM', 'RTTY', 'PSK31', 'FT8', 'FT4', 'JT65', 'JT9', 'MFSK', 'Q65', 'JS8', 'FSK441', 'ISCAT', 'JT6M', 'MSK144'];
|
||||
@@ -166,20 +175,21 @@
|
||||
errors.push('Target must be a positive number');
|
||||
}
|
||||
|
||||
if (!formData.rules.stations || formData.rules.stations.length === 0) {
|
||||
const stations = $stationsStore;
|
||||
if (!stations || stations.length === 0) {
|
||||
errors.push('Points rule requires at least one station');
|
||||
}
|
||||
|
||||
// Check for duplicate callsigns
|
||||
if (formData.rules.stations) {
|
||||
const callsigns = formData.rules.stations.map(s => s.callsign?.toUpperCase());
|
||||
if (stations) {
|
||||
const callsigns = stations.map(s => s.callsign?.toUpperCase());
|
||||
const duplicates = callsigns.filter((c, i) => callsigns.indexOf(c) !== i);
|
||||
if (duplicates.length > 0) {
|
||||
errors.push(`Duplicate station callsigns: ${[...new Set(duplicates)].join(', ')}`);
|
||||
}
|
||||
|
||||
// Check for zero or negative points
|
||||
formData.rules.stations.forEach((station, i) => {
|
||||
stations.forEach((station, i) => {
|
||||
if (!station.callsign || !station.callsign.trim()) {
|
||||
errors.push(`Station ${i + 1} is missing callsign`);
|
||||
}
|
||||
@@ -361,6 +371,10 @@
|
||||
}
|
||||
|
||||
async function handleSave() {
|
||||
// Sync stations from store to formData before saving
|
||||
const stations = $stationsStore;
|
||||
formData = { ...formData, rules: { ...formData.rules, stations } };
|
||||
|
||||
if (!performSafetyValidation()) {
|
||||
return;
|
||||
}
|
||||
@@ -709,23 +723,43 @@
|
||||
<div class="form-group">
|
||||
<label>Stations *</label>
|
||||
<div class="stations-editor">
|
||||
{#if formData.rules.stations && formData.rules.stations.length > 0}
|
||||
{#each formData.rules.stations as station, i}
|
||||
{#if $stationsStore && $stationsStore.length > 0}
|
||||
{#each $stationsStore as station, i (i)}
|
||||
<div class="station-row">
|
||||
<!-- Invisible reactivity trigger - forces Svelte to track station properties -->
|
||||
<span style="position:absolute;left:-9999px;width:1px;height:1px;overflow:hidden;">
|
||||
{station.callsign}|{station.points}
|
||||
</span>
|
||||
|
||||
<input
|
||||
type="text"
|
||||
bind:value={station.callsign}
|
||||
value={station.callsign}
|
||||
on:input={(e) => {
|
||||
stationsStore.update(s => {
|
||||
const newS = [...s];
|
||||
newS[i] = { ...newS[i], callsign: e.target.value };
|
||||
return newS;
|
||||
});
|
||||
}}
|
||||
placeholder="Callsign"
|
||||
/>
|
||||
<input
|
||||
type="number"
|
||||
min="1"
|
||||
bind:value={station.points}
|
||||
value={station.points}
|
||||
on:input={(e) => {
|
||||
stationsStore.update(s => {
|
||||
const newS = [...s];
|
||||
newS[i] = { ...newS[i], points: parseInt(e.target.value) || 0 };
|
||||
return newS;
|
||||
});
|
||||
}}
|
||||
placeholder="Points"
|
||||
/>
|
||||
<button
|
||||
class="btn-remove"
|
||||
on:click={() => formData.rules.stations.splice(i, 1)}
|
||||
on:click={() => {
|
||||
stationsStore.update(s => s.filter((_, idx) => idx !== i));
|
||||
}}
|
||||
>
|
||||
Remove
|
||||
</button>
|
||||
@@ -737,8 +771,7 @@
|
||||
<button
|
||||
class="btn btn-secondary"
|
||||
on:click={() => {
|
||||
if (!formData.rules.stations) formData.rules.stations = [];
|
||||
formData.rules.stations.push({ callsign: '', points: 1 });
|
||||
stationsStore.update(s => [...s, { callsign: '', points: 1 }]);
|
||||
}}
|
||||
>
|
||||
Add Station
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
<script>
|
||||
import { onMount } from 'svelte';
|
||||
import { browser } from '$app/environment';
|
||||
import { writable } from 'svelte/store';
|
||||
import { awardsAdminAPI } from '$lib/api.js';
|
||||
import FilterBuilder from '../components/FilterBuilder.svelte';
|
||||
import TestAwardModal from '../components/TestAwardModal.svelte';
|
||||
|
||||
// Create a store for stations to ensure reactivity
|
||||
const stationsStore = writable([]);
|
||||
|
||||
let loading = false;
|
||||
let saving = false;
|
||||
let error = null;
|
||||
@@ -28,6 +32,11 @@
|
||||
}
|
||||
};
|
||||
|
||||
// Update stations store when formData changes
|
||||
$: if (formData.rules?.stations) {
|
||||
stationsStore.set(formData.rules.stations.map(s => ({...s})));
|
||||
}
|
||||
|
||||
// Available bands and modes
|
||||
const ALL_BANDS = ['160m', '80m', '60m', '40m', '30m', '20m', '17m', '15m', '12m', '10m', '6m', '2m', '70cm', '23cm', '13cm', '9cm', '6cm', '3cm'];
|
||||
const ALL_MODES = ['CW', 'SSB', 'AM', 'FM', 'RTTY', 'PSK31', 'FT8', 'FT4', 'JT65', 'JT9', 'MFSK', 'Q65', 'JS8', 'FSK441', 'ISCAT', 'JT6M', 'MSK144'];
|
||||
@@ -173,20 +182,21 @@
|
||||
errors.push('Target must be a positive number');
|
||||
}
|
||||
|
||||
if (!formData.rules.stations || formData.rules.stations.length === 0) {
|
||||
const stations = $stationsStore;
|
||||
if (!stations || stations.length === 0) {
|
||||
errors.push('Points rule requires at least one station');
|
||||
}
|
||||
|
||||
// Check for duplicate callsigns
|
||||
if (formData.rules.stations) {
|
||||
const callsigns = formData.rules.stations.map(s => s.callsign?.toUpperCase());
|
||||
if (stations) {
|
||||
const callsigns = stations.map(s => s.callsign?.toUpperCase());
|
||||
const duplicates = callsigns.filter((c, i) => callsigns.indexOf(c) !== i);
|
||||
if (duplicates.length > 0) {
|
||||
errors.push(`Duplicate station callsigns: ${[...new Set(duplicates)].join(', ')}`);
|
||||
}
|
||||
|
||||
// Check for zero or negative points
|
||||
formData.rules.stations.forEach((station, i) => {
|
||||
stations.forEach((station, i) => {
|
||||
if (!station.callsign || !station.callsign.trim()) {
|
||||
errors.push(`Station ${i + 1} is missing callsign`);
|
||||
}
|
||||
@@ -368,6 +378,10 @@
|
||||
}
|
||||
|
||||
async function handleSave() {
|
||||
// Sync stations from store to formData before saving
|
||||
const stations = $stationsStore;
|
||||
formData = { ...formData, rules: { ...formData.rules, stations } };
|
||||
|
||||
if (!performSafetyValidation()) {
|
||||
return;
|
||||
}
|
||||
@@ -734,23 +748,44 @@
|
||||
<div class="form-group">
|
||||
<label>Stations *</label>
|
||||
<div class="stations-editor">
|
||||
{#if formData.rules.stations && formData.rules.stations.length > 0}
|
||||
{#each formData.rules.stations as station, i}
|
||||
{#if $stationsStore && $stationsStore.length > 0}
|
||||
{#each $stationsStore as station, i (i)}
|
||||
<div class="station-row">
|
||||
<!-- Invisible reactivity trigger - forces Svelte to track station properties -->
|
||||
<span style="position:absolute;left:-9999px;width:1px;height:1px;overflow:hidden;">
|
||||
{station.callsign}|{station.points}
|
||||
</span>
|
||||
|
||||
<input
|
||||
type="text"
|
||||
bind:value={station.callsign}
|
||||
value={station.callsign}
|
||||
on:input={(e) => {
|
||||
stationsStore.update(s => {
|
||||
const newS = [...s];
|
||||
newS[i] = { ...newS[i], callsign: e.target.value };
|
||||
return newS;
|
||||
});
|
||||
}}
|
||||
placeholder="Callsign"
|
||||
/>
|
||||
<input
|
||||
type="number"
|
||||
min="1"
|
||||
bind:value={station.points}
|
||||
value={station.points}
|
||||
on:input={(e) => {
|
||||
stationsStore.update(s => {
|
||||
const newS = [...s];
|
||||
newS[i] = { ...newS[i], points: parseInt(e.target.value) || 0 };
|
||||
return newS;
|
||||
});
|
||||
}}
|
||||
placeholder="Points"
|
||||
/>
|
||||
<button
|
||||
class="btn-remove"
|
||||
on:click={() => formData.rules.stations.splice(i, 1)}
|
||||
on:click={() => {
|
||||
stationsStore.update(s => s.filter((_, idx) => idx !== i));
|
||||
}}
|
||||
>
|
||||
Remove
|
||||
</button>
|
||||
@@ -762,8 +797,7 @@
|
||||
<button
|
||||
class="btn btn-secondary"
|
||||
on:click={() => {
|
||||
if (!formData.rules.stations) formData.rules.stations = [];
|
||||
formData.rules.stations.push({ callsign: '', points: 1 });
|
||||
stationsStore.update(s => [...s, { callsign: '', points: 1 }]);
|
||||
}}
|
||||
>
|
||||
Add Station
|
||||
|
||||
Reference in New Issue
Block a user