Merge pull request #15 from rock-n-code/feature/create-folder

Feature: Create folder
This commit is contained in:
Javier Cicchelli 2022-12-17 00:10:00 +01:00 committed by GitHub
commit fbef849423
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 361 additions and 17 deletions

View File

@ -85,7 +85,6 @@ private extension ContentView {
id: user.rootFolder.id,
name: user.rootFolder.name
),
createFolder: createFolder,
uploadFile: uploadFile,
showProfile: showProfile,
login: login

View File

@ -0,0 +1,47 @@
//
// CreateFolderUseCase.swift
// Browse
//
// Created by Javier Cicchelli on 16/12/2022.
// Copyright © 2022 Röck+Cöde. All rights reserved.
//
import APIService
import DependencyInjection
import Dependencies
struct CreateFolderUseCase {
// MARK: Properties
let apiService: APIService
// MARK: Functions
func callAsFunction(
id: String,
name: String,
username: String,
password: String
) async throws {
_ = try await apiService.createFolder(
id: id,
name: name,
credentials: .init(
username: username,
password: password
)
)
}
}
// MARK: - Initialisers
extension CreateFolderUseCase {
init() {
@Dependency(\.apiService) var apiService
self.init(apiService: apiService)
}
}

View File

@ -25,15 +25,26 @@
"message.type_not_supported.text.second" = "Please be patient while the support for this type of document is being built by our development team.";
"message.type_not_supported.button.text" = "Go back to folder";
// CreateFolderViewModifier
"create_folder.input_alert.title" = "Create a new folder";
"create_folder.input_alert.message" = "You are about to create a new folder inside the folder you are currently in.";
"create_folder.input_alert.text_field.placeholder" = "Name of the folder to create";
"create_folder.input_alert.button.cancel" = "Dismiss";
"create_folder.input_alert.button.submit" = "Create folder";
"create_folder.system_alert.title" = "An error occurred while creating a new folder";
"create_folder.system_alert.message" = "An unexpected error occurred while trying to create a new folder into the current folder.\n\nPlease check your Internet connection and try this operation at a later time.";
"create_folder.system_alert.button.cancel" = "Understood";
// DeleteItemViewModifier
"delete_item.action_sheet.title" = "Delete an item";
"delete_item.action_sheet.message %@" = "You are about to delete an item named \"%@\" from this folder.\n\nAre you sure you wish to proceed?";
"delete_item.action_sheet.button.ok" = "Yes, please delete it.";
"delete_item.action_sheet.button.cancel" = "No, I reconsidered.";
"delete_item.system_alert.title" = "...";
"delete_item.system_alert.message" = "...";
"delete_item.system_alert.button.dismiss" = "...";
"delete_item.system_alert.title" = "An error occurred when deleting a folder";
"delete_item.system_alert.message" = "An unexpected error occurred while trying to delete the indicated folder from the current folder.\n\nPlease check your Internet connection and try this operation at a later time.";
"delete_item.system_alert.button.cancel" = "Understood";
// BrowseView

View File

@ -0,0 +1,134 @@
//
// InputAlertView.swift
// Browse
//
// Created by Javier Cicchelli on 16/12/2022.
// Copyright © 2022 Röck+Cöde. All rights reserved.
//
import DataModels
import SwiftUI
import UIKit
struct InputAlertView: UIViewControllerRepresentable {
// MARK: Bindings
@Binding private var isPresenting: Bool
@Binding private var textFieldString: String
// MARK: Properties
private let title: String
private let message: String
private let textFieldPlaceholder: String
private let actions: [UIAlertAction]
// MARK: Initialisers
init(
isPresenting: Binding<Bool>,
title: String,
message: String,
textFieldPlaceholder: String,
textFieldString: Binding<String>,
actions: [UIAlertAction]
) {
self.title = title
self.message = message
self.textFieldPlaceholder = textFieldPlaceholder
self.actions = actions
self._isPresenting = isPresenting
self._textFieldString = textFieldString
}
// MARK: Functions
func makeUIViewController(
context: UIViewControllerRepresentableContext<InputAlertView>
) -> UIViewController {
.init()
}
func updateUIViewController(
_ viewController: UIViewController,
context: UIViewControllerRepresentableContext<InputAlertView>
) {
guard
context.coordinator.alert == nil,
isPresenting
else { return }
let alertController = {
let alert = UIAlertController(
title: title,
message: message,
preferredStyle: .alert
)
alert.addTextField { textField in
textField.placeholder = textFieldPlaceholder
textField.text = textFieldString
textField.delegate = context.coordinator
}
actions.forEach { action in
alert.addAction(action)
}
return alert
}()
context.coordinator.alert = alertController
Task { @MainActor in
viewController.present(
alertController,
animated: true
) {
isPresenting = false
context.coordinator.alert = nil
}
}
}
func makeCoordinator() -> InputAlertView.Coordinator {
Coordinator(self)
}
}
// MARK: - Coordinator
extension InputAlertView {
class Coordinator: NSObject, UITextFieldDelegate {
// MARK: Properties
var alert: UIAlertController?
private let component: InputAlertView
// MARK: Initialisers
init(_ component: InputAlertView) {
self.component = component
}
// MARK: UITextFieldDelegate
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
component.textFieldString = {
if let text = textField.text as NSString? {
return text.replacingCharacters(in: range, with: string)
} else {
return .empty
}
}()
return true
}
}
}

View File

@ -0,0 +1,11 @@
//
// String+Constants.swift
// Browse
//
// Created by Javier Cicchelli on 16/12/2022.
// Copyright © 2022 Röck+Cöde. All rights reserved.
//
extension String {
static let empty = ""
}

View File

@ -0,0 +1,19 @@
//
// String+Localisations.swift
// Browse
//
// Created by Javier Cicchelli on 16/12/2022.
// Copyright © 2022 Röck+Cöde. All rights reserved.
//
import Foundation
extension String {
static func localise(key: String) -> Self {
NSLocalizedString(
key,
bundle: .module,
comment: .empty
)
}
}

View File

@ -22,6 +22,18 @@ extension View {
))
}
func createFolder(
isPresenting: Binding<Bool>,
id: String,
submitted: @escaping ActionClosure
) -> some View {
modifier(CreateFolderViewModifier(
isPresenting: isPresenting,
id: id,
submitted: submitted
))
}
func delete(
item: Binding<(any FileSystemItem)?>,
deleted: @escaping ActionClosure

View File

@ -0,0 +1,109 @@
//
// CreateFolderViewModifier.swift
// Browse
//
// Created by Javier Cicchelli on 16/12/2022.
// Copyright © 2022 Röck+Cöde. All rights reserved.
//
import DataModels
import KeychainStorage
import SwiftUI
struct CreateFolderViewModifier: ViewModifier {
// MARK: Storages
@KeychainStorage(key: .KeychainStorage.account) private var account: Account?
// MARK: States
@State private var folderName: String = ""
@State private var showErrorAlert: Bool = false
// MARK: Bindings
@Binding var isPresenting: Bool
let id: String
let submitted: ActionClosure
private let createFolder: CreateFolderUseCase = .init()
// MARK: Body
func body(content: Content) -> some View {
content
.background(InputAlertView(
isPresenting: $isPresenting,
title: .localise(key: "create_folder.input_alert.title"),
message: .localise(key: "create_folder.input_alert.message"),
textFieldPlaceholder: .localise(key: "create_folder.input_alert.text_field.placeholder"),
textFieldString: $folderName,
actions: [
.init(
title: .localise(key: "create_folder.input_alert.button.cancel"),
style: .cancel,
handler: { _ in
folderName = .empty
}
),
.init(
title: .localise(key: "create_folder.input_alert.button.submit"),
style: .default,
handler: { _ in
Task {
await addFolder()
}
}
)
]
))
.alert(isPresented: $showErrorAlert) {
Alert(
title: Text(
"create_folder.system_alert.title",
bundle: .module
),
message: Text(
"create_folder.system_alert.message",
bundle: .module
),
dismissButton: .cancel(Text(
"create_folder.system_alert.button.cancel",
bundle: .module
)) {
folderName = .empty
}
)
}
}
}
// MARK: - Helpers
private extension CreateFolderViewModifier {
func addFolder() async {
guard let account else {
showErrorAlert = true
return
}
do {
_ = try await createFolder(
id: id,
name: folderName,
username: account.username,
password: account.password
)
folderName = .empty
submitted()
} catch {
showErrorAlert = true
}
}
}

View File

@ -115,6 +115,8 @@ private extension DeleteItemViewModifier {
password: account.password
)
item = nil
deleted()
} catch {
showErrorAlert = true

View File

@ -22,11 +22,11 @@ public struct BrowseView: View {
@State private var items: [any FileSystemItem] = []
@State private var stack: Stack?
@State private var itemToDelete: (any FileSystemItem)?
@State private var showCreateFolder: Bool = false
// MARK: Properties
private let folder: Folder
private let createFolder: ActionClosure
private let uploadFile: ActionClosure
private let showProfile: ActionClosure
private let login: ActionClosure
@ -37,13 +37,11 @@ public struct BrowseView: View {
public init(
folder: Folder,
createFolder: @escaping ActionClosure,
uploadFile: @escaping ActionClosure,
showProfile: @escaping ActionClosure,
login: @escaping ActionClosure
) {
self.folder = folder
self.createFolder = createFolder
self.uploadFile = uploadFile
self.showProfile = showProfile
self.login = login
@ -56,14 +54,24 @@ public struct BrowseView: View {
.navigationTitle(folder.name)
.toolbar {
BrowseToolbar(
createFolder: createFolder,
createFolder: {
showCreateFolder = true
},
uploadFile: uploadFile,
showProfile: showProfile
)
}
.createFolder(
isPresenting: $showCreateFolder,
id: folder.id
) {
Task {
await loadItems()
}
}
.delete(item: $itemToDelete) {
Task {
await updateItems()
await loadItems()
}
}
.task(id: folder) {
@ -129,7 +137,6 @@ private extension BrowseView {
.navigate(
to: BrowseView(
folder: folder,
createFolder: createFolder,
uploadFile: uploadFile,
showProfile: showProfile,
login: login
@ -196,11 +203,6 @@ private extension BrowseView {
status = .error
}
}
func updateItems() async {
items = items.filter { $0.id != itemToDelete?.id }
itemToDelete = nil
}
}
// MARK: - Previews
@ -212,8 +214,6 @@ struct BrowseView_Previews: PreviewProvider {
id: UUID().uuidString,
name: "Some folder name"
)) {
// create folder closure.
} uploadFile: {
// upload file closure.
} showProfile: {
// show profile closure.