diff --git a/README.md b/README.md
index 8e14cfd..3b861fe 100644
--- a/README.md
+++ b/README.md
@@ -232,7 +232,8 @@ Every `Config.*` value from `config.lua`, including server-only sections, and ev
file-owned because it decides whether SQL configuration is loaded. Lists, nested objects, vectors,
and numeric-keyed Lua tables use structured editors instead of raw JSON. Shipped schema rows stay
editable but cannot be renamed, converted, or removed. Every list and table still accepts any number
-of additional rows; administrator-added rows remain removable.
+of additional rows; administrator-added rows remain removable. Company job keys are intentionally
+fully removable because `Config.Companies.Definitions` is a freely managed job collection.
Media API keys and server peppers are never returned in plaintext to the NUI. Existing secrets are
shown only as configured and are replaced only when an administrator enters a new value.
@@ -562,8 +563,9 @@ Select `rtx`, `quasar`, `vms`, `rx`, `nolag`, `sn`, `esx_property`, or `qbx_prop
Company jobs, public profiles, service numbers, services, permissions, locations, and default
availability are configured under `Config.Companies.Definitions`. Definitions are not limited to the
-shipped jobs: add any number of company IDs in the in-game configurator, choose `Table`, and fill the
-freely configurable `Job` value in the generated full company template.
+shipped jobs: add any number of company IDs in the in-game configurator and fill the freely
+configurable `Job` value in the automatically generated full company template. Existing job keys can
+also be removed; the remaining Companies settings stay available as normal individual fields.
### Weazel News
diff --git a/frontend/src/components/AdminConfigValueEditor.vue b/frontend/src/components/AdminConfigValueEditor.vue
index 590583e..9184296 100644
--- a/frontend/src/components/AdminConfigValueEditor.vue
+++ b/frontend/src/components/AdminConfigValueEditor.vue
@@ -107,6 +107,16 @@ const tableStructure = computed(() =>
const mapStructure = computed(() =>
props.structure?.kind === 'map' ? props.structure : null,
)
+const canAddTableField = computed(() => {
+ const key = newObjectKey.value.trim()
+ if (
+ !key ||
+ key === '__skyType' ||
+ Object.prototype.hasOwnProperty.call(tableValue.value, key)
+ )
+ return false
+ return !tableStructure.value?.mutableKeys || /^[a-z0-9_-]+$/.test(key)
+})
const tableEntries = computed(() =>
Object.entries(tableValue.value).filter(
([key]) => key !== '__skyType' && (!mapType.value || key !== 'entries'),
@@ -163,6 +173,40 @@ function blankCollectionValue(kind: ValueKind, siblings: unknown[]): unknown {
return template === undefined ? blankValue(kind) : blankLike(template)
}
+function blankFromStructure(structure: AdminConfiguratorStructure): unknown {
+ if (structure.kind === 'optionalString') return ''
+ if (structure.kind === 'value') return blankValue(structure.valueType)
+ if (structure.kind === 'vector') {
+ const axes = ['x', 'y', 'z', 'w'].slice(
+ 0,
+ Number(structure.vectorType.slice(-1)),
+ )
+ return Object.fromEntries([
+ ['__skyType', structure.vectorType],
+ ...axes.map((axis) => [axis, 0]),
+ ])
+ }
+ if (structure.kind === 'list') {
+ return structure.items.map(blankFromStructure)
+ }
+ if (structure.kind === 'map') {
+ return {
+ __skyType: 'map',
+ entries: structure.entries.map((entry) => ({
+ key: entry.key,
+ keyType: entry.keyType,
+ value: blankFromStructure(entry.structure),
+ })),
+ }
+ }
+ return Object.fromEntries(
+ Object.entries(structure.fields).map(([key, field]) => [
+ key,
+ blankFromStructure(field),
+ ]),
+ )
+}
+
function updateScalar(event: Event): void {
const target = event.target
if (!(target instanceof HTMLInputElement)) return
@@ -217,12 +261,30 @@ function updateTableField(key: string, value: unknown): void {
emit('update:modelValue', { ...tableValue.value, [key]: value })
}
-function removeTableField(key: string): void {
- if (
- tableStructure.value &&
- Object.prototype.hasOwnProperty.call(tableStructure.value.fields, key)
+function tableFieldStructure(
+ key: string,
+): AdminConfiguratorStructure | undefined {
+ if (!tableStructure.value) return undefined
+ return (
+ tableStructure.value.fields[key] ??
+ (tableStructure.value.mutableKeys
+ ? tableStructure.value.template
+ : undefined)
)
- return
+}
+
+function isFixedTableField(key: string): boolean {
+ return (
+ tableStructure.value?.mutableKeys !== true &&
+ Object.prototype.hasOwnProperty.call(
+ tableStructure.value?.fields ?? {},
+ key,
+ )
+ )
+}
+
+function removeTableField(key: string): void {
+ if (isFixedTableField(key)) return
const next = { ...tableValue.value }
delete next[key]
emit('update:modelValue', next)
@@ -230,18 +292,18 @@ function removeTableField(key: string): void {
function addTableField(): void {
const key = newObjectKey.value.trim()
- if (
- !key ||
- key === '__skyType' ||
- Object.prototype.hasOwnProperty.call(tableValue.value, key)
- )
- return
+ if (!canAddTableField.value) return
+ const template = tableStructure.value?.mutableKeys
+ ? tableStructure.value.template
+ : undefined
emit('update:modelValue', {
...tableValue.value,
- [key]: blankCollectionValue(
- newObjectKind.value,
- Object.values(tableValue.value).filter((_, index) => index < 50),
- ),
+ [key]: template
+ ? blankFromStructure(template)
+ : blankCollectionValue(
+ newObjectKind.value,
+ Object.values(tableValue.value).filter((_, index) => index < 50),
+ ),
})
newObjectKey.value = ''
}
@@ -484,7 +546,7 @@ function mapEntryStructure(
{{ key }}