macOS: fix config form rendering
This commit is contained in:
committed by
Peter Steinberger
parent
cc2d617ea6
commit
3ec221c70e
@@ -9,58 +9,63 @@ struct ConfigSchemaForm: View {
|
|||||||
self.renderNode(schema, path: path)
|
self.renderNode(schema, path: path)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder
|
private func renderNode(_ schema: ConfigSchemaNode, path: ConfigPath) -> AnyView {
|
||||||
private func renderNode(_ schema: ConfigSchemaNode, path: ConfigPath) -> some View {
|
|
||||||
let value = store.configValue(at: path)
|
let value = store.configValue(at: path)
|
||||||
let label = hintForPath(path, hints: store.configUiHints)?.label ?? schema.title
|
let label = hintForPath(path, hints: store.configUiHints)?.label ?? schema.title
|
||||||
let help = hintForPath(path, hints: store.configUiHints)?.help ?? schema.description
|
let help = hintForPath(path, hints: store.configUiHints)?.help ?? schema.description
|
||||||
|
|
||||||
switch schema.schemaType {
|
switch schema.schemaType {
|
||||||
case "object":
|
case "object":
|
||||||
VStack(alignment: .leading, spacing: 12) {
|
return AnyView(
|
||||||
if let label {
|
VStack(alignment: .leading, spacing: 12) {
|
||||||
Text(label)
|
if let label {
|
||||||
.font(.callout.weight(.semibold))
|
Text(label)
|
||||||
|
.font(.callout.weight(.semibold))
|
||||||
|
}
|
||||||
|
if let help {
|
||||||
|
Text(help)
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
}
|
||||||
|
let properties = schema.properties
|
||||||
|
let sortedKeys = properties.keys.sorted { lhs, rhs in
|
||||||
|
let orderA = hintForPath(path + [.key(lhs)], hints: store.configUiHints)?.order ?? 0
|
||||||
|
let orderB = hintForPath(path + [.key(rhs)], hints: store.configUiHints)?.order ?? 0
|
||||||
|
if orderA != orderB { return orderA < orderB }
|
||||||
|
return lhs < rhs
|
||||||
|
}
|
||||||
|
ForEach(sortedKeys, id: \ .self) { key in
|
||||||
|
if let child = properties[key] {
|
||||||
|
self.renderNode(child, path: path + [.key(key)])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if schema.allowsAdditionalProperties {
|
||||||
|
self.renderAdditionalProperties(schema, path: path, value: value)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if let help {
|
)
|
||||||
Text(help)
|
case "array":
|
||||||
|
return AnyView(self.renderArray(schema, path: path, value: value, label: label, help: help))
|
||||||
|
case "boolean":
|
||||||
|
return AnyView(
|
||||||
|
Toggle(isOn: self.boolBinding(path)) {
|
||||||
|
if let label { Text(label) } else { Text("Enabled") }
|
||||||
|
}
|
||||||
|
.help(help ?? "")
|
||||||
|
)
|
||||||
|
case "number", "integer":
|
||||||
|
return AnyView(self.renderNumberField(schema, path: path, label: label, help: help))
|
||||||
|
case "string":
|
||||||
|
return AnyView(self.renderStringField(schema, path: path, label: label, help: help))
|
||||||
|
default:
|
||||||
|
return AnyView(
|
||||||
|
VStack(alignment: .leading, spacing: 6) {
|
||||||
|
if let label { Text(label).font(.callout.weight(.semibold)) }
|
||||||
|
Text("Unsupported field type.")
|
||||||
.font(.caption)
|
.font(.caption)
|
||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.secondary)
|
||||||
}
|
}
|
||||||
let properties = schema.properties
|
)
|
||||||
let sortedKeys = properties.keys.sorted { lhs, rhs in
|
|
||||||
let orderA = hintForPath(path + [.key(lhs)], hints: store.configUiHints)?.order ?? 0
|
|
||||||
let orderB = hintForPath(path + [.key(rhs)], hints: store.configUiHints)?.order ?? 0
|
|
||||||
if orderA != orderB { return orderA < orderB }
|
|
||||||
return lhs < rhs
|
|
||||||
}
|
|
||||||
ForEach(sortedKeys, id: \ .self) { key in
|
|
||||||
if let child = properties[key] {
|
|
||||||
self.renderNode(child, path: path + [.key(key)])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if schema.allowsAdditionalProperties {
|
|
||||||
self.renderAdditionalProperties(schema, path: path, value: value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
case "array":
|
|
||||||
self.renderArray(schema, path: path, value: value, label: label, help: help)
|
|
||||||
case "boolean":
|
|
||||||
Toggle(isOn: self.boolBinding(path)) {
|
|
||||||
if let label { Text(label) } else { Text("Enabled") }
|
|
||||||
}
|
|
||||||
.help(help ?? "")
|
|
||||||
case "number", "integer":
|
|
||||||
self.renderNumberField(schema, path: path, label: label, help: help)
|
|
||||||
case "string":
|
|
||||||
self.renderStringField(schema, path: path, label: label, help: help)
|
|
||||||
default:
|
|
||||||
VStack(alignment: .leading, spacing: 6) {
|
|
||||||
if let label { Text(label).font(.callout.weight(.semibold)) }
|
|
||||||
Text("Unsupported field type.")
|
|
||||||
.font(.caption)
|
|
||||||
.foregroundStyle(.secondary)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -171,49 +176,50 @@ struct ConfigSchemaForm: View {
|
|||||||
path: ConfigPath,
|
path: ConfigPath,
|
||||||
value: Any?) -> some View
|
value: Any?) -> some View
|
||||||
{
|
{
|
||||||
guard let additionalSchema = schema.additionalProperties else { return }
|
if let additionalSchema = schema.additionalProperties {
|
||||||
let dict = value as? [String: Any] ?? [:]
|
let dict = value as? [String: Any] ?? [:]
|
||||||
let reserved = Set(schema.properties.keys)
|
let reserved = Set(schema.properties.keys)
|
||||||
let extras = dict.keys.filter { !reserved.contains($0) }.sorted()
|
let extras = dict.keys.filter { !reserved.contains($0) }.sorted()
|
||||||
|
|
||||||
VStack(alignment: .leading, spacing: 8) {
|
VStack(alignment: .leading, spacing: 8) {
|
||||||
Text("Extra entries")
|
Text("Extra entries")
|
||||||
.font(.callout.weight(.semibold))
|
.font(.callout.weight(.semibold))
|
||||||
if extras.isEmpty {
|
if extras.isEmpty {
|
||||||
Text("No extra entries yet.")
|
Text("No extra entries yet.")
|
||||||
.font(.caption)
|
.font(.caption)
|
||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.secondary)
|
||||||
} else {
|
} else {
|
||||||
ForEach(extras, id: \ .self) { key in
|
ForEach(extras, id: \ .self) { key in
|
||||||
let itemPath: ConfigPath = path + [.key(key)]
|
let itemPath: ConfigPath = path + [.key(key)]
|
||||||
HStack(alignment: .top, spacing: 8) {
|
HStack(alignment: .top, spacing: 8) {
|
||||||
TextField("Key", text: self.mapKeyBinding(path: path, key: key))
|
TextField("Key", text: self.mapKeyBinding(path: path, key: key))
|
||||||
.textFieldStyle(.roundedBorder)
|
.textFieldStyle(.roundedBorder)
|
||||||
.frame(width: 160)
|
.frame(width: 160)
|
||||||
self.renderNode(additionalSchema, path: itemPath)
|
self.renderNode(additionalSchema, path: itemPath)
|
||||||
Button("Remove") {
|
Button("Remove") {
|
||||||
var next = dict
|
var next = dict
|
||||||
next.removeValue(forKey: key)
|
next.removeValue(forKey: key)
|
||||||
store.updateConfigValue(path: path, value: next)
|
store.updateConfigValue(path: path, value: next)
|
||||||
|
}
|
||||||
|
.buttonStyle(.bordered)
|
||||||
|
.controlSize(.small)
|
||||||
}
|
}
|
||||||
.buttonStyle(.bordered)
|
|
||||||
.controlSize(.small)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
Button("Add") {
|
||||||
Button("Add") {
|
var next = dict
|
||||||
var next = dict
|
var index = 1
|
||||||
var index = 1
|
var key = "new-\(index)"
|
||||||
var key = "new-\(index)"
|
while next[key] != nil {
|
||||||
while next[key] != nil {
|
index += 1
|
||||||
index += 1
|
key = "new-\(index)"
|
||||||
key = "new-\(index)"
|
}
|
||||||
|
next[key] = additionalSchema.defaultValue
|
||||||
|
store.updateConfigValue(path: path, value: next)
|
||||||
}
|
}
|
||||||
next[key] = additionalSchema.defaultValue
|
.buttonStyle(.bordered)
|
||||||
store.updateConfigValue(path: path, value: next)
|
.controlSize(.small)
|
||||||
}
|
}
|
||||||
.buttonStyle(.bordered)
|
|
||||||
.controlSize(.small)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -65,9 +65,9 @@ extension ChannelsStore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func updateConfigValue(path: ConfigPath, value: Any?) {
|
func updateConfigValue(path: ConfigPath, value: Any?) {
|
||||||
var root = self.configDraft
|
var root: Any = self.configDraft
|
||||||
setValue(&root, path: path, value: value)
|
setValue(&root, path: path, value: value)
|
||||||
self.configDraft = root
|
self.configDraft = root as? [String: Any] ?? self.configDraft
|
||||||
self.configDirty = true
|
self.configDirty = true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -125,7 +125,7 @@ private func setValue(_ root: inout Any, path: ConfigPath, value: Any?) {
|
|||||||
case .index(let index):
|
case .index(let index):
|
||||||
var array = root as? [Any] ?? []
|
var array = root as? [Any] ?? []
|
||||||
if index >= array.count {
|
if index >= array.count {
|
||||||
array.append(contentsOf: repeatElement(NSNull(), count: index - array.count + 1))
|
array.append(contentsOf: repeatElement(NSNull() as Any, count: index - array.count + 1))
|
||||||
}
|
}
|
||||||
if path.count == 1 {
|
if path.count == 1 {
|
||||||
if let value {
|
if let value {
|
||||||
|
|||||||
@@ -900,7 +900,7 @@ extension DebugSettings {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private struct PlainSettingsGroupBoxStyle: GroupBoxStyle {
|
struct PlainSettingsGroupBoxStyle: GroupBoxStyle {
|
||||||
func makeBody(configuration: Configuration) -> some View {
|
func makeBody(configuration: Configuration) -> some View {
|
||||||
VStack(alignment: .leading, spacing: 10) {
|
VStack(alignment: .leading, spacing: 10) {
|
||||||
configuration.label
|
configuration.label
|
||||||
|
|||||||
Reference in New Issue
Block a user