Skip to content

Commit 4563294

Browse files
authored
Merge pull request #18650 from wordpress-mobile/feature/stats-revamp-insights-management
Stats Revamp: Insights Management updates
2 parents 6b3e6bd + 28fdfd2 commit 4563294

File tree

4 files changed

+291
-30
lines changed

4 files changed

+291
-30
lines changed

WordPress/Classes/Utility/ImmuTable.swift

Lines changed: 38 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import UIKit
12
/**
23
ImmuTable represents the view model for a static UITableView.
34

@@ -291,6 +292,26 @@ open class ImmuTableViewHandler: NSObject, UITableViewDataSource, UITableViewDel
291292
return viewModel.sections[section].footerText
292293
}
293294

295+
open func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
296+
if target.responds(to: #selector(UITableViewDataSource.tableView(_:canEditRowAt:))) {
297+
return target.tableView?(tableView, canEditRowAt: indexPath) ?? false
298+
}
299+
300+
return false
301+
}
302+
303+
open func tableView(_ tableView: UITableView, canMoveRowAt indexPath: IndexPath) -> Bool {
304+
if target.responds(to: #selector(UITableViewDataSource.tableView(_:canMoveRowAt:))) {
305+
return target.tableView?(tableView, canMoveRowAt: indexPath) ?? false
306+
}
307+
308+
return false
309+
}
310+
311+
open func tableView(_ tableView: UITableView, moveRowAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) {
312+
target.tableView?(tableView, moveRowAt: sourceIndexPath, to: destinationIndexPath)
313+
}
314+
294315
// MARK: UITableViewDelegate
295316

296317
open func tableView(_ tableView: UITableView, willSelectRowAt indexPath: IndexPath) -> IndexPath? {
@@ -352,14 +373,27 @@ open class ImmuTableViewHandler: NSObject, UITableViewDataSource, UITableViewDel
352373
return nil
353374
}
354375

355-
open func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
356-
if target.responds(to: #selector(UITableViewDataSource.tableView(_:canEditRowAt:))) {
357-
return target.tableView?(tableView, canEditRowAt: indexPath) ?? false
376+
open func tableView(_ tableView: UITableView, targetIndexPathForMoveFromRowAt sourceIndexPath: IndexPath, toProposedIndexPath proposedDestinationIndexPath: IndexPath) -> IndexPath {
377+
if target.responds(to: #selector(UITableViewDelegate.tableView(_:targetIndexPathForMoveFromRowAt:toProposedIndexPath:))) {
378+
return target.tableView?(tableView, targetIndexPathForMoveFromRowAt: sourceIndexPath, toProposedIndexPath: proposedDestinationIndexPath) ?? proposedDestinationIndexPath
358379
}
359380

360-
return false
381+
return proposedDestinationIndexPath
361382
}
362383

384+
open func tableView(_ tableView: UITableView, editingStyleForRowAt indexPath: IndexPath) -> UITableViewCell.EditingStyle {
385+
if target.responds(to: #selector(UITableViewDelegate.tableView(_:editingStyleForRowAt:))) {
386+
return target.tableView?(tableView, editingStyleForRowAt: indexPath) ?? .none
387+
}
388+
389+
return .none
390+
}
391+
392+
open func tableView(_ tableView: UITableView, shouldIndentWhileEditingRowAt indexPath: IndexPath) -> Bool {
393+
return target.tableView?(tableView, shouldIndentWhileEditingRowAt: indexPath) ?? true
394+
}
395+
396+
363397
public func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
364398
if target.responds(to: #selector(UITableViewDelegate.tableView(_:trailingSwipeActionsConfigurationForRowAt:))) {
365399
return target.tableView?(tableView, trailingSwipeActionsConfigurationForRowAt: indexPath)

WordPress/Classes/ViewRelated/Stats/Insights/Insights Management/AddInsightTableViewController.swift

Lines changed: 214 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,34 @@
11
import UIKit
22
import Gridicons
33

4+
// This exists in addition to `SiteStatsInsightsDelegate` because `[StatSection]`
5+
// can't be represented in an Obj-C protocol.
6+
protocol StatsInsightsManagementDelegate: AnyObject {
7+
func userUpdatedActiveInsights(_ insights: [StatSection])
8+
}
9+
410
class AddInsightTableViewController: UITableViewController {
511

612
// MARK: - Properties
7-
13+
private weak var insightsManagementDelegate: StatsInsightsManagementDelegate?
814
private weak var insightsDelegate: SiteStatsInsightsDelegate?
9-
private var insightsShown = [StatSection]()
15+
16+
/// Stored so that we can check if the user has made any changes.
17+
private var originalInsightsShown = [StatSection]()
18+
private var insightsShown = [StatSection]() {
19+
didSet {
20+
updateSaveButton()
21+
}
22+
}
23+
24+
private var hasChanges: Bool {
25+
return insightsShown != originalInsightsShown
26+
}
27+
1028
private var selectedStat: StatSection?
1129

30+
private lazy var saveButton = UIBarButtonItem(barButtonSystemItem: .save, target: self, action: #selector(saveTapped))
31+
1232
private lazy var tableHandler: ImmuTableViewHandler = {
1333
return ImmuTableViewHandler(takeOver: self)
1434
}()
@@ -17,17 +37,20 @@ class AddInsightTableViewController: UITableViewController {
1737

1838
override init(style: UITableView.Style) {
1939
super.init(style: style)
20-
navigationItem.title = NSLocalizedString("Add New Stats Card", comment: "Add New Stats Card view title")
40+
41+
navigationItem.title = TextContent.title
2142
}
2243

2344
required init?(coder aDecoder: NSCoder) {
2445
fatalError("init(coder:) has not been implemented")
2546
}
2647

27-
convenience init(insightsDelegate: SiteStatsInsightsDelegate, insightsShown: [StatSection]) {
48+
convenience init(insightsDelegate: SiteStatsInsightsDelegate, insightsManagementDelegate: StatsInsightsManagementDelegate? = nil, insightsShown: [StatSection]) {
2849
self.init(style: .grouped)
2950
self.insightsDelegate = insightsDelegate
51+
self.insightsManagementDelegate = insightsManagementDelegate
3052
self.insightsShown = insightsShown
53+
self.originalInsightsShown = insightsShown
3154
}
3255

3356
// MARK: - View
@@ -39,19 +62,26 @@ class AddInsightTableViewController: UITableViewController {
3962
reloadViewModel()
4063
WPStyleGuide.configureColors(view: view, tableView: tableView)
4164
WPStyleGuide.configureAutomaticHeightRows(for: tableView)
42-
tableView.accessibilityIdentifier = "Add Insight Table"
65+
tableView.accessibilityIdentifier = TextContent.title
66+
67+
if FeatureFlag.statsNewAppearance.enabled {
68+
tableView.isEditing = true
69+
tableView.allowsSelectionDuringEditing = true
70+
}
4371

4472
navigationItem.leftBarButtonItem = UIBarButtonItem(image: .gridicon(.cross), style: .plain, target: self, action: #selector(doneTapped))
4573
}
4674

47-
override func viewWillDisappear(_ animated: Bool) {
48-
super.viewWillDisappear(animated)
49-
50-
if selectedStat == nil {
51-
insightsDelegate?.addInsightDismissed?()
75+
func handleDismissViaGesture(from presenter: UIViewController) {
76+
if FeatureFlag.statsNewAppearance.enabled && hasChanges {
77+
promptToSave(from: presenter)
78+
} else {
79+
trackDismiss()
5280
}
5381
}
5482

83+
// MARK: TableView Data Source / Delegate Overrides
84+
5585
override func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
5686
return 38
5787
}
@@ -60,10 +90,120 @@ class AddInsightTableViewController: UITableViewController {
6090
return 0
6191
}
6292

93+
override func tableView(_ tableView: UITableView, canMoveRowAt indexPath: IndexPath) -> Bool {
94+
guard FeatureFlag.statsNewAppearance.enabled else {
95+
return false
96+
}
97+
98+
return isActiveCardsSection(indexPath.section)
99+
}
100+
101+
override func tableView(_ tableView: UITableView, moveRowAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) {
102+
guard FeatureFlag.statsNewAppearance.enabled else {
103+
return
104+
}
105+
106+
if isActiveCardsSection(sourceIndexPath.section) && isActiveCardsSection(destinationIndexPath.section) {
107+
let item = insightsShown.remove(at: sourceIndexPath.row)
108+
insightsShown.insert(item, at: destinationIndexPath.row)
109+
reloadViewModel()
110+
}
111+
}
112+
113+
override func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
114+
guard FeatureFlag.statsNewAppearance.enabled else {
115+
return false
116+
}
117+
118+
return insightsShown.count > 0 && isActiveCardsSection(indexPath.section)
119+
}
120+
121+
override func tableView(_ tableView: UITableView, targetIndexPathForMoveFromRowAt sourceIndexPath: IndexPath, toProposedIndexPath proposedDestinationIndexPath: IndexPath) -> IndexPath {
122+
if isActiveCardsSection(proposedDestinationIndexPath.section) {
123+
return proposedDestinationIndexPath
124+
}
125+
126+
return IndexPath(row: insightsShown.count - 1, section: 0)
127+
}
128+
129+
override func tableView(_ tableView: UITableView, shouldIndentWhileEditingRowAt indexPath: IndexPath) -> Bool {
130+
return false
131+
}
132+
133+
private func isActiveCardsSection(_ sectionIndex: Int) -> Bool {
134+
return sectionIndex == 0
135+
}
136+
137+
// MARK: - Actions
138+
139+
private func updateSaveButton() {
140+
guard FeatureFlag.statsNewAppearance.enabled else {
141+
return
142+
}
143+
144+
if hasChanges {
145+
navigationItem.rightBarButtonItem = saveButton
146+
} else {
147+
navigationItem.rightBarButtonItem = nil
148+
}
149+
}
150+
63151
@objc private func doneTapped() {
64-
WPAnalytics.trackEvent(.statsInsightsManagementDismissed)
152+
if FeatureFlag.statsNewAppearance.enabled && hasChanges {
153+
promptToSave(from: self)
154+
} else {
155+
dismiss()
156+
}
157+
}
158+
159+
@objc func saveTapped() {
160+
saveChanges()
161+
65162
dismiss(animated: true, completion: nil)
66163
}
164+
165+
private func dismiss() {
166+
trackDismiss()
167+
168+
dismiss(animated: true, completion: nil)
169+
}
170+
171+
private func trackDismiss() {
172+
WPAnalytics.trackEvent(.statsInsightsManagementDismissed)
173+
insightsDelegate?.addInsightDismissed?()
174+
}
175+
176+
private func saveChanges() {
177+
insightsManagementDelegate?.userUpdatedActiveInsights(insightsShown)
178+
179+
WPAnalytics.track(.statsInsightsManagementSaved, properties: ["types": [insightsShown.map({$0.title})]])
180+
181+
// Update original to stop us detecting changes on dismiss
182+
originalInsightsShown = insightsShown
183+
}
184+
185+
private func promptToSave(from viewController: UIViewController?) {
186+
let alert = UIAlertController(title: nil, message: TextContent.savePromptMessage, preferredStyle: .actionSheet)
187+
alert.addAction(UIAlertAction(title: TextContent.savePromptSaveButton, style: .default, handler: { _ in
188+
self.saveTapped()
189+
}))
190+
alert.addAction(UIAlertAction(title: TextContent.savePromptDiscardButton, style: .destructive, handler: { _ in
191+
self.dismiss()
192+
}))
193+
alert.addCancelActionWithTitle(TextContent.savePromptCancelButton, handler: nil)
194+
viewController?.present(alert, animated: true, completion: nil)
195+
}
196+
197+
fileprivate enum TextContent {
198+
static let title = NSLocalizedString("stats.insights.management.title", value: "Manage Stats Cards", comment: "Title of the Stats Insights Management screen")
199+
static let activeCardsHeader = NSLocalizedString("stats.insights.management.activeCards", value: "Active Cards", comment: "Header title indicating which Stats Insights cards the user currently has set to active.")
200+
static let placeholderRowTitle = NSLocalizedString("stats.insights.management.selectCardsPrompt", value: "Select cards from the list below", comment: "Prompt displayed on the Stats Insights management screen telling the user to tap a row to add it to their list of active cards.")
201+
202+
static let savePromptMessage = NSLocalizedString("stats.insights.management.savePrompt.message", value: "You've made changes to your active Insights cards.", comment: "Title of alert in Stats Insights management, prompting the user to save changes to their list of active Stats cards.")
203+
static let savePromptSaveButton = NSLocalizedString("stats.insights.management.savePrompt.saveButton", value: "Save Changes", comment: "Title of button in Stats Insights management, prompting the user to save changes to their list of active Stats cards.")
204+
static let savePromptDiscardButton = NSLocalizedString("stats.insights.management.savePrompt.discardButton", value: "Discard Changes", comment: "Title of button in Stats Insights management, prompting the user to discard changes to their list of active Stats cards.")
205+
static let savePromptCancelButton = NSLocalizedString("stats.insights.management.savePrompt.cancelButton", value: "Cancel", comment: "Title of button to cancel an alert and take no action.")
206+
}
67207
}
68208

69209
private extension AddInsightTableViewController {
@@ -75,33 +215,84 @@ private extension AddInsightTableViewController {
75215
}
76216

77217
func tableViewModel() -> ImmuTable {
78-
return ImmuTable(sections: [ sectionForCategory(.general),
218+
return ImmuTable(sections: [ selectedStatsSection(),
219+
sectionForCategory(.general),
79220
sectionForCategory(.postsAndPages),
80-
sectionForCategory(.activity) ]
221+
sectionForCategory(.activity) ].compactMap({$0})
81222
)
82223
}
83224

84225
// MARK: - Table Sections
85226

86-
func sectionForCategory(_ category: InsightsCategories) -> ImmuTableSection {
87-
return ImmuTableSection(headerText: category.title,
88-
rows: category.insights.map {
89-
let enabled = !insightsShown.contains($0)
227+
func selectedStatsSection() -> ImmuTableSection? {
228+
guard FeatureFlag.statsNewAppearance.enabled else {
229+
return nil
230+
}
231+
232+
guard insightsShown.count > 0 else {
233+
return ImmuTableSection(headerText: TextContent.activeCardsHeader, rows: [placeholderRow])
234+
}
235+
236+
return ImmuTableSection(headerText: TextContent.activeCardsHeader,
237+
rows: insightsShown.map {
238+
return AddInsightStatRow(title: $0.insightManagementTitle,
239+
enabled: true,
240+
action: rowActionFor($0)) }
241+
)
242+
}
243+
244+
func sectionForCategory(_ category: InsightsCategories) -> ImmuTableSection? {
245+
guard FeatureFlag.statsNewAppearance.enabled else {
246+
return ImmuTableSection(headerText: category.title,
247+
rows: category.insights.map {
248+
let enabled = !insightsShown.contains($0)
249+
return AddInsightStatRow(title: $0.insightManagementTitle,
250+
enabled: enabled,
251+
action: enabled ? rowActionFor($0) : nil) }
252+
)
253+
}
90254

255+
let rows = category.insights.filter({ !self.insightsShown.contains($0) })
256+
guard rows.count > 0 else {
257+
return nil
258+
}
259+
260+
return ImmuTableSection(headerText: category.title,
261+
rows: rows.map {
91262
return AddInsightStatRow(title: $0.insightManagementTitle,
92-
enabled: enabled,
93-
action: enabled ? rowActionFor($0) : nil) }
263+
enabled: false,
264+
action: rowActionFor($0)) }
94265
)
95266
}
96267

97268
func rowActionFor(_ statSection: StatSection) -> ImmuTableAction {
98269
return { [unowned self] row in
99-
self.selectedStat = statSection
100-
self.insightsDelegate?.addInsightSelected?(statSection)
270+
if FeatureFlag.statsNewAppearance.enabled {
271+
toggleRow(for: statSection)
272+
} else {
273+
self.selectedStat = statSection
274+
self.insightsDelegate?.addInsightSelected?(statSection)
275+
276+
WPAnalytics.track(.statsInsightsManagementSaved, properties: ["types": [statSection.title]])
277+
self.dismiss(animated: true, completion: nil)
278+
}
279+
}
280+
}
101281

102-
WPAnalytics.track(.statsInsightsManagementSaved, properties: ["types": [statSection.title]])
103-
self.dismiss(animated: true, completion: nil)
282+
func toggleRow(for statSection: StatSection) {
283+
if let index = insightsShown.firstIndex(of: statSection) {
284+
insightsShown.remove(at: index)
285+
} else {
286+
insightsShown.append(statSection)
104287
}
288+
289+
reloadViewModel()
290+
}
291+
292+
var placeholderRow: ImmuTableRow {
293+
return AddInsightStatRow(title: TextContent.placeholderRowTitle,
294+
enabled: false,
295+
action: nil)
105296
}
106297

107298
// MARK: - Insights Categories

0 commit comments

Comments
 (0)