Skip to content

Commit 3b65a30

Browse files
committed
Add editing tags support to TagsView
1 parent 303aae9 commit 3b65a30

File tree

6 files changed

+450
-26
lines changed

6 files changed

+450
-26
lines changed

Modules/Sources/WordPressUI/Views/DataView/DataViewPaginatedResponse.swift

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,4 +119,18 @@ public final class DataViewPaginatedResponse<Element: Identifiable, PageIndex>:
119119
self.total = total - 1
120120
}
121121
}
122+
123+
public func replace(_ item: Element) {
124+
guard let index = items.firstIndex(where: { $0.id == item.id }) else {
125+
return
126+
}
127+
items[index] = item
128+
}
129+
130+
public func prepend(_ newItems: [Element]) {
131+
self.items = newItems + self.items
132+
if let total {
133+
self.total = total + newItems.count
134+
}
135+
}
122136
}

WordPress/Classes/ViewRelated/Post/Prepublishing/PrepublishingViewController.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -319,7 +319,7 @@ final class PrepublishingViewController: UIViewController, UITableViewDataSource
319319

320320
private func didTapTagCell() {
321321
let post = post as! Post
322-
let tagPickerViewController = TagsViewController(blog: post.blog, selectedTags: post.tags) {[weak self] tags in
322+
let tagPickerViewController = TagsViewController(blog: post.blog, selectedTags: post.tags) { [weak self] tags in
323323
guard let self else { return }
324324
WPAnalytics.track(.editorPostTagsChanged, properties: Constants.analyticsDefaultProperty)
325325

Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
1+
import SwiftUI
2+
import WordPressUI
3+
import WordPressKit
4+
import WordPressData
5+
import SVProgressHUD
6+
7+
struct EditTagView: View {
8+
@Environment(\.dismiss) private var dismiss
9+
@StateObject private var viewModel: EditTagViewModel
10+
11+
init(tag: RemotePostTag?, tagsService: TagsService) {
12+
self._viewModel = StateObject(wrappedValue: EditTagViewModel(tag: tag, tagsService: tagsService))
13+
}
14+
15+
var body: some View {
16+
Form {
17+
Section(Strings.tagSectionHeader) {
18+
HStack {
19+
TextField(Strings.tagNamePlaceholder, text: $viewModel.tagName)
20+
.textFieldStyle(.plain)
21+
.autocorrectionDisabled()
22+
.textInputAutocapitalization(.never)
23+
.keyboardType(.default)
24+
25+
if !viewModel.tagName.isEmpty {
26+
Button(action: {
27+
viewModel.tagName = ""
28+
}) {
29+
Image(systemName: "xmark.circle.fill")
30+
.foregroundColor(.secondary)
31+
}
32+
}
33+
}
34+
}
35+
36+
Section(Strings.descriptionSectionHeader) {
37+
TextField(Strings.descriptionPlaceholder, text: $viewModel.tagDescription, axis: .vertical)
38+
.textFieldStyle(.plain)
39+
.lineLimit(5...15)
40+
}
41+
42+
if viewModel.isExistingTag {
43+
Section {
44+
Button(action: {
45+
viewModel.showDeleteConfirmation = true
46+
}) {
47+
Text(SharedStrings.Button.delete)
48+
.foregroundColor(.red)
49+
}
50+
}
51+
}
52+
}
53+
.navigationTitle(viewModel.navigationTitle)
54+
.navigationBarTitleDisplayMode(.inline)
55+
.toolbar {
56+
ToolbarItem(placement: .navigationBarTrailing) {
57+
Button(SharedStrings.Button.save) {
58+
Task {
59+
let success = await viewModel.saveTag()
60+
if success {
61+
dismiss()
62+
}
63+
}
64+
}
65+
.disabled(viewModel.tagName.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty)
66+
}
67+
}
68+
.confirmationDialog(
69+
Strings.deleteConfirmationTitle,
70+
isPresented: $viewModel.showDeleteConfirmation,
71+
titleVisibility: .visible
72+
) {
73+
Button(SharedStrings.Button.delete, role: .destructive) {
74+
Task {
75+
let success = await viewModel.deleteTag()
76+
if success {
77+
dismiss()
78+
}
79+
}
80+
}
81+
Button(SharedStrings.Button.cancel, role: .cancel) { }
82+
} message: {
83+
Text(Strings.deleteConfirmationMessage)
84+
}
85+
.alert(SharedStrings.Error.generic, isPresented: $viewModel.showError) {
86+
Button(SharedStrings.Button.ok) { }
87+
} message: {
88+
Text(viewModel.errorMessage)
89+
}
90+
}
91+
}
92+
93+
@MainActor
94+
class EditTagViewModel: ObservableObject {
95+
@Published var tagName: String
96+
@Published var tagDescription: String
97+
@Published var showDeleteConfirmation = false
98+
@Published var showError = false
99+
@Published var errorMessage = ""
100+
101+
private let originalTag: RemotePostTag?
102+
private let tagsService: TagsService
103+
104+
var isExistingTag: Bool {
105+
originalTag != nil
106+
}
107+
108+
var navigationTitle: String {
109+
originalTag?.name ?? Strings.newTagTitle
110+
}
111+
112+
init(tag: RemotePostTag?, tagsService: TagsService) {
113+
self.originalTag = tag
114+
self.tagsService = tagsService
115+
self.tagName = tag?.name ?? ""
116+
self.tagDescription = tag?.tagDescription ?? ""
117+
}
118+
119+
func deleteTag() async -> Bool {
120+
guard let tag = originalTag else { return false }
121+
122+
SVProgressHUD.show()
123+
defer { SVProgressHUD.dismiss() }
124+
125+
do {
126+
try await tagsService.deleteTag(tag)
127+
128+
// Post notification to update the UI
129+
NotificationCenter.default.post(
130+
name: .tagDeleted,
131+
object: nil,
132+
userInfo: [TagNotificationUserInfoKeys.tagID: tag.tagID ?? 0]
133+
)
134+
return true
135+
} catch {
136+
errorMessage = error.localizedDescription
137+
showError = true
138+
return false
139+
}
140+
}
141+
142+
func saveTag() async -> Bool {
143+
SVProgressHUD.show()
144+
defer { SVProgressHUD.dismiss() }
145+
146+
let tagToSave: RemotePostTag
147+
if let existingTag = originalTag {
148+
tagToSave = existingTag
149+
} else {
150+
tagToSave = RemotePostTag()
151+
}
152+
153+
tagToSave.name = tagName.trimmingCharacters(in: .whitespacesAndNewlines)
154+
tagToSave.tagDescription = tagDescription.trimmingCharacters(in: .whitespacesAndNewlines)
155+
156+
do {
157+
let savedTag = try await tagsService.saveTag(tagToSave)
158+
159+
NotificationCenter.default.post(
160+
name: originalTag == nil ? .tagCreated : .tagUpdated,
161+
object: nil,
162+
userInfo: [TagNotificationUserInfoKeys.tag: savedTag]
163+
)
164+
return true
165+
} catch {
166+
errorMessage = error.localizedDescription
167+
showError = true
168+
return false
169+
}
170+
}
171+
}
172+
173+
private enum Strings {
174+
static let tagSectionHeader = NSLocalizedString(
175+
"edit.tag.section.tag",
176+
value: "Tag",
177+
comment: "Section header for tag name in edit tag view"
178+
)
179+
180+
static let descriptionSectionHeader = NSLocalizedString(
181+
"edit.tag.section.description",
182+
value: "Description",
183+
comment: "Section header for tag description in edit tag view"
184+
)
185+
186+
static let tagNamePlaceholder = NSLocalizedString(
187+
"edit.tag.name.placeholder",
188+
value: "Tag name",
189+
comment: "Placeholder text for tag name field"
190+
)
191+
192+
static let descriptionPlaceholder = NSLocalizedString(
193+
"edit.tag.description.placeholder",
194+
value: "Add a description...",
195+
comment: "Placeholder text for tag description field"
196+
)
197+
198+
static let newTagTitle = NSLocalizedString(
199+
"edit.tag.new.title",
200+
value: "New Tag",
201+
comment: "Navigation title for new tag creation"
202+
)
203+
204+
static let deleteConfirmationTitle = NSLocalizedString(
205+
"edit.tag.delete.confirmation.title",
206+
value: "Delete Tag",
207+
comment: "Title for delete tag confirmation dialog"
208+
)
209+
210+
static let deleteConfirmationMessage = NSLocalizedString(
211+
"edit.tag.delete.confirmation.message",
212+
value: "Are you sure you want to delete this tag?",
213+
comment: "Message for delete tag confirmation dialog"
214+
)
215+
}

WordPress/Classes/ViewRelated/Tags/TagsService.swift

Lines changed: 66 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,12 @@ class TagsService {
2121
return nil
2222
}
2323

24-
func getTags(number: Int = 100, offset: Int = 0) async throws -> [RemotePostTag] {
24+
func getTags(
25+
number: Int = 100,
26+
offset: Int = 0,
27+
orderBy: RemoteTaxonomyPagingResultsOrdering = .byName,
28+
order: RemoteTaxonomyPagingResultsOrder = .orderAscending
29+
) async throws -> [RemotePostTag] {
2530
guard let remote else {
2631
throw TagsServiceError.noRemoteService
2732
}
@@ -56,8 +61,68 @@ class TagsService {
5661
})
5762
}
5863
}
64+
65+
func deleteTag(_ tag: RemotePostTag) async throws {
66+
guard let remote else {
67+
throw TagsServiceError.noRemoteService
68+
}
69+
70+
guard tag.tagID != nil else {
71+
throw TagsServiceError.invalidTag
72+
}
73+
74+
return try await withCheckedThrowingContinuation { continuation in
75+
remote.delete(tag, success: {
76+
continuation.resume()
77+
}, failure: { error in
78+
continuation.resume(throwing: error)
79+
})
80+
}
81+
}
82+
83+
func saveTag(_ tag: RemotePostTag) async throws -> RemotePostTag {
84+
guard let remote else {
85+
throw TagsServiceError.noRemoteService
86+
}
87+
88+
return try await withCheckedThrowingContinuation { continuation in
89+
if tag.tagID == nil {
90+
remote.createTag(tag, success: { savedTag in
91+
continuation.resume(returning: savedTag)
92+
}, failure: { error in
93+
continuation.resume(throwing: error)
94+
})
95+
} else {
96+
remote.update(tag, success: { savedTag in
97+
continuation.resume(returning: savedTag)
98+
}, failure: { error in
99+
continuation.resume(throwing: error)
100+
})
101+
}
102+
}
103+
}
59104
}
60105

61106
enum TagsServiceError: Error {
62107
case noRemoteService
108+
case invalidTag
109+
}
110+
111+
extension TagsServiceError: LocalizedError {
112+
var errorDescription: String? {
113+
switch self {
114+
case .noRemoteService:
115+
return NSLocalizedString(
116+
"tags.error.no_remote_service",
117+
value: "Unable to connect to your site. Please check your connection and try again.",
118+
comment: "Error message when the tags service cannot connect to the remote site"
119+
)
120+
case .invalidTag:
121+
return NSLocalizedString(
122+
"tags.error.invalid_tag",
123+
value: "The tag information is invalid. Please try again.",
124+
comment: "Error message when tag data is invalid"
125+
)
126+
}
127+
}
63128
}

0 commit comments

Comments
 (0)