Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,12 @@ struct TransitionDetailView: View {
@State private var selectedContractId: String = ""
@State private var selectedDocumentType: String = ""
@State private var documentFieldValues: [String: Any] = [:]
/// Guards the one-time form setup in `.onAppear`. Contract / document-type
/// selection now pushes a child list onto the navigation stack; popping it
/// re-fires this view's `.onAppear`, and re-running `clearForm()` there
/// would wipe the in-progress form (including the just-picked contract).
/// Run setup only on the first appearance of each builder instance.
@State private var didInitializeForm = false

// Query for data contracts
@Query private var dataContracts: [PersistentDataContract]
Expand Down Expand Up @@ -119,12 +125,18 @@ struct TransitionDetailView: View {
.navigationTitle(transitionLabel)
.navigationBarTitleDisplayMode(.inline)
.onAppear {
// Initialize once per builder instance. Popping a pushed picker (the
// contract / document-type selection lists) re-fires `.onAppear`, and
// re-running clearForm() would wipe the in-progress form — including
// the contract the user just picked. A fresh builder (navigated to
// anew) is a new view instance with didInitializeForm == false, so it
// still resets correctly.
guard !didInitializeForm else { return }
didInitializeForm = true

clearForm()
// Re-apply caller-supplied pre-fills after every reset.
// `clearForm` runs on each appear and wipes back to schema
// defaults; merging the pre-fills on top keeps Pasteboard /
// Quick-Token entries visible when the user navigates back to
// the form mid-flow.
// Merge any caller-supplied pre-fills (Pasteboard / Quick-Token flows)
// on top of the schema defaults clearForm() just set.
for (k, v) in initialInputs {
formInputs[k] = v
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -333,26 +333,47 @@ struct TransitionInputView: View {
.background(Color.orange.opacity(0.1))
.cornerRadius(8)
} else {
Picker("Select Contract", selection: $value) {
Text("Select a contract...")
.tag("")
.accessibilityIdentifier("transition.\(input.name).contract.none")
ForEach(availableContracts, id: \.idBase58) { contract in
Text(getContractDisplayName(contract))
.tag(contract.idBase58)
.accessibilityIdentifier("transition.\(input.name).contract.\(contract.idBase58)")
// Push a real selectable list (rather than a menu / inline
// Picker) so UI automation (idb) can tap a specific contract by a
// stable per-row `…contractPicker.row.<idBase58>` identifier.
//
// We deliberately do NOT use `.pickerStyle(.navigationLink)` /
// `.accessibleFormPicker` here: that style only commits its
// selection binding when it has a `List`/`Form` ancestor, and
// TransitionDetailView hosts these inputs in a `ScrollView`/
// `VStack`. In that host the navigationLink picker renders and
// navigates but silently drops the selection. An explicit
// `NavigationLink` + selection list writes the binding directly,
// so it commits in any host. Shared by every document-transition
// builder (Purchase / Transfer / Update Price / Create / Replace
// / Delete), so all of them become idb-drivable through this.
NavigationLink {
ContractSelectionList(
inputName: input.name,
contracts: availableContracts,
displayName: getContractDisplayName,
selection: $value,
onSelect: { newValue in
selectedContractId = newValue
// Notify parent to update related fields
onSpecialAction("contractSelected:\(newValue)")
}
)
} label: {
HStack {
Text("Select Contract")
.foregroundColor(.primary)
Spacer()
Text(availableContracts.first(where: { $0.idBase58 == value })
.map(getContractDisplayName) ?? "Select a contract...")
.foregroundColor(.secondary)
}
}
.accessibleInlinePicker("transition.\(input.name).contractPicker")
.accessibilityIdentifier("transition.\(input.name).contractPicker")
.padding()
.frame(maxWidth: .infinity, alignment: .leading)
.background(Color.gray.opacity(0.1))
.cornerRadius(8)
.onChange(of: value) { _, newValue in
selectedContractId = newValue
// Notify parent to update related fields
onSpecialAction("contractSelected:\(newValue)")
}
}
}

Expand Down Expand Up @@ -426,26 +447,37 @@ struct TransitionInputView: View {
.background(Color.orange.opacity(0.1))
.cornerRadius(8)
} else {
Picker("Select Document Type", selection: $value) {
Text("Select a type...")
.tag("")
.accessibilityIdentifier("transition.\(input.name).documentType.none")
ForEach(availableDocTypes, id: \.name) { docType in
Text(docType.name)
.tag(docType.name)
.accessibilityIdentifier("transition.\(input.name).documentType.\(docType.name)")
// Same rationale as contractPicker(): push a real
// selectable list with stable per-row identifiers so idb
// can tap a document type. The inline Picker renders as an
// opaque control here (idb sees only a "Slider"), and a
// navigationLink Picker wouldn't commit its selection
// outside a List/Form ancestor.
NavigationLink {
DocumentTypeSelectionList(
inputName: input.name,
docTypeNames: availableDocTypes.map { $0.name },
selection: $value,
onSelect: { newValue in
selectedDocumentType = newValue
// Notify parent to update schema
onSpecialAction("documentTypeSelected:\(newValue)")
}
)
} label: {
HStack {
Text("Select Document Type")
.foregroundColor(.primary)
Spacer()
Text(value.isEmpty ? "Select a type..." : value)
.foregroundColor(.secondary)
}
}
.accessibleInlinePicker("transition.\(input.name).documentTypePicker")
.accessibilityIdentifier("transition.\(input.name).documentTypePicker")
.padding()
.frame(maxWidth: .infinity, alignment: .leading)
.background(Color.gray.opacity(0.1))
.cornerRadius(8)
.onChange(of: value) { _, newValue in
selectedDocumentType = newValue
// Notify parent to update schema
onSpecialAction("documentTypeSelected:\(newValue)")
}

// Show warning if document type has owner-only creation restriction
if isCreateOperation && !value.isEmpty,
Expand Down Expand Up @@ -626,3 +658,101 @@ struct TransitionInputView: View {
.environmentObject(appState)
}
}

/// Pushed contract-selection list backing `contractPicker()`.
///
/// Each contract is a `Button` row carrying a stable
/// `transition.<inputName>.contractPicker.row.<idBase58>` accessibility
/// identifier, so UI automation (idb) can tap a specific contract reliably.
/// The row's action writes the selection binding directly and dismisses —
/// unlike `.pickerStyle(.navigationLink)`, which only commits inside a
/// `List`/`Form` ancestor (TransitionDetailView hosts inputs in a
/// `ScrollView`/`VStack`, where that style drops the selection).
private struct ContractSelectionList: View {
let inputName: String
let contracts: [PersistentDataContract]
let displayName: (PersistentDataContract) -> String
@Binding var selection: String
let onSelect: (String) -> Void
@Environment(\.dismiss) private var dismiss

var body: some View {
List {
selectionRow(id: "", label: "Select a contract...")
ForEach(contracts, id: \.idBase58) { contract in
selectionRow(id: contract.idBase58, label: displayName(contract))
}
}
.navigationTitle("Select Contract")
.navigationBarTitleDisplayMode(.inline)
}

@ViewBuilder
private func selectionRow(id: String, label: String) -> some View {
Button {
selection = id
onSelect(id)
dismiss()
} label: {
HStack {
Text(label)
.foregroundColor(.primary)
Spacer()
if selection == id {
Image(systemName: "checkmark")
.foregroundColor(.accentColor)
}
}
}
.accessibilityIdentifier(
"transition.\(inputName).contractPicker.row.\(id.isEmpty ? "none" : id)"
)
}
}

/// Pushed document-type-selection list backing `documentTypePicker()`.
///
/// Mirrors `ContractSelectionList`: each type is a `Button` row carrying a
/// stable `transition.<inputName>.documentTypePicker.row.<name>`
/// accessibility identifier so UI automation can tap it, and the row writes
/// the selection binding directly + dismisses.
private struct DocumentTypeSelectionList: View {
let inputName: String
let docTypeNames: [String]
@Binding var selection: String
let onSelect: (String) -> Void
@Environment(\.dismiss) private var dismiss

var body: some View {
List {
selectionRow(id: "", label: "Select a type...")
ForEach(docTypeNames, id: \.self) { name in
selectionRow(id: name, label: name)
}
}
.navigationTitle("Select Document Type")
.navigationBarTitleDisplayMode(.inline)
}

@ViewBuilder
private func selectionRow(id: String, label: String) -> some View {
Button {
selection = id
onSelect(id)
dismiss()
} label: {
HStack {
Text(label)
.foregroundColor(.primary)
Spacer()
if selection == id {
Image(systemName: "checkmark")
.foregroundColor(.accentColor)
}
}
}
.accessibilityIdentifier(
"transition.\(inputName).documentTypePicker.row.\(id.isEmpty ? "none" : id)"
)
}
}
Loading