feat: configurable status mappings, scrollable settings, case-insensitive assignee
- Added FieldMappings.statusMappings: [FeishuValue: BugStatus] dict mapping Feishu select-option text to standard Bugger statuses. Pre-populated with common Chinese/English values: 开发进行中→inProgress, 验收完毕→resolved, Bug Triage→open, etc. - BugMapper.mapStatus() looks up Feishu value in the mapping, falling back to .unknown (shown as active bug). - Settings: status mapping section with one TextField per status showing comma-separated Feishu values. Editable and saved to config. - Settings: wrapped Form in ScrollView, removed fixed height, use minHeight/maxHeight — no more truncated buttons. - Assignee matching now uses case-insensitive contains instead of exact match, handling display name variations from Feishu API. Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
0f8565f65d
commit
723c50fc9d
|
|
@ -18,6 +18,28 @@ struct AppConfig: Codable, Equatable {
|
||||||
var reporterField: String = "Reporter"
|
var reporterField: String = "Reporter"
|
||||||
var createdAtField: String = "Created At"
|
var createdAtField: String = "Created At"
|
||||||
var updatedAtField: String = "Updated At"
|
var updatedAtField: String = "Updated At"
|
||||||
|
|
||||||
|
/// Map Feishu display values to standard BugStatus cases.
|
||||||
|
/// Keys are the Feishu select-option text; values are the standard status.
|
||||||
|
/// Unknown values default to .unknown (shown as active).
|
||||||
|
var statusMappings: [String: BugStatus] = [
|
||||||
|
"Bug Triage": .open,
|
||||||
|
"Open": .open,
|
||||||
|
"New": .open,
|
||||||
|
"待处理": .open,
|
||||||
|
"开发进行中": .inProgress,
|
||||||
|
"In Progress": .inProgress,
|
||||||
|
"验收中": .inReview,
|
||||||
|
"In Review": .inReview,
|
||||||
|
"验收完毕": .resolved,
|
||||||
|
"Resolved": .resolved,
|
||||||
|
"Fixed": .resolved,
|
||||||
|
"取消": .closed,
|
||||||
|
"Closed": .closed,
|
||||||
|
"Won't fix": .closed,
|
||||||
|
"转需求": .closed,
|
||||||
|
"验收不通过": .closed,
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
var isConfigured: Bool {
|
var isConfigured: Bool {
|
||||||
|
|
|
||||||
|
|
@ -117,7 +117,7 @@ enum BugMapper {
|
||||||
id: record.recordId,
|
id: record.recordId,
|
||||||
title: stringValue(in: fields, key: mappings.titleField) ?? "Untitled",
|
title: stringValue(in: fields, key: mappings.titleField) ?? "Untitled",
|
||||||
priority: BugPriority(rawValue: stringValue(in: fields, key: mappings.priorityField) ?? "") ?? .unknown,
|
priority: BugPriority(rawValue: stringValue(in: fields, key: mappings.priorityField) ?? "") ?? .unknown,
|
||||||
status: BugStatus(rawValue: stringValue(in: fields, key: mappings.statusField) ?? "") ?? .unknown,
|
status: mapStatus(stringValue(in: fields, key: mappings.statusField), mappings: mappings),
|
||||||
assignee: userName(in: fields, key: mappings.assigneeField) ?? "Unknown",
|
assignee: userName(in: fields, key: mappings.assigneeField) ?? "Unknown",
|
||||||
reporter: userName(in: fields, key: mappings.reporterField),
|
reporter: userName(in: fields, key: mappings.reporterField),
|
||||||
createdAt: FeishuDateParser.parse(rawValue(in: fields, key: mappings.createdAtField)) ?? .distantPast,
|
createdAt: FeishuDateParser.parse(rawValue(in: fields, key: mappings.createdAtField)) ?? .distantPast,
|
||||||
|
|
@ -158,4 +158,10 @@ enum BugMapper {
|
||||||
private static func userName(in fields: [String: JSONValue], key: String) -> String? {
|
private static func userName(in fields: [String: JSONValue], key: String) -> String? {
|
||||||
fields[key]?.firstUserName ?? fields[key]?.stringValue
|
fields[key]?.firstUserName ?? fields[key]?.stringValue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static func mapStatus(_ feishuValue: String?, mappings: AppConfig.FieldMappings) -> BugStatus {
|
||||||
|
guard let value = feishuValue else { return .unknown }
|
||||||
|
// Look up in the user-configured mapping; fall back to .unknown (shown as active)
|
||||||
|
return mappings.statusMappings[value] ?? .unknown
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,9 @@ final class FeishuService {
|
||||||
|
|
||||||
return allRecords
|
return allRecords
|
||||||
.map { BugMapper.map($0, config: config) }
|
.map { BugMapper.map($0, config: config) }
|
||||||
.filter { $0.assignee == assigneeName }
|
.filter { bug in
|
||||||
|
bug.assignee.localizedCaseInsensitiveContains(assigneeName)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func fetchRecordCount(
|
func fetchRecordCount(
|
||||||
|
|
|
||||||
|
|
@ -5,12 +5,14 @@ struct SettingsView: View {
|
||||||
@State private var config: AppConfig
|
@State private var config: AppConfig
|
||||||
@State private var isTestingConnection = false
|
@State private var isTestingConnection = false
|
||||||
@State private var connectionResult: String?
|
@State private var connectionResult: String?
|
||||||
|
@State private var showAdvanced = false
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
_config = State(initialValue: AppStateService.shared.config ?? AppConfig())
|
_config = State(initialValue: AppStateService.shared.config ?? AppConfig())
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
|
ScrollView {
|
||||||
Form {
|
Form {
|
||||||
Section("Feishu Bitable") {
|
Section("Feishu Bitable") {
|
||||||
TextField("App Token (from Bitable URL)", text: $config.appToken)
|
TextField("App Token (from Bitable URL)", text: $config.appToken)
|
||||||
|
|
@ -71,6 +73,16 @@ struct SettingsView: View {
|
||||||
TextField("Updated At field", text: $config.fieldMappings.updatedAtField)
|
TextField("Updated At field", text: $config.fieldMappings.updatedAtField)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Section("Status Mappings") {
|
||||||
|
Text("Map your Feishu status values to standard Bugger statuses.")
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
|
||||||
|
ForEach(BugStatus.allCases.filter { $0 != .unknown }, id: \.self) { status in
|
||||||
|
statusMappingRow(for: status)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
HStack {
|
HStack {
|
||||||
Button("Save") {
|
Button("Save") {
|
||||||
save()
|
save()
|
||||||
|
|
@ -85,8 +97,52 @@ struct SettingsView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.padding()
|
.padding()
|
||||||
.frame(minWidth: 500, idealWidth: 520, minHeight: 560)
|
|
||||||
}
|
}
|
||||||
|
.frame(minWidth: 500, idealWidth: 520, minHeight: 400, maxHeight: 800)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Status Mapping Row
|
||||||
|
|
||||||
|
private func statusMappingRow(for status: BugStatus) -> some View {
|
||||||
|
let binding = Binding<String>(
|
||||||
|
get: {
|
||||||
|
// Collect all Feishu values that map to this status
|
||||||
|
config.fieldMappings.statusMappings
|
||||||
|
.filter { $0.value == status }
|
||||||
|
.keys
|
||||||
|
.sorted()
|
||||||
|
.joined(separator: ", ")
|
||||||
|
},
|
||||||
|
set: { newValue in
|
||||||
|
// Remove old mappings for this status
|
||||||
|
config.fieldMappings.statusMappings = config.fieldMappings.statusMappings
|
||||||
|
.filter { $0.value != status }
|
||||||
|
// Add new mappings from comma-separated input
|
||||||
|
let parts = newValue
|
||||||
|
.components(separatedBy: ",")
|
||||||
|
.map { $0.trimmingCharacters(in: .whitespaces) }
|
||||||
|
.filter { !$0.isEmpty }
|
||||||
|
for part in parts {
|
||||||
|
config.fieldMappings.statusMappings[part] = status
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return VStack(alignment: .leading, spacing: 2) {
|
||||||
|
HStack {
|
||||||
|
StatusPill(status: status)
|
||||||
|
Text(status.label)
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
}
|
||||||
|
TextField(status.description, text: binding)
|
||||||
|
.font(.caption)
|
||||||
|
.textFieldStyle(.roundedBorder)
|
||||||
|
}
|
||||||
|
.padding(.vertical, 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Actions
|
||||||
|
|
||||||
private func save() {
|
private func save() {
|
||||||
AppStateService.shared.saveConfig(config)
|
AppStateService.shared.saveConfig(config)
|
||||||
|
|
@ -128,3 +184,29 @@ struct SettingsView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - BugStatus display helpers
|
||||||
|
|
||||||
|
extension BugStatus {
|
||||||
|
var label: String {
|
||||||
|
switch self {
|
||||||
|
case .open: "Open"
|
||||||
|
case .inProgress: "In Progress"
|
||||||
|
case .inReview: "In Review"
|
||||||
|
case .resolved: "Resolved"
|
||||||
|
case .closed: "Closed"
|
||||||
|
case .unknown: "Unknown"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var description: String {
|
||||||
|
switch self {
|
||||||
|
case .open: "New / Triage (e.g. Bug Triage, Open, 待处理)"
|
||||||
|
case .inProgress: "Being worked on (e.g. 开发进行中)"
|
||||||
|
case .inReview: "Under review / testing (e.g. 验收中)"
|
||||||
|
case .resolved: "Fixed & verified (e.g. 验收完毕, Fixed)"
|
||||||
|
case .closed: "Done / cancelled (e.g. Closed, 取消, Won't fix)"
|
||||||
|
case .unknown: "Unmapped"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue