added shared support
This commit is contained in:
@@ -0,0 +1,22 @@
|
||||
# SonarDesk iOS + Team — Phase 1
|
||||
|
||||
Added the non-destructive shared workspace domain foundation.
|
||||
|
||||
## Added
|
||||
- `WorkspaceKind`: personal/team
|
||||
- `WorkspaceRole`: owner/admin/developer/viewer
|
||||
- `SonarWorkspace`
|
||||
- `WorkspaceMember`
|
||||
- `WorkspaceStore`
|
||||
- iCloud Drive persistence for workspace metadata
|
||||
- Explicit credential boundary: SonarQube tokens stay device-local in Keychain
|
||||
|
||||
## Next in Xcode
|
||||
1. Add a SonarDesk iOS app target (`net.absdev.SonarDesk.iOS`).
|
||||
2. Enable the existing iCloud container `iCloud.net.absdev.SonarDesk` plus CloudKit.
|
||||
3. Share API/models/workspace files with both targets.
|
||||
4. Add an iOS-specific app entry point and `NavigationSplitView`.
|
||||
5. Implement Dashboard, Projects and Project Detail.
|
||||
6. Introduce CloudKit private/shared workspace repository and migrate the workspace JSON records.
|
||||
|
||||
No existing macOS source was modified in Phase 1; the new synchronized-folder files are automatically visible to the current Xcode project.
|
||||
@@ -0,0 +1,32 @@
|
||||
# SonarDesk shared workspace foundation
|
||||
|
||||
This is the first compatibility-safe step toward macOS + iOS + Team.
|
||||
|
||||
## Storage boundaries
|
||||
|
||||
- iCloud Drive currently remains the persistence mechanism for existing macOS data.
|
||||
- Workspace metadata is stored in `Data/workspaces.json` so current releases can adopt the model without a destructive migration.
|
||||
- SonarQube credentials remain in Keychain and are **not** written to iCloud Drive.
|
||||
- `ThisDeviceOnly` remains the default credential policy. An iPhone therefore authenticates independently.
|
||||
|
||||
## Next migration
|
||||
|
||||
Move workspace/team records to CloudKit private/shared databases:
|
||||
|
||||
- Personal workspace -> private database
|
||||
- Team workspace -> CKShare/shared database
|
||||
- Team membership and roles -> shared records
|
||||
- Reports -> iCloud Drive assets/CloudKit asset metadata
|
||||
|
||||
The existing JSON workspace model is intentionally Codable so it can be migrated to CloudKit records without changing UI-facing domain types.
|
||||
|
||||
## iOS target
|
||||
|
||||
The iOS target should share:
|
||||
|
||||
- Models/Models.swift
|
||||
- Services/SonarQubeClient.swift
|
||||
- Services/KeychainService.swift
|
||||
- Shared/Workspaces/*
|
||||
|
||||
macOS-only UI and exporters stay out of the iOS target. The first iOS screens are Dashboard, Projects and Project Detail.
|
||||
@@ -0,0 +1,49 @@
|
||||
import Foundation
|
||||
|
||||
enum WorkspaceKind: String, Codable, CaseIterable, Sendable {
|
||||
case personal
|
||||
case team
|
||||
}
|
||||
|
||||
enum WorkspaceRole: String, Codable, CaseIterable, Sendable {
|
||||
case owner
|
||||
case admin
|
||||
case developer
|
||||
case viewer
|
||||
}
|
||||
|
||||
struct SonarWorkspace: Codable, Identifiable, Hashable, Sendable {
|
||||
var id: UUID
|
||||
var name: String
|
||||
var kind: WorkspaceKind
|
||||
var role: WorkspaceRole
|
||||
var serverIDs: [UUID]
|
||||
var createdAt: Date
|
||||
var updatedAt: Date
|
||||
|
||||
init(id: UUID = UUID(), name: String, kind: WorkspaceKind, role: WorkspaceRole,
|
||||
serverIDs: [UUID] = [], createdAt: Date = Date(), updatedAt: Date = Date()) {
|
||||
self.id = id
|
||||
self.name = name
|
||||
self.kind = kind
|
||||
self.role = role
|
||||
self.serverIDs = serverIDs
|
||||
self.createdAt = createdAt
|
||||
self.updatedAt = updatedAt
|
||||
}
|
||||
|
||||
static let personal = SonarWorkspace(
|
||||
id: UUID(uuidString: "00000000-0000-0000-0000-000000000001")!,
|
||||
name: "Personal",
|
||||
kind: .personal,
|
||||
role: .owner
|
||||
)
|
||||
}
|
||||
|
||||
struct WorkspaceMember: Codable, Identifiable, Hashable, Sendable {
|
||||
var id: UUID
|
||||
var workspaceID: UUID
|
||||
var displayName: String
|
||||
var role: WorkspaceRole
|
||||
var joinedAt: Date
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import Foundation
|
||||
import Combine
|
||||
|
||||
@MainActor
|
||||
final class WorkspaceStore: ObservableObject {
|
||||
@Published private(set) var workspaces: [SonarWorkspace] = [.personal]
|
||||
@Published var selectedWorkspaceID: UUID = SonarWorkspace.personal.id
|
||||
|
||||
private let driveStore = ICloudDriveStore.shared
|
||||
private let fileName = "workspaces.json"
|
||||
|
||||
var selectedWorkspace: SonarWorkspace {
|
||||
workspaces.first(where: { $0.id == selectedWorkspaceID }) ?? .personal
|
||||
}
|
||||
|
||||
init() {
|
||||
load()
|
||||
}
|
||||
|
||||
func load() {
|
||||
do {
|
||||
guard let saved = try driveStore.read([SonarWorkspace].self, fileName: fileName), !saved.isEmpty else {
|
||||
persist()
|
||||
return
|
||||
}
|
||||
workspaces = saved
|
||||
if !workspaces.contains(where: { $0.id == selectedWorkspaceID }) {
|
||||
selectedWorkspaceID = workspaces.first?.id ?? SonarWorkspace.personal.id
|
||||
}
|
||||
} catch {
|
||||
// Keep Personal usable if iCloud is temporarily unavailable.
|
||||
workspaces = [.personal]
|
||||
}
|
||||
}
|
||||
|
||||
func createTeam(name: String) {
|
||||
let clean = name.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
guard !clean.isEmpty else { return }
|
||||
let team = SonarWorkspace(name: clean, kind: .team, role: .owner)
|
||||
workspaces.append(team)
|
||||
selectedWorkspaceID = team.id
|
||||
persist()
|
||||
}
|
||||
|
||||
func select(_ workspace: SonarWorkspace) {
|
||||
selectedWorkspaceID = workspace.id
|
||||
}
|
||||
|
||||
private func persist() {
|
||||
try? driveStore.write(workspaces, fileName: fileName)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user