Let an untouched Save keep the name it was already showing

This commit is contained in:
Arkadiusz Fal
2026-09-09 23:59:57 +02:00
parent 7417866537
commit 0c6c7763db
6 changed files with 249 additions and 36 deletions

View File

@@ -22,6 +22,51 @@ struct AccountRenameRequest: Identifiable {
/// What the field is prefilled with: the label the row is showing now.
let initialText: String
/// Whether that prefill is a name the user chose, as opposed to the label
/// the account falls back to on this device. It is what tells an untouched
/// save to keep the name from one that must not publish a device-local
/// fallback — see `DataContextValidator.renamedDisplayName`.
let hasUserSetLabel: Bool
}
extension AccountRenameRequest {
/// The request for an account that still resolves through a source.
///
/// The single place the alert's strings are derived, so both surfaces that
/// offer Rename ask the validator the same two questions: what the row is
/// showing, and what clearing the field goes back to.
@MainActor
static func forAccount(_ account: Account, resolvedBy validator: DataContextValidator) -> Self {
Self(
id: account.id,
defaultName: validator.defaultDisplayName(for: account),
initialText: validator.displayName(for: account),
hasUserSetLabel: account.hasUserSetDisplayName
)
}
/// The request for an account with no source to resolve through.
///
/// The fallback is the caller's to name: the validator's last resort for an
/// account whose instance is gone is the words "Source missing", which is a
/// diagnosis and not a name to offer the field.
static func forAccount(_ account: Account, fallingBackTo defaultName: String) -> Self {
Self(
id: account.id,
defaultName: defaultName,
initialText: account.displayName ?? "",
hasUserSetLabel: account.hasUserSetDisplayName
)
}
}
extension Account {
/// Whether the account carries a label the user chose. An empty string is
/// no more a name than `nil` is, and both arrive here from old records.
var hasUserSetDisplayName: Bool {
!(displayName ?? "").isEmpty
}
}
/// The rename alert, shared by every screen that lists accounts.
@@ -40,8 +85,14 @@ private struct AccountRenameAlertModifier: ViewModifier {
// Keyed on the ID rather than on the request: the prefill has to be
// in place before the alert's field is built, and re-entering the
// same row is the only time the text should be reset.
//
// Seeded only when a request appears, never blanked when one goes
// away. Dismissal sets the request to nil while the alert's buttons
// are still live, and Save reads this text when it runs — blanking
// here would let the two race, with an emptied field committing as
// "clear the name".
.onChange(of: request?.id) { _, _ in
text = request?.initialText ?? ""
if let request { text = request.initialText }
}
.alert(
String(localized: "accounts.rename.title"),
@@ -56,7 +107,11 @@ private struct AccountRenameAlertModifier: ViewModifier {
Button(String(localized: "common.save")) {
onCommit(
pending.id,
DataContextValidator.renamedDisplayName(text, fallingBackTo: pending.defaultName)
DataContextValidator.renamedDisplayName(
text,
prefilledWith: pending.initialText,
hasUserSetLabel: pending.hasUserSetLabel
)
)
}

View File

@@ -349,18 +349,10 @@ struct AccountsListView: View {
/// prefill a field with.
private func makeRenameRequest(for account: Account, instance: Instance?) -> AccountRenameRequest {
guard instance != nil, let validator = appEnvironment?.dataContextValidator else {
return AccountRenameRequest(
id: account.id,
defaultName: String(localized: "accounts.orphan.unnamed"),
initialText: account.displayName ?? ""
)
return .forAccount(account, fallingBackTo: String(localized: "accounts.orphan.unnamed"))
}
return AccountRenameRequest(
id: account.id,
defaultName: validator.defaultDisplayName(for: account),
initialText: validator.displayName(for: account)
)
return .forAccount(account, resolvedBy: validator)
}
private func status(for account: Account) -> AccountStatus {

View File

@@ -195,13 +195,16 @@ struct InstanceAccountsSection: View {
}
/// What the rename field starts on, and what clearing it goes back to.
///
/// The section always has its instance, so the only reason to name the
/// fallback here is a missing environment, where the row's own label is
/// already the instance's.
private func makeRenameRequest(for account: Account) -> AccountRenameRequest {
AccountRenameRequest(
id: account.id,
defaultName: appEnvironment?.dataContextValidator.defaultDisplayName(for: account)
?? instance.displayName,
initialText: displayName(for: account)
)
guard let validator = appEnvironment?.dataContextValidator else {
return .forAccount(account, fallingBackTo: instance.displayName)
}
return .forAccount(account, resolvedBy: validator)
}
private func status(for account: Account) -> AccountStatus {