From 17c4a252c25e797e1df5b563cc2bd7417b5ff829 Mon Sep 17 00:00:00 2001 From: Milen Pivchev Date: Thu, 13 Feb 2025 18:59:57 +0100 Subject: [PATCH 01/20] WIP Signed-off-by: Milen Pivchev --- iOSClient/Assistant/NCAssistant.swift | 45 +++++++++++++++++++++------ 1 file changed, 35 insertions(+), 10 deletions(-) diff --git a/iOSClient/Assistant/NCAssistant.swift b/iOSClient/Assistant/NCAssistant.swift index cfe7fc492b..bfea931215 100644 --- a/iOSClient/Assistant/NCAssistant.swift +++ b/iOSClient/Assistant/NCAssistant.swift @@ -32,9 +32,7 @@ struct NCAssistant: View { Button(action: { presentationMode.wrappedValue.dismiss() }) { - Image(systemName: "xmark") - .font(Font.system(.body).weight(.light)) - .foregroundStyle(Color(NCBrandColor.shared.iconImageColor)) + Text("_close_") } } ToolbarItem(placement: .topBarTrailing) { @@ -60,6 +58,7 @@ struct NCAssistant: View { .padding(20) .frame(height: 50) } + .background(.ultraThinMaterial) } } } @@ -96,6 +95,28 @@ struct TaskList: View { var body: some View { List(model.filteredTasks, id: \.id) { task in TaskItem(task: task) + .contextMenu { + Button { + } label: { + Label { + Text("_copy_") + } icon: { + Image(systemName: "document.on.document") + } + + } + + // Button { + // Label { + // } icon: { + // Image(systemImage: "copy") + // } + // + // } + Button("Test") { + + } + } } .if(!model.types.isEmpty) { view in view.refreshable { @@ -148,12 +169,16 @@ struct TaskItem: View { NavigationLink(destination: NCAssistantTaskDetail(task: task)) { VStack(alignment: .leading) { Text(task.input ?? "") - .lineLimit(4) + .lineLimit(1) + + Text(task.output ?? "") + .lineLimit(1) + .foregroundStyle(.secondary) HStack { Label( title: { - Text(NSLocalizedString(task.statusInfo.stringKey, comment: "")) + Text(NSLocalizedString(task.status == 3 /*Completed*/ ? NCUtility().dateDiff(.init(timeIntervalSince1970: TimeInterval(task.completionExpectedAt ?? 0))) : task.statusInfo.stringKey, comment: "")) }, icon: { Image(systemName: task.statusInfo.imageSystemName) @@ -164,11 +189,11 @@ struct TaskItem: View { .padding(.top, 1) .labelStyle(CustomLabelStyle()) - if let completionExpectedAt = task.completionExpectedAt { - Text(NCUtility().dateDiff(.init(timeIntervalSince1970: TimeInterval(completionExpectedAt)))) - .frame(maxWidth: .infinity, alignment: .trailing) - .foregroundStyle(.tertiary) - } +// if let completionExpectedAt = task.completionExpectedAt { +// Text(NCUtility().dateDiff(.init(timeIntervalSince1970: TimeInterval(completionExpectedAt)))) +// .frame(maxWidth: .infinity, alignment: .trailing) +// .foregroundStyle(.tertiary) +// } } } .swipeActions { From db1b94424c3d6e69d2038bfdf281b39246a6c61d Mon Sep 17 00:00:00 2001 From: Milen Pivchev Date: Thu, 13 Feb 2025 19:07:42 +0100 Subject: [PATCH 02/20] refactor Signed-off-by: Milen Pivchev --- Nextcloud.xcodeproj/project.pbxproj | 2 +- iOSClient/Assistant/Models/NCAssistantTask.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Nextcloud.xcodeproj/project.pbxproj b/Nextcloud.xcodeproj/project.pbxproj index 72eb9b6398..32ba5982c0 100644 --- a/Nextcloud.xcodeproj/project.pbxproj +++ b/Nextcloud.xcodeproj/project.pbxproj @@ -5948,7 +5948,7 @@ isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/nextcloud/NextcloudKit"; requirement = { - branch = develop; + branch = "assistant-v2"; kind = branch; }; }; diff --git a/iOSClient/Assistant/Models/NCAssistantTask.swift b/iOSClient/Assistant/Models/NCAssistantTask.swift index ceaf400b52..591a209761 100644 --- a/iOSClient/Assistant/Models/NCAssistantTask.swift +++ b/iOSClient/Assistant/Models/NCAssistantTask.swift @@ -12,7 +12,7 @@ import NextcloudKit import SwiftUI class NCAssistantTask: ObservableObject { - @Published var types: [NKTextProcessingTaskType] = [] + @Published var types: [NKTextProcessingTaskTypeV2.TaskTypes] = [] @Published var filteredTasks: [NKTextProcessingTask] = [] @Published var selectedType: NKTextProcessingTaskType? @Published var selectedTask: NKTextProcessingTask? From c6bb2111a5803dc39eb20cfa0a308cff476089a3 Mon Sep 17 00:00:00 2001 From: Milen Pivchev Date: Fri, 14 Feb 2025 11:18:36 +0100 Subject: [PATCH 03/20] WIP Signed-off-by: Milen Pivchev --- Nextcloud.xcodeproj/project.pbxproj | 4 + .../Assistant/Models/NCAssistantTask.swift | 28 ++- .../Assistant/Models/NCAssistantTaskV2.swift | 201 ++++++++++++++++++ 3 files changed, 229 insertions(+), 4 deletions(-) create mode 100644 iOSClient/Assistant/Models/NCAssistantTaskV2.swift diff --git a/Nextcloud.xcodeproj/project.pbxproj b/Nextcloud.xcodeproj/project.pbxproj index 32ba5982c0..0177f3fdda 100644 --- a/Nextcloud.xcodeproj/project.pbxproj +++ b/Nextcloud.xcodeproj/project.pbxproj @@ -68,6 +68,7 @@ F314F1142A30E2DE00BC7FAB /* View+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7E8A390295DC5E0006CB2D0 /* View+Extension.swift */; }; F321DA8A2B71205A00DDA0E6 /* NCTrashSelectTabBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = F321DA892B71205A00DDA0E6 /* NCTrashSelectTabBar.swift */; }; F32FADA92D1176E3007035E2 /* UIButton+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = F32FADA82D1176DE007035E2 /* UIButton+Extension.swift */; }; + F3374A6D2D5E6E79002A38F9 /* NCAssistantTaskV2.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3374A6C2D5E6E79002A38F9 /* NCAssistantTaskV2.swift */; }; F33918C42C7CD8F2002D9AA1 /* FileNameValidator+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = F33918C32C7CD8F2002D9AA1 /* FileNameValidator+Extensions.swift */; }; F33918C52C7CD8F2002D9AA1 /* FileNameValidator+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = F33918C32C7CD8F2002D9AA1 /* FileNameValidator+Extensions.swift */; }; F33918C62C7CD8F2002D9AA1 /* FileNameValidator+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = F33918C32C7CD8F2002D9AA1 /* FileNameValidator+Extensions.swift */; }; @@ -1227,6 +1228,7 @@ F310B1EE2BA862F1001C42F5 /* NCViewerMedia+VisionKit.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NCViewerMedia+VisionKit.swift"; sourceTree = ""; }; F321DA892B71205A00DDA0E6 /* NCTrashSelectTabBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCTrashSelectTabBar.swift; sourceTree = ""; }; F32FADA82D1176DE007035E2 /* UIButton+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIButton+Extension.swift"; sourceTree = ""; }; + F3374A6C2D5E6E79002A38F9 /* NCAssistantTaskV2.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCAssistantTaskV2.swift; sourceTree = ""; }; F33918C32C7CD8F2002D9AA1 /* FileNameValidator+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FileNameValidator+Extensions.swift"; sourceTree = ""; }; F33EE6F12BF4C9B200CA1A51 /* PKCS12.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PKCS12.swift; sourceTree = ""; }; F343A4B22A1E01FF00DDA874 /* PHAsset+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PHAsset+Extension.swift"; sourceTree = ""; }; @@ -2061,6 +2063,7 @@ isa = PBXGroup; children = ( F3A047932BD2668800658E7B /* NCAssistantTask.swift */, + F3374A6C2D5E6E79002A38F9 /* NCAssistantTaskV2.swift */, ); path = Models; sourceTree = ""; @@ -4639,6 +4642,7 @@ F3A0479A2BD2668800658E7B /* NCAssistantTaskDetail.swift in Sources */, F38F71252B6BBDC300473CDC /* NCCollectionViewCommonSelectTabBar.swift in Sources */, F7E4D9C422ED929B003675FD /* NCShareCommentsCell.swift in Sources */, + F3374A6D2D5E6E79002A38F9 /* NCAssistantTaskV2.swift in Sources */, F7327E202B73A42F00A462C7 /* NCNetworking+Download.swift in Sources */, F76882332C0DD1E7001CF441 /* NCDisplayModel.swift in Sources */, F717402E24F699A5000C87D5 /* NCFavorite.swift in Sources */, diff --git a/iOSClient/Assistant/Models/NCAssistantTask.swift b/iOSClient/Assistant/Models/NCAssistantTask.swift index 591a209761..63ed67e635 100644 --- a/iOSClient/Assistant/Models/NCAssistantTask.swift +++ b/iOSClient/Assistant/Models/NCAssistantTask.swift @@ -12,15 +12,25 @@ import NextcloudKit import SwiftUI class NCAssistantTask: ObservableObject { - @Published var types: [NKTextProcessingTaskTypeV2.TaskTypes] = [] - @Published var filteredTasks: [NKTextProcessingTask] = [] - @Published var selectedType: NKTextProcessingTaskType? - @Published var selectedTask: NKTextProcessingTask? +// @Published var types: [NKTextProcessingTaskType] = [] +// @Published var filteredTasks: [NKTextProcessingTask] = [] +// @Published var selectedType: NKTextProcessingTaskType? +// @Published var selectedTask: NKTextProcessingTask? + + let useV2 = true + + @Published var typesV2: [NKTextProcessingTaskTypeV2.TaskTypeData] = [] + @Published var filteredTasksV2: [NKTextProcessingTaskV2.Task] = [] + @Published var selectedTypeV2: NKTextProcessingTaskTypeV2.TaskTypeData? + @Published var selectedTaskV2: NKTextProcessingTaskV2.Task? + @Published var hasError: Bool = false @Published var isLoading: Bool = false @Published var controller: NCMainTabBarController? private var tasks: [NKTextProcessingTask] = [] + private var tasksV2: [NKTextProcessingTaskV2.Task] = [] + private let excludedTypeIds = ["OCA\\ContextChat\\TextProcessing\\ContextChatTaskType"] private var session: NCSession.Session { NCSession.shared.getSession(controller: controller) @@ -45,6 +55,16 @@ class NCAssistantTask: ObservableObject { self.filteredTasks = filteredTasks.sorted(by: { $0.completionExpectedAt ?? 0 > $1.completionExpectedAt ?? 0 }) } + func filterTasksV2(ofType type: NKTextProcessingTaskTypeV2.TaskTypeData?) { + if let type { + self.filteredTasksV2 = tasksV2.filter({ $0.type == type.id }) + } else { + self.filteredTasksV2 = tasksV2 + } + + self.filteredTasksV2 = filteredTasksV2.sorted(by: { $0.completionExpectedAt ?? 0 > $1.completionExpectedAt ?? 0 }) + } + func selectTaskType(_ type: NKTextProcessingTaskType?) { selectedType = type filterTasks(ofType: self.selectedType) diff --git a/iOSClient/Assistant/Models/NCAssistantTaskV2.swift b/iOSClient/Assistant/Models/NCAssistantTaskV2.swift new file mode 100644 index 0000000000..d8333376d9 --- /dev/null +++ b/iOSClient/Assistant/Models/NCAssistantTaskV2.swift @@ -0,0 +1,201 @@ +// +// NCAssistantModel.swift +// Nextcloud +// +// Created by Milen on 08.04.24. +// Copyright © 2024 Marino Faggiana. All rights reserved. +// + +import Foundation +import UIKit +import NextcloudKit +import SwiftUI + +class NCAssistantTask: ObservableObject { +// @Published var types: [NKTextProcessingTaskType] = [] +// @Published var filteredTasks: [NKTextProcessingTask] = [] +// @Published var selectedType: NKTextProcessingTaskType? +// @Published var selectedTask: NKTextProcessingTask? + + let useV2 = true + + @Published var types: [NKTextProcessingTaskTypeV2.TaskTypeData] = [] + @Published var filteredTasks: [NKTextProcessingTaskV2.Task] = [] + @Published var selectedType: NKTextProcessingTaskTypeV2.TaskTypeData? + @Published var selectedTask: NKTextProcessingTaskV2.Task? + + @Published var hasError: Bool = false + @Published var isLoading: Bool = false + @Published var controller: NCMainTabBarController? + + private var tasks: [NKTextProcessingTaskV2.Task] = [] + + private let excludedTypeIds = ["OCA\\ContextChat\\TextProcessing\\ContextChatTaskType"] + private var session: NCSession.Session { + NCSession.shared.getSession(controller: controller) + } + + init(controller: NCMainTabBarController?) { + self.controller = controller + load() + } + + func load() { + loadAllTypes() + } + + func filterTasks(ofType type: NKTextProcessingTaskTypeV2.TaskTypeData?) { + if let type { + self.filteredTasks = tasks.filter({ $0.type == type.id }) + } else { + self.filteredTasks = tasks + } + + self.filteredTasks = filteredTasks.sorted(by: { $0.completionExpectedAt ?? 0 > $1.completionExpectedAt ?? 0 }) + } + + func selectTaskType(_ type: NKTextProcessingTaskTypeV2.TaskTypeData?) { + selectedType = type + filterTasks(ofType: self.selectedType) + } + + func selectTask(_ task: NKTextProcessingTaskV2.Task) { + selectedTask = task +// guard let id = task.id else { return } + isLoading = true + + NextcloudKit.shared.textProcessingGetTasksV2(taskType: task.type ?? "", account: session.account, completion: { account, tasks, responseData, error in + self.isLoading = false + + if error != .success { + self.hasError = true + return + } + + self.selectedTask = task + }) + } + + func scheduleTask(input: String) { + isLoading = true + + NextcloudKit.shared.textProcessingScheduleV2(input: input, taskType: selectedType!, account: session.account, completion: { account, task, responseData, error in + self.isLoading = false + + if error != .success { + self.hasError = true + return + } + + guard let task else { return } + + withAnimation { + self.tasks.insert(task, at: 0) + self.filteredTasks.insert(task, at: 0) + } + }) + + } + + func deleteTask(_ task: NKTextProcessingTask) { + guard let id = task.id else { return } + isLoading = true + + NextcloudKit.shared.textProcessingDeleteTask(taskId: id, account: session.account) { _, task, _, error in + self.isLoading = false + + if error != .success { + self.hasError = true + return + } + + withAnimation { + self.tasks.removeAll(where: { $0.id == task?.id }) + self.filteredTasks.removeAll(where: { $0.id == task?.id }) + } + + } + } + + private func loadAllTypes() { + isLoading = true + + NextcloudKit.shared.textProcessingGetTypes(account: session.account) { _, types, _, error in + self.isLoading = false + + if error != .success { + self.hasError = true + return + } + + guard let filteredTypes = types?.filter({ !self.excludedTypeIds.contains($0.id ?? "")}), !filteredTypes.isEmpty else { return } + + withAnimation { + self.types = filteredTypes + } + + if self.selectedType == nil { + self.selectTaskType(filteredTypes.first) + } + + self.loadAllTasks() + } + } + + private func loadAllTasks(appId: String = "assistant") { + isLoading = true + + NextcloudKit.shared.textProcessingTaskList(appId: appId, account: session.account) { _, tasks, _, error in + self.isLoading = false + + if error != .success { + self.hasError = true + return + } + + guard let tasks = tasks else { return } + self.tasks = tasks + self.filterTasks(ofType: self.selectedType) + } + } +} + +extension NCAssistantTask { + public func loadDummyData() { + let loremIpsum = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum." + + var tasks: [NKTextProcessingTask] = [] + + for index in 1...10 { + tasks.append(NKTextProcessingTask(id: index, type: "OCP\\TextProcessing\\FreePromptTaskType", status: index, userId: "christine", appId: "assistant", input: loremIpsum, output: loremIpsum, identifier: "", completionExpectedAt: 1712666412)) + } + + self.types = [ + NKTextProcessingTaskType(id: "1", name: "Free Prompt", description: ""), + NKTextProcessingTaskType(id: "2", name: "Summarize", description: ""), + NKTextProcessingTaskType(id: "3", name: "Generate headline", description: ""), + NKTextProcessingTaskType(id: "4", name: "Reformulate", description: "") + ] + self.tasks = tasks + self.filteredTasks = tasks + self.selectedType = types[0] + self.selectedTask = filteredTasks[0] + + } +} + +extension NKTextProcessingTask { + struct StatusInfo { + let stringKey, imageSystemName: String + } + + var statusInfo: StatusInfo { + return switch status { + case 1: StatusInfo(stringKey: "_assistant_task_scheduled_", imageSystemName: "clock") + case 2: StatusInfo(stringKey: "_assistant_task_in_progress_", imageSystemName: "clock.badge") + case 3: StatusInfo(stringKey: "_assistant_task_completed_", imageSystemName: "checkmark.circle") + case 4: StatusInfo(stringKey: "_assistant_task_failed_", imageSystemName: "exclamationmark.circle") + default: StatusInfo(stringKey: "_assistant_task_unknown_", imageSystemName: "questionmark.circle") + } + } +} From fc5c7e3467e79c4b7a633e494d0f97ca25f340da Mon Sep 17 00:00:00 2001 From: Milen Pivchev Date: Fri, 14 Feb 2025 14:57:46 +0100 Subject: [PATCH 04/20] WIP Signed-off-by: Milen Pivchev --- .../Assistant/Models/NCAssistantTask.swift | 30 ++--- .../Assistant/Models/NCAssistantTaskV2.swift | 120 +++++++++--------- iOSClient/Assistant/NCAssistant.swift | 76 ++++++++--- 3 files changed, 125 insertions(+), 101 deletions(-) diff --git a/iOSClient/Assistant/Models/NCAssistantTask.swift b/iOSClient/Assistant/Models/NCAssistantTask.swift index 63ed67e635..83aeb46034 100644 --- a/iOSClient/Assistant/Models/NCAssistantTask.swift +++ b/iOSClient/Assistant/Models/NCAssistantTask.swift @@ -12,24 +12,24 @@ import NextcloudKit import SwiftUI class NCAssistantTask: ObservableObject { -// @Published var types: [NKTextProcessingTaskType] = [] -// @Published var filteredTasks: [NKTextProcessingTask] = [] -// @Published var selectedType: NKTextProcessingTaskType? -// @Published var selectedTask: NKTextProcessingTask? + @Published var types: [NKTextProcessingTaskType] = [] + @Published var filteredTasks: [NKTextProcessingTask] = [] + @Published var selectedType: NKTextProcessingTaskType? + @Published var selectedTask: NKTextProcessingTask? - let useV2 = true +// let useV2 = true - @Published var typesV2: [NKTextProcessingTaskTypeV2.TaskTypeData] = [] - @Published var filteredTasksV2: [NKTextProcessingTaskV2.Task] = [] - @Published var selectedTypeV2: NKTextProcessingTaskTypeV2.TaskTypeData? - @Published var selectedTaskV2: NKTextProcessingTaskV2.Task? +// @Published var typesV2: [NKTextProcessingTaskTypeV2.TaskTypeData] = [] +// @Published var filteredTasksV2: [NKTextProcessingTaskV2.Task] = [] +// @Published var selectedTypeV2: NKTextProcessingTaskTypeV2.TaskTypeData? +// @Published var selectedTaskV2: NKTextProcessingTaskV2.Task? @Published var hasError: Bool = false @Published var isLoading: Bool = false @Published var controller: NCMainTabBarController? private var tasks: [NKTextProcessingTask] = [] - private var tasksV2: [NKTextProcessingTaskV2.Task] = [] +// private var tasksV2: [NKTextProcessingTaskV2.Task] = [] private let excludedTypeIds = ["OCA\\ContextChat\\TextProcessing\\ContextChatTaskType"] private var session: NCSession.Session { @@ -55,16 +55,6 @@ class NCAssistantTask: ObservableObject { self.filteredTasks = filteredTasks.sorted(by: { $0.completionExpectedAt ?? 0 > $1.completionExpectedAt ?? 0 }) } - func filterTasksV2(ofType type: NKTextProcessingTaskTypeV2.TaskTypeData?) { - if let type { - self.filteredTasksV2 = tasksV2.filter({ $0.type == type.id }) - } else { - self.filteredTasksV2 = tasksV2 - } - - self.filteredTasksV2 = filteredTasksV2.sorted(by: { $0.completionExpectedAt ?? 0 > $1.completionExpectedAt ?? 0 }) - } - func selectTaskType(_ type: NKTextProcessingTaskType?) { selectedType = type filterTasks(ofType: self.selectedType) diff --git a/iOSClient/Assistant/Models/NCAssistantTaskV2.swift b/iOSClient/Assistant/Models/NCAssistantTaskV2.swift index d8333376d9..856584be92 100644 --- a/iOSClient/Assistant/Models/NCAssistantTaskV2.swift +++ b/iOSClient/Assistant/Models/NCAssistantTaskV2.swift @@ -11,7 +11,7 @@ import UIKit import NextcloudKit import SwiftUI -class NCAssistantTask: ObservableObject { +class NCAssistantTaskV2: ObservableObject { // @Published var types: [NKTextProcessingTaskType] = [] // @Published var filteredTasks: [NKTextProcessingTask] = [] // @Published var selectedType: NKTextProcessingTaskType? @@ -19,18 +19,18 @@ class NCAssistantTask: ObservableObject { let useV2 = true - @Published var types: [NKTextProcessingTaskTypeV2.TaskTypeData] = [] - @Published var filteredTasks: [NKTextProcessingTaskV2.Task] = [] - @Published var selectedType: NKTextProcessingTaskTypeV2.TaskTypeData? - @Published var selectedTask: NKTextProcessingTaskV2.Task? + @Published var types: [TaskTypeData] = [] + @Published var filteredTasks: [AssistantTask] = [] + @Published var selectedType: TaskTypeData? + @Published var selectedTask: AssistantTask? @Published var hasError: Bool = false @Published var isLoading: Bool = false @Published var controller: NCMainTabBarController? - private var tasks: [NKTextProcessingTaskV2.Task] = [] + private var tasks: [AssistantTask] = [] - private let excludedTypeIds = ["OCA\\ContextChat\\TextProcessing\\ContextChatTaskType"] +// private let excludedTypeIds = ["OCA\\ContextChat\\TextProcessing\\ContextChatTaskType"] private var session: NCSession.Session { NCSession.shared.getSession(controller: controller) } @@ -44,7 +44,7 @@ class NCAssistantTask: ObservableObject { loadAllTypes() } - func filterTasks(ofType type: NKTextProcessingTaskTypeV2.TaskTypeData?) { + func filterTasks(ofType type: TaskTypeData?) { if let type { self.filteredTasks = tasks.filter({ $0.type == type.id }) } else { @@ -54,12 +54,12 @@ class NCAssistantTask: ObservableObject { self.filteredTasks = filteredTasks.sorted(by: { $0.completionExpectedAt ?? 0 > $1.completionExpectedAt ?? 0 }) } - func selectTaskType(_ type: NKTextProcessingTaskTypeV2.TaskTypeData?) { + func selectTaskType(_ type: TaskTypeData?) { selectedType = type filterTasks(ofType: self.selectedType) } - func selectTask(_ task: NKTextProcessingTaskV2.Task) { + func selectTask(_ task: AssistantTask) { selectedTask = task // guard let id = task.id else { return } isLoading = true @@ -97,11 +97,11 @@ class NCAssistantTask: ObservableObject { } - func deleteTask(_ task: NKTextProcessingTask) { - guard let id = task.id else { return } + func deleteTask(_ task: AssistantTask) { isLoading = true - NextcloudKit.shared.textProcessingDeleteTask(taskId: id, account: session.account) { _, task, _, error in + NextcloudKit.shared.textProcessingDeleteTaskV2(taskId: task.id, account: session.account) { account, responseData, error in + self.isLoading = false if error != .success { @@ -110,8 +110,8 @@ class NCAssistantTask: ObservableObject { } withAnimation { - self.tasks.removeAll(where: { $0.id == task?.id }) - self.filteredTasks.removeAll(where: { $0.id == task?.id }) + self.tasks.removeAll(where: { $0.id == task.id }) + self.filteredTasks.removeAll(where: { $0.id == task.id }) } } @@ -120,7 +120,7 @@ class NCAssistantTask: ObservableObject { private func loadAllTypes() { isLoading = true - NextcloudKit.shared.textProcessingGetTypes(account: session.account) { _, types, _, error in + NextcloudKit.shared.textProcessingGetTypesV2(account: session.account) { account, types, responseData, error in self.isLoading = false if error != .success { @@ -128,14 +128,14 @@ class NCAssistantTask: ObservableObject { return } - guard let filteredTypes = types?.filter({ !self.excludedTypeIds.contains($0.id ?? "")}), !filteredTypes.isEmpty else { return } +// guard let filteredTypes = types?.types.filter({ !self.excludedTypeIds.contains($0.id)}), !filteredTypes.isEmpty else { return } withAnimation { - self.types = filteredTypes + self.types = types ?? [] } if self.selectedType == nil { - self.selectTaskType(filteredTypes.first) + self.selectTaskType(types?.first) } self.loadAllTasks() @@ -145,7 +145,7 @@ class NCAssistantTask: ObservableObject { private func loadAllTasks(appId: String = "assistant") { isLoading = true - NextcloudKit.shared.textProcessingTaskList(appId: appId, account: session.account) { _, tasks, _, error in + NextcloudKit.shared.textProcessingGetTasksV2(taskType: "core:text2text", account: session.account) { account, tasks, responseData, error in self.isLoading = false if error != .success { @@ -154,48 +154,48 @@ class NCAssistantTask: ObservableObject { } guard let tasks = tasks else { return } - self.tasks = tasks + self.tasks = tasks.tasks self.filterTasks(ofType: self.selectedType) } } } -extension NCAssistantTask { - public func loadDummyData() { - let loremIpsum = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum." - - var tasks: [NKTextProcessingTask] = [] - - for index in 1...10 { - tasks.append(NKTextProcessingTask(id: index, type: "OCP\\TextProcessing\\FreePromptTaskType", status: index, userId: "christine", appId: "assistant", input: loremIpsum, output: loremIpsum, identifier: "", completionExpectedAt: 1712666412)) - } - - self.types = [ - NKTextProcessingTaskType(id: "1", name: "Free Prompt", description: ""), - NKTextProcessingTaskType(id: "2", name: "Summarize", description: ""), - NKTextProcessingTaskType(id: "3", name: "Generate headline", description: ""), - NKTextProcessingTaskType(id: "4", name: "Reformulate", description: "") - ] - self.tasks = tasks - self.filteredTasks = tasks - self.selectedType = types[0] - self.selectedTask = filteredTasks[0] - - } -} - -extension NKTextProcessingTask { - struct StatusInfo { - let stringKey, imageSystemName: String - } - - var statusInfo: StatusInfo { - return switch status { - case 1: StatusInfo(stringKey: "_assistant_task_scheduled_", imageSystemName: "clock") - case 2: StatusInfo(stringKey: "_assistant_task_in_progress_", imageSystemName: "clock.badge") - case 3: StatusInfo(stringKey: "_assistant_task_completed_", imageSystemName: "checkmark.circle") - case 4: StatusInfo(stringKey: "_assistant_task_failed_", imageSystemName: "exclamationmark.circle") - default: StatusInfo(stringKey: "_assistant_task_unknown_", imageSystemName: "questionmark.circle") - } - } -} +//extension NCAssistantTaskV2 { +// public func loadDummyData() { +// let loremIpsum = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum." +// +// var tasks: [NKTextProcessingTask] = [] +// +// for index in 1...10 { +// tasks.append(NKTextProcessingTask(id: index, type: "OCP\\TextProcessing\\FreePromptTaskType", status: index, userId: "christine", appId: "assistant", input: loremIpsum, output: loremIpsum, identifier: "", completionExpectedAt: 1712666412)) +// } +// +// self.types = [ +// NKTextProcessingTaskType(id: "1", name: "Free Prompt", description: ""), +// NKTextProcessingTaskType(id: "2", name: "Summarize", description: ""), +// NKTextProcessingTaskType(id: "3", name: "Generate headline", description: ""), +// NKTextProcessingTaskType(id: "4", name: "Reformulate", description: "") +// ] +// self.tasks = tasks +// self.filteredTasks = tasks +// self.selectedType = types[0] +// self.selectedTask = filteredTasks[0] +// +// } +//} +// +//extension NKTextProcessingTask { +// struct StatusInfo { +// let stringKey, imageSystemName: String +// } +// +// var statusInfo: StatusInfo { +// return switch status { +// case 1: StatusInfo(stringKey: "_assistant_task_scheduled_", imageSystemName: "clock") +// case 2: StatusInfo(stringKey: "_assistant_task_in_progress_", imageSystemName: "clock.badge") +// case 3: StatusInfo(stringKey: "_assistant_task_completed_", imageSystemName: "checkmark.circle") +// case 4: StatusInfo(stringKey: "_assistant_task_failed_", imageSystemName: "exclamationmark.circle") +// default: StatusInfo(stringKey: "_assistant_task_unknown_", imageSystemName: "questionmark.circle") +// } +// } +//} diff --git a/iOSClient/Assistant/NCAssistant.swift b/iOSClient/Assistant/NCAssistant.swift index bfea931215..0ddaed7dba 100644 --- a/iOSClient/Assistant/NCAssistant.swift +++ b/iOSClient/Assistant/NCAssistant.swift @@ -11,22 +11,19 @@ import NextcloudKit import PopupView struct NCAssistant: View { + let useV2 = true @EnvironmentObject var model: NCAssistantTask + @EnvironmentObject var modelV2: NCAssistantTaskV2 @State var presentNewTaskDialog = false @State var input = "" @Environment(\.presentationMode) var presentationMode var body: some View { NavigationView { - ZStack { - TaskList() + TaskList() + + EmptyView() - if model.types.isEmpty, !model.isLoading { - NCAssistantEmptyView(titleKey: "_no_types_", subtitleKey: "_no_types_subtitle_") - } else if model.filteredTasks.isEmpty, !model.isLoading { - NCAssistantEmptyView(titleKey: "_no_tasks_", subtitleKey: "_create_task_subtitle_") - } - } .toolbar { ToolbarItem(placement: .topBarLeading) { Button(action: { @@ -41,25 +38,14 @@ struct NCAssistant: View { .font(Font.system(.body).weight(.light)) .foregroundStyle(Color(NCBrandColor.shared.iconImageColor)) } - .disabled(model.selectedType == nil) + .disabled(useV2 ? modelV2.selectedType == nil : model.selectedType == nil) } } .navigationBarTitleDisplayMode(.inline) .navigationTitle(NSLocalizedString("_assistant_", comment: "")) .frame(maxWidth: .infinity, maxHeight: .infinity) .safeAreaInset(edge: .top, spacing: -10) { - ScrollViewReader { scrollProxy in - ScrollView(.horizontal, showsIndicators: false) { - LazyHStack { - ForEach(model.types, id: \.id) { type in - TypeButton(taskType: type, scrollProxy: scrollProxy) - } - } - .padding(20) - .frame(height: 50) - } - .background(.ultraThinMaterial) - } + ExtractedView() } } .navigationViewStyle(.stack) @@ -223,3 +209,51 @@ private struct CustomLabelStyle: LabelStyle { } } } + +struct EmptyView: View { + var body: some View { + { + + if useV2 { + if ((modelV2.types?.isEmpty) != nil), !modelV2.isLoading { + NCAssistantEmptyView(titleKey: "_no_types_", subtitleKey: "_no_types_subtitle_") + } else if modelV2.filteredTasks.isEmpty, !model.isLoading { + NCAssistantEmptyView(titleKey: "_no_tasks_", subtitleKey: "_create_task_subtitle_") + } + } else { + if model.types.isEmpty, !model.isLoading { + NCAssistantEmptyView(titleKey: "_no_types_", subtitleKey: "_no_types_subtitle_") + } else if model.filteredTasks.isEmpty, !model.isLoading { + NCAssistantEmptyView(titleKey: "_no_tasks_", subtitleKey: "_create_task_subtitle_") + } + } + } + } +} + +struct ExtractedView: View { + @EnvironmentObject var model: NCAssistantTask + @EnvironmentObject var modelV2: NCAssistantTaskV2 + let useV2: Bool + + var body: some View { + ScrollViewReader { scrollProxy in + ScrollView(.horizontal, showsIndicators: false) { + LazyHStack { + if useV2 { + ForEach(modelV2.types, id: \.id) { type in + TypeButton(taskType: type, scrollProxy: scrollProxy) + } + } else { + ForEach(model.types, id: \.id) { type in + TypeButton(taskType: type, scrollProxy: scrollProxy) + } + } + } + .padding(20) + .frame(height: 50) + } + .background(.ultraThinMaterial) + } + } +} From 12c80c18b8a57d3944b43c8775c4e9f0b5517fee Mon Sep 17 00:00:00 2001 From: Milen Pivchev Date: Tue, 18 Feb 2025 16:55:21 +0100 Subject: [PATCH 05/20] WIP Signed-off-by: Milen Pivchev --- Nextcloud.xcodeproj/project.pbxproj | 34 ++- .../Components/AssistantLabelStyle.swift | 20 ++ .../NCAssistantEmptyView.swift | 2 +- .../Assistant/Components/StatusInfo.swift | 34 +++ .../NCAssistantCreateNewTask.swift | 9 +- .../Assistant/Models/NCAssistantModel.swift | 264 ++++++++++++++++++ .../Assistant/Models/NCAssistantTask.swift | 201 ------------- .../Assistant/Models/NCAssistantTaskV2.swift | 201 ------------- iOSClient/Assistant/NCAssistant.swift | 140 ++++------ .../Task Detail/NCAssistantTaskDetail.swift | 44 ++- iOSClient/More/Cells/BaseNCMoreCell.swift | 2 +- .../More/Cells/NCMoreAppSuggestionsCell.swift | 6 +- iOSClient/More/NCMore.swift | 7 +- iOSClient/NCGlobal.swift | 1 + 14 files changed, 428 insertions(+), 537 deletions(-) create mode 100644 iOSClient/Assistant/Components/AssistantLabelStyle.swift rename iOSClient/Assistant/{ => Components}/NCAssistantEmptyView.swift (95%) create mode 100644 iOSClient/Assistant/Components/StatusInfo.swift create mode 100644 iOSClient/Assistant/Models/NCAssistantModel.swift delete mode 100644 iOSClient/Assistant/Models/NCAssistantTask.swift delete mode 100644 iOSClient/Assistant/Models/NCAssistantTaskV2.swift diff --git a/Nextcloud.xcodeproj/project.pbxproj b/Nextcloud.xcodeproj/project.pbxproj index 0177f3fdda..fa2912267f 100644 --- a/Nextcloud.xcodeproj/project.pbxproj +++ b/Nextcloud.xcodeproj/project.pbxproj @@ -68,7 +68,8 @@ F314F1142A30E2DE00BC7FAB /* View+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7E8A390295DC5E0006CB2D0 /* View+Extension.swift */; }; F321DA8A2B71205A00DDA0E6 /* NCTrashSelectTabBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = F321DA892B71205A00DDA0E6 /* NCTrashSelectTabBar.swift */; }; F32FADA92D1176E3007035E2 /* UIButton+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = F32FADA82D1176DE007035E2 /* UIButton+Extension.swift */; }; - F3374A6D2D5E6E79002A38F9 /* NCAssistantTaskV2.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3374A6C2D5E6E79002A38F9 /* NCAssistantTaskV2.swift */; }; + F3374A812D64AB9F002A38F9 /* StatusInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3374A802D64AB9E002A38F9 /* StatusInfo.swift */; }; + F3374A842D64AC31002A38F9 /* AssistantLabelStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3374A832D64AC2C002A38F9 /* AssistantLabelStyle.swift */; }; F33918C42C7CD8F2002D9AA1 /* FileNameValidator+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = F33918C32C7CD8F2002D9AA1 /* FileNameValidator+Extensions.swift */; }; F33918C52C7CD8F2002D9AA1 /* FileNameValidator+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = F33918C32C7CD8F2002D9AA1 /* FileNameValidator+Extensions.swift */; }; F33918C62C7CD8F2002D9AA1 /* FileNameValidator+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = F33918C32C7CD8F2002D9AA1 /* FileNameValidator+Extensions.swift */; }; @@ -165,7 +166,7 @@ F39A1EE22D0AF8A400DAD522 /* Albums.swift in Sources */ = {isa = PBXBuildFile; fileRef = F39A1EE12D0AF8A200DAD522 /* Albums.swift */; }; F3A047972BD2668800658E7B /* NCAssistantEmptyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3A0478F2BD2668800658E7B /* NCAssistantEmptyView.swift */; }; F3A047982BD2668800658E7B /* NCAssistantCreateNewTask.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3A047912BD2668800658E7B /* NCAssistantCreateNewTask.swift */; }; - F3A047992BD2668800658E7B /* NCAssistantTask.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3A047932BD2668800658E7B /* NCAssistantTask.swift */; }; + F3A047992BD2668800658E7B /* NCAssistantModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3A047932BD2668800658E7B /* NCAssistantModel.swift */; }; F3A0479A2BD2668800658E7B /* NCAssistantTaskDetail.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3A047952BD2668800658E7B /* NCAssistantTaskDetail.swift */; }; F3A0479B2BD2668800658E7B /* NCAssistant.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3A047962BD2668800658E7B /* NCAssistant.swift */; }; F3A0479E2BD268B500658E7B /* PopupView in Frameworks */ = {isa = PBXBuildFile; productRef = F3A0479D2BD268B500658E7B /* PopupView */; }; @@ -1228,7 +1229,8 @@ F310B1EE2BA862F1001C42F5 /* NCViewerMedia+VisionKit.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NCViewerMedia+VisionKit.swift"; sourceTree = ""; }; F321DA892B71205A00DDA0E6 /* NCTrashSelectTabBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCTrashSelectTabBar.swift; sourceTree = ""; }; F32FADA82D1176DE007035E2 /* UIButton+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIButton+Extension.swift"; sourceTree = ""; }; - F3374A6C2D5E6E79002A38F9 /* NCAssistantTaskV2.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCAssistantTaskV2.swift; sourceTree = ""; }; + F3374A802D64AB9E002A38F9 /* StatusInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusInfo.swift; sourceTree = ""; }; + F3374A832D64AC2C002A38F9 /* AssistantLabelStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssistantLabelStyle.swift; sourceTree = ""; }; F33918C32C7CD8F2002D9AA1 /* FileNameValidator+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FileNameValidator+Extensions.swift"; sourceTree = ""; }; F33EE6F12BF4C9B200CA1A51 /* PKCS12.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PKCS12.swift; sourceTree = ""; }; F343A4B22A1E01FF00DDA874 /* PHAsset+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PHAsset+Extension.swift"; sourceTree = ""; }; @@ -1247,7 +1249,7 @@ F39A1EE12D0AF8A200DAD522 /* Albums.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Albums.swift; sourceTree = ""; }; F3A0478F2BD2668800658E7B /* NCAssistantEmptyView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NCAssistantEmptyView.swift; sourceTree = ""; }; F3A047912BD2668800658E7B /* NCAssistantCreateNewTask.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NCAssistantCreateNewTask.swift; sourceTree = ""; }; - F3A047932BD2668800658E7B /* NCAssistantTask.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NCAssistantTask.swift; sourceTree = ""; }; + F3A047932BD2668800658E7B /* NCAssistantModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NCAssistantModel.swift; sourceTree = ""; }; F3A047952BD2668800658E7B /* NCAssistantTaskDetail.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NCAssistantTaskDetail.swift; sourceTree = ""; }; F3A047962BD2668800658E7B /* NCAssistant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NCAssistant.swift; sourceTree = ""; }; F3A7AFC52A41AA82001FC89C /* BaseUIXCTestCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseUIXCTestCase.swift; sourceTree = ""; }; @@ -2020,6 +2022,16 @@ path = Tests; sourceTree = ""; }; + F3374A7F2D64AB40002A38F9 /* Components */ = { + isa = PBXGroup; + children = ( + F3374A832D64AC2C002A38F9 /* AssistantLabelStyle.swift */, + F3374A802D64AB9E002A38F9 /* StatusInfo.swift */, + F3A0478F2BD2668800658E7B /* NCAssistantEmptyView.swift */, + ); + path = Components; + sourceTree = ""; + }; F37208762BAB4B4B006B5430 /* Common */ = { isa = PBXGroup; children = ( @@ -2042,11 +2054,11 @@ F3A0478E2BD2668800658E7B /* Assistant */ = { isa = PBXGroup; children = ( + F3A047962BD2668800658E7B /* NCAssistant.swift */, F3A047902BD2668800658E7B /* Create Task */, - F3A047922BD2668800658E7B /* Models */, F3A047942BD2668800658E7B /* Task Detail */, - F3A047962BD2668800658E7B /* NCAssistant.swift */, - F3A0478F2BD2668800658E7B /* NCAssistantEmptyView.swift */, + F3374A7F2D64AB40002A38F9 /* Components */, + F3A047922BD2668800658E7B /* Models */, ); path = Assistant; sourceTree = ""; @@ -2062,8 +2074,7 @@ F3A047922BD2668800658E7B /* Models */ = { isa = PBXGroup; children = ( - F3A047932BD2668800658E7B /* NCAssistantTask.swift */, - F3374A6C2D5E6E79002A38F9 /* NCAssistantTaskV2.swift */, + F3A047932BD2668800658E7B /* NCAssistantModel.swift */, ); path = Models; sourceTree = ""; @@ -4364,6 +4375,7 @@ F71CD6CA2930D7B1006C95C1 /* NCApplicationHandle.swift in Sources */, F3754A7D2CF87D600009312E /* SetupPasscodeView.swift in Sources */, F73EF7D72B0226080087E6E9 /* NCManageDatabase+Tip.swift in Sources */, + F3374A842D64AC31002A38F9 /* AssistantLabelStyle.swift in Sources */, F74BAE172C7E2F4E0028D4FA /* FileProviderDomain.swift in Sources */, F76882402C0DD30B001CF441 /* ViewOnAppear.swift in Sources */, F790110E21415BF600D7B136 /* NCViewerRichDocument.swift in Sources */, @@ -4432,6 +4444,7 @@ F77A697D250A0FBC00FF1708 /* NCCollectionViewCommon+Menu.swift in Sources */, F7BF9D822934CA21009EE9A6 /* NCManageDatabase+LayoutForView.swift in Sources */, F351D1A62D0AF25000930F94 /* PHAssetCollection+Extension.swift in Sources */, + F3374A812D64AB9F002A38F9 /* StatusInfo.swift in Sources */, AF7E504E27A2D8FF00B5E4AF /* UIBarButton+Extension.swift in Sources */, F7A846DE2BB01ACB0024816F /* NCTrashCellProtocol.swift in Sources */, F799DF852C4B7E56003410B5 /* NCSectionHeader.swift in Sources */, @@ -4574,7 +4587,7 @@ AF3FDCC22796ECC300710F60 /* NCTrash+CollectionView.swift in Sources */, F70D7C3725FFBF82002B9E34 /* NCCollectionViewCommon.swift in Sources */, F76D364628A4F8BF00214537 /* NCActivityIndicator.swift in Sources */, - F3A047992BD2668800658E7B /* NCAssistantTask.swift in Sources */, + F3A047992BD2668800658E7B /* NCAssistantModel.swift in Sources */, F76882322C0DD1E7001CF441 /* NCAutoUploadView.swift in Sources */, F36E64F72B9245210085ABB5 /* NCCollectionViewCommon+SelectTabBar.swift in Sources */, F79A65C62191D95E00FF6DCC /* NCSelect.swift in Sources */, @@ -4642,7 +4655,6 @@ F3A0479A2BD2668800658E7B /* NCAssistantTaskDetail.swift in Sources */, F38F71252B6BBDC300473CDC /* NCCollectionViewCommonSelectTabBar.swift in Sources */, F7E4D9C422ED929B003675FD /* NCShareCommentsCell.swift in Sources */, - F3374A6D2D5E6E79002A38F9 /* NCAssistantTaskV2.swift in Sources */, F7327E202B73A42F00A462C7 /* NCNetworking+Download.swift in Sources */, F76882332C0DD1E7001CF441 /* NCDisplayModel.swift in Sources */, F717402E24F699A5000C87D5 /* NCFavorite.swift in Sources */, diff --git a/iOSClient/Assistant/Components/AssistantLabelStyle.swift b/iOSClient/Assistant/Components/AssistantLabelStyle.swift new file mode 100644 index 0000000000..1d4b021489 --- /dev/null +++ b/iOSClient/Assistant/Components/AssistantLabelStyle.swift @@ -0,0 +1,20 @@ +// +// AssistantLabelStyle.swift +// Nextcloud +// +// Created by Milen Pivchev on 18.02.25. +// Copyright © 2025 Marino Faggiana. All rights reserved. +// + +import SwiftUI + +struct CustomLabelStyle: LabelStyle { + var spacing: Double = 5 + + func makeBody(configuration: Configuration) -> some View { + HStack(spacing: spacing) { + configuration.icon + configuration.title + } + } +} diff --git a/iOSClient/Assistant/NCAssistantEmptyView.swift b/iOSClient/Assistant/Components/NCAssistantEmptyView.swift similarity index 95% rename from iOSClient/Assistant/NCAssistantEmptyView.swift rename to iOSClient/Assistant/Components/NCAssistantEmptyView.swift index d8781d2c91..d24cca06ca 100644 --- a/iOSClient/Assistant/NCAssistantEmptyView.swift +++ b/iOSClient/Assistant/Components/NCAssistantEmptyView.swift @@ -9,7 +9,7 @@ import SwiftUI struct NCAssistantEmptyView: View { - @EnvironmentObject var model: NCAssistantTask + @EnvironmentObject var model: NCAssistantModel let titleKey, subtitleKey: String var body: some View { diff --git a/iOSClient/Assistant/Components/StatusInfo.swift b/iOSClient/Assistant/Components/StatusInfo.swift new file mode 100644 index 0000000000..cb9e5d4501 --- /dev/null +++ b/iOSClient/Assistant/Components/StatusInfo.swift @@ -0,0 +1,34 @@ +// +// StatusInfo.swift +// Nextcloud +// +// Created by Milen Pivchev on 18.02.25. +// Copyright © 2025 Marino Faggiana. All rights reserved. +// + +import SwiftUI +import NextcloudKit + +struct StatusInfo: View { + let task: AssistantTask + var showStatusText = false + + var body: some View { + HStack { + Label( + title: { + Text("\(task.statusDate) (\(showStatusText ? NSLocalizedString(task.statusInfo.stringKey, comment: "") : ""))") + .font(.callout) + .foregroundStyle(.secondary) + }, + icon: { + Image(systemName: task.statusInfo.imageSystemName) + .renderingMode(.original) + .font(Font.system(.body).weight(.light)) + } + ) + .padding(.top, 1) + .labelStyle(CustomLabelStyle()) + } + } +} diff --git a/iOSClient/Assistant/Create Task/NCAssistantCreateNewTask.swift b/iOSClient/Assistant/Create Task/NCAssistantCreateNewTask.swift index 953c6bbf19..ad54ab16b9 100644 --- a/iOSClient/Assistant/Create Task/NCAssistantCreateNewTask.swift +++ b/iOSClient/Assistant/Create Task/NCAssistantCreateNewTask.swift @@ -9,7 +9,7 @@ import SwiftUI struct NCAssistantCreateNewTask: View { - @EnvironmentObject var model: NCAssistantTask + @EnvironmentObject var model: NCAssistantModel @State var text = "" @FocusState private var inFocus: Bool @Environment(\.presentationMode) var presentationMode @@ -55,13 +55,14 @@ struct NCAssistantCreateNewTask: View { } #Preview { - let model = NCAssistantTask(controller: nil) - + let model = NCAssistantModel(controller: nil) + return NCAssistantCreateNewTask() .environmentObject(model) .onAppear { model.loadDummyData() - }} + } +} private extension View { func transparentScrolling() -> some View { diff --git a/iOSClient/Assistant/Models/NCAssistantModel.swift b/iOSClient/Assistant/Models/NCAssistantModel.swift new file mode 100644 index 0000000000..55cdc11d47 --- /dev/null +++ b/iOSClient/Assistant/Models/NCAssistantModel.swift @@ -0,0 +1,264 @@ +// +// NCAssistantModel.swift +// Nextcloud +// +// Created by Milen on 08.04.24. +// Copyright © 2024 Marino Faggiana. All rights reserved. +// + +import Foundation +import UIKit +import NextcloudKit +import SwiftUI + +class NCAssistantModel: ObservableObject { + @Published var types: [TaskTypeData] = [] + @Published var filteredTasks: [AssistantTask] = [] + @Published var selectedType: TaskTypeData? + @Published var selectedTask: AssistantTask? + + @Published var hasError: Bool = false + @Published var isLoading: Bool = false + @Published var isRefreshing: Bool = false + @Published var controller: NCMainTabBarController? + + private var tasks: [AssistantTask] = [] + + private var session: NCSession.Session + + private var useV2: Bool + + init(controller: NCMainTabBarController?) { + self.controller = controller + session = NCSession.shared.getSession(controller: controller) + useV2 = NCCapabilities.shared.getCapabilities(account: session.account).capabilityServerVersionMajor >= NCGlobal.shared.nextcloudVersion30 + loadAllTypes() + } + + func refresh() { + isRefreshing = true + loadAllTasks(type: selectedType) + } + + func filterTasks(ofType type: TaskTypeData?) { + if let type { + self.filteredTasks = tasks.filter({ $0.type == type.id }) + } else { + self.filteredTasks = tasks + } + + self.filteredTasks = filteredTasks.sorted(by: { $0.completionExpectedAt ?? 0 > $1.completionExpectedAt ?? 0 }) + } + + func selectTaskType(_ type: TaskTypeData?) { + selectedType = type + + filteredTasks.removeAll() + loadAllTasks(type: type) + } + + func selectTask(_ task: AssistantTask) { + selectedTask = task + isLoading = true + + if useV2 { + NextcloudKit.shared.textProcessingGetTasksV2(taskType: task.type ?? "", account: session.account, completion: { account, tasks, responseData, error in + self.isLoading = false + + if error != .success { + self.hasError = true + return + } + + self.selectedTask = task + }) + } else { + NextcloudKit.shared.textProcessingGetTask(taskId: Int(task.id), account: session.account) { _, task, _, error in + self.isLoading = false + + if error != .success { + self.hasError = true + return + } + + guard let task else { return } + self.selectedTask = NKTextProcessingTask.toV2(tasks: [task]).tasks.first + } + } + } + + func scheduleTask(input: String) { + isLoading = true + + if useV2 { + guard let selectedType else { return } + NextcloudKit.shared.textProcessingScheduleV2(input: input, taskType: selectedType, account: session.account) { account, task, responseData, error in + handle(task: task, error: error) + } + } else { + NextcloudKit.shared.textProcessingSchedule(input: input, typeId: selectedType?.id ?? "", identifier: "assistant", account: session.account) { _, task, _, error in + guard let task, let taskV2 = NKTextProcessingTask.toV2(tasks: [task]).tasks.first else { return } + handle(task: taskV2, error: error) + } + } + + func handle(task: AssistantTask?, error: NKError?) { + self.isLoading = false + + if error != .success { + self.hasError = true + return + } + + guard let task else { return } + + withAnimation { + self.tasks.insert(task, at: 0) + self.filteredTasks.insert(task, at: 0) + } + } + } + + func deleteTask(_ task: AssistantTask) { + isLoading = true + + NextcloudKit.shared.textProcessingDeleteTaskV2(taskId: task.id, account: session.account) { _, _, error in + handle(task: task, error: error) + } + NextcloudKit.shared.textProcessingDeleteTask(taskId: Int(task.id), account: session.account) { _, _, _, error in + handle(task: task, error: error) + } + + func handle(task: AssistantTask, error: NKError?) { + self.isLoading = false + + if error != .success { + self.hasError = true + return + } + + withAnimation { + self.tasks.removeAll(where: { $0.id == task.id }) + self.filteredTasks.removeAll(where: { $0.id == task.id }) + } + } + } + + func shareTask(_ task: AssistantTask) { + let activityVC = UIActivityViewController(activityItems: [(task.input?.input ?? "") + "\n\n" + (task.output?.output ?? "")], applicationActivities: nil) + controller?.presentedViewController?.present(activityVC, animated: true, completion: nil) // presentedViewController = the UIHostingController presenting the Assistant + } + + private func loadAllTypes() { + isLoading = true + + if useV2 { + NextcloudKit.shared.textProcessingGetTypesV2(account: session.account) { account, types, responseData, error in + handle(types: types, error: error) + } + } else { + NextcloudKit.shared.textProcessingGetTypes(account: session.account) { _, types, _, error in + guard let types else { return } + let typesV2 = NKTextProcessingTaskType.toV2(type: types).types + + handle(types: typesV2, error: error) + } + } + + func handle(types: [TaskTypeData]?, error: NKError) { + self.isLoading = false + + if error != .success { + self.hasError = true + return + } + + guard let types else { return } + + withAnimation { + self.types = types + } + + if self.selectedType == nil { + self.selectTaskType(types.first) + } + + self.loadAllTasks(type: selectedType) + } + } + + private func loadAllTasks(appId: String = "assistant", type: TaskTypeData?) { + isLoading = true + + if useV2 { + NextcloudKit.shared.textProcessingGetTasksV2(taskType: type?.id ?? "", account: session.account) { _, tasks, _, error in + guard let tasks = tasks?.tasks.filter({ $0.appId == "assistant" }) else { return } + handle(tasks: tasks, error: error) + } + } else { + NextcloudKit.shared.textProcessingTaskList(appId: appId, account: session.account) { _, tasks, _, error in + guard let tasks else { return } + handle(tasks: NKTextProcessingTask.toV2(tasks: tasks).tasks, error: error) + } + } + + func handle(tasks: [AssistantTask], error: NKError?) { + isLoading = false + isRefreshing = false + + if error != .success { + self.hasError = true + return + } + + self.tasks = tasks + self.filterTasks(ofType: self.selectedType) + } + } +} + +extension NCAssistantModel { + public func loadDummyData() { + let loremIpsum = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum." + + var tasks: [AssistantTask] = [] + + for index in 1...10 { + tasks.append(AssistantTask(id: 1, type: "", status: "", userId: "", appId: "", input: .init(input: loremIpsum), output: .init(output: loremIpsum), completionExpectedAt: 1712666412, progress: nil, lastUpdated: nil, scheduledAt: nil, endedAt: nil)) + } + + self.types = [ + TaskTypeData(id: "1", name: "Free Prompt", description: "", inputShape: nil, outputShape: nil), + TaskTypeData(id: "2", name: "Summarize", description: "", inputShape: nil, outputShape: nil), + TaskTypeData(id: "3", name: "Generate headline", description: "", inputShape: nil, outputShape: nil), + TaskTypeData(id: "4", name: "Reformulate", description: "", inputShape: nil, outputShape: nil), + ] + + self.tasks = tasks + self.filteredTasks = tasks + self.selectedType = types[0] + self.selectedTask = filteredTasks[0] + } +} + + +extension AssistantTask { + struct StatusInfo { + let stringKey, imageSystemName: String + } + + var statusInfo: StatusInfo { + return switch status { + case "0", "STATUS_UNKNOWN": StatusInfo(stringKey: "_assistant_task_unknown_", imageSystemName: "questionmark.circle") + case "1", "STATUS_SCHEDULED": StatusInfo(stringKey: "_assistant_task_scheduled_", imageSystemName: "clock.badge") + case "2", "STATUS_RUNNING": StatusInfo(stringKey: "_assistant_task_in_progress_", imageSystemName: "arrow.2.circlepath") + case "3", "STATUS_SUCCESSFUL": StatusInfo(stringKey: "_assistant_task_completed_", imageSystemName: "checkmark.circle") + case "4", "STATUS_FAILED": StatusInfo(stringKey: "_assistant_task_failed_", imageSystemName: "exclamationmark.circle") + default: StatusInfo(stringKey: "_assistant_task_unknown_", imageSystemName: "questionmark.circle") + } + } + + var statusDate: String { + return NCUtility().getTitleFromDate(.init(timeIntervalSince1970: TimeInterval((lastUpdated ?? completionExpectedAt) ?? 0))) + } +} diff --git a/iOSClient/Assistant/Models/NCAssistantTask.swift b/iOSClient/Assistant/Models/NCAssistantTask.swift deleted file mode 100644 index 83aeb46034..0000000000 --- a/iOSClient/Assistant/Models/NCAssistantTask.swift +++ /dev/null @@ -1,201 +0,0 @@ -// -// NCAssistantModel.swift -// Nextcloud -// -// Created by Milen on 08.04.24. -// Copyright © 2024 Marino Faggiana. All rights reserved. -// - -import Foundation -import UIKit -import NextcloudKit -import SwiftUI - -class NCAssistantTask: ObservableObject { - @Published var types: [NKTextProcessingTaskType] = [] - @Published var filteredTasks: [NKTextProcessingTask] = [] - @Published var selectedType: NKTextProcessingTaskType? - @Published var selectedTask: NKTextProcessingTask? - -// let useV2 = true - -// @Published var typesV2: [NKTextProcessingTaskTypeV2.TaskTypeData] = [] -// @Published var filteredTasksV2: [NKTextProcessingTaskV2.Task] = [] -// @Published var selectedTypeV2: NKTextProcessingTaskTypeV2.TaskTypeData? -// @Published var selectedTaskV2: NKTextProcessingTaskV2.Task? - - @Published var hasError: Bool = false - @Published var isLoading: Bool = false - @Published var controller: NCMainTabBarController? - - private var tasks: [NKTextProcessingTask] = [] -// private var tasksV2: [NKTextProcessingTaskV2.Task] = [] - - private let excludedTypeIds = ["OCA\\ContextChat\\TextProcessing\\ContextChatTaskType"] - private var session: NCSession.Session { - NCSession.shared.getSession(controller: controller) - } - - init(controller: NCMainTabBarController?) { - self.controller = controller - load() - } - - func load() { - loadAllTypes() - } - - func filterTasks(ofType type: NKTextProcessingTaskType?) { - if let type { - self.filteredTasks = tasks.filter({ $0.type == type.id }) - } else { - self.filteredTasks = tasks - } - - self.filteredTasks = filteredTasks.sorted(by: { $0.completionExpectedAt ?? 0 > $1.completionExpectedAt ?? 0 }) - } - - func selectTaskType(_ type: NKTextProcessingTaskType?) { - selectedType = type - filterTasks(ofType: self.selectedType) - } - - func selectTask(_ task: NKTextProcessingTask) { - selectedTask = task - guard let id = task.id else { return } - isLoading = true - - NextcloudKit.shared.textProcessingGetTask(taskId: id, account: session.account) { _, task, _, error in - self.isLoading = false - - if error != .success { - self.hasError = true - return - } - - self.selectedTask = task - } - } - - func scheduleTask(input: String) { - isLoading = true - - NextcloudKit.shared.textProcessingSchedule(input: input, typeId: selectedType?.id ?? "", identifier: "assistant", account: session.account) { _, task, _, error in - self.isLoading = false - - if error != .success { - self.hasError = true - return - } - - guard let task else { return } - - withAnimation { - self.tasks.insert(task, at: 0) - self.filteredTasks.insert(task, at: 0) - } - } - } - - func deleteTask(_ task: NKTextProcessingTask) { - guard let id = task.id else { return } - isLoading = true - - NextcloudKit.shared.textProcessingDeleteTask(taskId: id, account: session.account) { _, task, _, error in - self.isLoading = false - - if error != .success { - self.hasError = true - return - } - - withAnimation { - self.tasks.removeAll(where: { $0.id == task?.id }) - self.filteredTasks.removeAll(where: { $0.id == task?.id }) - } - - } - } - - private func loadAllTypes() { - isLoading = true - - NextcloudKit.shared.textProcessingGetTypes(account: session.account) { _, types, _, error in - self.isLoading = false - - if error != .success { - self.hasError = true - return - } - - guard let filteredTypes = types?.filter({ !self.excludedTypeIds.contains($0.id ?? "")}), !filteredTypes.isEmpty else { return } - - withAnimation { - self.types = filteredTypes - } - - if self.selectedType == nil { - self.selectTaskType(filteredTypes.first) - } - - self.loadAllTasks() - } - } - - private func loadAllTasks(appId: String = "assistant") { - isLoading = true - - NextcloudKit.shared.textProcessingTaskList(appId: appId, account: session.account) { _, tasks, _, error in - self.isLoading = false - - if error != .success { - self.hasError = true - return - } - - guard let tasks = tasks else { return } - self.tasks = tasks - self.filterTasks(ofType: self.selectedType) - } - } -} - -extension NCAssistantTask { - public func loadDummyData() { - let loremIpsum = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum." - - var tasks: [NKTextProcessingTask] = [] - - for index in 1...10 { - tasks.append(NKTextProcessingTask(id: index, type: "OCP\\TextProcessing\\FreePromptTaskType", status: index, userId: "christine", appId: "assistant", input: loremIpsum, output: loremIpsum, identifier: "", completionExpectedAt: 1712666412)) - } - - self.types = [ - NKTextProcessingTaskType(id: "1", name: "Free Prompt", description: ""), - NKTextProcessingTaskType(id: "2", name: "Summarize", description: ""), - NKTextProcessingTaskType(id: "3", name: "Generate headline", description: ""), - NKTextProcessingTaskType(id: "4", name: "Reformulate", description: "") - ] - self.tasks = tasks - self.filteredTasks = tasks - self.selectedType = types[0] - self.selectedTask = filteredTasks[0] - - } -} - -extension NKTextProcessingTask { - struct StatusInfo { - let stringKey, imageSystemName: String - } - - var statusInfo: StatusInfo { - return switch status { - case 1: StatusInfo(stringKey: "_assistant_task_scheduled_", imageSystemName: "clock") - case 2: StatusInfo(stringKey: "_assistant_task_in_progress_", imageSystemName: "clock.badge") - case 3: StatusInfo(stringKey: "_assistant_task_completed_", imageSystemName: "checkmark.circle") - case 4: StatusInfo(stringKey: "_assistant_task_failed_", imageSystemName: "exclamationmark.circle") - default: StatusInfo(stringKey: "_assistant_task_unknown_", imageSystemName: "questionmark.circle") - } - } -} diff --git a/iOSClient/Assistant/Models/NCAssistantTaskV2.swift b/iOSClient/Assistant/Models/NCAssistantTaskV2.swift deleted file mode 100644 index 856584be92..0000000000 --- a/iOSClient/Assistant/Models/NCAssistantTaskV2.swift +++ /dev/null @@ -1,201 +0,0 @@ -// -// NCAssistantModel.swift -// Nextcloud -// -// Created by Milen on 08.04.24. -// Copyright © 2024 Marino Faggiana. All rights reserved. -// - -import Foundation -import UIKit -import NextcloudKit -import SwiftUI - -class NCAssistantTaskV2: ObservableObject { -// @Published var types: [NKTextProcessingTaskType] = [] -// @Published var filteredTasks: [NKTextProcessingTask] = [] -// @Published var selectedType: NKTextProcessingTaskType? -// @Published var selectedTask: NKTextProcessingTask? - - let useV2 = true - - @Published var types: [TaskTypeData] = [] - @Published var filteredTasks: [AssistantTask] = [] - @Published var selectedType: TaskTypeData? - @Published var selectedTask: AssistantTask? - - @Published var hasError: Bool = false - @Published var isLoading: Bool = false - @Published var controller: NCMainTabBarController? - - private var tasks: [AssistantTask] = [] - -// private let excludedTypeIds = ["OCA\\ContextChat\\TextProcessing\\ContextChatTaskType"] - private var session: NCSession.Session { - NCSession.shared.getSession(controller: controller) - } - - init(controller: NCMainTabBarController?) { - self.controller = controller - load() - } - - func load() { - loadAllTypes() - } - - func filterTasks(ofType type: TaskTypeData?) { - if let type { - self.filteredTasks = tasks.filter({ $0.type == type.id }) - } else { - self.filteredTasks = tasks - } - - self.filteredTasks = filteredTasks.sorted(by: { $0.completionExpectedAt ?? 0 > $1.completionExpectedAt ?? 0 }) - } - - func selectTaskType(_ type: TaskTypeData?) { - selectedType = type - filterTasks(ofType: self.selectedType) - } - - func selectTask(_ task: AssistantTask) { - selectedTask = task -// guard let id = task.id else { return } - isLoading = true - - NextcloudKit.shared.textProcessingGetTasksV2(taskType: task.type ?? "", account: session.account, completion: { account, tasks, responseData, error in - self.isLoading = false - - if error != .success { - self.hasError = true - return - } - - self.selectedTask = task - }) - } - - func scheduleTask(input: String) { - isLoading = true - - NextcloudKit.shared.textProcessingScheduleV2(input: input, taskType: selectedType!, account: session.account, completion: { account, task, responseData, error in - self.isLoading = false - - if error != .success { - self.hasError = true - return - } - - guard let task else { return } - - withAnimation { - self.tasks.insert(task, at: 0) - self.filteredTasks.insert(task, at: 0) - } - }) - - } - - func deleteTask(_ task: AssistantTask) { - isLoading = true - - NextcloudKit.shared.textProcessingDeleteTaskV2(taskId: task.id, account: session.account) { account, responseData, error in - - self.isLoading = false - - if error != .success { - self.hasError = true - return - } - - withAnimation { - self.tasks.removeAll(where: { $0.id == task.id }) - self.filteredTasks.removeAll(where: { $0.id == task.id }) - } - - } - } - - private func loadAllTypes() { - isLoading = true - - NextcloudKit.shared.textProcessingGetTypesV2(account: session.account) { account, types, responseData, error in - self.isLoading = false - - if error != .success { - self.hasError = true - return - } - -// guard let filteredTypes = types?.types.filter({ !self.excludedTypeIds.contains($0.id)}), !filteredTypes.isEmpty else { return } - - withAnimation { - self.types = types ?? [] - } - - if self.selectedType == nil { - self.selectTaskType(types?.first) - } - - self.loadAllTasks() - } - } - - private func loadAllTasks(appId: String = "assistant") { - isLoading = true - - NextcloudKit.shared.textProcessingGetTasksV2(taskType: "core:text2text", account: session.account) { account, tasks, responseData, error in - self.isLoading = false - - if error != .success { - self.hasError = true - return - } - - guard let tasks = tasks else { return } - self.tasks = tasks.tasks - self.filterTasks(ofType: self.selectedType) - } - } -} - -//extension NCAssistantTaskV2 { -// public func loadDummyData() { -// let loremIpsum = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum." -// -// var tasks: [NKTextProcessingTask] = [] -// -// for index in 1...10 { -// tasks.append(NKTextProcessingTask(id: index, type: "OCP\\TextProcessing\\FreePromptTaskType", status: index, userId: "christine", appId: "assistant", input: loremIpsum, output: loremIpsum, identifier: "", completionExpectedAt: 1712666412)) -// } -// -// self.types = [ -// NKTextProcessingTaskType(id: "1", name: "Free Prompt", description: ""), -// NKTextProcessingTaskType(id: "2", name: "Summarize", description: ""), -// NKTextProcessingTaskType(id: "3", name: "Generate headline", description: ""), -// NKTextProcessingTaskType(id: "4", name: "Reformulate", description: "") -// ] -// self.tasks = tasks -// self.filteredTasks = tasks -// self.selectedType = types[0] -// self.selectedTask = filteredTasks[0] -// -// } -//} -// -//extension NKTextProcessingTask { -// struct StatusInfo { -// let stringKey, imageSystemName: String -// } -// -// var statusInfo: StatusInfo { -// return switch status { -// case 1: StatusInfo(stringKey: "_assistant_task_scheduled_", imageSystemName: "clock") -// case 2: StatusInfo(stringKey: "_assistant_task_in_progress_", imageSystemName: "clock.badge") -// case 3: StatusInfo(stringKey: "_assistant_task_completed_", imageSystemName: "checkmark.circle") -// case 4: StatusInfo(stringKey: "_assistant_task_failed_", imageSystemName: "exclamationmark.circle") -// default: StatusInfo(stringKey: "_assistant_task_unknown_", imageSystemName: "questionmark.circle") -// } -// } -//} diff --git a/iOSClient/Assistant/NCAssistant.swift b/iOSClient/Assistant/NCAssistant.swift index 0ddaed7dba..4bcdf7c1a5 100644 --- a/iOSClient/Assistant/NCAssistant.swift +++ b/iOSClient/Assistant/NCAssistant.swift @@ -11,19 +11,29 @@ import NextcloudKit import PopupView struct NCAssistant: View { - let useV2 = true - @EnvironmentObject var model: NCAssistantTask - @EnvironmentObject var modelV2: NCAssistantTaskV2 + @EnvironmentObject var model: NCAssistantModel @State var presentNewTaskDialog = false + @State var presentEditTask = false @State var input = "" @Environment(\.presentationMode) var presentationMode var body: some View { NavigationView { - TaskList() + ZStack { + TaskList(presentEditTask: $presentEditTask) - EmptyView() + if model.isLoading, !model.isRefreshing { + ProgressView() + .controlSize(.regular) + } + if model.types.isEmpty, !model.isLoading { + NCAssistantEmptyView(titleKey: "_no_types_", subtitleKey: "_no_types_subtitle_") + } else if model.filteredTasks.isEmpty, !model.isLoading { + NCAssistantEmptyView(titleKey: "_no_tasks_", subtitleKey: "_create_task_subtitle_") + } + + } .toolbar { ToolbarItem(placement: .topBarLeading) { Button(action: { @@ -38,16 +48,19 @@ struct NCAssistant: View { .font(Font.system(.body).weight(.light)) .foregroundStyle(Color(NCBrandColor.shared.iconImageColor)) } - .disabled(useV2 ? modelV2.selectedType == nil : model.selectedType == nil) + .disabled(model.selectedType == nil) } } .navigationBarTitleDisplayMode(.inline) .navigationTitle(NSLocalizedString("_assistant_", comment: "")) .frame(maxWidth: .infinity, maxHeight: .infinity) .safeAreaInset(edge: .top, spacing: -10) { - ExtractedView() + TypeList() } } + .background( // navigationDestination + NavigationLink(destination: NCAssistantCreateNewTask(), isActive: $presentEditTask) { EmptyView() } + ) .navigationViewStyle(.stack) .popup(isPresented: $model.hasError) { Text(NSLocalizedString("_error_occurred_", comment: "")) @@ -66,7 +79,7 @@ struct NCAssistant: View { } #Preview { - let model = NCAssistantTask(controller: nil) + let model = NCAssistantModel(controller: nil) return NCAssistant() .environmentObject(model) @@ -76,46 +89,46 @@ struct NCAssistant: View { } struct TaskList: View { - @EnvironmentObject var model: NCAssistantTask + @EnvironmentObject var model: NCAssistantModel + @Binding var presentEditTask: Bool var body: some View { List(model.filteredTasks, id: \.id) { task in TaskItem(task: task) .contextMenu { Button { + model.shareTask(task) } label: { Label { - Text("_copy_") + Text("_share_") } icon: { - Image(systemName: "document.on.document") + Image(systemName: "square.and.arrow.up") } - } - // Button { - // Label { - // } icon: { - // Image(systemImage: "copy") - // } - // - // } - Button("Test") { - + Button { + presentEditTask.toggle() + } label: { + Label { + Text("_edit_") + } icon: { + Image(systemName: "pencil") + } } } } .if(!model.types.isEmpty) { view in view.refreshable { - model.load() + model.refresh() } } } } struct TypeButton: View { - @EnvironmentObject var model: NCAssistantTask + @EnvironmentObject var model: NCAssistantModel - let taskType: NKTextProcessingTaskType? + let taskType: TaskTypeData? var scrollProxy: ScrollViewProxy var body: some View { @@ -130,12 +143,13 @@ struct TypeButton: View { } .padding(.horizontal) .padding(.vertical, 7) - .foregroundStyle(model.selectedType?.id == taskType?.id ? .white : .primary) + .foregroundStyle(.primary) + .background(.ultraThinMaterial) .if(model.selectedType?.id == taskType?.id) { view in - view.background(Color(NCBrandColor.shared.getElement(account: model.controller?.account))) - } - .if(model.selectedType?.id != taskType?.id) { view in - view.background(.ultraThinMaterial) + view + .foregroundStyle(.white) + .background(Color(NCBrandColor.shared.getElement(account: model.controller?.account))) + } .clipShape(.capsule) .overlay( @@ -147,24 +161,26 @@ struct TypeButton: View { } struct TaskItem: View { - @EnvironmentObject var model: NCAssistantTask + @EnvironmentObject var model: NCAssistantModel @State var showDeleteConfirmation = false - let task: NKTextProcessingTask + let task: AssistantTask var body: some View { NavigationLink(destination: NCAssistantTaskDetail(task: task)) { VStack(alignment: .leading) { - Text(task.input ?? "") + Text(task.input?.input ?? "") .lineLimit(1) - Text(task.output ?? "") + Text(task.output?.output ?? "") .lineLimit(1) .foregroundStyle(.secondary) HStack { Label( title: { - Text(NSLocalizedString(task.status == 3 /*Completed*/ ? NCUtility().dateDiff(.init(timeIntervalSince1970: TimeInterval(task.completionExpectedAt ?? 0))) : task.statusInfo.stringKey, comment: "")) + Text(task.statusDate) + .font(.callout) + .foregroundStyle(.secondary) }, icon: { Image(systemName: task.statusInfo.imageSystemName) @@ -174,12 +190,6 @@ struct TaskItem: View { ) .padding(.top, 1) .labelStyle(CustomLabelStyle()) - -// if let completionExpectedAt = task.completionExpectedAt { -// Text(NCUtility().dateDiff(.init(timeIntervalSince1970: TimeInterval(completionExpectedAt)))) -// .frame(maxWidth: .infinity, alignment: .trailing) -// .foregroundStyle(.tertiary) -// } } } .swipeActions { @@ -199,55 +209,15 @@ struct TaskItem: View { } } -private struct CustomLabelStyle: LabelStyle { - var spacing: Double = 5 - - func makeBody(configuration: Configuration) -> some View { - HStack(spacing: spacing) { - configuration.icon - configuration.title - } - } -} - -struct EmptyView: View { - var body: some View { - { - - if useV2 { - if ((modelV2.types?.isEmpty) != nil), !modelV2.isLoading { - NCAssistantEmptyView(titleKey: "_no_types_", subtitleKey: "_no_types_subtitle_") - } else if modelV2.filteredTasks.isEmpty, !model.isLoading { - NCAssistantEmptyView(titleKey: "_no_tasks_", subtitleKey: "_create_task_subtitle_") - } - } else { - if model.types.isEmpty, !model.isLoading { - NCAssistantEmptyView(titleKey: "_no_types_", subtitleKey: "_no_types_subtitle_") - } else if model.filteredTasks.isEmpty, !model.isLoading { - NCAssistantEmptyView(titleKey: "_no_tasks_", subtitleKey: "_create_task_subtitle_") - } - } - } - } -} - -struct ExtractedView: View { - @EnvironmentObject var model: NCAssistantTask - @EnvironmentObject var modelV2: NCAssistantTaskV2 - let useV2: Bool +struct TypeList: View { + @EnvironmentObject var model: NCAssistantModel var body: some View { ScrollViewReader { scrollProxy in ScrollView(.horizontal, showsIndicators: false) { - LazyHStack { - if useV2 { - ForEach(modelV2.types, id: \.id) { type in - TypeButton(taskType: type, scrollProxy: scrollProxy) - } - } else { - ForEach(model.types, id: \.id) { type in - TypeButton(taskType: type, scrollProxy: scrollProxy) - } + HStack { + ForEach(model.types, id: \.id) { type in + TypeButton(taskType: type, scrollProxy: scrollProxy) } } .padding(20) diff --git a/iOSClient/Assistant/Task Detail/NCAssistantTaskDetail.swift b/iOSClient/Assistant/Task Detail/NCAssistantTaskDetail.swift index 3544fc6fc8..b73852746a 100644 --- a/iOSClient/Assistant/Task Detail/NCAssistantTaskDetail.swift +++ b/iOSClient/Assistant/Task Detail/NCAssistantTaskDetail.swift @@ -10,8 +10,8 @@ import SwiftUI import NextcloudKit struct NCAssistantTaskDetail: View { - @EnvironmentObject var model: NCAssistantTask - let task: NKTextProcessingTask + @EnvironmentObject var model: NCAssistantModel + let task: AssistantTask var body: some View { ZStack(alignment: .bottom) { @@ -28,9 +28,9 @@ struct NCAssistantTaskDetail: View { } #Preview { - let model = NCAssistantTask(controller: nil) + let model = NCAssistantModel(controller: nil) - return NCAssistantTaskDetail(task: NKTextProcessingTask(id: 1, type: "OCP\\TextProcessing\\FreePromptTaskType", status: 1, userId: "christine", appId: "assistant", input: "", output: "", identifier: "", completionExpectedAt: 1712666412)) + return NCAssistantTaskDetail(task: model.selectedTask!) .environmentObject(model) .onAppear { model.loadDummyData() @@ -38,8 +38,8 @@ struct NCAssistantTaskDetail: View { } struct InputOutputScrollView: View { - @EnvironmentObject var model: NCAssistantTask - let task: NKTextProcessingTask + @EnvironmentObject var model: NCAssistantModel + let task: AssistantTask var body: some View { ScrollView { @@ -47,7 +47,7 @@ struct InputOutputScrollView: View { Text(NSLocalizedString("_input_", comment: "")).font(.headline) .padding(.top, 10) - Text(model.selectedTask?.input ?? "") + Text(model.selectedTask?.input?.input ?? "") .frame(maxWidth: .infinity, alignment: .topLeading) .padding() .background(Color(NCBrandColor.shared.textColor2).opacity(0.1)) @@ -56,7 +56,7 @@ struct InputOutputScrollView: View { Text(NSLocalizedString("_output_", comment: "")).font(.headline) .padding(.top, 10) - Text(model.selectedTask?.output ?? "") + Text(model.selectedTask?.output?.output ?? "") .frame(maxWidth: .infinity, alignment: .topLeading) .padding() .background(Color(NCBrandColor.shared.textColor2).opacity(0.1)) @@ -71,32 +71,20 @@ struct InputOutputScrollView: View { } struct BottomDetailsBar: View { - @EnvironmentObject var model: NCAssistantTask - let task: NKTextProcessingTask + @EnvironmentObject var model: NCAssistantModel + let task: AssistantTask var body: some View { VStack(spacing: 0) { Divider() - HStack(alignment: .bottom) { - Label( - title: { - Text(NSLocalizedString(model.selectedTask?.statusInfo.stringKey ?? "", comment: "")) - }, icon: { - Image(systemName: model.selectedTask?.statusInfo.imageSystemName ?? "") - .renderingMode(.original) - .font(Font.system(.body).weight(.light)) - } - ) - .frame(maxWidth: .infinity, alignment: .leading) - if let completionExpectedAt = task.completionExpectedAt { - Text(NCUtility().dateDiff(.init(timeIntervalSince1970: TimeInterval(completionExpectedAt)))) - .frame(maxWidth: .infinity, alignment: .trailing) - } + HStack { + StatusInfo(task: task, showStatusText: true) + .frame(maxWidth: .infinity, alignment: .leading) + .padding() + .background(.bar) + .frame(alignment: .bottom) } - .padding() - .background(.bar) - .frame(alignment: .bottom) } } } diff --git a/iOSClient/More/Cells/BaseNCMoreCell.swift b/iOSClient/More/Cells/BaseNCMoreCell.swift index 68887aa501..9b5b705cd1 100644 --- a/iOSClient/More/Cells/BaseNCMoreCell.swift +++ b/iOSClient/More/Cells/BaseNCMoreCell.swift @@ -42,7 +42,7 @@ class BaseNCMoreCell: UITableViewCell { } } - open func setupCell(account: String) {} + open func setupCell(account: String, controller: NCMainTabBarController?) {} override func awakeFromNib() { super.awakeFromNib() diff --git a/iOSClient/More/Cells/NCMoreAppSuggestionsCell.swift b/iOSClient/More/Cells/NCMoreAppSuggestionsCell.swift index fc54e92f9d..2dac0e0604 100644 --- a/iOSClient/More/Cells/NCMoreAppSuggestionsCell.swift +++ b/iOSClient/More/Cells/NCMoreAppSuggestionsCell.swift @@ -49,13 +49,15 @@ class NCMoreAppSuggestionsCell: BaseNCMoreCell { moreAppsView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(moreAppsTapped))) } - override func setupCell(account: String) { + override func setupCell(account: String, controller: NCMainTabBarController?) { assistantView.isHidden = !NCCapabilities.shared.getCapabilities(account: account).capabilityAssistantEnabled + self.controller = controller } @objc func assistantTapped() { if let viewController = self.window?.rootViewController { - let assistant = NCAssistant().environmentObject(NCAssistantTask(controller: controller)) + let assistant = NCAssistant() + .environmentObject(NCAssistantModel(controller: self.controller)) let hostingController = UIHostingController(rootView: assistant) viewController.present(hostingController, animated: true, completion: nil) } diff --git a/iOSClient/More/NCMore.swift b/iOSClient/More/NCMore.swift index 6158857843..8e32724a26 100644 --- a/iOSClient/More/NCMore.swift +++ b/iOSClient/More/NCMore.swift @@ -302,12 +302,12 @@ class NCMore: UIViewController, UITableViewDelegate, UITableViewDataSource { if section.type == .moreApps { guard let cell = tableView.dequeueReusableCell(withIdentifier: NCMoreAppSuggestionsCell.reuseIdentifier, for: indexPath) as? NCMoreAppSuggestionsCell else { return UITableViewCell() } - cell.setupCell(account: session.account) + cell.setupCell(account: session.account, controller: controller) return cell } else { guard let cell = tableView.dequeueReusableCell(withIdentifier: CCCellMore.reuseIdentifier, for: indexPath) as? CCCellMore else { return UITableViewCell() } - cell.setupCell(account: session.account) + cell.setupCell(account: session.account, controller: controller) let item = sections[indexPath.section].items[indexPath.row] @@ -381,7 +381,8 @@ class NCMore: UIViewController, UITableViewDelegate, UITableViewDataSource { alertController.addAction(actionNo) self.present(alertController, animated: true, completion: nil) } else if item.url == "openAssistant" { - let assistant = NCAssistant().environmentObject(NCAssistantTask(controller: self.controller)) + let assistant = NCAssistant() + .environmentObject(NCAssistantModel(controller: self.controller)) let hostingController = UIHostingController(rootView: assistant) present(hostingController, animated: true, completion: nil) } else if item.url == "openSettings" { diff --git a/iOSClient/NCGlobal.swift b/iOSClient/NCGlobal.swift index d4fb10919f..cf9b979af7 100644 --- a/iOSClient/NCGlobal.swift +++ b/iOSClient/NCGlobal.swift @@ -70,6 +70,7 @@ final class NCGlobal: Sendable { let nextcloudVersion26: Int = 26 let nextcloudVersion27: Int = 27 let nextcloudVersion28: Int = 28 + let nextcloudVersion30: Int = 30 let nextcloudVersion31: Int = 31 let nextcloudVersion99: Int = 99 From 24500521d38cde1c5647ee276833f598fa608e3e Mon Sep 17 00:00:00 2001 From: Milen Pivchev Date: Wed, 19 Feb 2025 17:55:43 +0100 Subject: [PATCH 06/20] WIP Signed-off-by: Milen Pivchev --- .../NCAssistantCreateNewTask.swift | 5 +- .../Assistant/Models/NCAssistantModel.swift | 19 ++++--- iOSClient/Assistant/NCAssistant.swift | 56 +++++++++++++------ .../Task Detail/NCAssistantTaskDetail.swift | 9 +++ .../en.lproj/Localizable.strings | 2 + 5 files changed, 64 insertions(+), 27 deletions(-) diff --git a/iOSClient/Assistant/Create Task/NCAssistantCreateNewTask.swift b/iOSClient/Assistant/Create Task/NCAssistantCreateNewTask.swift index ad54ab16b9..04b80689a9 100644 --- a/iOSClient/Assistant/Create Task/NCAssistantCreateNewTask.swift +++ b/iOSClient/Assistant/Create Task/NCAssistantCreateNewTask.swift @@ -13,6 +13,7 @@ struct NCAssistantCreateNewTask: View { @State var text = "" @FocusState private var inFocus: Bool @Environment(\.presentationMode) var presentationMode + var editMode = false var body: some View { VStack { @@ -41,11 +42,11 @@ struct NCAssistantCreateNewTask: View { model.scheduleTask(input: text) presentationMode.wrappedValue.dismiss() }, label: { - Text(NSLocalizedString("_create_", comment: "")) + Text(NSLocalizedString(editMode ? "_edit_" : "_create_", comment: "")) }) .disabled(text.isEmpty) } - .navigationTitle(String(format: NSLocalizedString("_new_task_", comment: ""), model.selectedType?.name ?? "")) + .navigationTitle(String(format: NSLocalizedString(editMode ? "_edit_task_" : "_new_task_", comment: ""), model.selectedType?.name ?? "")) .navigationBarTitleDisplayMode(.inline) .padding() .onAppear { diff --git a/iOSClient/Assistant/Models/NCAssistantModel.swift b/iOSClient/Assistant/Models/NCAssistantModel.swift index 55cdc11d47..d46f06ba89 100644 --- a/iOSClient/Assistant/Models/NCAssistantModel.swift +++ b/iOSClient/Assistant/Models/NCAssistantModel.swift @@ -24,14 +24,15 @@ class NCAssistantModel: ObservableObject { private var tasks: [AssistantTask] = [] - private var session: NCSession.Session + private let session: NCSession.Session - private var useV2: Bool + private let useV2: Bool init(controller: NCMainTabBarController?) { self.controller = controller session = NCSession.shared.getSession(controller: controller) useV2 = NCCapabilities.shared.getCapabilities(account: session.account).capabilityServerVersionMajor >= NCGlobal.shared.nextcloudVersion30 +// useV2 = false loadAllTypes() } @@ -121,12 +122,16 @@ class NCAssistantModel: ObservableObject { func deleteTask(_ task: AssistantTask) { isLoading = true + print("WTF") - NextcloudKit.shared.textProcessingDeleteTaskV2(taskId: task.id, account: session.account) { _, _, error in - handle(task: task, error: error) - } - NextcloudKit.shared.textProcessingDeleteTask(taskId: Int(task.id), account: session.account) { _, _, _, error in - handle(task: task, error: error) + if useV2 { + NextcloudKit.shared.textProcessingDeleteTaskV2(taskId: task.id, account: session.account) { _, _, error in + handle(task: task, error: error) + } + } else { + NextcloudKit.shared.textProcessingDeleteTask(taskId: Int(task.id), account: session.account) { _, _, _, error in + handle(task: task, error: error) + } } func handle(task: AssistantTask, error: NKError?) { diff --git a/iOSClient/Assistant/NCAssistant.swift b/iOSClient/Assistant/NCAssistant.swift index 4bcdf7c1a5..7de60226f6 100644 --- a/iOSClient/Assistant/NCAssistant.swift +++ b/iOSClient/Assistant/NCAssistant.swift @@ -12,15 +12,13 @@ import PopupView struct NCAssistant: View { @EnvironmentObject var model: NCAssistantModel - @State var presentNewTaskDialog = false - @State var presentEditTask = false @State var input = "" @Environment(\.presentationMode) var presentationMode var body: some View { NavigationView { ZStack { - TaskList(presentEditTask: $presentEditTask) + TaskList() if model.isLoading, !model.isRefreshing { ProgressView() @@ -57,10 +55,8 @@ struct NCAssistant: View { .safeAreaInset(edge: .top, spacing: -10) { TypeList() } + } - .background( // navigationDestination - NavigationLink(destination: NCAssistantCreateNewTask(), isActive: $presentEditTask) { EmptyView() } - ) .navigationViewStyle(.stack) .popup(isPresented: $model.hasError) { Text(NSLocalizedString("_error_occurred_", comment: "")) @@ -90,11 +86,15 @@ struct NCAssistant: View { struct TaskList: View { @EnvironmentObject var model: NCAssistantModel - @Binding var presentEditTask: Bool + @State var presentEditTask = false + @State var showDeleteConfirmation = false + + @State var taskToEdit: AssistantTask? + @State var taskToDelete: AssistantTask? var body: some View { List(model.filteredTasks, id: \.id) { task in - TaskItem(task: task) + TaskItem(showDeleteConfirmation: $showDeleteConfirmation, taskToDelete: $taskToDelete, task: task) .contextMenu { Button { model.shareTask(task) @@ -107,7 +107,8 @@ struct TaskList: View { } Button { - presentEditTask.toggle() + taskToEdit = task + presentEditTask = true } label: { Label { Text("_edit_") @@ -115,6 +116,17 @@ struct TaskList: View { Image(systemName: "pencil") } } + + Button(role: .destructive) { + taskToDelete = task + showDeleteConfirmation = true + } label: { + Label { + Text("_delete_") + } icon: { + Image(systemName: "trash") + } + } } } .if(!model.types.isEmpty) { view in @@ -122,6 +134,19 @@ struct TaskList: View { model.refresh() } } + .confirmationDialog("", isPresented: $showDeleteConfirmation) { + Button(NSLocalizedString("_delete_", comment: ""), role: .destructive) { + withAnimation { + guard let taskToDelete else { return } + model.deleteTask(taskToDelete) + } + } + } + .sheet(isPresented: $presentEditTask) { [taskToEdit] in + NavigationView { + NCAssistantCreateNewTask(text: taskToEdit?.input?.input ?? "", editMode: true) + } + } } } @@ -162,8 +187,9 @@ struct TypeButton: View { struct TaskItem: View { @EnvironmentObject var model: NCAssistantModel - @State var showDeleteConfirmation = false - let task: AssistantTask + @Binding var showDeleteConfirmation: Bool + @Binding var taskToDelete: AssistantTask? + var task: AssistantTask var body: some View { NavigationLink(destination: NCAssistantTaskDetail(task: task)) { @@ -194,17 +220,11 @@ struct TaskItem: View { } .swipeActions { Button(NSLocalizedString("_delete_", comment: "")) { + taskToDelete = task showDeleteConfirmation = true } .tint(.red) } - .confirmationDialog("", isPresented: $showDeleteConfirmation) { - Button(NSLocalizedString("_delete_", comment: ""), role: .destructive) { - withAnimation { - model.deleteTask(task) - } - } - } } } } diff --git a/iOSClient/Assistant/Task Detail/NCAssistantTaskDetail.swift b/iOSClient/Assistant/Task Detail/NCAssistantTaskDetail.swift index b73852746a..78354a862e 100644 --- a/iOSClient/Assistant/Task Detail/NCAssistantTaskDetail.swift +++ b/iOSClient/Assistant/Task Detail/NCAssistantTaskDetail.swift @@ -19,6 +19,13 @@ struct NCAssistantTaskDetail: View { BottomDetailsBar(task: task) } + .toolbar { + Button(action: { + model.shareTask(task) + }, label: { + Image(systemName: "square.and.arrow.up") + }) + } .navigationBarTitleDisplayMode(.inline) .navigationTitle(NSLocalizedString("_task_details_", comment: "")) .onAppear { @@ -52,6 +59,7 @@ struct InputOutputScrollView: View { .padding() .background(Color(NCBrandColor.shared.textColor2).opacity(0.1)) .clipShape(.rect(cornerRadius: 8)) + .textSelection(.enabled) Text(NSLocalizedString("_output_", comment: "")).font(.headline) .padding(.top, 10) @@ -61,6 +69,7 @@ struct InputOutputScrollView: View { .padding() .background(Color(NCBrandColor.shared.textColor2).opacity(0.1)) .clipShape(.rect(cornerRadius: 8)) + .textSelection(.enabled) } .padding(.horizontal) diff --git a/iOSClient/Supporting Files/en.lproj/Localizable.strings b/iOSClient/Supporting Files/en.lproj/Localizable.strings index d224f8058c..8ff4c06bac 100644 --- a/iOSClient/Supporting Files/en.lproj/Localizable.strings +++ b/iOSClient/Supporting Files/en.lproj/Localizable.strings @@ -22,6 +22,7 @@ "_itunes_" = "iTunes"; "_cancel_" = "Cancel"; +"_edit_" = "Edit"; "_tap_to_cancel_" = "Tap to cancel"; "_cancel_request_" = "Do you want to cancel?"; "_upload_file_" = "Upload file"; @@ -1120,6 +1121,7 @@ "_new_task_" = "New %@ task"; "_no_tasks_" = "No tasks in here"; "_create_task_subtitle_" = "Use the + button to create one"; +"_edit_task_" = "Edit %@ task"; "_no_types_" = "No provider found"; "_no_types_subtitle_" = "AI Providers need to be installed to use the Assistant"; From a98b1e8eb96aeee353903b35f725727cc7c84c1a Mon Sep 17 00:00:00 2001 From: Milen Pivchev Date: Wed, 19 Feb 2025 18:57:51 +0100 Subject: [PATCH 07/20] WIP Signed-off-by: Milen Pivchev --- .../Assistant/Models/NCAssistantModel.swift | 27 +++++++------------ iOSClient/Assistant/NCAssistant.swift | 10 +++++++ 2 files changed, 20 insertions(+), 17 deletions(-) diff --git a/iOSClient/Assistant/Models/NCAssistantModel.swift b/iOSClient/Assistant/Models/NCAssistantModel.swift index d46f06ba89..64cbb53a1f 100644 --- a/iOSClient/Assistant/Models/NCAssistantModel.swift +++ b/iOSClient/Assistant/Models/NCAssistantModel.swift @@ -32,7 +32,7 @@ class NCAssistantModel: ObservableObject { self.controller = controller session = NCSession.shared.getSession(controller: controller) useV2 = NCCapabilities.shared.getCapabilities(account: session.account).capabilityServerVersionMajor >= NCGlobal.shared.nextcloudVersion30 -// useV2 = false + // useV2 = false loadAllTypes() } @@ -63,7 +63,7 @@ class NCAssistantModel: ObservableObject { isLoading = true if useV2 { - NextcloudKit.shared.textProcessingGetTasksV2(taskType: task.type ?? "", account: session.account, completion: { account, tasks, responseData, error in + NextcloudKit.shared.textProcessingGetTasksV2(taskType: task.type ?? "", account: session.account, completion: { _, tasks, _, error in self.isLoading = false if error != .success { @@ -93,7 +93,7 @@ class NCAssistantModel: ObservableObject { if useV2 { guard let selectedType else { return } - NextcloudKit.shared.textProcessingScheduleV2(input: input, taskType: selectedType, account: session.account) { account, task, responseData, error in + NextcloudKit.shared.textProcessingScheduleV2(input: input, taskType: selectedType, account: session.account) { _, task, _, error in handle(task: task, error: error) } } else { @@ -113,16 +113,13 @@ class NCAssistantModel: ObservableObject { guard let task else { return } - withAnimation { - self.tasks.insert(task, at: 0) - self.filteredTasks.insert(task, at: 0) - } + self.tasks.insert(task, at: 0) + self.filteredTasks.insert(task, at: 0) } } func deleteTask(_ task: AssistantTask) { isLoading = true - print("WTF") if useV2 { NextcloudKit.shared.textProcessingDeleteTaskV2(taskId: task.id, account: session.account) { _, _, error in @@ -142,10 +139,8 @@ class NCAssistantModel: ObservableObject { return } - withAnimation { - self.tasks.removeAll(where: { $0.id == task.id }) - self.filteredTasks.removeAll(where: { $0.id == task.id }) - } + self.tasks.removeAll(where: { $0.id == task.id }) + self.filteredTasks.removeAll(where: { $0.id == task.id }) } } @@ -158,7 +153,7 @@ class NCAssistantModel: ObservableObject { isLoading = true if useV2 { - NextcloudKit.shared.textProcessingGetTypesV2(account: session.account) { account, types, responseData, error in + NextcloudKit.shared.textProcessingGetTypesV2(account: session.account) { _, types, _, error in handle(types: types, error: error) } } else { @@ -180,9 +175,7 @@ class NCAssistantModel: ObservableObject { guard let types else { return } - withAnimation { - self.types = types - } + self.types = types if self.selectedType == nil { self.selectTaskType(types.first) @@ -228,7 +221,7 @@ extension NCAssistantModel { var tasks: [AssistantTask] = [] - for index in 1...10 { + for _ in 1...10 { tasks.append(AssistantTask(id: 1, type: "", status: "", userId: "", appId: "", input: .init(input: loremIpsum), output: .init(output: loremIpsum), completionExpectedAt: 1712666412, progress: nil, lastUpdated: nil, scheduledAt: nil, endedAt: nil)) } diff --git a/iOSClient/Assistant/NCAssistant.swift b/iOSClient/Assistant/NCAssistant.swift index 7de60226f6..71323615a2 100644 --- a/iOSClient/Assistant/NCAssistant.swift +++ b/iOSClient/Assistant/NCAssistant.swift @@ -106,6 +106,16 @@ struct TaskList: View { } } + Button { + model.scheduleTask(input: task.input?.input ?? "") + } label: { + Label { + Text("_retry_") + } icon: { + Image(systemName: "arrow.trianglehead.clockwise") + } + } + Button { taskToEdit = task presentEditTask = true From a935eda29c61e0fb642d33799eec710ee827c66f Mon Sep 17 00:00:00 2001 From: Milen Pivchev Date: Thu, 20 Feb 2025 12:01:53 +0100 Subject: [PATCH 08/20] Change CI code Signed-off-by: Milen Pivchev --- Tests/Server.sh | 10 +++++++++- .../Assistant/Task Detail/NCAssistantTaskDetail.swift | 1 - 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/Tests/Server.sh b/Tests/Server.sh index 9d2a851495..ad442a3efa 100755 --- a/Tests/Server.sh +++ b/Tests/Server.sh @@ -15,11 +15,19 @@ docker run \ --detach \ --name $CONTAINER_NAME \ --publish 8080:80 \ + -e BRANCH=$SERVER_VERSION \ ghcr.io/nextcloud/continuous-integration-shallow-server:latest # Wait a moment until the server is ready. -sleep 2 +sleep 20 # Enable File Download Limit App. docker exec $CONTAINER_NAME su www-data -c "git clone --depth 1 -b $SERVER_VERSION https://github.com/nextcloud/files_downloadlimit.git /var/www/html/apps/files_downloadlimit/" docker exec $CONTAINER_NAME su www-data -c "php /var/www/html/occ app:enable files_downloadlimit" + +# Enable Assistant and Testing app +docker exec $CONTAINER_NAME su www-data -c "php /var/www/html/occ app:enable assistant" +docker exec $CONTAINER_NAME su www-data -c "php /var/www/html/occ app:enable testing" + +#Testing app generates fake Assitant responses via cronjob. Reduce cronjob downtime so it's quicker. +docker exec $CONTAINER_NAME su www-data -c "set -e; while true; do php /var/www/html/occ background-job:worker -v -t 10 \"OC\TaskProcessing\SynchronousBackgroundJob\"; done" diff --git a/iOSClient/Assistant/Task Detail/NCAssistantTaskDetail.swift b/iOSClient/Assistant/Task Detail/NCAssistantTaskDetail.swift index 78354a862e..ce487732ee 100644 --- a/iOSClient/Assistant/Task Detail/NCAssistantTaskDetail.swift +++ b/iOSClient/Assistant/Task Detail/NCAssistantTaskDetail.swift @@ -70,7 +70,6 @@ struct InputOutputScrollView: View { .background(Color(NCBrandColor.shared.textColor2).opacity(0.1)) .clipShape(.rect(cornerRadius: 8)) .textSelection(.enabled) - } .padding(.horizontal) .padding(.bottom, 80) From 55e52e3edadef6c88b2e932fd6d54be39d43d741 Mon Sep 17 00:00:00 2001 From: Milen Pivchev Date: Thu, 20 Feb 2025 18:45:02 +0100 Subject: [PATCH 09/20] UI tests Signed-off-by: Milen Pivchev --- Nextcloud.xcodeproj/project.pbxproj | 16 +- Tests/NextcloudUITests/AssistantTests.swift | 559 ++++++++++++++++++ Tests/NextcloudUITests/BaseUIXCTestCase.swift | 108 ++++ .../NextcloudUITests/DownloadLimitTests.swift | 105 +--- .../Responses/CapabilitiesResponse.swift | 4 +- ...esponse.swift => CapabilityResponse.swift} | 4 +- .../UITestBackend/UITestBackend.swift | 4 +- Tests/TestConstants.swift | 2 +- .../NCAssistantCreateNewTask.swift | 1 + iOSClient/Assistant/NCAssistant.swift | 2 + 10 files changed, 692 insertions(+), 113 deletions(-) create mode 100644 Tests/NextcloudUITests/AssistantTests.swift create mode 100644 Tests/NextcloudUITests/BaseUIXCTestCase.swift rename Tests/NextcloudUITests/UITestBackend/Responses/{DownloadLimitCapabilityResponse.swift => CapabilityResponse.swift} (58%) diff --git a/Nextcloud.xcodeproj/project.pbxproj b/Nextcloud.xcodeproj/project.pbxproj index 8a53fae9af..997f5b8f1a 100644 --- a/Nextcloud.xcodeproj/project.pbxproj +++ b/Nextcloud.xcodeproj/project.pbxproj @@ -40,7 +40,7 @@ AA8E03DC2D2FBAC200E7E89C /* DownloadLimitTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA8E03DB2D2FBAAD00E7E89C /* DownloadLimitTests.swift */; }; AA8E041D2D300FDE00E7E89C /* NCShareNetworkingDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA8E041C2D300FDE00E7E89C /* NCShareNetworkingDelegate.swift */; }; AAA7BC2E2D3E39F1008F1A22 /* CapabilitiesResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAA7BC2D2D3E39EC008F1A22 /* CapabilitiesResponse.swift */; }; - AAA7BC302D3E3B88008F1A22 /* DownloadLimitCapabilityResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAA7BC2F2D3E3B83008F1A22 /* DownloadLimitCapabilityResponse.swift */; }; + AAA7BC302D3E3B88008F1A22 /* CapabilityResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAA7BC2F2D3E3B83008F1A22 /* CapabilityResponse.swift */; }; AABD0C8A2D5F67A400F009E6 /* XCUIElement.swift in Sources */ = {isa = PBXBuildFile; fileRef = AABD0C892D5F67A200F009E6 /* XCUIElement.swift */; }; AABD0C9B2D5F73FC00F009E6 /* Placeholder.swift in Sources */ = {isa = PBXBuildFile; fileRef = AABD0C9A2D5F73FA00F009E6 /* Placeholder.swift */; }; AAE330042D2ED20200B04903 /* NCShareNavigationTitleSetting.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAE330032D2ED1FF00B04903 /* NCShareNavigationTitleSetting.swift */; }; @@ -96,6 +96,8 @@ F32FADA92D1176E3007035E2 /* UIButton+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = F32FADA82D1176DE007035E2 /* UIButton+Extension.swift */; }; F3374A812D64AB9F002A38F9 /* StatusInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3374A802D64AB9E002A38F9 /* StatusInfo.swift */; }; F3374A842D64AC31002A38F9 /* AssistantLabelStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3374A832D64AC2C002A38F9 /* AssistantLabelStyle.swift */; }; + F3374A942D674454002A38F9 /* AssistantTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3374A932D674454002A38F9 /* AssistantTests.swift */; }; + F3374A962D6744A4002A38F9 /* BaseUIXCTestCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3374A952D6744A4002A38F9 /* BaseUIXCTestCase.swift */; }; F33918C42C7CD8F2002D9AA1 /* FileNameValidator+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = F33918C32C7CD8F2002D9AA1 /* FileNameValidator+Extensions.swift */; }; F33918C52C7CD8F2002D9AA1 /* FileNameValidator+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = F33918C32C7CD8F2002D9AA1 /* FileNameValidator+Extensions.swift */; }; F33918C62C7CD8F2002D9AA1 /* FileNameValidator+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = F33918C32C7CD8F2002D9AA1 /* FileNameValidator+Extensions.swift */; }; @@ -1215,7 +1217,7 @@ AA8E041C2D300FDE00E7E89C /* NCShareNetworkingDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCShareNetworkingDelegate.swift; sourceTree = ""; }; AA8E041E2D3114E200E7E89C /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; AAA7BC2D2D3E39EC008F1A22 /* CapabilitiesResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CapabilitiesResponse.swift; sourceTree = ""; }; - AAA7BC2F2D3E3B83008F1A22 /* DownloadLimitCapabilityResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownloadLimitCapabilityResponse.swift; sourceTree = ""; }; + AAA7BC2F2D3E3B83008F1A22 /* CapabilityResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CapabilityResponse.swift; sourceTree = ""; }; AABD0C862D5F58C400F009E6 /* Server.sh */ = {isa = PBXFileReference; lastKnownFileType = text.script.sh; path = Server.sh; sourceTree = ""; }; AABD0C892D5F67A200F009E6 /* XCUIElement.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XCUIElement.swift; sourceTree = ""; }; AABD0C9A2D5F73FA00F009E6 /* Placeholder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Placeholder.swift; sourceTree = ""; }; @@ -1268,6 +1270,8 @@ F32FADA82D1176DE007035E2 /* UIButton+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIButton+Extension.swift"; sourceTree = ""; }; F3374A802D64AB9E002A38F9 /* StatusInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusInfo.swift; sourceTree = ""; }; F3374A832D64AC2C002A38F9 /* AssistantLabelStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssistantLabelStyle.swift; sourceTree = ""; }; + F3374A932D674454002A38F9 /* AssistantTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssistantTests.swift; sourceTree = ""; }; + F3374A952D6744A4002A38F9 /* BaseUIXCTestCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseUIXCTestCase.swift; sourceTree = ""; }; F33918C32C7CD8F2002D9AA1 /* FileNameValidator+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FileNameValidator+Extensions.swift"; sourceTree = ""; }; F33EE6F12BF4C9B200CA1A51 /* PKCS12.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PKCS12.swift; sourceTree = ""; }; F343A4B22A1E01FF00DDA874 /* PHAsset+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PHAsset+Extension.swift"; sourceTree = ""; }; @@ -2022,7 +2026,7 @@ children = ( AAA7BC2D2D3E39EC008F1A22 /* CapabilitiesResponse.swift */, AA3C85F12D394B3600F74F12 /* DownloadLimitResponse.swift */, - AAA7BC2F2D3E3B83008F1A22 /* DownloadLimitCapabilityResponse.swift */, + AAA7BC2F2D3E3B83008F1A22 /* CapabilityResponse.swift */, AA3C85ED2D36BCCB00F74F12 /* SharesResponse.swift */, AA3C85EA2D36BBF400F74F12 /* OCSResponse.swift */, ); @@ -2074,9 +2078,11 @@ C0046CDB2A17B98400D87C9D /* NextcloudUITests */ = { isa = PBXGroup; children = ( + F3374A952D6744A4002A38F9 /* BaseUIXCTestCase.swift */, AABD0C882D5F631600F009E6 /* Extensions */, AA3C85E92D36BBDE00F74F12 /* UITestBackend */, AA8E03DB2D2FBAAD00E7E89C /* DownloadLimitTests.swift */, + F3374A932D674454002A38F9 /* AssistantTests.swift */, AA74AA962D3172CE00BE3458 /* UITestError.swift */, ); path = NextcloudUITests; @@ -4038,7 +4044,8 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - AAA7BC302D3E3B88008F1A22 /* DownloadLimitCapabilityResponse.swift in Sources */, + AAA7BC302D3E3B88008F1A22 /* CapabilityResponse.swift in Sources */, + F3374A942D674454002A38F9 /* AssistantTests.swift in Sources */, AA3C85EB2D36BBFB00F74F12 /* OCSResponse.swift in Sources */, AA74AA972D3172D100BE3458 /* UITestError.swift in Sources */, AA3C85E82D36B08C00F74F12 /* UITestBackend.swift in Sources */, @@ -4047,6 +4054,7 @@ AA3C85EE2D36BCCE00F74F12 /* SharesResponse.swift in Sources */, AAA7BC2E2D3E39F1008F1A22 /* CapabilitiesResponse.swift in Sources */, AA3C85F22D394B3A00F74F12 /* DownloadLimitResponse.swift in Sources */, + F3374A962D6744A4002A38F9 /* BaseUIXCTestCase.swift in Sources */, F37208812BAB5979006B5430 /* TestConstants.swift in Sources */, AABD0C9B2D5F73FC00F009E6 /* Placeholder.swift in Sources */, ); diff --git a/Tests/NextcloudUITests/AssistantTests.swift b/Tests/NextcloudUITests/AssistantTests.swift new file mode 100644 index 0000000000..868022673f --- /dev/null +++ b/Tests/NextcloudUITests/AssistantTests.swift @@ -0,0 +1,559 @@ +// SPDX-FileCopyrightText: Nextcloud GmbH +// SPDX-FileCopyrightText: 2025 Iva Horn +// SPDX-License-Identifier: GPL-3.0-or-later + +import XCTest + +/// +/// User interface tests for the download limits management on shares. +/// +/// > To Do: Check whether this can be converted to Swift Testing. +/// +@MainActor +final class AssistantTests: BaseUIXCTestCase { + /// + /// The Nextcloud server API abstraction object. + /// + var backend: UITestBackend! + + /// + /// Name of the file to work with. + /// + /// The leading underscore is required for the file to appear at the top of the list. + /// Obviously, this is fragile by making some assumptions of the user interface state. + /// + let testFileName = "_Xcode UI Test Subject.md" + + // MARK: - Helpers + + /// + /// Pull to refresh on the first found collection view to reveal the new file on the server. + /// + func pullToRefresh(file: StaticString = #file, line: UInt = #line) { + let cell = app.collectionViews.firstMatch.staticTexts.firstMatch + + guard cell.exists else { + XCTFail("Apparently no collection view cell is visible!", file: file, line: line) + return + } + + let start = cell.coordinate(withNormalizedOffset: CGVectorMake(0, 0)) + let finish = cell.coordinate(withNormalizedOffset: CGVectorMake(0, 20)) + + start.press(forDuration: 0.2, thenDragTo: finish) + } + + // MARK: - Lifecycle + + override func setUp() async throws { + try await super.setUp() + continueAfterFailure = false + + // Handle alerts presented by the system. + addUIInterruptionMonitor(withDescription: "Allow Notifications", for: "Allow") + addUIInterruptionMonitor(withDescription: "Save Password", for: "Not Now") + + // Launch the app. + app = XCUIApplication() + app.launchArguments = ["UI_TESTING"] + app.launch() + + try logIn() + + // Set up test backend communication. + backend = UITestBackend() + + try await backend.assertCapability(true, capability: \.assistant) +// try await backend.delete(testFileName) +// try await backend.prepareTestFile(testFileName) + } + + // MARK: - Tests + + func testCreateAssistantTask() async throws { + let taskInput = "TestTask" + let button = app.tabBars["Tab Bar"].buttons["More"] + guard button.await() else { return } + button.tap() + + let talkStaticText = app.tables.staticTexts["Assistant"] + talkStaticText.tap() + + app.navigationBars["Assistant"].buttons["CreateButton"].tap() + + app.textViews["InputTextEditor"].typeText(taskInput) + app.navigationBars["New Free text to text prompt task"]/*@START_MENU_TOKEN@*/.buttons["Create"]/*[[".otherElements[\"Create\"].buttons[\"Create\"]",".buttons[\"Create\"]"],[[[-1,1],[-1,0]]],[0]]@END_MENU_TOKEN@*/.tap() + + let cell = app.collectionViews.children(matching: .cell).element(boundBy: 0) + + pullToRefresh() + + try await aMoment() + + XCTAssert(cell.staticTexts[taskInput].exists) + + } + + func testEditAssistantTask() async throws { + try await testCreateAssistantTask() + + let taskInputEdited = "TestTask" + +// XCUIApplication().collectionViews.children(matching: .cell).element(boundBy: 0).buttons["TestTask, This is a fake result: \n\n- Prompt: TestTask\n- Model: model_2\n- Maximum number of words: 1234, Today"]/*@START_MENU_TOKEN@*/.press(forDuration: 1.5);/*[[".tap()",".press(forDuration: 1.5);"],[[[-1,1],[-1,0]]],[0]]@END_MENU_TOKEN@*/ +// XCUIApplication().collectionViews.children(matching: .cell).element(boundBy: 0)/*@START_MENU_TOKEN@*/.staticTexts["This is a fake result: \n\n- Prompt: TestTask\n- Model: model_2\n- Maximum number of words: 1234"].press(forDuration: 1.3);/*[[".buttons[\"TestTask, This is a fake result: \\n\\n- Prompt: TestTask\\n- Model: model_2\\n- Maximum number of words: 1234, Today\"].staticTexts[\"This is a fake result: \\n\\n- Prompt: TestTask\\n- Model: model_2\\n- Maximum number of words: 1234\"]",".tap()",".press(forDuration: 1.3);",".staticTexts[\"This is a fake result: \\n\\n- Prompt: TestTask\\n- Model: model_2\\n- Maximum number of words: 1234\"]"],[[[-1,3,1],[-1,0,1]],[[-1,2],[-1,1]]],[0,0]]@END_MENU_TOKEN@*/ +// XCUIApplication().collectionViews.children(matching: .cell).element(boundBy: 0).buttons["TestTask, This is a fake result: \n\n- Prompt: TestTask\n- Model: model_2\n- Maximum number of words: 1234, Today"].tap() +// + + let cell = app.collectionViews.children(matching: .cell).element(boundBy: 0) + + XCUIApplication().tabBars["Tab Bar"].buttons["More"].tap() +// XCUIApplication().collectionViews.children(matching: .cell).element(boundBy: 0)/*@START_MENU_TOKEN@*/.staticTexts["This is a fake result: \n\n- Prompt: TestTask\n- Model: model_2\n- Maximum number of words: 1234"].press(forDuration: 2.1);/*[[".buttons[\"TestTask, This is a fake result: \\n\\n- Prompt: TestTask\\n- Model: model_2\\n- Maximum number of words: 1234, Today\"].staticTexts[\"This is a fake result: \\n\\n- Prompt: TestTask\\n- Model: model_2\\n- Maximum number of words: 1234\"]",".tap()",".press(forDuration: 2.1);",".buttons[\"TaskContextMenu\"].staticTexts[\"This is a fake result: \\n\\n- Prompt: TestTask\\n- Model: model_2\\n- Maximum number of words: 1234\"]",".staticTexts[\"This is a fake result: \\n\\n- Prompt: TestTask\\n- Model: model_2\\n- Maximum number of words: 1234\"]"],[[[-1,4,1],[-1,3,1],[-1,0,1]],[[-1,2],[-1,1]]],[0,0]]@END_MENU_TOKEN@*/ +// XCUIApplication().collectionViews.children(matching: .cell).element(boundBy: 0)/*@START_MENU_TOKEN@*/.staticTexts["TestTask"].press(forDuration: 2.2);/*[[".buttons[\"TestTask, This is a fake result: \\n\\n- Prompt: TestTask\\n- Model: model_2\\n- Maximum number of words: 1234, Today\"].staticTexts[\"TestTask\"]",".tap()",".press(forDuration: 2.2);",".buttons[\"TaskContextMenu\"].staticTexts[\"TestTask\"]",".staticTexts[\"TestTask\"]"],[[[-1,4,1],[-1,3,1],[-1,0,1]],[[-1,2],[-1,1]]],[0,0]]@END_MENU_TOKEN@*/ + XCUIApplication().collectionViews.children(matching: .cell).element(boundBy: 0)/*@START_MENU_TOKEN@*/.staticTexts["TestTask"].press(forDuration: 1.7);/*[[".buttons[\"TestTask, This is a fake result: \\n\\n- Prompt: TestTask\\n- Model: model_2\\n- Maximum number of words: 1234, Today\"].staticTexts[\"TestTask\"]",".tap()",".press(forDuration: 1.7);",".buttons[\"TaskContextMenu\"].staticTexts[\"TestTask\"]",".staticTexts[\"TestTask\"]"],[[[-1,4,1],[-1,3,1],[-1,0,1]],[[-1,2],[-1,1]]],[0,0]]@END_MENU_TOKEN@*/ +// XCUIApplication().sheets.scrollViews.otherElements.buttons["Delete"].tap() + print(app.debugDescription) + + let editButton = app.otherElements.containing(.staticText, identifier: "Edit").firstMatch + XCTAssertTrue(editButton.waitForExistence(timeout: 2), "Edit button not found in context menu") + editButton.tap() + + } + + func testShareWithoutDownloadLimitCapability() async throws { + // This cannot be implemented at the time of writing. + // There is no way to disable and enable server apps via web API. + // The Xcode UI test process cannot access Docker. + throw XCTSkip("Not implemented yet!") + } + + func testNewShareWithoutDownloadLimit() async throws { + pullToRefresh() + + // Tap share button. + + let shareButton = app.buttons["Cell/\(testFileName)/shareButton"] + + guard shareButton.exists else { + throw UITestError.waitForExistence(shareButton) + } + + shareButton.tap() + + // Tap add share link button. + + let addShareLinkButton = app.buttons["addShareLink"] + guard addShareLinkButton.await() else { return } + addShareLinkButton.tap() + + // Tap confirm share button. + + let confirmShareButton = app.buttons["confirmShare"] + + guard confirmShareButton.exists else { + throw UITestError.waitForExistence(confirmShareButton) + } + + confirmShareButton.tap() + confirmShareButton.awaitInexistence() + + // Then + let shares = try await backend.getShares(byPath: "/\(testFileName)") + XCTAssertEqual(shares.count, 1, "Only one share existing on \(testFileName)") + + guard let token = shares.first?.token else { + throw UITestError.missingValue + } + + try await backend.assertNoDownloadLimit(by: token) + } + + func testNewShareWithDownloadLimit() async throws { + pullToRefresh() + + // Tap share button. + + let shareButton = app.buttons["Cell/\(testFileName)/shareButton"] + guard shareButton.await() else { return } + shareButton.tap() + + // Tap add share link button. + + let addShareLinkButton = app.buttons["addShareLink"] + guard addShareLinkButton.await() else { return } + addShareLinkButton.tap() + + // Tap download limits. + + let downloadLimitCell = app.cells["downloadLimit"] + guard downloadLimitCell.await() else { return } + downloadLimitCell.tap() + + // Tap download limit switch. + + let downloadLimitSwitch = app.switches["downloadLimitSwitch"] + guard downloadLimitSwitch.await() else { return } + downloadLimitSwitch.tap() + + // Update allowed downloads. + + let allowedDownloadsTextField = app.textFields["downloadLimitTextField"] + guard allowedDownloadsTextField.await() else { return } + allowedDownloadsTextField.tap() + allowedDownloadsTextField.typeText("3") + + // Tap navigation back button. + + app.navigationBars.buttons.firstMatch.tap() + + // Tap confirm share button. + + let confirmShareButton = app.buttons["confirmShare"] + guard confirmShareButton.await() else { return } + confirmShareButton.tap() + confirmShareButton.awaitInexistence() + + // Then + let shares = try await backend.getShares(byPath: "/\(testFileName)") + XCTAssertEqual(shares.count, 1, "Only one share existing on \(testFileName)") + + guard let token = shares.first?.token else { + throw UITestError.missingValue + } + + try await backend.assertDownloadLimit(by: token, count: 0, limit: 3) + } + + func testShareOfFolder() async throws { + let testSubject = "_Xcode UI Test Subject" + try await backend.createFolder(testSubject) + pullToRefresh() + + // Tap share button. + + let shareButton = app.buttons["Cell/\(testSubject)/shareButton"] + guard shareButton.await() else { return } + shareButton.tap() + + // Tap add share link button. + + let addShareLinkButton = app.buttons["addShareLink"] + guard addShareLinkButton.await() else { return } + addShareLinkButton.tap() + + // Verify download limits being unavailable. + + let downloadLimitCell = app.cells["downloadLimit"] + XCTAssertFalse(downloadLimitCell.exists, "Folder shares cannot have download limits") + + // Cleanup + + try await backend.delete(testSubject) + } + + func testAddingDownloadLimitToExistingShare() async throws { + try await backend.createShare(byPath: testFileName) + pullToRefresh() + + // Tap share button. + + let shareButton = app.buttons["Cell/\(testFileName)/shareButton"] + guard shareButton.await() else { return } + shareButton.tap() + + // Tap show share link details button. + + let showShareLinkDetailsButton = app.buttons["showShareLinkDetails"] + guard showShareLinkDetailsButton.await() else { return } + showShareLinkDetailsButton.tap() + + // Tap share link details button in share menu sheet. + + let shareMenuDetailsCell = app.cells["shareMenu/details"] + guard shareMenuDetailsCell.await() else { return } + shareMenuDetailsCell.tap() + + // Tap download limits. + + let downloadLimitCell = app.cells["downloadLimit"] + guard downloadLimitCell.await() else { return } + downloadLimitCell.tap() + + // Tap download limit switch. + + let downloadLimitSwitch = app.switches["downloadLimitSwitch"] + guard downloadLimitSwitch.await() else { return } + downloadLimitSwitch.tap() + + // Update allowed downloads. + + let allowedDownloadsTextField = app.textFields["downloadLimitTextField"] + guard allowedDownloadsTextField.await() else { return } + allowedDownloadsTextField.tap() + allowedDownloadsTextField.typeText("3") + + // Tap navigation back button. + + app.navigationBars.buttons.firstMatch.tap() + + // Tap confirm share button. + + let confirmShareButton = app.buttons["confirmShare"] + guard confirmShareButton.await() else { return } + confirmShareButton.tap() + confirmShareButton.awaitInexistence() + + // Then + let shares = try await backend.getShares(byPath: "\(testFileName)") + XCTAssertEqual(shares.count, 1, "Only one share existing on \(testFileName)") + + guard let token = shares.first?.token else { + throw UITestError.missingValue + } + + try await aMoment() + try await backend.assertDownloadLimit(by: token, count: 0, limit: 3) + } + + func testUpdatingDownloadLimitOnExistingShare() async throws { + try await backend.createShare(byPath: testFileName) + var shares = try await backend.getShares(byPath: testFileName) + + guard let token = shares.first?.token else { + XCTFail("Failed to fetch token of share for test file!") + return + } + + try await backend.setDownloadLimit(to: 4, by: token) + pullToRefresh() + + // Tap share button. + + let shareButton = app.buttons["Cell/\(testFileName)/shareButton"] + guard shareButton.await() else { return } + shareButton.tap() + + // Tap show share link details button. + + let showShareLinkDetailsButton = app.buttons["showShareLinkDetails"] + guard showShareLinkDetailsButton.await() else { return } + showShareLinkDetailsButton.tap() + + // Tap share link details button in share menu sheet. + + let shareMenuDetailsCell = app.cells["shareMenu/details"] + guard shareMenuDetailsCell.await() else { return } + shareMenuDetailsCell.tap() + + // Tap download limits. + + let downloadLimitCell = app.cells["downloadLimit"] + guard downloadLimitCell.await() else { return } + downloadLimitCell.tap() + + // Check download limit switch. + + let downloadLimitSwitch = app.switches["downloadLimitSwitch"].firstMatch + guard downloadLimitSwitch.await() else { return } + + guard let downloadLimitSwitchValue = (downloadLimitSwitch.value as? String) else { + XCTFail("Failed to get value of user interface control!") + return + } + + guard downloadLimitSwitchValue == "1" else { + XCTFail("The switch is not on!") + return + } + + // Check and update allowed downloads. + + let allowedDownloadsTextField = app.textFields["downloadLimitTextField"] + guard allowedDownloadsTextField.await() else { return } + + guard let allowedDownloadsTextFieldValue = (allowedDownloadsTextField.value as? String) else { + XCTFail("Failed to get value of user interface control!") + return + } + + guard allowedDownloadsTextFieldValue == "4" else { + XCTFail("The text field value is wrong!") + return + } + + allowedDownloadsTextField.tap() + allowedDownloadsTextField.typeText("6") + + // Tap navigation back button. + + app.navigationBars.buttons.firstMatch.tap() + + // Tap confirm share button. + + let confirmShareButton = app.buttons["confirmShare"] + guard confirmShareButton.await() else { return } + confirmShareButton.tap() + confirmShareButton.awaitInexistence() + + // Then + shares = try await backend.getShares(byPath: "\(testFileName)") + XCTAssertEqual(shares.count, 1, "Only one share existing on \(testFileName)") + + guard let token = shares.first?.token else { + throw UITestError.missingValue + } + + try await aMoment() + try await backend.assertDownloadLimit(by: token, count: 0, limit: 6) + } + + func testDiscardingDownloadLimitChangesOnExistingShare() async throws { + try await backend.createShare(byPath: testFileName) + var shares = try await backend.getShares(byPath: testFileName) + + guard let token = shares.first?.token else { + XCTFail("Failed to fetch token of share for test file!") + return + } + + try await backend.setDownloadLimit(to: 8, by: token) + pullToRefresh() + + // Tap share button. + + let shareButton = app.buttons["Cell/\(testFileName)/shareButton"] + guard shareButton.await() else { return } + shareButton.tap() + + // Tap show share link details button. + + let showShareLinkDetailsButton = app.buttons["showShareLinkDetails"] + guard showShareLinkDetailsButton.await() else { return } + showShareLinkDetailsButton.tap() + + // Tap share link details button in share menu sheet. + + let shareMenuDetailsCell = app.cells["shareMenu/details"] + guard shareMenuDetailsCell.await() else { return } + shareMenuDetailsCell.tap() + + // Tap download limits. + + let downloadLimitCell = app.cells["downloadLimit"] + guard downloadLimitCell.await() else { return } + downloadLimitCell.tap() + + // Check download limit switch. + + let downloadLimitSwitch = app.switches["downloadLimitSwitch"].firstMatch + guard downloadLimitSwitch.await() else { return } + + guard let downloadLimitSwitchValue = (downloadLimitSwitch.value as? String) else { + XCTFail("Failed to get value of user interface control!") + return + } + + guard downloadLimitSwitchValue == "1" else { + XCTFail("The switch is not on!") + return + } + + // Check and update allowed downloads. + + let allowedDownloadsTextField = app.textFields["downloadLimitTextField"] + guard allowedDownloadsTextField.await() else { return } + + guard let allowedDownloadsTextFieldValue = (allowedDownloadsTextField.value as? String) else { + XCTFail("Failed to get value of user interface control!") + return + } + + allowedDownloadsTextField.tap() + allowedDownloadsTextField.typeText("9") + + // Tap navigation back button. + + app.navigationBars.buttons.firstMatch.tap() + + // Tap confirm share button. + + let cancelShareButton = app.buttons["cancelShare"] + guard cancelShareButton.await() else { return } + cancelShareButton.tap() + + // Then + shares = try await backend.getShares(byPath: "\(testFileName)") + XCTAssertEqual(shares.count, 1, "Only one share existing on \(testFileName)") + + guard let token = shares.first?.token else { + throw UITestError.missingValue + } + + try await aMoment() + try await backend.assertDownloadLimit(by: token, count: 0, limit: 8) + } + + func testRemovingDownloadLimitFromExistingShare() async throws { + try await backend.createShare(byPath: testFileName) + var shares = try await backend.getShares(byPath: testFileName) + + guard let token = shares.first?.token else { + XCTFail("Failed to fetch token of share for test file!") + return + } + + try await backend.setDownloadLimit(to: 4, by: token) + pullToRefresh() + + // Tap share button. + + let shareButton = app.buttons["Cell/\(testFileName)/shareButton"] + guard shareButton.await() else { return } + shareButton.tap() + + // Tap show share link details button. + + let showShareLinkDetailsButton = app.buttons["showShareLinkDetails"] + guard showShareLinkDetailsButton.await() else { return } + showShareLinkDetailsButton.tap() + + // Tap share link details button in share menu sheet. + + let shareMenuDetailsCell = app.cells["shareMenu/details"] + guard shareMenuDetailsCell.await() else { return } + shareMenuDetailsCell.tap() + + // Tap download limits. + + let downloadLimitCell = app.cells["downloadLimit"] + guard downloadLimitCell.await() else { return } + downloadLimitCell.tap() + + // Check download limit switch. + + let downloadLimitSwitch = app.switches["downloadLimitSwitch"].firstMatch + guard downloadLimitSwitch.await() else { return } + downloadLimitSwitch.tap() + + // Tap navigation back button. + + app.navigationBars.buttons.firstMatch.tap() + + // Tap confirm share button. + + let confirmShareButton = app.buttons["confirmShare"] + guard confirmShareButton.await() else { return } + confirmShareButton.tap() + confirmShareButton.awaitInexistence() + + // Then + shares = try await backend.getShares(byPath: "\(testFileName)") + XCTAssertEqual(shares.count, 1, "Only one share existing on \(testFileName)") + + guard let token = shares.first?.token else { + throw UITestError.missingValue + } + + try await aMoment() + try await backend.assertDownloadLimit(by: token, count: nil, limit: nil) + } +} diff --git a/Tests/NextcloudUITests/BaseUIXCTestCase.swift b/Tests/NextcloudUITests/BaseUIXCTestCase.swift new file mode 100644 index 0000000000..11d7cbd5b3 --- /dev/null +++ b/Tests/NextcloudUITests/BaseUIXCTestCase.swift @@ -0,0 +1,108 @@ +// +// Helpers.swift +// NextcloudUITests +// +// Created by Milen Pivchev on 20.02.25. +// Copyright © 2025 Marino Faggiana. All rights reserved. +// + +import Foundation +import XCTest + +@MainActor +class BaseUIXCTestCase: XCTestCase { + var app: XCUIApplication! + + /// + /// Generic convenience method to define user interface interruption monitors. + /// + /// This is called every time an alert from outside the app's user interface is presented (in example system prompt about saving a password). + /// Then the button is tapped defined by the given `label`. + /// + /// - Parameters: + /// - description: The human readable description for the monitor to create. + /// - label: The localized text on the alert action to tap. + /// + /// + func addUIInterruptionMonitor(withDescription description: String, for label: String) { + addUIInterruptionMonitor(withDescription: description) { alert in + let button = alert.buttons[label] + + if button.exists { + button.tap() + return true + } + + return false + } + } + + /// + /// Let the current `Task` rest for ``TestConstants/controlExistenceTimeout``. + /// + /// Some asynchronous background activities like the follow up request to define a download limit have no effect on the visible user interface. + /// Hence their outcome can only be assumed after a brief period of time. + /// + func aMoment() async throws { + try await Task.sleep(for: .seconds(TestConstants.controlExistenceTimeout)) + } + + /// + /// Automation of the sign-in, if required. + /// + /// + func logIn() throws { + guard app.buttons["login"].exists else { + return + } + + app.buttons["login"].tap() + + let serverAddressTextField = app.textFields["serverAddress"].firstMatch + guard serverAddressTextField.await() else { return } + + serverAddressTextField.tap() + serverAddressTextField.typeText(TestConstants.server) + + app.buttons["submitServerAddress"].tap() + + let webView = app.webViews.firstMatch + + guard webView.await() else { + throw UITestError.waitForExistence(webView) + } + + let loginButton = webView.buttons["Log in"] + + if loginButton.await() { + loginButton.tap() + } + + let usernameTextField = webView.textFields.firstMatch + + if usernameTextField.await() { + guard usernameTextField.await() else { return } + usernameTextField.tap() + usernameTextField.typeText(TestConstants.username) + + let passwordSecureTextField = webView.secureTextFields.firstMatch + passwordSecureTextField.tap() + passwordSecureTextField.typeText(TestConstants.password) + + webView.buttons.firstMatch.tap() + } + + let grantButton = webView.buttons["Grant access"] + + guard grantButton.await() else { + throw UITestError.waitForExistence(grantButton) + } + + grantButton.tap() + grantButton.awaitInexistence() + + // Switch back from Safari to our app. + app.activate() + app.buttons["accountSwitcher"].await() + } +} diff --git a/Tests/NextcloudUITests/DownloadLimitTests.swift b/Tests/NextcloudUITests/DownloadLimitTests.swift index b1debaf14a..acf55a0fc8 100644 --- a/Tests/NextcloudUITests/DownloadLimitTests.swift +++ b/Tests/NextcloudUITests/DownloadLimitTests.swift @@ -10,9 +10,7 @@ import XCTest /// > To Do: Check whether this can be converted to Swift Testing. /// @MainActor -final class DownloadLimitTests: XCTestCase { - var app: XCUIApplication! - +final class DownloadLimitTests: BaseUIXCTestCase { /// /// The Nextcloud server API abstraction object. /// @@ -26,45 +24,6 @@ final class DownloadLimitTests: XCTestCase { /// let testFileName = "_Xcode UI Test Subject.md" - // MARK: - Helpers - - /// - /// Generic convenience method to define user interface interruption monitors. - /// - /// This is called every time an alert from outside the app's user interface is presented (in example system prompt about saving a password). - /// Then the button is tapped defined by the given `label`. - /// - /// - Parameters: - /// - description: The human readable description for the monitor to create. - /// - label: The localized text on the alert action to tap. - /// - /// > Important: This is a candidate for outsourcing into a dedicated library, if not NextcloudKit. - /// - func addUIInterruptionMonitor(withDescription description: String, for label: String) { - addUIInterruptionMonitor(withDescription: description) { alert in - let button = alert.buttons[label] - - if button.exists { - button.tap() - return true - } - - return false - } - } - - /// - /// Let the current `Task` rest for ``TestConstants/controlExistenceTimeout``. - /// - /// Some asynchronous background activities like the follow up request to define a download limit have no effect on the visible user interface. - /// Hence their outcome can only be assumed after a brief period of time. - /// - /// The odd name may stick out but reads natural in an `try await aMoment()`. - /// - func aMoment() async throws { - try await Task.sleep(for: .seconds(TestConstants.controlExistenceTimeout)) - } - /// /// Pull to refresh on the first found collection view to reveal the new file on the server. /// @@ -82,66 +41,6 @@ final class DownloadLimitTests: XCTestCase { start.press(forDuration: 0.2, thenDragTo: finish) } - /// - /// Automation of the sign-in, if required. - /// - /// > Important: This is a candidate for outsourcing into a dedicated library, if not NextcloudKit. - /// - func logIn() throws { - guard app.buttons["login"].exists else { - return - } - - app.buttons["login"].tap() - - let serverAddressTextField = app.textFields["serverAddress"].firstMatch - guard serverAddressTextField.await() else { return } - - serverAddressTextField.tap() - serverAddressTextField.typeText(TestConstants.server) - - app.buttons["submitServerAddress"].tap() - - let webView = app.webViews.firstMatch - - guard webView.await() else { - throw UITestError.waitForExistence(webView) - } - - let loginButton = webView.buttons["Log in"] - - if loginButton.await() { - loginButton.tap() - } - - let usernameTextField = webView.textFields.firstMatch - - if usernameTextField.await() { - guard usernameTextField.await() else { return } - usernameTextField.tap() - usernameTextField.typeText(TestConstants.username) - - let passwordSecureTextField = webView.secureTextFields.firstMatch - passwordSecureTextField.tap() - passwordSecureTextField.typeText(TestConstants.password) - - webView.buttons.firstMatch.tap() - } - - let grantButton = webView.buttons["Grant access"] - - guard grantButton.await() else { - throw UITestError.waitForExistence(grantButton) - } - - grantButton.tap() - grantButton.awaitInexistence() - - // Switch back from Safari to our app. - app.activate() - app.buttons["accountSwitcher"].await() - } - // MARK: - Lifecycle override func setUp() async throws { @@ -162,7 +61,7 @@ final class DownloadLimitTests: XCTestCase { // Set up test backend communication. backend = UITestBackend() - try await backend.assertDownloadLimitCapability(true) + try await backend.assertCapability(true, capability: \.downloadLimit) try await backend.delete(testFileName) try await backend.prepareTestFile(testFileName) } diff --git a/Tests/NextcloudUITests/UITestBackend/Responses/CapabilitiesResponse.swift b/Tests/NextcloudUITests/UITestBackend/Responses/CapabilitiesResponse.swift index 425afc1b68..13d02ba5b0 100644 --- a/Tests/NextcloudUITests/UITestBackend/Responses/CapabilitiesResponse.swift +++ b/Tests/NextcloudUITests/UITestBackend/Responses/CapabilitiesResponse.swift @@ -12,9 +12,11 @@ struct CapabilitiesResponse: Decodable { struct CapabilitiesResponseCapabilitiesComponent: Decodable { enum CodingKeys: String, CodingKey { case downloadLimit = "downloadlimit" + case assistant = "assistant" } - let downloadLimit: DownloadLimitCapabilityResponse? + let downloadLimit: CapabilityResponse? + let assistant: CapabilityResponse? } let capabilities: CapabilitiesResponseCapabilitiesComponent diff --git a/Tests/NextcloudUITests/UITestBackend/Responses/DownloadLimitCapabilityResponse.swift b/Tests/NextcloudUITests/UITestBackend/Responses/CapabilityResponse.swift similarity index 58% rename from Tests/NextcloudUITests/UITestBackend/Responses/DownloadLimitCapabilityResponse.swift rename to Tests/NextcloudUITests/UITestBackend/Responses/CapabilityResponse.swift index 262330be60..b2504cc66e 100644 --- a/Tests/NextcloudUITests/UITestBackend/Responses/DownloadLimitCapabilityResponse.swift +++ b/Tests/NextcloudUITests/UITestBackend/Responses/CapabilityResponse.swift @@ -3,9 +3,9 @@ // SPDX-License-Identifier: GPL-3.0-or-later /// -/// The schema for the `downloadlimit` capability response model embedded in ``CapabilitiesResponse``. +/// Capability response for a given capability.. /// -struct DownloadLimitCapabilityResponse: Decodable { +struct CapabilityResponse: Decodable { /// /// The download capability is enabled. /// diff --git a/Tests/NextcloudUITests/UITestBackend/UITestBackend.swift b/Tests/NextcloudUITests/UITestBackend/UITestBackend.swift index 8d84b2d2d7..d7069e842a 100644 --- a/Tests/NextcloudUITests/UITestBackend/UITestBackend.swift +++ b/Tests/NextcloudUITests/UITestBackend/UITestBackend.swift @@ -106,7 +106,7 @@ class UITestBackend { /// /// Assert the (in)availability of the download limit capability on the server. /// - func assertDownloadLimitCapability(_ expectation: Bool, file: StaticString = #file, line: UInt = #line) async throws { + func assertCapability(_ expectation: Bool, capability: KeyPath, file: StaticString = #file, line: UInt = #line) async throws { let request = makeOCSRequest(path: "cloud/capabilities") let (data, info) = try await urlSession.data(for: request) let statusCode = (info as! HTTPURLResponse).statusCode @@ -117,7 +117,7 @@ class UITestBackend { } let response = try jsonDecoder.decode(OCSResponse.self, from: data) - let reality = response.data.capabilities.downloadLimit?.enabled ?? false + let reality = response.data.capabilities[keyPath: capability]?.enabled ?? false XCTAssertEqual(expectation, reality, file: file, line: line) } diff --git a/Tests/TestConstants.swift b/Tests/TestConstants.swift index 62e697e470..fe7f5bfaf0 100644 --- a/Tests/TestConstants.swift +++ b/Tests/TestConstants.swift @@ -13,7 +13,7 @@ enum TestConstants { /// /// The default number of seconds to wait for the appearance of user interface controls during user interface tests. /// - static let controlExistenceTimeout: Double = 60 + static let controlExistenceTimeout: Double = 20 /// /// The full base URL for the server to run against. diff --git a/iOSClient/Assistant/Create Task/NCAssistantCreateNewTask.swift b/iOSClient/Assistant/Create Task/NCAssistantCreateNewTask.swift index 04b80689a9..27156c5aad 100644 --- a/iOSClient/Assistant/Create Task/NCAssistantCreateNewTask.swift +++ b/iOSClient/Assistant/Create Task/NCAssistantCreateNewTask.swift @@ -33,6 +33,7 @@ struct NCAssistantCreateNewTask: View { .transparentScrolling() .background(Color(NCBrandColor.shared.textColor2).opacity(0.1)) .focused($inFocus) + .accessibilityIdentifier("InputTextEditor") } .background(Color(NCBrandColor.shared.textColor2).opacity(0.1)) .clipShape(.rect(cornerRadius: 8)) diff --git a/iOSClient/Assistant/NCAssistant.swift b/iOSClient/Assistant/NCAssistant.swift index 71323615a2..44e9ed93cc 100644 --- a/iOSClient/Assistant/NCAssistant.swift +++ b/iOSClient/Assistant/NCAssistant.swift @@ -47,6 +47,7 @@ struct NCAssistant: View { .foregroundStyle(Color(NCBrandColor.shared.iconImageColor)) } .disabled(model.selectedType == nil) + .accessibilityIdentifier("CreateButton") } } .navigationBarTitleDisplayMode(.inline) @@ -138,6 +139,7 @@ struct TaskList: View { } } } + .accessibilityIdentifier("TaskContextMenu") } .if(!model.types.isEmpty) { view in view.refreshable { From 41d7f133d81666284977f2ed28293e568cb24551 Mon Sep 17 00:00:00 2001 From: Milen Pivchev Date: Fri, 21 Feb 2025 17:19:02 +0100 Subject: [PATCH 10/20] WIP Signed-off-by: Milen Pivchev --- Tests/NextcloudUITests/AssistantTests.swift | 539 +++--------------- Tests/NextcloudUITests/BaseUIXCTestCase.swift | 38 +- .../NextcloudUITests/DownloadLimitTests.swift | 24 +- Tests/TestConstants.swift | 2 +- iOSClient/Assistant/NCAssistant.swift | 3 + 5 files changed, 110 insertions(+), 496 deletions(-) diff --git a/Tests/NextcloudUITests/AssistantTests.swift b/Tests/NextcloudUITests/AssistantTests.swift index 868022673f..14a19d7aae 100644 --- a/Tests/NextcloudUITests/AssistantTests.swift +++ b/Tests/NextcloudUITests/AssistantTests.swift @@ -11,37 +11,10 @@ import XCTest /// @MainActor final class AssistantTests: BaseUIXCTestCase { - /// - /// The Nextcloud server API abstraction object. - /// - var backend: UITestBackend! - - /// - /// Name of the file to work with. - /// - /// The leading underscore is required for the file to appear at the top of the list. - /// Obviously, this is fragile by making some assumptions of the user interface state. - /// - let testFileName = "_Xcode UI Test Subject.md" - - // MARK: - Helpers - - /// - /// Pull to refresh on the first found collection view to reveal the new file on the server. - /// - func pullToRefresh(file: StaticString = #file, line: UInt = #line) { - let cell = app.collectionViews.firstMatch.staticTexts.firstMatch - - guard cell.exists else { - XCTFail("Apparently no collection view cell is visible!", file: file, line: line) - return - } - - let start = cell.coordinate(withNormalizedOffset: CGVectorMake(0, 0)) - let finish = cell.coordinate(withNormalizedOffset: CGVectorMake(0, 20)) - - start.press(forDuration: 0.2, thenDragTo: finish) - } + let taskInputCreated = "TestTaskCreated" + NSUUID().uuidString + let taskInputRetried = "TestTaskRetried" + NSUUID().uuidString + let taskInputToEdit = "TestTaskToEdit" + NSUUID().uuidString + let taskInputDeleted = "TestTaskDeleted" + NSUUID().uuidString // MARK: - Lifecycle @@ -58,502 +31,126 @@ final class AssistantTests: BaseUIXCTestCase { app.launchArguments = ["UI_TESTING"] app.launch() - try logIn() + try await logIn() // Set up test backend communication. backend = UITestBackend() try await backend.assertCapability(true, capability: \.assistant) -// try await backend.delete(testFileName) -// try await backend.prepareTestFile(testFileName) } - // MARK: - Tests - - func testCreateAssistantTask() async throws { - let taskInput = "TestTask" + /// + /// Leads to the Assistant screen. + /// + private func goToAssistant() { let button = app.tabBars["Tab Bar"].buttons["More"] guard button.await() else { return } button.tap() let talkStaticText = app.tables.staticTexts["Assistant"] talkStaticText.tap() + } + /// + /// Leads to the Assistant screen. + /// + private func createTask(input: String) { app.navigationBars["Assistant"].buttons["CreateButton"].tap() - - app.textViews["InputTextEditor"].typeText(taskInput) - app.navigationBars["New Free text to text prompt task"]/*@START_MENU_TOKEN@*/.buttons["Create"]/*[[".otherElements[\"Create\"].buttons[\"Create\"]",".buttons[\"Create\"]"],[[[-1,1],[-1,0]]],[0]]@END_MENU_TOKEN@*/.tap() - - let cell = app.collectionViews.children(matching: .cell).element(boundBy: 0) - - pullToRefresh() - - try await aMoment() - - XCTAssert(cell.staticTexts[taskInput].exists) + app.textViews["InputTextEditor"].typeText(input) + app.navigationBars["New Free text to text prompt task"].buttons["Create"].tap() } - func testEditAssistantTask() async throws { - try await testCreateAssistantTask() - - let taskInputEdited = "TestTask" - -// XCUIApplication().collectionViews.children(matching: .cell).element(boundBy: 0).buttons["TestTask, This is a fake result: \n\n- Prompt: TestTask\n- Model: model_2\n- Maximum number of words: 1234, Today"]/*@START_MENU_TOKEN@*/.press(forDuration: 1.5);/*[[".tap()",".press(forDuration: 1.5);"],[[[-1,1],[-1,0]]],[0]]@END_MENU_TOKEN@*/ -// XCUIApplication().collectionViews.children(matching: .cell).element(boundBy: 0)/*@START_MENU_TOKEN@*/.staticTexts["This is a fake result: \n\n- Prompt: TestTask\n- Model: model_2\n- Maximum number of words: 1234"].press(forDuration: 1.3);/*[[".buttons[\"TestTask, This is a fake result: \\n\\n- Prompt: TestTask\\n- Model: model_2\\n- Maximum number of words: 1234, Today\"].staticTexts[\"This is a fake result: \\n\\n- Prompt: TestTask\\n- Model: model_2\\n- Maximum number of words: 1234\"]",".tap()",".press(forDuration: 1.3);",".staticTexts[\"This is a fake result: \\n\\n- Prompt: TestTask\\n- Model: model_2\\n- Maximum number of words: 1234\"]"],[[[-1,3,1],[-1,0,1]],[[-1,2],[-1,1]]],[0,0]]@END_MENU_TOKEN@*/ -// XCUIApplication().collectionViews.children(matching: .cell).element(boundBy: 0).buttons["TestTask, This is a fake result: \n\n- Prompt: TestTask\n- Model: model_2\n- Maximum number of words: 1234, Today"].tap() -// - + private func retryTask() { let cell = app.collectionViews.children(matching: .cell).element(boundBy: 0) + cell.staticTexts[taskInputRetried].press(forDuration: 2); - XCUIApplication().tabBars["Tab Bar"].buttons["More"].tap() -// XCUIApplication().collectionViews.children(matching: .cell).element(boundBy: 0)/*@START_MENU_TOKEN@*/.staticTexts["This is a fake result: \n\n- Prompt: TestTask\n- Model: model_2\n- Maximum number of words: 1234"].press(forDuration: 2.1);/*[[".buttons[\"TestTask, This is a fake result: \\n\\n- Prompt: TestTask\\n- Model: model_2\\n- Maximum number of words: 1234, Today\"].staticTexts[\"This is a fake result: \\n\\n- Prompt: TestTask\\n- Model: model_2\\n- Maximum number of words: 1234\"]",".tap()",".press(forDuration: 2.1);",".buttons[\"TaskContextMenu\"].staticTexts[\"This is a fake result: \\n\\n- Prompt: TestTask\\n- Model: model_2\\n- Maximum number of words: 1234\"]",".staticTexts[\"This is a fake result: \\n\\n- Prompt: TestTask\\n- Model: model_2\\n- Maximum number of words: 1234\"]"],[[[-1,4,1],[-1,3,1],[-1,0,1]],[[-1,2],[-1,1]]],[0,0]]@END_MENU_TOKEN@*/ -// XCUIApplication().collectionViews.children(matching: .cell).element(boundBy: 0)/*@START_MENU_TOKEN@*/.staticTexts["TestTask"].press(forDuration: 2.2);/*[[".buttons[\"TestTask, This is a fake result: \\n\\n- Prompt: TestTask\\n- Model: model_2\\n- Maximum number of words: 1234, Today\"].staticTexts[\"TestTask\"]",".tap()",".press(forDuration: 2.2);",".buttons[\"TaskContextMenu\"].staticTexts[\"TestTask\"]",".staticTexts[\"TestTask\"]"],[[[-1,4,1],[-1,3,1],[-1,0,1]],[[-1,2],[-1,1]]],[0,0]]@END_MENU_TOKEN@*/ - XCUIApplication().collectionViews.children(matching: .cell).element(boundBy: 0)/*@START_MENU_TOKEN@*/.staticTexts["TestTask"].press(forDuration: 1.7);/*[[".buttons[\"TestTask, This is a fake result: \\n\\n- Prompt: TestTask\\n- Model: model_2\\n- Maximum number of words: 1234, Today\"].staticTexts[\"TestTask\"]",".tap()",".press(forDuration: 1.7);",".buttons[\"TaskContextMenu\"].staticTexts[\"TestTask\"]",".staticTexts[\"TestTask\"]"],[[[-1,4,1],[-1,3,1],[-1,0,1]],[[-1,2],[-1,1]]],[0,0]]@END_MENU_TOKEN@*/ -// XCUIApplication().sheets.scrollViews.otherElements.buttons["Delete"].tap() - print(app.debugDescription) - - let editButton = app.otherElements.containing(.staticText, identifier: "Edit").firstMatch - XCTAssertTrue(editButton.waitForExistence(timeout: 2), "Edit button not found in context menu") - editButton.tap() - + let retryButton = app.buttons["TaskRetryContextMenu"] + XCTAssertTrue(retryButton.waitForExistence(timeout: 2), "Edit button not found in context menu") + retryButton.tap() } - func testShareWithoutDownloadLimitCapability() async throws { - // This cannot be implemented at the time of writing. - // There is no way to disable and enable server apps via web API. - // The Xcode UI test process cannot access Docker. - throw XCTSkip("Not implemented yet!") - } - - func testNewShareWithoutDownloadLimit() async throws { - pullToRefresh() - - // Tap share button. - - let shareButton = app.buttons["Cell/\(testFileName)/shareButton"] - - guard shareButton.exists else { - throw UITestError.waitForExistence(shareButton) - } - - shareButton.tap() - - // Tap add share link button. - - let addShareLinkButton = app.buttons["addShareLink"] - guard addShareLinkButton.await() else { return } - addShareLinkButton.tap() - - // Tap confirm share button. - - let confirmShareButton = app.buttons["confirmShare"] - - guard confirmShareButton.exists else { - throw UITestError.waitForExistence(confirmShareButton) - } - - confirmShareButton.tap() - confirmShareButton.awaitInexistence() - - // Then - let shares = try await backend.getShares(byPath: "/\(testFileName)") - XCTAssertEqual(shares.count, 1, "Only one share existing on \(testFileName)") + private func editTask() { + let cell = app.collectionViews.children(matching: .cell).element(boundBy: 0) + cell.staticTexts[taskInputToEdit].press(forDuration: 2); - guard let token = shares.first?.token else { - throw UITestError.missingValue - } + let editButton = app.buttons["TaskEditContextMenu"] + XCTAssertTrue(editButton.waitForExistence(timeout: 2), "Edit button not found in context menu") + editButton.tap() - try await backend.assertNoDownloadLimit(by: token) + app.textViews["InputTextEditor"].typeText("Edited") + app.navigationBars["Edit Free text to text prompt task"].buttons["Edit"].tap() } - func testNewShareWithDownloadLimit() async throws { - pullToRefresh() - - // Tap share button. - - let shareButton = app.buttons["Cell/\(testFileName)/shareButton"] - guard shareButton.await() else { return } - shareButton.tap() - - // Tap add share link button. - - let addShareLinkButton = app.buttons["addShareLink"] - guard addShareLinkButton.await() else { return } - addShareLinkButton.tap() - - // Tap download limits. - - let downloadLimitCell = app.cells["downloadLimit"] - guard downloadLimitCell.await() else { return } - downloadLimitCell.tap() - - // Tap download limit switch. - - let downloadLimitSwitch = app.switches["downloadLimitSwitch"] - guard downloadLimitSwitch.await() else { return } - downloadLimitSwitch.tap() - - // Update allowed downloads. - - let allowedDownloadsTextField = app.textFields["downloadLimitTextField"] - guard allowedDownloadsTextField.await() else { return } - allowedDownloadsTextField.tap() - allowedDownloadsTextField.typeText("3") - - // Tap navigation back button. - - app.navigationBars.buttons.firstMatch.tap() - - // Tap confirm share button. - - let confirmShareButton = app.buttons["confirmShare"] - guard confirmShareButton.await() else { return } - confirmShareButton.tap() - confirmShareButton.awaitInexistence() - - // Then - let shares = try await backend.getShares(byPath: "/\(testFileName)") - XCTAssertEqual(shares.count, 1, "Only one share existing on \(testFileName)") + private func deleteTask() { + let cell = app.collectionViews.children(matching: .cell).element(boundBy: 0) + cell.staticTexts[taskInputDeleted].press(forDuration: 2); - guard let token = shares.first?.token else { - throw UITestError.missingValue - } + let deleteButton = app.buttons["TaskDeleteContextMenu"] + XCTAssertTrue(deleteButton.waitForExistence(timeout: 2), "Edit button not found in context menu") + deleteButton.tap() - try await backend.assertDownloadLimit(by: token, count: 0, limit: 3) + app.sheets.scrollViews.otherElements.buttons["Delete"].tap() } - func testShareOfFolder() async throws { - let testSubject = "_Xcode UI Test Subject" - try await backend.createFolder(testSubject) - pullToRefresh() - - // Tap share button. - - let shareButton = app.buttons["Cell/\(testSubject)/shareButton"] - guard shareButton.await() else { return } - shareButton.tap() - - // Tap add share link button. - - let addShareLinkButton = app.buttons["addShareLink"] - guard addShareLinkButton.await() else { return } - addShareLinkButton.tap() - - // Verify download limits being unavailable. - - let downloadLimitCell = app.cells["downloadLimit"] - XCTAssertFalse(downloadLimitCell.exists, "Folder shares cannot have download limits") + // MARK: - Tests - // Cleanup + func testCreateAssistantTask() async throws { + goToAssistant() - try await backend.delete(testSubject) - } + createTask(input: taskInputCreated) - func testAddingDownloadLimitToExistingShare() async throws { - try await backend.createShare(byPath: testFileName) pullToRefresh() - // Tap share button. - - let shareButton = app.buttons["Cell/\(testFileName)/shareButton"] - guard shareButton.await() else { return } - shareButton.tap() - - // Tap show share link details button. - - let showShareLinkDetailsButton = app.buttons["showShareLinkDetails"] - guard showShareLinkDetailsButton.await() else { return } - showShareLinkDetailsButton.tap() - - // Tap share link details button in share menu sheet. - - let shareMenuDetailsCell = app.cells["shareMenu/details"] - guard shareMenuDetailsCell.await() else { return } - shareMenuDetailsCell.tap() - - // Tap download limits. - - let downloadLimitCell = app.cells["downloadLimit"] - guard downloadLimitCell.await() else { return } - downloadLimitCell.tap() - - // Tap download limit switch. - - let downloadLimitSwitch = app.switches["downloadLimitSwitch"] - guard downloadLimitSwitch.await() else { return } - downloadLimitSwitch.tap() - - // Update allowed downloads. - - let allowedDownloadsTextField = app.textFields["downloadLimitTextField"] - guard allowedDownloadsTextField.await() else { return } - allowedDownloadsTextField.tap() - allowedDownloadsTextField.typeText("3") - - // Tap navigation back button. - - app.navigationBars.buttons.firstMatch.tap() - - // Tap confirm share button. - - let confirmShareButton = app.buttons["confirmShare"] - guard confirmShareButton.await() else { return } - confirmShareButton.tap() - confirmShareButton.awaitInexistence() - - // Then - let shares = try await backend.getShares(byPath: "\(testFileName)") - XCTAssertEqual(shares.count, 1, "Only one share existing on \(testFileName)") - - guard let token = shares.first?.token else { - throw UITestError.missingValue - } - try await aMoment() - try await backend.assertDownloadLimit(by: token, count: 0, limit: 3) - } - - func testUpdatingDownloadLimitOnExistingShare() async throws { - try await backend.createShare(byPath: testFileName) - var shares = try await backend.getShares(byPath: testFileName) - - guard let token = shares.first?.token else { - XCTFail("Failed to fetch token of share for test file!") - return - } - - try await backend.setDownloadLimit(to: 4, by: token) - pullToRefresh() - - // Tap share button. - - let shareButton = app.buttons["Cell/\(testFileName)/shareButton"] - guard shareButton.await() else { return } - shareButton.tap() - - // Tap show share link details button. - - let showShareLinkDetailsButton = app.buttons["showShareLinkDetails"] - guard showShareLinkDetailsButton.await() else { return } - showShareLinkDetailsButton.tap() - - // Tap share link details button in share menu sheet. - let shareMenuDetailsCell = app.cells["shareMenu/details"] - guard shareMenuDetailsCell.await() else { return } - shareMenuDetailsCell.tap() - - // Tap download limits. - - let downloadLimitCell = app.cells["downloadLimit"] - guard downloadLimitCell.await() else { return } - downloadLimitCell.tap() - - // Check download limit switch. - - let downloadLimitSwitch = app.switches["downloadLimitSwitch"].firstMatch - guard downloadLimitSwitch.await() else { return } - - guard let downloadLimitSwitchValue = (downloadLimitSwitch.value as? String) else { - XCTFail("Failed to get value of user interface control!") - return - } - - guard downloadLimitSwitchValue == "1" else { - XCTFail("The switch is not on!") - return - } - - // Check and update allowed downloads. - - let allowedDownloadsTextField = app.textFields["downloadLimitTextField"] - guard allowedDownloadsTextField.await() else { return } - - guard let allowedDownloadsTextFieldValue = (allowedDownloadsTextField.value as? String) else { - XCTFail("Failed to get value of user interface control!") - return - } - - guard allowedDownloadsTextFieldValue == "4" else { - XCTFail("The text field value is wrong!") - return - } - - allowedDownloadsTextField.tap() - allowedDownloadsTextField.typeText("6") - - // Tap navigation back button. - - app.navigationBars.buttons.firstMatch.tap() - - // Tap confirm share button. - - let confirmShareButton = app.buttons["confirmShare"] - guard confirmShareButton.await() else { return } - confirmShareButton.tap() - confirmShareButton.awaitInexistence() - - // Then - shares = try await backend.getShares(byPath: "\(testFileName)") - XCTAssertEqual(shares.count, 1, "Only one share existing on \(testFileName)") - - guard let token = shares.first?.token else { - throw UITestError.missingValue - } - - try await aMoment() - try await backend.assertDownloadLimit(by: token, count: 0, limit: 6) + let cell = app.collectionViews.children(matching: .cell).element(boundBy: 0) + XCTAssert(cell.staticTexts[taskInputCreated].exists) } - func testDiscardingDownloadLimitChangesOnExistingShare() async throws { - try await backend.createShare(byPath: testFileName) - var shares = try await backend.getShares(byPath: testFileName) + func testRetryAssistantTask() async throws { + goToAssistant() - guard let token = shares.first?.token else { - XCTFail("Failed to fetch token of share for test file!") - return - } + createTask(input: taskInputRetried) - try await backend.setDownloadLimit(to: 8, by: token) - pullToRefresh() + retryTask() - // Tap share button. - - let shareButton = app.buttons["Cell/\(testFileName)/shareButton"] - guard shareButton.await() else { return } - shareButton.tap() - - // Tap show share link details button. - - let showShareLinkDetailsButton = app.buttons["showShareLinkDetails"] - guard showShareLinkDetailsButton.await() else { return } - showShareLinkDetailsButton.tap() - - // Tap share link details button in share menu sheet. - - let shareMenuDetailsCell = app.cells["shareMenu/details"] - guard shareMenuDetailsCell.await() else { return } - shareMenuDetailsCell.tap() - - // Tap download limits. - - let downloadLimitCell = app.cells["downloadLimit"] - guard downloadLimitCell.await() else { return } - downloadLimitCell.tap() - - // Check download limit switch. - - let downloadLimitSwitch = app.switches["downloadLimitSwitch"].firstMatch - guard downloadLimitSwitch.await() else { return } - - guard let downloadLimitSwitchValue = (downloadLimitSwitch.value as? String) else { - XCTFail("Failed to get value of user interface control!") - return - } - - guard downloadLimitSwitchValue == "1" else { - XCTFail("The switch is not on!") - return - } - - // Check and update allowed downloads. - - let allowedDownloadsTextField = app.textFields["downloadLimitTextField"] - guard allowedDownloadsTextField.await() else { return } - - guard let allowedDownloadsTextFieldValue = (allowedDownloadsTextField.value as? String) else { - XCTFail("Failed to get value of user interface control!") - return - } - - allowedDownloadsTextField.tap() - allowedDownloadsTextField.typeText("9") - - // Tap navigation back button. - - app.navigationBars.buttons.firstMatch.tap() - - // Tap confirm share button. - - let cancelShareButton = app.buttons["cancelShare"] - guard cancelShareButton.await() else { return } - cancelShareButton.tap() - - // Then - shares = try await backend.getShares(byPath: "\(testFileName)") - XCTAssertEqual(shares.count, 1, "Only one share existing on \(testFileName)") - - guard let token = shares.first?.token else { - throw UITestError.missingValue - } - - try await aMoment() - try await backend.assertDownloadLimit(by: token, count: 0, limit: 8) - } - - func testRemovingDownloadLimitFromExistingShare() async throws { - try await backend.createShare(byPath: testFileName) - var shares = try await backend.getShares(byPath: testFileName) - - guard let token = shares.first?.token else { - XCTFail("Failed to fetch token of share for test file!") - return - } - - try await backend.setDownloadLimit(to: 4, by: token) pullToRefresh() - // Tap share button. - - let shareButton = app.buttons["Cell/\(testFileName)/shareButton"] - guard shareButton.await() else { return } - shareButton.tap() - - // Tap show share link details button. - - let showShareLinkDetailsButton = app.buttons["showShareLinkDetails"] - guard showShareLinkDetailsButton.await() else { return } - showShareLinkDetailsButton.tap() - - // Tap share link details button in share menu sheet. + try await aMoment() - let shareMenuDetailsCell = app.cells["shareMenu/details"] - guard shareMenuDetailsCell.await() else { return } - shareMenuDetailsCell.tap() + let matchingElements = app.collectionViews.cells.staticTexts.matching(identifier: taskInputRetried) + print(app.collectionViews.staticTexts.debugDescription) + XCTAssertEqual(matchingElements.count, 2, "Expected 2 elements") + } - // Tap download limits. + func testEditAssistantTask() async throws { + goToAssistant() - let downloadLimitCell = app.cells["downloadLimit"] - guard downloadLimitCell.await() else { return } - downloadLimitCell.tap() + createTask(input: taskInputToEdit) - // Check download limit switch. + editTask() - let downloadLimitSwitch = app.switches["downloadLimitSwitch"].firstMatch - guard downloadLimitSwitch.await() else { return } - downloadLimitSwitch.tap() + pullToRefresh() - // Tap navigation back button. + try await aMoment() - app.navigationBars.buttons.firstMatch.tap() + let cell = app.collectionViews.children(matching: .cell).element(boundBy: 0) + XCTAssert(cell.staticTexts[taskInputToEdit + "Edited"].exists) + } - // Tap confirm share button. + func testDeleteAssistantTask() async throws { + goToAssistant() - let confirmShareButton = app.buttons["confirmShare"] - guard confirmShareButton.await() else { return } - confirmShareButton.tap() - confirmShareButton.awaitInexistence() + createTask(input: taskInputDeleted) - // Then - shares = try await backend.getShares(byPath: "\(testFileName)") - XCTAssertEqual(shares.count, 1, "Only one share existing on \(testFileName)") + deleteTask() - guard let token = shares.first?.token else { - throw UITestError.missingValue - } + pullToRefresh() try await aMoment() - try await backend.assertDownloadLimit(by: token, count: nil, limit: nil) + + let cell = app.collectionViews.children(matching: .cell).element(boundBy: 0) + XCTAssert(!cell.staticTexts[taskInputDeleted].exists) } } diff --git a/Tests/NextcloudUITests/BaseUIXCTestCase.swift b/Tests/NextcloudUITests/BaseUIXCTestCase.swift index 11d7cbd5b3..6993e1602d 100644 --- a/Tests/NextcloudUITests/BaseUIXCTestCase.swift +++ b/Tests/NextcloudUITests/BaseUIXCTestCase.swift @@ -13,6 +13,11 @@ import XCTest class BaseUIXCTestCase: XCTestCase { var app: XCUIApplication! + /// + /// The Nextcloud server API abstraction object. + /// + var backend: UITestBackend! + /// /// Generic convenience method to define user interface interruption monitors. /// @@ -37,6 +42,16 @@ class BaseUIXCTestCase: XCTestCase { } } + /// + /// Let the current `Task` rest for 2 seconds. + /// + /// Some asynchronous background activities like the follow up request to define a download limit have no effect on the visible user interface. + /// Hence their outcome can only be assumed after a brief period of time. + /// + func aSmallMoment() async throws { + try await Task.sleep(for: .seconds(2)) + } + /// /// Let the current `Task` rest for ``TestConstants/controlExistenceTimeout``. /// @@ -51,7 +66,7 @@ class BaseUIXCTestCase: XCTestCase { /// Automation of the sign-in, if required. /// /// - func logIn() throws { + func logIn() async throws { guard app.buttons["login"].exists else { return } @@ -92,6 +107,8 @@ class BaseUIXCTestCase: XCTestCase { webView.buttons.firstMatch.tap() } + try await aSmallMoment() + let grantButton = webView.buttons["Grant access"] guard grantButton.await() else { @@ -104,5 +121,24 @@ class BaseUIXCTestCase: XCTestCase { // Switch back from Safari to our app. app.activate() app.buttons["accountSwitcher"].await() + + try await aSmallMoment() + } + + /// + /// Pull to refresh on the first found collection view to reveal the new file on the server. + /// + func pullToRefresh(file: StaticString = #file, line: UInt = #line) { + let cell = app.collectionViews.firstMatch.staticTexts.firstMatch + + guard cell.exists else { + XCTFail("Apparently no collection view cell is visible!", file: file, line: line) + return + } + + let start = cell.coordinate(withNormalizedOffset: CGVectorMake(0, 0)) + let finish = cell.coordinate(withNormalizedOffset: CGVectorMake(0, 20)) + + start.press(forDuration: 0.2, thenDragTo: finish) } } diff --git a/Tests/NextcloudUITests/DownloadLimitTests.swift b/Tests/NextcloudUITests/DownloadLimitTests.swift index acf55a0fc8..34e51cda1d 100644 --- a/Tests/NextcloudUITests/DownloadLimitTests.swift +++ b/Tests/NextcloudUITests/DownloadLimitTests.swift @@ -11,11 +11,6 @@ import XCTest /// @MainActor final class DownloadLimitTests: BaseUIXCTestCase { - /// - /// The Nextcloud server API abstraction object. - /// - var backend: UITestBackend! - /// /// Name of the file to work with. /// @@ -24,23 +19,6 @@ final class DownloadLimitTests: BaseUIXCTestCase { /// let testFileName = "_Xcode UI Test Subject.md" - /// - /// Pull to refresh on the first found collection view to reveal the new file on the server. - /// - func pullToRefresh(file: StaticString = #file, line: UInt = #line) { - let cell = app.collectionViews.firstMatch.staticTexts.firstMatch - - guard cell.exists else { - XCTFail("Apparently no collection view cell is visible!", file: file, line: line) - return - } - - let start = cell.coordinate(withNormalizedOffset: CGVectorMake(0, 0)) - let finish = cell.coordinate(withNormalizedOffset: CGVectorMake(0, 20)) - - start.press(forDuration: 0.2, thenDragTo: finish) - } - // MARK: - Lifecycle override func setUp() async throws { @@ -56,7 +34,7 @@ final class DownloadLimitTests: BaseUIXCTestCase { app.launchArguments = ["UI_TESTING"] app.launch() - try logIn() + try await logIn() // Set up test backend communication. backend = UITestBackend() diff --git a/Tests/TestConstants.swift b/Tests/TestConstants.swift index fe7f5bfaf0..1cd3fd6901 100644 --- a/Tests/TestConstants.swift +++ b/Tests/TestConstants.swift @@ -13,7 +13,7 @@ enum TestConstants { /// /// The default number of seconds to wait for the appearance of user interface controls during user interface tests. /// - static let controlExistenceTimeout: Double = 20 + static let controlExistenceTimeout: Double = 10 /// /// The full base URL for the server to run against. diff --git a/iOSClient/Assistant/NCAssistant.swift b/iOSClient/Assistant/NCAssistant.swift index 44e9ed93cc..4bd49992b1 100644 --- a/iOSClient/Assistant/NCAssistant.swift +++ b/iOSClient/Assistant/NCAssistant.swift @@ -116,6 +116,7 @@ struct TaskList: View { Image(systemName: "arrow.trianglehead.clockwise") } } + .accessibilityIdentifier("TaskRetryContextMenu") Button { taskToEdit = task @@ -127,6 +128,7 @@ struct TaskList: View { Image(systemName: "pencil") } } + .accessibilityIdentifier("TaskEditContextMenu") Button(role: .destructive) { taskToDelete = task @@ -138,6 +140,7 @@ struct TaskList: View { Image(systemName: "trash") } } + .accessibilityIdentifier("TaskDeleteContextMenu") } .accessibilityIdentifier("TaskContextMenu") } From e78e1895bbe45ff88d0e95cfefe237916011c29b Mon Sep 17 00:00:00 2001 From: Milen Pivchev Date: Fri, 21 Feb 2025 17:52:25 +0100 Subject: [PATCH 11/20] WIP Signed-off-by: Milen Pivchev --- Tests/NextcloudUITests/AssistantTests.swift | 5 --- .../NextcloudUITests/DownloadLimitTests.swift | 2 -- .../Assistant/Models/NCAssistantModel.swift | 32 +++++++++---------- 3 files changed, 15 insertions(+), 24 deletions(-) diff --git a/Tests/NextcloudUITests/AssistantTests.swift b/Tests/NextcloudUITests/AssistantTests.swift index 14a19d7aae..8ec529ada7 100644 --- a/Tests/NextcloudUITests/AssistantTests.swift +++ b/Tests/NextcloudUITests/AssistantTests.swift @@ -7,8 +7,6 @@ import XCTest /// /// User interface tests for the download limits management on shares. /// -/// > To Do: Check whether this can be converted to Swift Testing. -/// @MainActor final class AssistantTests: BaseUIXCTestCase { let taskInputCreated = "TestTaskCreated" + NSUUID().uuidString @@ -51,9 +49,6 @@ final class AssistantTests: BaseUIXCTestCase { talkStaticText.tap() } - /// - /// Leads to the Assistant screen. - /// private func createTask(input: String) { app.navigationBars["Assistant"].buttons["CreateButton"].tap() diff --git a/Tests/NextcloudUITests/DownloadLimitTests.swift b/Tests/NextcloudUITests/DownloadLimitTests.swift index 34e51cda1d..b234ff537e 100644 --- a/Tests/NextcloudUITests/DownloadLimitTests.swift +++ b/Tests/NextcloudUITests/DownloadLimitTests.swift @@ -7,8 +7,6 @@ import XCTest /// /// User interface tests for the download limits management on shares. /// -/// > To Do: Check whether this can be converted to Swift Testing. -/// @MainActor final class DownloadLimitTests: BaseUIXCTestCase { /// diff --git a/iOSClient/Assistant/Models/NCAssistantModel.swift b/iOSClient/Assistant/Models/NCAssistantModel.swift index 64cbb53a1f..fe4cdb84dd 100644 --- a/iOSClient/Assistant/Models/NCAssistantModel.swift +++ b/iOSClient/Assistant/Models/NCAssistantModel.swift @@ -63,28 +63,26 @@ class NCAssistantModel: ObservableObject { isLoading = true if useV2 { - NextcloudKit.shared.textProcessingGetTasksV2(taskType: task.type ?? "", account: session.account, completion: { _, tasks, _, error in - self.isLoading = false - - if error != .success { - self.hasError = true - return - } - - self.selectedTask = task + NextcloudKit.shared.textProcessingGetTasksV2(taskType: task.type ?? "", account: session.account, completion: { _, _, _, error in + handle(task: task, error: error) }) } else { NextcloudKit.shared.textProcessingGetTask(taskId: Int(task.id), account: session.account) { _, task, _, error in - self.isLoading = false + guard let task else { return } + let taskV2 = NKTextProcessingTask.toV2(tasks: [task]).tasks.first + handle(task: taskV2, error: error) + } + } - if error != .success { - self.hasError = true - return - } + func handle(task: AssistantTask?, error: NKError?) { + self.isLoading = false - guard let task else { return } - self.selectedTask = NKTextProcessingTask.toV2(tasks: [task]).tasks.first + if error != .success { + self.hasError = true + return } + + self.selectedTask = task } } @@ -229,7 +227,7 @@ extension NCAssistantModel { TaskTypeData(id: "1", name: "Free Prompt", description: "", inputShape: nil, outputShape: nil), TaskTypeData(id: "2", name: "Summarize", description: "", inputShape: nil, outputShape: nil), TaskTypeData(id: "3", name: "Generate headline", description: "", inputShape: nil, outputShape: nil), - TaskTypeData(id: "4", name: "Reformulate", description: "", inputShape: nil, outputShape: nil), + TaskTypeData(id: "4", name: "Reformulate", description: "", inputShape: nil, outputShape: nil) ] self.tasks = tasks From 4c4d09b88438bccaa327aaf6f759d9ee01a60b53 Mon Sep 17 00:00:00 2001 From: Milen Pivchev Date: Mon, 24 Feb 2025 15:07:08 +0100 Subject: [PATCH 12/20] Refactor date Signed-off-by: Milen Pivchev --- Share/NCShareExtension+DataSource.swift | 2 +- Widget/Files/FilesData.swift | 2 +- iOSClient/Activity/NCActivity.swift | 4 ++-- .../Assistant/Models/NCAssistantModel.swift | 3 +-- .../Collection Common/Cell/NCListCell.swift | 2 +- .../NCCreateFormUploadConflict.swift | 8 ++++---- iOSClient/Notification/NCNotification.swift | 2 +- iOSClient/Select/NCSelect.swift | 4 ++-- iOSClient/Share/NCShareHeader.swift | 2 +- iOSClient/Transfers/NCTransferCell.swift | 2 +- .../Trash/Cell/NCTrashCellProtocol.swift | 2 +- iOSClient/Utility/NCUtility+Date.swift | 19 ++++++++++++++----- 12 files changed, 30 insertions(+), 22 deletions(-) diff --git a/Share/NCShareExtension+DataSource.swift b/Share/NCShareExtension+DataSource.swift index f8dfabb3c5..0589dd9f27 100644 --- a/Share/NCShareExtension+DataSource.swift +++ b/Share/NCShareExtension+DataSource.swift @@ -154,7 +154,7 @@ extension NCShareExtension: UICollectionViewDataSource { cell.imageItem.image = NCImageCache.shared.getFolder(account: metadata.account) } - cell.labelInfo.text = utility.dateDiff(metadata.date as Date) + cell.labelInfo.text = utility.getRelativeDateTitle(metadata.date as Date) let lockServerUrl = utilityFileSystem.stringAppendServerUrl(metadata.serverUrl, addFileName: metadata.fileName) let tableDirectory = self.database.getTableDirectory(predicate: NSPredicate(format: "account == %@ AND serverUrl == %@", session.account, lockServerUrl)) diff --git a/Widget/Files/FilesData.swift b/Widget/Files/FilesData.swift index e60ce369aa..5788b0d2af 100644 --- a/Widget/Files/FilesData.swift +++ b/Widget/Files/FilesData.swift @@ -204,7 +204,7 @@ func getFilesDataEntry(configuration: AccountIntent?, isPreview: Bool, displaySi } // SUBTITLE - let subTitle = utility.dateDiff(file.date as Date) + " · " + utilityFileSystem.transformedSize(file.size) + let subTitle = utility.getRelativeDateTitle(file.date as Date) + " · " + utilityFileSystem.transformedSize(file.size) // URL: nextcloud://open-file?path=Talk/IMG_0000123.jpg&user=marinofaggiana&link=https://cloud.nextcloud.com/f/123 guard var path = utilityFileSystem.getPath(path: file.path, user: file.user, fileName: file.fileName).urlEncoded else { continue } diff --git a/iOSClient/Activity/NCActivity.swift b/iOSClient/Activity/NCActivity.swift index 665e9e947f..8e59f3bd96 100644 --- a/iOSClient/Activity/NCActivity.swift +++ b/iOSClient/Activity/NCActivity.swift @@ -245,7 +245,7 @@ extension NCActivity: UITableViewDataSource { cell.labelUser.text = comment.actorDisplayName cell.labelUser.textColor = NCBrandColor.shared.textColor // Date - cell.labelDate.text = utility.dateDiff(comment.creationDateTime as Date) + cell.labelDate.text = utility.getRelativeDateTitle(comment.creationDateTime as Date) cell.labelDate.textColor = .lightGray // Message cell.labelMessage.text = comment.message @@ -353,7 +353,7 @@ extension NCActivity: UITableViewDataSource { $0.color = UIColor.lightGray } - subject += "\n" + "" + utility.dateDiff(activity.date as Date) + "" + subject += "\n" + "" + utility.getRelativeDateTitle(activity.date as Date) + "" cell.subject.attributedText = subject.set(style: StyleGroup(base: normal, ["bold": bold, "date": date])) } diff --git a/iOSClient/Assistant/Models/NCAssistantModel.swift b/iOSClient/Assistant/Models/NCAssistantModel.swift index fe4cdb84dd..504daad0e2 100644 --- a/iOSClient/Assistant/Models/NCAssistantModel.swift +++ b/iOSClient/Assistant/Models/NCAssistantModel.swift @@ -237,7 +237,6 @@ extension NCAssistantModel { } } - extension AssistantTask { struct StatusInfo { let stringKey, imageSystemName: String @@ -255,6 +254,6 @@ extension AssistantTask { } var statusDate: String { - return NCUtility().getTitleFromDate(.init(timeIntervalSince1970: TimeInterval((lastUpdated ?? completionExpectedAt) ?? 0))) + return NCUtility().getRelativeDateTitle(.init(timeIntervalSince1970: TimeInterval((lastUpdated ?? completionExpectedAt) ?? 0))) } } diff --git a/iOSClient/Main/Collection Common/Cell/NCListCell.swift b/iOSClient/Main/Collection Common/Cell/NCListCell.swift index c777d513f3..711fefe711 100755 --- a/iOSClient/Main/Collection Common/Cell/NCListCell.swift +++ b/iOSClient/Main/Collection Common/Cell/NCListCell.swift @@ -250,7 +250,7 @@ class NCListCell: UICollectionViewCell, UIGestureRecognizerDelegate, NCCellProto } func writeInfoDateSize(date: NSDate, size: Int64) { - labelInfo.text = NCUtility().dateDiff(date as Date) + labelInfo.text = NCUtility().getRelativeDateTitle(date as Date) labelSubinfo.text = NCUtilityFileSystem().transformedSize(size) } diff --git a/iOSClient/Main/Create cloud/NCCreateFormUploadConflict.swift b/iOSClient/Main/Create cloud/NCCreateFormUploadConflict.swift index d3a5bbc337..5c84cd5281 100644 --- a/iOSClient/Main/Create cloud/NCCreateFormUploadConflict.swift +++ b/iOSClient/Main/Create cloud/NCCreateFormUploadConflict.swift @@ -342,7 +342,7 @@ extension NCCreateFormUploadConflict: UITableViewDataSource { cell.imageAlreadyExistingFile.image = UIImage(named: metadataAlreadyExists.iconName) } } - cell.labelDetailAlreadyExistingFile.text = utility.dateDiff(metadataAlreadyExists.date as Date) + "\n" + utilityFileSystem.transformedSize(metadataAlreadyExists.size) + cell.labelDetailAlreadyExistingFile.text = utility.getRelativeDateTitle(metadataAlreadyExists.date as Date) + "\n" + utilityFileSystem.transformedSize(metadataAlreadyExists.size) if metadatasConflictAlreadyExistingFiles.contains(metadataNewFile.ocId) { cell.switchAlreadyExistingFile.isOn = true @@ -381,7 +381,7 @@ extension NCCreateFormUploadConflict: UITableViewDataSource { let fileDictionary = try FileManager.default.attributesOfItem(atPath: fileNamePath) let fileSize = fileDictionary[FileAttributeKey.size] as? Int64 ?? 0 - cell.labelDetailNewFile.text = utility.dateDiff(date) + "\n" + utilityFileSystem.transformedSize(fileSize) + cell.labelDetailNewFile.text = utility.getRelativeDateTitle(date) + "\n" + utilityFileSystem.transformedSize(fileSize) } catch { print("Error: \(error)") } @@ -405,7 +405,7 @@ extension NCCreateFormUploadConflict: UITableViewDataSource { DispatchQueue.main.async { cell.imageNewFile.image = image } } } - DispatchQueue.main.async { cell.labelDetailNewFile.text = self.utility.dateDiff(date) + "\n" + self.utilityFileSystem.transformedSize(fileSize) } + DispatchQueue.main.async { cell.labelDetailNewFile.text = self.utility.getRelativeDateTitle(date) + "\n" + self.utilityFileSystem.transformedSize(fileSize) } } catch { print("Error: \(error)") } } } @@ -424,7 +424,7 @@ extension NCCreateFormUploadConflict: UITableViewDataSource { let fileDictionary = try FileManager.default.attributesOfItem(atPath: filePathNewFile) let fileSize = fileDictionary[FileAttributeKey.size] as? Int64 ?? 0 - cell.labelDetailNewFile.text = utility.dateDiff(metadataNewFile.date as Date) + "\n" + utilityFileSystem.transformedSize(fileSize) + cell.labelDetailNewFile.text = utility.getRelativeDateTitle(metadataNewFile.date as Date) + "\n" + utilityFileSystem.transformedSize(fileSize) } catch { print("Error: \(error)") } diff --git a/iOSClient/Notification/NCNotification.swift b/iOSClient/Notification/NCNotification.swift index 64917c6e2d..322a519aa9 100644 --- a/iOSClient/Notification/NCNotification.swift +++ b/iOSClient/Notification/NCNotification.swift @@ -157,7 +157,7 @@ class NCNotification: UITableViewController, NCNotificationCellDelegate { cell.date.text = DateFormatter.localizedString(from: notification.date as Date, dateStyle: .medium, timeStyle: .medium) cell.notification = notification - cell.date.text = utility.dateDiff(notification.date as Date) + cell.date.text = utility.getRelativeDateTitle(notification.date as Date) cell.date.textColor = NCBrandColor.shared.iconImageColor2 cell.subject.text = notification.subject cell.subject.textColor = NCBrandColor.shared.textColor diff --git a/iOSClient/Select/NCSelect.swift b/iOSClient/Select/NCSelect.swift index 42bf12204c..34e3f6b49c 100644 --- a/iOSClient/Select/NCSelect.swift +++ b/iOSClient/Select/NCSelect.swift @@ -361,11 +361,11 @@ extension NCSelect: UICollectionViewDataSource { } cell.imageItem.image = cell.imageItem.image?.colorizeFolder(metadata: metadata) - cell.labelInfo.text = utility.dateDiff(metadata.date as Date) + cell.labelInfo.text = utility.getRelativeDateTitle(metadata.date as Date) } else { - cell.labelInfo.text = utility.dateDiff(metadata.date as Date) + " · " + utilityFileSystem.transformedSize(metadata.size) + cell.labelInfo.text = utility.getRelativeDateTitle(metadata.date as Date) + " · " + utilityFileSystem.transformedSize(metadata.size) // image local if self.database.getTableLocalFile(ocId: metadata.ocId) != nil { diff --git a/iOSClient/Share/NCShareHeader.swift b/iOSClient/Share/NCShareHeader.swift index a6fa46927c..a18f2b6114 100644 --- a/iOSClient/Share/NCShareHeader.swift +++ b/iOSClient/Share/NCShareHeader.swift @@ -57,7 +57,7 @@ class NCShareHeader: UIView { fileName.text = metadata.fileNameView fileName.textColor = NCBrandColor.shared.textColor info.textColor = NCBrandColor.shared.textColor2 - info.text = utilityFileSystem.transformedSize(metadata.size) + ", " + NCUtility().dateDiff(metadata.date as Date) + info.text = utilityFileSystem.transformedSize(metadata.size) + ", " + NCUtility().getRelativeDateTitle(metadata.date as Date) tagListView.addTags(Array(metadata.tags)) diff --git a/iOSClient/Transfers/NCTransferCell.swift b/iOSClient/Transfers/NCTransferCell.swift index 7df6bcedbc..6244f14a50 100755 --- a/iOSClient/Transfers/NCTransferCell.swift +++ b/iOSClient/Transfers/NCTransferCell.swift @@ -157,7 +157,7 @@ class NCTransferCell: UICollectionViewCell, UIGestureRecognizerDelegate, NCCellP } func writeInfoDateSize(date: NSDate, size: Int64) { - labelInfo.text = NCUtility().dateDiff(date as Date) + " · " + NCUtilityFileSystem().transformedSize(size) + labelInfo.text = NCUtility().getRelativeDateTitle(date as Date) + " · " + NCUtilityFileSystem().transformedSize(size) } func setIconOutlines() { diff --git a/iOSClient/Trash/Cell/NCTrashCellProtocol.swift b/iOSClient/Trash/Cell/NCTrashCellProtocol.swift index a07941c575..8c82abf6db 100644 --- a/iOSClient/Trash/Cell/NCTrashCellProtocol.swift +++ b/iOSClient/Trash/Cell/NCTrashCellProtocol.swift @@ -39,7 +39,7 @@ extension NCTrashCellProtocol where Self: UICollectionViewCell { self.labelTitle.text = tableTrash.trashbinFileName self.labelTitle.textColor = NCBrandColor.shared.textColor if self is NCTrashListCell { - self.labelInfo?.text = NCUtility().dateDiff(tableTrash.trashbinDeletionTime as Date) + self.labelInfo?.text = NCUtility().getRelativeDateTitle(tableTrash.trashbinDeletionTime as Date) } else { let dateFormatter = DateFormatter() dateFormatter.dateStyle = .short diff --git a/iOSClient/Utility/NCUtility+Date.swift b/iOSClient/Utility/NCUtility+Date.swift index 39241ab26a..55fa93f48c 100644 --- a/iOSClient/Utility/NCUtility+Date.swift +++ b/iOSClient/Utility/NCUtility+Date.swift @@ -24,9 +24,12 @@ import UIKit extension NCUtility { - + /// Returns a localized string representing the given date in a user-friendly format. + /// The function handles the following cases: + /// - If the date is today: Returns "Today". + /// - If the date is yesterday: Returns "Yesterday". + /// - Otherwise, it returns the date in a long format (e.g., "10 February 2025"). func getTitleFromDate(_ date: Date) -> String { - guard let yesterday = Calendar.current.date(byAdding: .day, value: -1, to: Date()) else { return DateFormatter.localizedString(from: date, dateStyle: .long, timeStyle: .none) } @@ -46,8 +49,14 @@ extension NCUtility { } } - func dateDiff(_ date: Date?) -> String { - + /// Represents date as relative time: (e.g., "1 minute ago", "2 hours ago", "3 days ago", or a formatted date). + /// The function handles the following cases: + /// - Less than a minute: Returns "Less than a minute ago". + /// - Less than an hour: Returns the number of minutes (e.g., "5 minutes ago"). + /// - Less than a day: Returns the number of hours (e.g., "2 hours ago"). + /// - Less than a month: Returns the number of days (e.g., "3 days ago"). + /// - More than a month: Returns the full formatted date (e.g., "Jan 10, 2025"). + func getRelativeDateTitle(_ date: Date?) -> String { guard let date else { return "" } let today = Date() var ti = date.timeIntervalSince(today) @@ -78,7 +87,7 @@ extension NCUtility { } else { let formatter = DateFormatter() formatter.formatterBehavior = .behavior10_4 - formatter.dateStyle = .medium + formatter.dateStyle = .medium // Returns formatted date, e.g., "Jan 10, 2025" return formatter.string(from: date) } } From 555e8e34a50df3535d3115a3d3b1a431395f7ce9 Mon Sep 17 00:00:00 2001 From: Milen Pivchev Date: Mon, 24 Feb 2025 15:11:32 +0100 Subject: [PATCH 13/20] Refacgtor Signed-off-by: Milen Pivchev --- Tests/NextcloudUITests/AssistantTests.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Tests/NextcloudUITests/AssistantTests.swift b/Tests/NextcloudUITests/AssistantTests.swift index 8ec529ada7..10dad67850 100644 --- a/Tests/NextcloudUITests/AssistantTests.swift +++ b/Tests/NextcloudUITests/AssistantTests.swift @@ -9,10 +9,10 @@ import XCTest /// @MainActor final class AssistantTests: BaseUIXCTestCase { - let taskInputCreated = "TestTaskCreated" + NSUUID().uuidString - let taskInputRetried = "TestTaskRetried" + NSUUID().uuidString - let taskInputToEdit = "TestTaskToEdit" + NSUUID().uuidString - let taskInputDeleted = "TestTaskDeleted" + NSUUID().uuidString + let taskInputCreated = "TestTaskCreated" + UUID().uuidString + let taskInputRetried = "TestTaskRetried" + UUID()().uuidString + let taskInputToEdit = "TestTaskToEdit" + UUID().uuidString + let taskInputDeleted = "TestTaskDeleted" + UUID().uuidString // MARK: - Lifecycle From ef892ae7ce6419374732ec336bc3d2571c8fa311 Mon Sep 17 00:00:00 2001 From: Milen Pivchev Date: Mon, 24 Feb 2025 15:18:09 +0100 Subject: [PATCH 14/20] Refactor Signed-off-by: Milen Pivchev --- Nextcloud.xcodeproj/project.pbxproj | 16 ++++++++-------- ...sistantTests.swift => AssistantUITests.swift} | 2 +- ...mitTests.swift => DownloadLimitUITests.swift} | 2 +- 3 files changed, 10 insertions(+), 10 deletions(-) rename Tests/NextcloudUITests/{AssistantTests.swift => AssistantUITests.swift} (98%) rename Tests/NextcloudUITests/{DownloadLimitTests.swift => DownloadLimitUITests.swift} (99%) diff --git a/Nextcloud.xcodeproj/project.pbxproj b/Nextcloud.xcodeproj/project.pbxproj index d77388a471..094d8dfea3 100644 --- a/Nextcloud.xcodeproj/project.pbxproj +++ b/Nextcloud.xcodeproj/project.pbxproj @@ -37,7 +37,7 @@ AA8D31702D4123B200FE2775 /* DownloadLimitViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA8D31692D4123B200FE2775 /* DownloadLimitViewModel.swift */; }; AA8D31712D4123B200FE2775 /* NCShareDownloadLimitViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA8D316C2D4123B200FE2775 /* NCShareDownloadLimitViewController.swift */; }; AA8E03DA2D2ED83300E7E89C /* TransientShare.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA8E03D92D2ED83300E7E89C /* TransientShare.swift */; }; - AA8E03DC2D2FBAC200E7E89C /* DownloadLimitTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA8E03DB2D2FBAAD00E7E89C /* DownloadLimitTests.swift */; }; + AA8E03DC2D2FBAC200E7E89C /* DownloadLimitUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA8E03DB2D2FBAAD00E7E89C /* DownloadLimitUITests.swift */; }; AA8E041D2D300FDE00E7E89C /* NCShareNetworkingDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA8E041C2D300FDE00E7E89C /* NCShareNetworkingDelegate.swift */; }; AAA7BC2E2D3E39F1008F1A22 /* CapabilitiesResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAA7BC2D2D3E39EC008F1A22 /* CapabilitiesResponse.swift */; }; AAA7BC302D3E3B88008F1A22 /* CapabilityResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAA7BC2F2D3E3B83008F1A22 /* CapabilityResponse.swift */; }; @@ -96,7 +96,7 @@ F32FADA92D1176E3007035E2 /* UIButton+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = F32FADA82D1176DE007035E2 /* UIButton+Extension.swift */; }; F3374A812D64AB9F002A38F9 /* StatusInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3374A802D64AB9E002A38F9 /* StatusInfo.swift */; }; F3374A842D64AC31002A38F9 /* AssistantLabelStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3374A832D64AC2C002A38F9 /* AssistantLabelStyle.swift */; }; - F3374A942D674454002A38F9 /* AssistantTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3374A932D674454002A38F9 /* AssistantTests.swift */; }; + F3374A942D674454002A38F9 /* AssistantUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3374A932D674454002A38F9 /* AssistantUITests.swift */; }; F3374A962D6744A4002A38F9 /* BaseUIXCTestCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3374A952D6744A4002A38F9 /* BaseUIXCTestCase.swift */; }; F33918C42C7CD8F2002D9AA1 /* FileNameValidator+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = F33918C32C7CD8F2002D9AA1 /* FileNameValidator+Extensions.swift */; }; F33918C52C7CD8F2002D9AA1 /* FileNameValidator+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = F33918C32C7CD8F2002D9AA1 /* FileNameValidator+Extensions.swift */; }; @@ -1213,7 +1213,7 @@ AA8D316B2D4123B200FE2775 /* NCShareDownloadLimitTableViewControllerDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCShareDownloadLimitTableViewControllerDelegate.swift; sourceTree = ""; }; AA8D316C2D4123B200FE2775 /* NCShareDownloadLimitViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCShareDownloadLimitViewController.swift; sourceTree = ""; }; AA8E03D92D2ED83300E7E89C /* TransientShare.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransientShare.swift; sourceTree = ""; }; - AA8E03DB2D2FBAAD00E7E89C /* DownloadLimitTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownloadLimitTests.swift; sourceTree = ""; }; + AA8E03DB2D2FBAAD00E7E89C /* DownloadLimitUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownloadLimitUITests.swift; sourceTree = ""; }; AA8E041C2D300FDE00E7E89C /* NCShareNetworkingDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCShareNetworkingDelegate.swift; sourceTree = ""; }; AA8E041E2D3114E200E7E89C /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; AAA7BC2D2D3E39EC008F1A22 /* CapabilitiesResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CapabilitiesResponse.swift; sourceTree = ""; }; @@ -1270,7 +1270,7 @@ F32FADA82D1176DE007035E2 /* UIButton+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIButton+Extension.swift"; sourceTree = ""; }; F3374A802D64AB9E002A38F9 /* StatusInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusInfo.swift; sourceTree = ""; }; F3374A832D64AC2C002A38F9 /* AssistantLabelStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssistantLabelStyle.swift; sourceTree = ""; }; - F3374A932D674454002A38F9 /* AssistantTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssistantTests.swift; sourceTree = ""; }; + F3374A932D674454002A38F9 /* AssistantUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssistantUITests.swift; sourceTree = ""; }; F3374A952D6744A4002A38F9 /* BaseUIXCTestCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseUIXCTestCase.swift; sourceTree = ""; }; F33918C32C7CD8F2002D9AA1 /* FileNameValidator+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FileNameValidator+Extensions.swift"; sourceTree = ""; }; F33EE6F12BF4C9B200CA1A51 /* PKCS12.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PKCS12.swift; sourceTree = ""; }; @@ -2077,8 +2077,8 @@ F3374A952D6744A4002A38F9 /* BaseUIXCTestCase.swift */, AABD0C882D5F631600F009E6 /* Extensions */, AA3C85E92D36BBDE00F74F12 /* UITestBackend */, - AA8E03DB2D2FBAAD00E7E89C /* DownloadLimitTests.swift */, - F3374A932D674454002A38F9 /* AssistantTests.swift */, + AA8E03DB2D2FBAAD00E7E89C /* DownloadLimitUITests.swift */, + F3374A932D674454002A38F9 /* AssistantUITests.swift */, AA74AA962D3172CE00BE3458 /* UITestError.swift */, ); path = NextcloudUITests; @@ -4040,11 +4040,11 @@ buildActionMask = 2147483647; files = ( AAA7BC302D3E3B88008F1A22 /* CapabilityResponse.swift in Sources */, - F3374A942D674454002A38F9 /* AssistantTests.swift in Sources */, + F3374A942D674454002A38F9 /* AssistantUITests.swift in Sources */, AA3C85EB2D36BBFB00F74F12 /* OCSResponse.swift in Sources */, AA74AA972D3172D100BE3458 /* UITestError.swift in Sources */, AA3C85E82D36B08C00F74F12 /* UITestBackend.swift in Sources */, - AA8E03DC2D2FBAC200E7E89C /* DownloadLimitTests.swift in Sources */, + AA8E03DC2D2FBAC200E7E89C /* DownloadLimitUITests.swift in Sources */, AABD0C8A2D5F67A400F009E6 /* XCUIElement.swift in Sources */, AA3C85EE2D36BCCE00F74F12 /* SharesResponse.swift in Sources */, AAA7BC2E2D3E39F1008F1A22 /* CapabilitiesResponse.swift in Sources */, diff --git a/Tests/NextcloudUITests/AssistantTests.swift b/Tests/NextcloudUITests/AssistantUITests.swift similarity index 98% rename from Tests/NextcloudUITests/AssistantTests.swift rename to Tests/NextcloudUITests/AssistantUITests.swift index 10dad67850..e7eecb70e6 100644 --- a/Tests/NextcloudUITests/AssistantTests.swift +++ b/Tests/NextcloudUITests/AssistantUITests.swift @@ -8,7 +8,7 @@ import XCTest /// User interface tests for the download limits management on shares. /// @MainActor -final class AssistantTests: BaseUIXCTestCase { +final class AssistantUITests: BaseUIXCTestCase { let taskInputCreated = "TestTaskCreated" + UUID().uuidString let taskInputRetried = "TestTaskRetried" + UUID()().uuidString let taskInputToEdit = "TestTaskToEdit" + UUID().uuidString diff --git a/Tests/NextcloudUITests/DownloadLimitTests.swift b/Tests/NextcloudUITests/DownloadLimitUITests.swift similarity index 99% rename from Tests/NextcloudUITests/DownloadLimitTests.swift rename to Tests/NextcloudUITests/DownloadLimitUITests.swift index b234ff537e..975fe1f865 100644 --- a/Tests/NextcloudUITests/DownloadLimitTests.swift +++ b/Tests/NextcloudUITests/DownloadLimitUITests.swift @@ -8,7 +8,7 @@ import XCTest /// User interface tests for the download limits management on shares. /// @MainActor -final class DownloadLimitTests: BaseUIXCTestCase { +final class DownloadLimitUITests: BaseUIXCTestCase { /// /// Name of the file to work with. /// From 257f8e537d48f2746fe69e86377475db49dd0a3c Mon Sep 17 00:00:00 2001 From: Milen Pivchev Date: Mon, 24 Feb 2025 16:23:03 +0100 Subject: [PATCH 15/20] WIP Signed-off-by: Milen Pivchev --- Tests/NextcloudUITests/AssistantUITests.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/NextcloudUITests/AssistantUITests.swift b/Tests/NextcloudUITests/AssistantUITests.swift index e7eecb70e6..86fd86bd19 100644 --- a/Tests/NextcloudUITests/AssistantUITests.swift +++ b/Tests/NextcloudUITests/AssistantUITests.swift @@ -10,7 +10,7 @@ import XCTest @MainActor final class AssistantUITests: BaseUIXCTestCase { let taskInputCreated = "TestTaskCreated" + UUID().uuidString - let taskInputRetried = "TestTaskRetried" + UUID()().uuidString + let taskInputRetried = "TestTaskRetried" + UUID().uuidString let taskInputToEdit = "TestTaskToEdit" + UUID().uuidString let taskInputDeleted = "TestTaskDeleted" + UUID().uuidString From db9a6d5d1775381a2735a8060b7adffef4869d1b Mon Sep 17 00:00:00 2001 From: Milen Pivchev Date: Wed, 5 Mar 2025 14:30:10 +0100 Subject: [PATCH 16/20] PR changes Signed-off-by: Milen Pivchev --- iOSClient/Assistant/Components/StatusInfo.swift | 4 +++- iOSClient/Assistant/NCAssistant.swift | 11 ++++++----- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/iOSClient/Assistant/Components/StatusInfo.swift b/iOSClient/Assistant/Components/StatusInfo.swift index cb9e5d4501..7240924e87 100644 --- a/iOSClient/Assistant/Components/StatusInfo.swift +++ b/iOSClient/Assistant/Components/StatusInfo.swift @@ -17,7 +17,9 @@ struct StatusInfo: View { HStack { Label( title: { - Text("\(task.statusDate) (\(showStatusText ? NSLocalizedString(task.statusInfo.stringKey, comment: "") : ""))") + let text = (showStatusText && task.statusInfo.stringKey != "_assistant_task_completed_") ? "(\(NSLocalizedString(task.statusInfo.stringKey, comment: "")))" : "" + + Text("\(task.statusDate) \(text)") .font(.callout) .foregroundStyle(.secondary) }, diff --git a/iOSClient/Assistant/NCAssistant.swift b/iOSClient/Assistant/NCAssistant.swift index 4bd49992b1..1cb45104b1 100644 --- a/iOSClient/Assistant/NCAssistant.swift +++ b/iOSClient/Assistant/NCAssistant.swift @@ -208,13 +208,15 @@ struct TaskItem: View { var body: some View { NavigationLink(destination: NCAssistantTaskDetail(task: task)) { - VStack(alignment: .leading) { + VStack(alignment: .leading, spacing: 8) { Text(task.input?.input ?? "") .lineLimit(1) - Text(task.output?.output ?? "") - .lineLimit(1) - .foregroundStyle(.secondary) + if let output = task.output?.output, !output.isEmpty { + Text(output) + .lineLimit(1) + .foregroundStyle(.secondary) + } HStack { Label( @@ -229,7 +231,6 @@ struct TaskItem: View { .font(Font.system(.body).weight(.light)) } ) - .padding(.top, 1) .labelStyle(CustomLabelStyle()) } } From 72f3f024482589f1b00fd050b4fc0c785153c964 Mon Sep 17 00:00:00 2001 From: Milen Pivchev Date: Thu, 6 Mar 2025 15:29:28 +0100 Subject: [PATCH 17/20] Update Tests/NextcloudUITests/AssistantUITests.swift Co-authored-by: Iva Horn Signed-off-by: Milen Pivchev --- Tests/NextcloudUITests/AssistantUITests.swift | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Tests/NextcloudUITests/AssistantUITests.swift b/Tests/NextcloudUITests/AssistantUITests.swift index 86fd86bd19..bce341aed9 100644 --- a/Tests/NextcloudUITests/AssistantUITests.swift +++ b/Tests/NextcloudUITests/AssistantUITests.swift @@ -52,7 +52,9 @@ final class AssistantUITests: BaseUIXCTestCase { private func createTask(input: String) { app.navigationBars["Assistant"].buttons["CreateButton"].tap() - app.textViews["InputTextEditor"].typeText(input) + let inputTextEditor = app.textViews["InputTextEditor"] + inputTextEditor.await() + inputTextEditor.typeText(input) app.navigationBars["New Free text to text prompt task"].buttons["Create"].tap() } From 7483eed55e60cbf36160b0758c8658f4e229c295 Mon Sep 17 00:00:00 2001 From: Milen Pivchev Date: Tue, 11 Mar 2025 12:40:46 +0100 Subject: [PATCH 18/20] PR fixes Signed-off-by: Milen Pivchev --- iOSClient/Assistant/Components/StatusInfo.swift | 10 +++------- iOSClient/Assistant/Models/NCAssistantModel.swift | 12 +++++------- 2 files changed, 8 insertions(+), 14 deletions(-) diff --git a/iOSClient/Assistant/Components/StatusInfo.swift b/iOSClient/Assistant/Components/StatusInfo.swift index 7240924e87..2fb660f0f6 100644 --- a/iOSClient/Assistant/Components/StatusInfo.swift +++ b/iOSClient/Assistant/Components/StatusInfo.swift @@ -1,10 +1,6 @@ -// -// StatusInfo.swift -// Nextcloud -// -// Created by Milen Pivchev on 18.02.25. -// Copyright © 2025 Marino Faggiana. All rights reserved. -// +// SPDX-FileCopyrightText: Nextcloud GmbH +// SPDX-FileCopyrightText: 2025 Milen Pivchev +// SPDX-License-Identifier: GPL-3.0-or-later import SwiftUI import NextcloudKit diff --git a/iOSClient/Assistant/Models/NCAssistantModel.swift b/iOSClient/Assistant/Models/NCAssistantModel.swift index 504daad0e2..c81c5e53a8 100644 --- a/iOSClient/Assistant/Models/NCAssistantModel.swift +++ b/iOSClient/Assistant/Models/NCAssistantModel.swift @@ -1,10 +1,6 @@ -// -// NCAssistantModel.swift -// Nextcloud -// -// Created by Milen on 08.04.24. -// Copyright © 2024 Marino Faggiana. All rights reserved. -// +// SPDX-FileCopyrightText: Nextcloud GmbH +// SPDX-FileCopyrightText: 2025 Milen Pivchev +// SPDX-License-Identifier: GPL-3.0-or-later import Foundation import UIKit @@ -213,6 +209,7 @@ class NCAssistantModel: ObservableObject { } } +#if DEBUG extension NCAssistantModel { public func loadDummyData() { let loremIpsum = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum." @@ -236,6 +233,7 @@ extension NCAssistantModel { self.selectedTask = filteredTasks[0] } } +#endif extension AssistantTask { struct StatusInfo { From 30b2262c318bc579461445a4b11186356a928020 Mon Sep 17 00:00:00 2001 From: Milen Pivchev Date: Tue, 11 Mar 2025 12:45:14 +0100 Subject: [PATCH 19/20] PR fixes 2 Signed-off-by: Milen Pivchev --- Tests/Server.sh | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Tests/Server.sh b/Tests/Server.sh index ad442a3efa..3c5f4d0687 100755 --- a/Tests/Server.sh +++ b/Tests/Server.sh @@ -19,6 +19,7 @@ docker run \ ghcr.io/nextcloud/continuous-integration-shallow-server:latest # Wait a moment until the server is ready. +echo "Please wait until the server is provisioned…" sleep 20 # Enable File Download Limit App. @@ -31,3 +32,5 @@ docker exec $CONTAINER_NAME su www-data -c "php /var/www/html/occ app:enable tes #Testing app generates fake Assitant responses via cronjob. Reduce cronjob downtime so it's quicker. docker exec $CONTAINER_NAME su www-data -c "set -e; while true; do php /var/www/html/occ background-job:worker -v -t 10 \"OC\TaskProcessing\SynchronousBackgroundJob\"; done" + +echo "Server provisioning done." From 6aa4894ce44186b4d9e5dfd532791a5ed30b6b60 Mon Sep 17 00:00:00 2001 From: Milen Pivchev Date: Tue, 11 Mar 2025 12:46:42 +0100 Subject: [PATCH 20/20] PR fixes 3 Signed-off-by: Milen Pivchev --- Tests/NextcloudUITests/BaseUIXCTestCase.swift | 12 +++--------- .../Assistant/Components/AssistantLabelStyle.swift | 10 +++------- 2 files changed, 6 insertions(+), 16 deletions(-) diff --git a/Tests/NextcloudUITests/BaseUIXCTestCase.swift b/Tests/NextcloudUITests/BaseUIXCTestCase.swift index 6993e1602d..5b54c30c5c 100644 --- a/Tests/NextcloudUITests/BaseUIXCTestCase.swift +++ b/Tests/NextcloudUITests/BaseUIXCTestCase.swift @@ -1,10 +1,6 @@ -// -// Helpers.swift -// NextcloudUITests -// -// Created by Milen Pivchev on 20.02.25. -// Copyright © 2025 Marino Faggiana. All rights reserved. -// +// SPDX-FileCopyrightText: Nextcloud GmbH +// SPDX-FileCopyrightText: 2025 Milen Pivchev +// SPDX-License-Identifier: GPL-3.0-or-later import Foundation import XCTest @@ -118,8 +114,6 @@ class BaseUIXCTestCase: XCTestCase { grantButton.tap() grantButton.awaitInexistence() - // Switch back from Safari to our app. - app.activate() app.buttons["accountSwitcher"].await() try await aSmallMoment() diff --git a/iOSClient/Assistant/Components/AssistantLabelStyle.swift b/iOSClient/Assistant/Components/AssistantLabelStyle.swift index 1d4b021489..5941fb0713 100644 --- a/iOSClient/Assistant/Components/AssistantLabelStyle.swift +++ b/iOSClient/Assistant/Components/AssistantLabelStyle.swift @@ -1,10 +1,6 @@ -// -// AssistantLabelStyle.swift -// Nextcloud -// -// Created by Milen Pivchev on 18.02.25. -// Copyright © 2025 Marino Faggiana. All rights reserved. -// +// SPDX-FileCopyrightText: Nextcloud GmbH +// SPDX-FileCopyrightText: 2025 Milen Pivchev +// SPDX-License-Identifier: GPL-3.0-or-later import SwiftUI