From 693c9bd6f9fb8e8d911c9beaa1985859755591b4 Mon Sep 17 00:00:00 2001 From: "Leon.Schmidt" <159480018+leonw21342315@users.noreply.github.com> Date: Wed, 26 Aug 2026 18:46:51 +0200 Subject: [PATCH] FIX - allow configurable radio channel jobs (#46) Mark locked-channel job maps mutable in the configurator schema and initialize new structured list rows from their schema template so required frequency ranges remain valid. --- .../src/components/AdminConfigValueEditor.vue | 8 ++--- .../components/AdminPanel.contract.test.ts | 3 ++ .../utils/adminConfiguratorDefaults.test.ts | 34 ++++++++++++++++++- frontend/testserver/configurator-fixture.cjs | 19 +++++++++++ .../testserver/configurator-fixture.test.ts | 23 +++++++++++++ .../source/server/phone_configurator.lua | 15 ++++++++ 6 files changed, 97 insertions(+), 5 deletions(-) diff --git a/frontend/src/components/AdminConfigValueEditor.vue b/frontend/src/components/AdminConfigValueEditor.vue index 5af001a..4c5e2a0 100644 --- a/frontend/src/components/AdminConfigValueEditor.vue +++ b/frontend/src/components/AdminConfigValueEditor.vue @@ -380,10 +380,10 @@ function toggleOptionalString(event: Event): void { function addListRow(): void { const rows = Array.isArray(props.modelValue) ? [...props.modelValue] : [] const index = rows.length - const value = rows.length - ? blankLike(rows[0]) - : listTemplate.value - ? blankFromConfiguratorStructure(listTemplate.value) + const value = listTemplate.value + ? blankFromConfiguratorStructure(listTemplate.value) + : rows.length + ? blankLike(rows[0]) : blankValue(newArrayKind.value) rows.push(value) emit('update:modelValue', rows) diff --git a/frontend/src/components/AdminPanel.contract.test.ts b/frontend/src/components/AdminPanel.contract.test.ts index 4d3bbcf..07f4463 100644 --- a/frontend/src/components/AdminPanel.contract.test.ts +++ b/frontend/src/components/AdminPanel.contract.test.ts @@ -440,6 +440,9 @@ describe('standalone admin panel contracts', () => { expect(source).toContain("selectConfiguratorScope('media')") expect(source).not.toContain('class="admin-panel-config-meta"') expect(configuratorValueEditor).toContain('function addListRow()') + expect(configuratorValueEditor).toMatch( + /const value = listTemplate\.value\s+\? blankFromConfiguratorStructure\(listTemplate\.value\)\s+: rows\.length\s+\? blankLike\(rows\[0\]\)/, + ) expect(configuratorValueEditor).toContain('function addTableField()') expect(configuratorValueEditor).toContain( 'const canExtendTable = computed(', diff --git a/frontend/src/utils/adminConfiguratorDefaults.test.ts b/frontend/src/utils/adminConfiguratorDefaults.test.ts index fc3e2c9..c2824be 100644 --- a/frontend/src/utils/adminConfiguratorDefaults.test.ts +++ b/frontend/src/utils/adminConfiguratorDefaults.test.ts @@ -1,7 +1,10 @@ import { describe, expect, it } from 'vitest' import type { AdminConfiguratorStructure } from '@/types/admin' -import { createMutableTableEntry } from '@/utils/adminConfiguratorDefaults' +import { + blankFromConfiguratorStructure, + createMutableTableEntry, +} from '@/utils/adminConfiguratorDefaults' describe('admin configurator defaults', () => { it('creates a usable company draft from its key and the server defaults', () => { @@ -111,4 +114,33 @@ describe('admin configurator defaults', () => { false, ) }) + + it('preserves required nested list fields in schema-derived rows', () => { + const structure: AdminConfiguratorStructure = { + fields: { + jobs: { + fields: { + police: { kind: 'value', valueType: 'boolean' }, + }, + kind: 'table', + mutableKeys: true, + template: { kind: 'value', valueType: 'boolean' }, + }, + range: { + items: [ + { kind: 'value', valueType: 'number' }, + { kind: 'value', valueType: 'number' }, + ], + kind: 'list', + template: { kind: 'value', valueType: 'number' }, + }, + }, + kind: 'table', + } + + expect(blankFromConfiguratorStructure(structure)).toEqual({ + jobs: { police: false }, + range: [0, 0], + }) + }) }) diff --git a/frontend/testserver/configurator-fixture.cjs b/frontend/testserver/configurator-fixture.cjs index 6d02ee5..3971201 100644 --- a/frontend/testserver/configurator-fixture.cjs +++ b/frontend/testserver/configurator-fixture.cjs @@ -540,6 +540,25 @@ function buildStructure(value, scope, path) { if (scope === 'config' && path === 'Phone.Keybind') { return { kind: 'optionalString' } } + if ( + scope === 'config' && + /^Radio\.LockedChannels\.\d+\.jobs$/.test(path) && + value !== null && + typeof value === 'object' && + !Array.isArray(value) + ) { + return { + fields: Object.fromEntries( + Object.entries(value).map(([key, child]) => [ + key, + buildStructure(child, scope, `${path}.${key}`), + ]), + ), + kind: 'table', + mutableKeys: true, + template: { kind: 'value', valueType: 'boolean' }, + } + } if ( scope === 'config' && path === 'Companies.Definitions' && diff --git a/frontend/testserver/configurator-fixture.test.ts b/frontend/testserver/configurator-fixture.test.ts index e5deea8..142e228 100644 --- a/frontend/testserver/configurator-fixture.test.ts +++ b/frontend/testserver/configurator-fixture.test.ts @@ -193,6 +193,29 @@ describe('admin configurator fixture', () => { }) }) + it('allows custom jobs in locked radio channel entries', () => { + const radio = loadConfiguratorSections() + .flatMap((section) => section.fields) + .find((field) => field.path === 'Radio') + const lockedChannels = radio?.structure?.fields?.LockedChannels + const jobs = lockedChannels?.items?.[0]?.fields?.jobs + + expect(jobs).toMatchObject({ + fields: { + ambulance: { kind: 'value', valueType: 'boolean' }, + police: { kind: 'value', valueType: 'boolean' }, + }, + kind: 'table', + mutableKeys: true, + template: { kind: 'value', valueType: 'boolean' }, + }) + expect(lockedChannels?.template?.fields?.jobs).toMatchObject({ + kind: 'table', + mutableKeys: true, + template: { kind: 'value', valueType: 'boolean' }, + }) + }) + it('publishes fixed schemas for every empty configurable collection', () => { const fields = loadConfiguratorSections().flatMap( (section) => section.fields, diff --git a/sky_phone/source/server/phone_configurator.lua b/sky_phone/source/server/phone_configurator.lua index 8b009d9..e6f86d7 100644 --- a/sky_phone/source/server/phone_configurator.lua +++ b/sky_phone/source/server/phone_configurator.lua @@ -637,6 +637,21 @@ local function build_structure(value, scope, path) if scope == "config" and path == "Phone.Keybind" then return { kind = "optionalString" } end + if scope == "config" + and path:match("^Radio%.LockedChannels%.%d+%.jobs$") + and value_type == "table" + then + local fields = {} + for key, child in pairs(value) do + fields[key] = build_structure(child, scope, path .. "." .. tostring(key)) + end + return { + fields = fields, + kind = "table", + mutableKeys = true, + template = { kind = "value", valueType = "boolean" }, + } + end if scope == "config" and path == "Companies.Definitions" and value_type == "table" then local keys = {} for key in pairs(value) do