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:
@@ -1,9 +1,9 @@
|
|||||||
{
|
{
|
||||||
"id": "sat-rs44",
|
"id": "sat-rs44",
|
||||||
"name": "RS-44 Satellite",
|
"name": "44 on RS-44",
|
||||||
"description": "Work 44 QSOs on satellite RS-44",
|
"description": "Work 44 QSOs on satellite RS-44",
|
||||||
"caption": "Make 44 unique QSOs via the RS-44 satellite. Each QSO with a different callsign counts toward the total.",
|
"caption": "Make 44 unique QSOs via the RS-44 satellite. Each QSO with a different callsign counts toward the total.",
|
||||||
"category": "custom",
|
"category": "satellite",
|
||||||
"rules": {
|
"rules": {
|
||||||
"type": "counter",
|
"type": "counter",
|
||||||
"target": 44,
|
"target": 44,
|
||||||
@@ -19,5 +19,6 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
}
|
"modeGroups": {}
|
||||||
|
}
|
||||||
@@ -30,8 +30,7 @@ function loadAwardDefinitions() {
|
|||||||
try {
|
try {
|
||||||
// Auto-discover all JSON files in the award-definitions directory
|
// Auto-discover all JSON files in the award-definitions directory
|
||||||
const files = readdirSync(AWARD_DEFINITIONS_DIR)
|
const files = readdirSync(AWARD_DEFINITIONS_DIR)
|
||||||
.filter(f => f.endsWith('.json'))
|
.filter(f => f.endsWith('.json'));
|
||||||
.sort();
|
|
||||||
|
|
||||||
for (const file of files) {
|
for (const file of files) {
|
||||||
try {
|
try {
|
||||||
@@ -47,6 +46,32 @@ function loadAwardDefinitions() {
|
|||||||
logger.error('Error loading award definitions', { error: error.message });
|
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
|
// Cache the definitions for future calls
|
||||||
cachedAwardDefinitions = definitions;
|
cachedAwardDefinitions = definitions;
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,14 @@
|
|||||||
<script>
|
<script>
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
import { browser } from '$app/environment';
|
import { browser } from '$app/environment';
|
||||||
|
import { writable, derived } from 'svelte/store';
|
||||||
import { awardsAdminAPI } from '$lib/api.js';
|
import { awardsAdminAPI } from '$lib/api.js';
|
||||||
import FilterBuilder from '../components/FilterBuilder.svelte';
|
import FilterBuilder from '../components/FilterBuilder.svelte';
|
||||||
import TestAwardModal from '../components/TestAwardModal.svelte';
|
import TestAwardModal from '../components/TestAwardModal.svelte';
|
||||||
|
|
||||||
|
// Create a store for stations to ensure reactivity
|
||||||
|
const stationsStore = writable([]);
|
||||||
|
|
||||||
let loading = true;
|
let loading = true;
|
||||||
let saving = false;
|
let saving = false;
|
||||||
let error = null;
|
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
|
// 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_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'];
|
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');
|
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');
|
errors.push('Points rule requires at least one station');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for duplicate callsigns
|
// Check for duplicate callsigns
|
||||||
if (formData.rules.stations) {
|
if (stations) {
|
||||||
const callsigns = formData.rules.stations.map(s => s.callsign?.toUpperCase());
|
const callsigns = stations.map(s => s.callsign?.toUpperCase());
|
||||||
const duplicates = callsigns.filter((c, i) => callsigns.indexOf(c) !== i);
|
const duplicates = callsigns.filter((c, i) => callsigns.indexOf(c) !== i);
|
||||||
if (duplicates.length > 0) {
|
if (duplicates.length > 0) {
|
||||||
errors.push(`Duplicate station callsigns: ${[...new Set(duplicates)].join(', ')}`);
|
errors.push(`Duplicate station callsigns: ${[...new Set(duplicates)].join(', ')}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for zero or negative points
|
// Check for zero or negative points
|
||||||
formData.rules.stations.forEach((station, i) => {
|
stations.forEach((station, i) => {
|
||||||
if (!station.callsign || !station.callsign.trim()) {
|
if (!station.callsign || !station.callsign.trim()) {
|
||||||
errors.push(`Station ${i + 1} is missing callsign`);
|
errors.push(`Station ${i + 1} is missing callsign`);
|
||||||
}
|
}
|
||||||
@@ -361,6 +371,10 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function handleSave() {
|
async function handleSave() {
|
||||||
|
// Sync stations from store to formData before saving
|
||||||
|
const stations = $stationsStore;
|
||||||
|
formData = { ...formData, rules: { ...formData.rules, stations } };
|
||||||
|
|
||||||
if (!performSafetyValidation()) {
|
if (!performSafetyValidation()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -709,23 +723,43 @@
|
|||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>Stations *</label>
|
<label>Stations *</label>
|
||||||
<div class="stations-editor">
|
<div class="stations-editor">
|
||||||
{#if formData.rules.stations && formData.rules.stations.length > 0}
|
{#if $stationsStore && $stationsStore.length > 0}
|
||||||
{#each formData.rules.stations as station, i}
|
{#each $stationsStore as station, i (i)}
|
||||||
<div class="station-row">
|
<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
|
<input
|
||||||
type="text"
|
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"
|
placeholder="Callsign"
|
||||||
/>
|
/>
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
min="1"
|
value={station.points}
|
||||||
bind: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"
|
placeholder="Points"
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
class="btn-remove"
|
class="btn-remove"
|
||||||
on:click={() => formData.rules.stations.splice(i, 1)}
|
on:click={() => {
|
||||||
|
stationsStore.update(s => s.filter((_, idx) => idx !== i));
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
Remove
|
Remove
|
||||||
</button>
|
</button>
|
||||||
@@ -737,8 +771,7 @@
|
|||||||
<button
|
<button
|
||||||
class="btn btn-secondary"
|
class="btn btn-secondary"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
if (!formData.rules.stations) formData.rules.stations = [];
|
stationsStore.update(s => [...s, { callsign: '', points: 1 }]);
|
||||||
formData.rules.stations.push({ callsign: '', points: 1 });
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Add Station
|
Add Station
|
||||||
|
|||||||
@@ -1,10 +1,14 @@
|
|||||||
<script>
|
<script>
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
import { browser } from '$app/environment';
|
import { browser } from '$app/environment';
|
||||||
|
import { writable } from 'svelte/store';
|
||||||
import { awardsAdminAPI } from '$lib/api.js';
|
import { awardsAdminAPI } from '$lib/api.js';
|
||||||
import FilterBuilder from '../components/FilterBuilder.svelte';
|
import FilterBuilder from '../components/FilterBuilder.svelte';
|
||||||
import TestAwardModal from '../components/TestAwardModal.svelte';
|
import TestAwardModal from '../components/TestAwardModal.svelte';
|
||||||
|
|
||||||
|
// Create a store for stations to ensure reactivity
|
||||||
|
const stationsStore = writable([]);
|
||||||
|
|
||||||
let loading = false;
|
let loading = false;
|
||||||
let saving = false;
|
let saving = false;
|
||||||
let error = null;
|
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
|
// 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_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'];
|
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');
|
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');
|
errors.push('Points rule requires at least one station');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for duplicate callsigns
|
// Check for duplicate callsigns
|
||||||
if (formData.rules.stations) {
|
if (stations) {
|
||||||
const callsigns = formData.rules.stations.map(s => s.callsign?.toUpperCase());
|
const callsigns = stations.map(s => s.callsign?.toUpperCase());
|
||||||
const duplicates = callsigns.filter((c, i) => callsigns.indexOf(c) !== i);
|
const duplicates = callsigns.filter((c, i) => callsigns.indexOf(c) !== i);
|
||||||
if (duplicates.length > 0) {
|
if (duplicates.length > 0) {
|
||||||
errors.push(`Duplicate station callsigns: ${[...new Set(duplicates)].join(', ')}`);
|
errors.push(`Duplicate station callsigns: ${[...new Set(duplicates)].join(', ')}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for zero or negative points
|
// Check for zero or negative points
|
||||||
formData.rules.stations.forEach((station, i) => {
|
stations.forEach((station, i) => {
|
||||||
if (!station.callsign || !station.callsign.trim()) {
|
if (!station.callsign || !station.callsign.trim()) {
|
||||||
errors.push(`Station ${i + 1} is missing callsign`);
|
errors.push(`Station ${i + 1} is missing callsign`);
|
||||||
}
|
}
|
||||||
@@ -368,6 +378,10 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function handleSave() {
|
async function handleSave() {
|
||||||
|
// Sync stations from store to formData before saving
|
||||||
|
const stations = $stationsStore;
|
||||||
|
formData = { ...formData, rules: { ...formData.rules, stations } };
|
||||||
|
|
||||||
if (!performSafetyValidation()) {
|
if (!performSafetyValidation()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -734,23 +748,44 @@
|
|||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>Stations *</label>
|
<label>Stations *</label>
|
||||||
<div class="stations-editor">
|
<div class="stations-editor">
|
||||||
{#if formData.rules.stations && formData.rules.stations.length > 0}
|
{#if $stationsStore && $stationsStore.length > 0}
|
||||||
{#each formData.rules.stations as station, i}
|
{#each $stationsStore as station, i (i)}
|
||||||
<div class="station-row">
|
<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
|
<input
|
||||||
type="text"
|
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"
|
placeholder="Callsign"
|
||||||
/>
|
/>
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
min="1"
|
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"
|
placeholder="Points"
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
class="btn-remove"
|
class="btn-remove"
|
||||||
on:click={() => formData.rules.stations.splice(i, 1)}
|
on:click={() => {
|
||||||
|
stationsStore.update(s => s.filter((_, idx) => idx !== i));
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
Remove
|
Remove
|
||||||
</button>
|
</button>
|
||||||
@@ -762,8 +797,7 @@
|
|||||||
<button
|
<button
|
||||||
class="btn btn-secondary"
|
class="btn btn-secondary"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
if (!formData.rules.stations) formData.rules.stations = [];
|
stationsStore.update(s => [...s, { callsign: '', points: 1 }]);
|
||||||
formData.rules.stations.push({ callsign: '', points: 1 });
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Add Station
|
Add Station
|
||||||
|
|||||||
Reference in New Issue
Block a user