diff --git a/frontend/src/components/AdminConfigValueEditor.vue b/frontend/src/components/AdminConfigValueEditor.vue index 8ba8429..efb4d11 100644 --- a/frontend/src/components/AdminConfigValueEditor.vue +++ b/frontend/src/components/AdminConfigValueEditor.vue @@ -21,6 +21,7 @@ export type AdminConfigEditorLabels = { emptyList: string emptyTable: string entry: string + general: string keyPlaceholder: string list: string remove: string @@ -59,6 +60,7 @@ type SerializedMapEntry = { keyType: MapKeyKind value: unknown } +type RootTableTab = { count: number; id: string; key?: string; label: string } const newArrayKind = ref('string') const newObjectKey = ref('') @@ -67,6 +69,7 @@ const newMapKey = ref('') const newMapKeyKind = ref('string') const newMapValueKind = ref('string') const expandedEntry = ref(null) +const selectedTableTab = ref('') const isList = computed( () => @@ -112,12 +115,19 @@ const mapEntries = computed(() => { const listStructure = computed(() => props.structure?.kind === 'list' ? props.structure : null, ) +const listTemplate = computed( + () => listStructure.value?.template ?? listStructure.value?.items[0], +) const tableStructure = computed(() => props.structure?.kind === 'table' ? props.structure : null, ) const mapStructure = computed(() => props.structure?.kind === 'map' ? props.structure : null, ) +const mapTemplate = computed(() => mapStructure.value?.template) +const newMapEntryKeyKind = computed( + () => mapStructure.value?.keyType ?? newMapKeyKind.value, +) const canAddTableField = computed(() => { const key = newObjectKey.value.trim() if ( @@ -138,11 +148,49 @@ const tableEntries = computed(() => ([key]) => key !== '__skyType' && (!mapType.value || key !== 'entries'), ), ) +const rootTableTabs = computed(() => { + if (props.depth !== 0 || !tableStructure.value || mapType.value) return [] + const structuredEntries = tableEntries.value.filter(([key, value]) => + isStructuredValue(value, tableFieldStructure(key)), + ) + if (!structuredEntries.length) return [] + const scalarCount = tableEntries.value.length - structuredEntries.length + return [ + ...(scalarCount + ? [{ count: scalarCount, id: 'general', label: props.labels.general }] + : []), + ...structuredEntries.map(([key]) => ({ + count: configuratorStructureSize(tableFieldStructure(key)), + id: `field:${key}`, + key, + label: key, + })), + ] +}) +const activeRootTableTab = computed( + () => + rootTableTabs.value.find((tab) => tab.id === selectedTableTab.value) ?? + rootTableTabs.value[0] ?? + null, +) +const activeRootTableField = computed(() => { + const key = activeRootTableTab.value?.key + if (!key) return null + const entry = tableEntries.value.find(([candidate]) => candidate === key) + return entry ? { key: entry[0], value: entry[1] } : null +}) +const visibleTableEntries = computed(() => { + if (!rootTableTabs.value.length) return tableEntries.value + if (activeRootTableField.value) return [] + return tableEntries.value.filter( + ([key, value]) => !isStructuredValue(value, tableFieldStructure(key)), + ) +}) const isMaskedSecret = computed(() => props.modelValue === '***REDACTED***') const parsedNewMapKey = computed(() => { const key = newMapKey.value.trim() if (!key) return null - if (newMapKeyKind.value === 'string') return key + if (newMapEntryKeyKind.value === 'string') return key const numeric = Number(key) return Number.isFinite(numeric) ? numeric : null }) @@ -150,7 +198,7 @@ const canAddMapEntry = computed(() => { const key = parsedNewMapKey.value if (key === null) return false return !mapEntries.value.some( - (entry) => entry.keyType === newMapKeyKind.value && entry.key === key, + (entry) => entry.keyType === newMapEntryKeyKind.value && entry.key === key, ) }) @@ -216,6 +264,56 @@ function structuredValueLabel( return props.labels.table } +function structureTypeLabel(structure: AdminConfiguratorStructure): string { + if (structure.kind === 'optionalString') return props.labels.types.string + if (structure.kind === 'value') return props.labels.types[structure.valueType] + if (structure.kind === 'list') return props.labels.types.list + if (structure.kind === 'vector') { + return `${props.labels.vector} ${structure.vectorType.slice(-1)}` + } + return props.labels.types.table +} + +function configuratorStructureSize( + structure: AdminConfiguratorStructure | undefined, +): number { + if ( + !structure || + structure.kind === 'optionalString' || + structure.kind === 'value' + ) { + return 1 + } + if (structure.kind === 'vector') { + return Number(structure.vectorType.slice(-1)) + } + if (structure.kind === 'list') { + return Math.max( + 1, + structure.items.reduce( + (total, item) => total + configuratorStructureSize(item), + 0, + ), + ) + } + if (structure.kind === 'map') { + return Math.max( + 1, + structure.entries.reduce( + (total, entry) => total + configuratorStructureSize(entry.structure), + 0, + ), + ) + } + return Math.max( + 1, + Object.values(structure.fields).reduce( + (total, field) => total + configuratorStructureSize(field), + 0, + ), + ) +} + function toggleStructuredEntry(entry: string): void { expandedEntry.value = expandedEntry.value === entry ? null : entry } @@ -228,6 +326,12 @@ function listEntryPath(index: number): string { return `${props.path || props.ariaLabel}[${index + 1}]` } +function listItemStructure( + index: number, +): AdminConfiguratorStructure | undefined { + return listStructure.value?.items[index] ?? listTemplate.value +} + function mapValuePath(entry: SerializedMapEntry): string { return props.path ? `${props.path}.${entry.key}` : String(entry.key) } @@ -302,10 +406,14 @@ function addListRow(): void { const index = rows.length const value = rows.length ? blankLike(rows[0]) - : blankValue(newArrayKind.value) + : listTemplate.value + ? blankFromStructure(listTemplate.value) + : blankValue(newArrayKind.value) rows.push(value) emit('update:modelValue', rows) - if (isStructuredValue(value)) expandedEntry.value = `list:${index}` + if (isStructuredValue(value, listTemplate.value)) { + expandedEntry.value = `list:${index}` + } } function updateListRow(index: number, value: unknown): void { @@ -374,6 +482,7 @@ function addTableField(): void { }) if (isStructuredValue(value, template)) { expandedEntry.value = `table:${key}` + if (props.depth === 0) selectedTableTab.value = `field:${key}` } newObjectKey.value = '' } @@ -395,7 +504,7 @@ function updateMapKey(index: number, event: Event): void { if (!(target instanceof HTMLInputElement)) return const current = mapEntries.value[index] if (!current) return - if (mapEntryStructure(current)) { + if (fixedMapEntryStructure(current)) { target.value = String(current.key) return } @@ -430,7 +539,7 @@ function updateMapValue(index: number, value: unknown): void { function removeMapEntry(index: number): void { const entry = mapEntries.value[index] - if (!entry || mapEntryStructure(entry)) return + if (!entry || fixedMapEntryStructure(entry)) return emitMap( mapEntries.value.filter((_, candidateIndex) => candidateIndex !== index), ) @@ -440,25 +549,27 @@ function removeMapEntry(index: number): void { function addMapEntry(): void { if (!canAddMapEntry.value || parsedNewMapKey.value === null) return const key = parsedNewMapKey.value - const value = blankCollectionValue( - newMapValueKind.value, - mapEntries.value.slice(0, 50).map((entry) => entry.value), - ) + const value = mapTemplate.value + ? blankFromStructure(mapTemplate.value) + : blankCollectionValue( + newMapValueKind.value, + mapEntries.value.slice(0, 50).map((entry) => entry.value), + ) emitMap([ ...mapEntries.value, { key, - keyType: newMapKeyKind.value, + keyType: newMapEntryKeyKind.value, value, }, ]) - if (isStructuredValue(value)) { - expandedEntry.value = `map:${newMapKeyKind.value}:${key}` + if (isStructuredValue(value, mapTemplate.value)) { + expandedEntry.value = `map:${newMapEntryKeyKind.value}:${key}` } newMapKey.value = '' } -function mapEntryStructure( +function fixedMapEntryStructure( entry: SerializedMapEntry, ): AdminConfiguratorStructure | undefined { return mapStructure.value?.entries.find( @@ -466,6 +577,12 @@ function mapEntryStructure( candidate.keyType === entry.keyType && candidate.key === entry.key, )?.structure } + +function mapEntryStructure( + entry: SerializedMapEntry, +): AdminConfiguratorStructure | undefined { + return fixedMapEntryStructure(entry) ?? mapTemplate.value +}