feat: 支持构建时注入飞书配置并优化设置页面体验
This commit is contained in:
parent
5e4587909d
commit
4d67d8bffd
|
|
@ -4,3 +4,8 @@
|
|||
FEISHU_APP_ID = cli_your_app_id
|
||||
FEISHU_APP_SECRET = your_app_secret
|
||||
FEISHU_BASE_DOMAIN = xorbitlab.feishu.cn
|
||||
|
||||
// Bitable app token & table ID (from the Bitable URL). Baked into the app at
|
||||
// build time as defaults so internal users don't have to enter them in Settings.
|
||||
FEISHU_APP_TOKEN = your_bitable_app_token
|
||||
FEISHU_TABLE_ID = your_bitable_table_id
|
||||
|
|
|
|||
|
|
@ -28,5 +28,9 @@
|
|||
<string>$(FEISHU_APP_SECRET)</string>
|
||||
<key>FEISHU_BASE_DOMAIN</key>
|
||||
<string>$(FEISHU_BASE_DOMAIN)</string>
|
||||
<key>FEISHU_APP_TOKEN</key>
|
||||
<string>$(FEISHU_APP_TOKEN)</string>
|
||||
<key>FEISHU_TABLE_ID</key>
|
||||
<string>$(FEISHU_TABLE_ID)</string>
|
||||
</dict>
|
||||
</plist>
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import Foundation
|
||||
|
||||
struct AppConfig: Codable, Equatable {
|
||||
var appToken: String = ""
|
||||
var tableId: String = ""
|
||||
var appToken: String = BundledDefault.appToken
|
||||
var tableId: String = BundledDefault.tableId
|
||||
var assigneeName: String = ""
|
||||
var fieldMappings: FieldMappings = FieldMappings()
|
||||
var pollIntervalSeconds: Int = 300
|
||||
|
|
@ -11,7 +11,7 @@ struct AppConfig: Codable, Equatable {
|
|||
var dailyRefreshTimes: [String] = []
|
||||
var showFloatingWidget: Bool = false
|
||||
var launchAtLogin: Bool = true
|
||||
var feishuBaseDomain: String = "xorbitlab.feishu.cn"
|
||||
var feishuBaseDomain: String = BundledDefault.feishuBaseDomain
|
||||
/// When false (the default), Bugger waits for the next scheduled poll
|
||||
/// instead of fetching immediately on launch. Avoids a crash-on-start
|
||||
/// loop if the table fetch is failing.
|
||||
|
|
@ -66,15 +66,31 @@ struct AppConfig: Codable, Equatable {
|
|||
|
||||
init(from decoder: Decoder) throws {
|
||||
let c = try decoder.container(keyedBy: CodingKeys.self)
|
||||
appToken = try c.decodeIfPresent(String.self, forKey: .appToken) ?? ""
|
||||
tableId = try c.decodeIfPresent(String.self, forKey: .tableId) ?? ""
|
||||
appToken = try c.decodeIfPresent(String.self, forKey: .appToken) ?? BundledDefault.appToken
|
||||
tableId = try c.decodeIfPresent(String.self, forKey: .tableId) ?? BundledDefault.tableId
|
||||
assigneeName = try c.decodeIfPresent(String.self, forKey: .assigneeName) ?? ""
|
||||
fieldMappings = try c.decodeIfPresent(FieldMappings.self, forKey: .fieldMappings) ?? FieldMappings()
|
||||
pollIntervalSeconds = try c.decodeIfPresent(Int.self, forKey: .pollIntervalSeconds) ?? 300
|
||||
dailyRefreshTimes = try c.decodeIfPresent([String].self, forKey: .dailyRefreshTimes) ?? []
|
||||
showFloatingWidget = try c.decodeIfPresent(Bool.self, forKey: .showFloatingWidget) ?? false
|
||||
launchAtLogin = try c.decodeIfPresent(Bool.self, forKey: .launchAtLogin) ?? true
|
||||
feishuBaseDomain = try c.decodeIfPresent(String.self, forKey: .feishuBaseDomain) ?? "xorbitlab.feishu.cn"
|
||||
feishuBaseDomain = try c.decodeIfPresent(String.self, forKey: .feishuBaseDomain) ?? BundledDefault.feishuBaseDomain
|
||||
refreshOnStart = try c.decodeIfPresent(Bool.self, forKey: .refreshOnStart) ?? false
|
||||
}
|
||||
}
|
||||
|
||||
/// Values baked into the app at build time via Config.xcconfig → Info.plist.
|
||||
/// Used as defaults so internal users don't have to enter them in Settings.
|
||||
enum BundledDefault {
|
||||
static var appToken: String { string(for: "FEISHU_APP_TOKEN") }
|
||||
static var tableId: String { string(for: "FEISHU_TABLE_ID") }
|
||||
static var feishuBaseDomain: String { string(for: "FEISHU_BASE_DOMAIN") }
|
||||
|
||||
private static func string(for key: String) -> String {
|
||||
let raw = (Bundle.main.object(forInfoDictionaryKey: key) as? String) ?? ""
|
||||
// Xcode leaves an undefined $(...) substitution as the literal string;
|
||||
// treat that as "not set" rather than showing "$(FEISHU_APP_TOKEN)".
|
||||
if raw.hasPrefix("$(") { return "" }
|
||||
return raw.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,11 +13,29 @@ struct SettingsView: View {
|
|||
|
||||
var body: some View {
|
||||
ScrollView {
|
||||
VStack(spacing: 12) {
|
||||
// Highlighted "Start here" card lives outside the Form so its
|
||||
// Spacer + multi-element HStack don't disturb the Form's
|
||||
// two-column label/control layout (which was pushing the
|
||||
// Feishu Bitable fields off the right edge of the window).
|
||||
HStack(spacing: 12) {
|
||||
Image(systemName: "person.fill")
|
||||
.font(.title2)
|
||||
.foregroundStyle(.tint)
|
||||
Text("Your Name")
|
||||
.font(.headline)
|
||||
TextField("e.g. Alice", text: $config.assigneeName)
|
||||
.textFieldStyle(.roundedBorder)
|
||||
Spacer()
|
||||
}
|
||||
.padding(10)
|
||||
.background(RoundedRectangle(cornerRadius: 8).fill(.tint.opacity(0.08)))
|
||||
.overlay(RoundedRectangle(cornerRadius: 8).strokeBorder(.tint.opacity(0.35), lineWidth: 1))
|
||||
|
||||
Form {
|
||||
Section("Feishu Bitable") {
|
||||
TextField("App Token (from Bitable URL)", text: $config.appToken)
|
||||
TextField("Table ID", text: $config.tableId)
|
||||
TextField("Your Name (as in Assignee column, optional)", text: $config.assigneeName)
|
||||
TextField("Feishu domain", text: $config.feishuBaseDomain)
|
||||
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
|
|
@ -107,7 +125,13 @@ struct SettingsView: View {
|
|||
Toggle("Launch at login", isOn: $config.launchAtLogin)
|
||||
}
|
||||
|
||||
Section("Field Mappings") {
|
||||
Section {
|
||||
DisclosureGroup(isExpanded: $showAdvanced) {
|
||||
Text("Field Mappings")
|
||||
.font(.caption)
|
||||
.fontWeight(.semibold)
|
||||
.foregroundStyle(.secondary)
|
||||
|
||||
TextField("Title field", text: $config.fieldMappings.titleField)
|
||||
TextField("Priority field", text: $config.fieldMappings.priorityField)
|
||||
TextField("Status field", text: $config.fieldMappings.statusField)
|
||||
|
|
@ -116,9 +140,11 @@ struct SettingsView: View {
|
|||
TextField("Customer field", text: $config.fieldMappings.customerField)
|
||||
TextField("Created At field", text: $config.fieldMappings.createdAtField)
|
||||
TextField("Updated At field", text: $config.fieldMappings.updatedAtField)
|
||||
}
|
||||
|
||||
Section("Status Mappings") {
|
||||
Text("Status Mappings")
|
||||
.font(.caption)
|
||||
.fontWeight(.semibold)
|
||||
.foregroundStyle(.secondary)
|
||||
Text("Map your Feishu status values to standard Bugger statuses.")
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
|
|
@ -126,6 +152,9 @@ struct SettingsView: View {
|
|||
ForEach(BugStatus.allCases.filter { $0 != .unknown }, id: \.self) { status in
|
||||
statusMappingRow(for: status)
|
||||
}
|
||||
} label: {
|
||||
Label("Advanced: field & status mappings", systemImage: "slider.horizontal.3")
|
||||
}
|
||||
}
|
||||
|
||||
HStack {
|
||||
|
|
@ -140,7 +169,8 @@ struct SettingsView: View {
|
|||
}
|
||||
.foregroundStyle(.red)
|
||||
}
|
||||
}
|
||||
} // Form
|
||||
} // VStack
|
||||
.padding()
|
||||
}
|
||||
.frame(minWidth: 500, idealWidth: 520, minHeight: 400, maxHeight: 800)
|
||||
|
|
|
|||
Loading…
Reference in New Issue