diff --git a/Sources/Helpers/String+URL.swift b/Sources/Helpers/String+URL.swift new file mode 100644 index 0000000..95b360c --- /dev/null +++ b/Sources/Helpers/String+URL.swift @@ -0,0 +1,21 @@ +// Copyright (c) 2024 Anass Bouassaba. +// +// Use of this software is governed by the Business Source License +// included in the file LICENSE in the root of this repository. +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the GNU Affero General Public License v3.0 only, included in the file +// AGPL-3.0-only in the root of this repository. + +import Foundation + +extension String { + func isValidURL() -> Bool { + guard !isEmpty else { return false } + if let url = URL(string: self), let scheme = url.scheme, let host = url.host { + return ["http", "https"].contains(scheme) && !host.isEmpty + } + return false + } +} diff --git a/Sources/Screens/Server/ServerCreate.swift b/Sources/Screens/Server/ServerCreate.swift index 8e35c72..5bdc2b6 100644 --- a/Sources/Screens/Server/ServerCreate.swift +++ b/Sources/Screens/Server/ServerCreate.swift @@ -10,7 +10,7 @@ import SwiftUI -struct ServerCreate: View { +struct ServerCreate: View, FormValidatable { @Environment(\.modelContext) private var context @Environment(\.dismiss) var dismiss @State private var name = "" @@ -20,18 +20,16 @@ struct ServerCreate: View { var body: some View { Form { - Section(header: VOSectionHeader("Name")) { + Section(header: VOSectionHeader("Details")) { TextField("Name", text: $name) .disabled(isProcessing) } - Section(header: VOSectionHeader("API URL")) { - TextField("API URL", text: $apiURL) + Section(header: VOSectionHeader("URLs")) { + TextField("API", text: $apiURL) .textInputAutocapitalization(.never) .autocorrectionDisabled() .disabled(isProcessing) - } - Section(header: VOSectionHeader("Identity Provider URL")) { - TextField("Identity Provider URL", text: $idpURL) + TextField("Identity Provider", text: $idpURL) .textInputAutocapitalization(.never) .autocorrectionDisabled() .disabled(isProcessing) @@ -77,7 +75,9 @@ struct ServerCreate: View { } } - private func isValid() -> Bool { - !normalizedName.isEmpty && !apiURL.isEmpty && !idpURL.isEmpty + // MARK: - FormValidatable + + func isValid() -> Bool { + !normalizedName.isEmpty && apiURL.isValidURL() && idpURL.isValidURL() } } diff --git a/Sources/Screens/Server/ServerEditAPIURL.swift b/Sources/Screens/Server/ServerEditAPIURL.swift new file mode 100644 index 0000000..bbeda1c --- /dev/null +++ b/Sources/Screens/Server/ServerEditAPIURL.swift @@ -0,0 +1,69 @@ +// Copyright (c) 2024 Anass Bouassaba. +// +// Use of this software is governed by the Business Source License +// included in the file LICENSE in the root of this repository. +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the GNU Affero General Public License v3.0 only, included in the file +// AGPL-3.0-only in the root of this repository. + +import SwiftUI + +struct ServerEditAPIURL: View, FormValidatable { + @EnvironmentObject private var tokenStore: TokenStore + @Environment(\.dismiss) private var dismiss + @Environment(\.modelContext) private var context + @State private var value = "" + @State private var isProcessing = false + private let server: Server + + init(_ server: Server) { + self.server = server + } + + var body: some View { + Form { + TextField("API URL", text: $value) + } + .navigationBarTitleDisplayMode(.inline) + .navigationTitle("Change API URL") + .toolbar { + ToolbarItem(placement: .topBarTrailing) { + if isProcessing { + ProgressView() + } else { + Button("Save") { + performSave() + } + .disabled(!isValid()) + } + } + } + .onAppear { + value = server.apiURL + } + } + + private func performSave() { + isProcessing = true + Task { + server.apiURL = value + try? context.save() + + UserDefaults.standard.server = server + tokenStore.recreateClient() + + DispatchQueue.main.async { + dismiss() + isProcessing = false + } + } + } + + // MARK: - FormValidatable + + func isValid() -> Bool { + value.isValidURL() + } +} diff --git a/Sources/Screens/Server/ServerEditIdentityProviderURL.swift b/Sources/Screens/Server/ServerEditIdentityProviderURL.swift new file mode 100644 index 0000000..a8da74f --- /dev/null +++ b/Sources/Screens/Server/ServerEditIdentityProviderURL.swift @@ -0,0 +1,69 @@ +// Copyright (c) 2024 Anass Bouassaba. +// +// Use of this software is governed by the Business Source License +// included in the file LICENSE in the root of this repository. +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the GNU Affero General Public License v3.0 only, included in the file +// AGPL-3.0-only in the root of this repository. + +import SwiftUI + +struct ServerEditIdentityProviderURL: View, FormValidatable { + @EnvironmentObject private var tokenStore: TokenStore + @Environment(\.dismiss) private var dismiss + @Environment(\.modelContext) private var context + @State private var value = "" + @State private var isProcessing = false + private let server: Server + + init(_ server: Server) { + self.server = server + } + + var body: some View { + Form { + TextField("Identity Provider URL", text: $value) + } + .navigationBarTitleDisplayMode(.inline) + .navigationTitle("Change Identity Provider URL") + .toolbar { + ToolbarItem(placement: .topBarTrailing) { + if isProcessing { + ProgressView() + } else { + Button("Save") { + performSave() + } + .disabled(!isValid()) + } + } + } + .onAppear { + value = server.idpURL + } + } + + private func performSave() { + isProcessing = true + Task { + server.idpURL = value + try? context.save() + + UserDefaults.standard.server = server + tokenStore.recreateClient() + + DispatchQueue.main.async { + dismiss() + isProcessing = false + } + } + } + + // MARK: - FormValidatable + + func isValid() -> Bool { + value.isValidURL() + } +} diff --git a/Sources/Screens/Server/ServerEditName.swift b/Sources/Screens/Server/ServerEditName.swift new file mode 100644 index 0000000..a8a10d3 --- /dev/null +++ b/Sources/Screens/Server/ServerEditName.swift @@ -0,0 +1,72 @@ +// Copyright (c) 2024 Anass Bouassaba. +// +// Use of this software is governed by the Business Source License +// included in the file LICENSE in the root of this repository. +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the GNU Affero General Public License v3.0 only, included in the file +// AGPL-3.0-only in the root of this repository. + +import SwiftUI + +struct ServerEditName: View, FormValidatable { + @EnvironmentObject private var tokenStore: TokenStore + @Environment(\.dismiss) private var dismiss + @Environment(\.modelContext) private var context + @State private var value = "" + @State private var isProcessing = false + private let server: Server + + init(_ server: Server) { + self.server = server + } + + var body: some View { + Form { + TextField("Name", text: $value) + } + .navigationBarTitleDisplayMode(.inline) + .navigationTitle("Change Name") + .toolbar { + ToolbarItem(placement: .topBarTrailing) { + if isProcessing { + ProgressView() + } else { + Button("Save") { + performSave() + } + .disabled(!isValid()) + } + } + } + .onAppear { + value = server.name + } + } + + private var normalizedValue: String { + value.trimmingCharacters(in: .whitespaces) + } + + private func performSave() { + isProcessing = true + Task { + server.name = normalizedValue + try? context.save() + + UserDefaults.standard.server = server + + DispatchQueue.main.async { + dismiss() + isProcessing = false + } + } + } + + // MARK: - FormValidatable + + func isValid() -> Bool { + !normalizedValue.isEmpty + } +} diff --git a/Sources/Screens/Server/ServerOverview.swift b/Sources/Screens/Server/ServerOverview.swift index 78f1377..3c38bdd 100644 --- a/Sources/Screens/Server/ServerOverview.swift +++ b/Sources/Screens/Server/ServerOverview.swift @@ -28,31 +28,64 @@ struct ServerOverview: View { var body: some View { Form { - Section(header: VOSectionHeader("API URL")) { - Text(server.apiURL) - } - Section(header: VOSectionHeader("Identity Provider URL")) { - Text(server.idpURL) + Section(header: VOSectionHeader("Name")) { + NavigationLink(destination: ServerEditName(server)) { + HStack { + Text("Name") + Spacer() + Text(server.name) + .lineLimit(1) + .truncationMode(.tail) + .foregroundStyle(.secondary) + } + } + .disabled(server.isCloud) } - Section { - Button { - activateConfirmationIsPresented = true - } label: { + Section(header: VOSectionHeader("URLs")) { + NavigationLink(destination: ServerEditAPIURL(server)) { HStack { - Text("Activate Server") - if isActivating { - Spacer() - ProgressView() - } + Text("API") + Spacer() + Text(server.apiURL) + .lineLimit(1) + .truncationMode(.middle) + .foregroundStyle(.secondary) } } - .disabled(server.isActive || isProcessing) - .confirmationDialog("Activate Server", isPresented: $activateConfirmationIsPresented) { - Button("Activate") { - performActivate() + .disabled(server.isCloud) + NavigationLink(destination: ServerEditIdentityProviderURL(server)) { + HStack { + Text("Identity Provider") + Spacer() + Text(server.idpURL) + .lineLimit(1) + .truncationMode(.middle) + .foregroundStyle(.secondary) + } + } + .disabled(server.isCloud) + } + Section(header: VOSectionHeader("Advanced")) { + if !server.isActive { + Button { + activateConfirmationIsPresented = true + } label: { + HStack { + Text("Activate Server") + if isActivating { + Spacer() + ProgressView() + } + } + } + .disabled(isProcessing) + .confirmationDialog("Activate Server", isPresented: $activateConfirmationIsPresented) { + Button("Activate") { + performActivate() + } + } message: { + Text("Are you sure you want to activate this server?") } - } message: { - Text("Are you sure you want to activate this server?") } if !server.isCloud { Button(role: .destructive) { diff --git a/Voltaserve.xcodeproj/project.pbxproj b/Voltaserve.xcodeproj/project.pbxproj index c86b43a..0e6eb10 100644 --- a/Voltaserve.xcodeproj/project.pbxproj +++ b/Voltaserve.xcodeproj/project.pbxproj @@ -135,6 +135,9 @@ Organization/OrganizationSettings.swift, Organization/OrganizationStore.swift, Server/ServerCreate.swift, + Server/ServerEditAPIURL.swift, + Server/ServerEditIdentityProviderURL.swift, + Server/ServerEditName.swift, Server/ServerList.swift, Server/ServerModel.swift, Server/ServerOverview.swift,