-
Notifications
You must be signed in to change notification settings - Fork 1.1k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Use PostRepository to fetch pages in the Menu screen #21949
Changes from 4 commits
fb3fad8
0e63b7f
187e134
53d634a
5339b86
ed781a5
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -9,8 +9,6 @@ | |
#import "WordPress-Swift.h" | ||
@import WordPressKit; | ||
|
||
NS_ASSUME_NONNULL_BEGIN | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why was this removed? I'm guessing because it's not necessary in There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Indeed. No benefit whatsoever in using it in .m files. |
||
|
||
@implementation MenusService | ||
|
||
#pragma mark - Menus availability | ||
|
@@ -161,47 +159,21 @@ - (void)deleteMenu:(Menu *)menu | |
failure:failure]; | ||
} | ||
|
||
- (void)generateDefaultMenuItemsForBlog:(Blog *)blog | ||
success:(nullable void(^)(NSArray <MenuItem *> * _Nullable defaultItems))success | ||
failure:(nullable MenusServiceFailureBlock)failure | ||
{ | ||
// Get the latest list of Pages available to the site. | ||
PostServiceSyncOptions *options = [[PostServiceSyncOptions alloc] init]; | ||
options.statuses = @[PostStatusPublish]; | ||
options.order = PostServiceResultsOrderAscending; | ||
options.orderBy = PostServiceResultsOrderingByTitle; | ||
PostService *postService = [[PostService alloc] initWithManagedObjectContext:self.managedObjectContext]; | ||
[postService syncPostsOfType:PostServiceTypePage | ||
withOptions:options | ||
forBlog:blog | ||
success:^(NSArray *pages) { | ||
[self.managedObjectContext performBlock:^{ | ||
|
||
if (!pages.count) { | ||
success(nil); | ||
return; | ||
} | ||
NSMutableArray *items = [NSMutableArray arrayWithCapacity:pages.count]; | ||
// Create menu items for the top parent pages. | ||
for (Page *page in pages) { | ||
if ([page.parentID integerValue] > 0) { | ||
continue; | ||
} | ||
MenuItem *pageItem = [NSEntityDescription insertNewObjectForEntityForName:[MenuItem entityName] inManagedObjectContext:self.managedObjectContext]; | ||
pageItem.contentID = page.postID; | ||
pageItem.name = page.titleForDisplay; | ||
pageItem.type = MenuItemTypePage; | ||
[items addObject:pageItem]; | ||
} | ||
|
||
[[ContextManager sharedInstance] saveContext:self.managedObjectContext withCompletionBlock:^{ | ||
if (success) { | ||
success(items); | ||
} | ||
} onQueue:dispatch_get_main_queue()]; | ||
}]; | ||
} | ||
failure:failure]; | ||
- (MenuItem *)createItemWithPageID:(NSManagedObjectID *)pageObjectID inContext:(NSManagedObjectContext *)context { | ||
Page *page = [context existingObjectWithID:pageObjectID error:nil]; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Have you considered passing an error object? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That didn't cross my mind. That's a good idea though: turning this Objective-C function to a throwing Swift function. But unfortunately, after giving it go, I noticed Swift turns the function to |
||
if (page == nil) { | ||
return nil; | ||
} | ||
|
||
if ([page.parentID integerValue] > 0) { | ||
return nil; | ||
} | ||
|
||
MenuItem *pageItem = [NSEntityDescription insertNewObjectForEntityForName:[MenuItem entityName] inManagedObjectContext:self.managedObjectContext]; | ||
pageItem.contentID = page.postID; | ||
pageItem.name = page.titleForDisplay; | ||
pageItem.type = MenuItemTypePage; | ||
return pageItem; | ||
} | ||
|
||
#pragma mark - private | ||
|
@@ -435,5 +407,3 @@ - (RemoteMenuItem *)remoteItemFromItem:(MenuItem *)item withItems:(NSOrderedSet< | |
} | ||
|
||
@end | ||
|
||
NS_ASSUME_NONNULL_END |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -15,8 +15,6 @@ | |
#import <WordPressShared/WPFontManager.h> | ||
#import <WordPressShared/WPStyleGuide.h> | ||
|
||
|
||
|
||
static CGFloat const ScrollViewOffsetAdjustmentPadding = 10.0; | ||
|
||
@interface MenusViewController () <UIScrollViewDelegate, MenuHeaderViewControllerDelegate, MenuDetailsViewControllerDelegate, MenuItemsViewControllerDelegate> | ||
|
@@ -32,9 +30,6 @@ @interface MenusViewController () <UIScrollViewDelegate, MenuHeaderViewControlle | |
@property (nonatomic, strong, readonly) UILabel *itemsLoadingLabel; | ||
@property (nonatomic, strong, readonly) UIBarButtonItem *saveButtonItem; | ||
|
||
@property (nonatomic, strong, readonly) Blog *blog; | ||
@property (nonatomic, strong, readonly) MenusService *menusService; | ||
|
||
@property (nonatomic, strong) MenuLocation *selectedMenuLocation; | ||
@property (nonatomic, strong) Menu *updatedMenuForSaving; | ||
@property (nonatomic, strong) Menu *initialMenuSelection; | ||
|
@@ -45,6 +40,8 @@ @interface MenusViewController () <UIScrollViewDelegate, MenuHeaderViewControlle | |
@property (nonatomic, assign) BOOL needsSave; | ||
@property (nonatomic, assign) BOOL isSaving; | ||
|
||
@property (nonatomic, strong) void (^fetchAllPagesCancellationHandler)(void); | ||
|
||
@end | ||
|
||
@implementation MenusViewController | ||
|
@@ -176,6 +173,15 @@ - (void)viewWillDisappear:(BOOL)animated | |
[[NSNotificationCenter defaultCenter] removeObserver:self]; | ||
} | ||
|
||
- (void)viewDidDisappear:(BOOL)animated | ||
{ | ||
[super viewDidDisappear:animated]; | ||
|
||
if (self.navigationController == nil && self.fetchAllPagesCancellationHandler != nil) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why does the code check for the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. To make sure we only cancel the fetching when user taps the Back button. |
||
self.fetchAllPagesCancellationHandler(); | ||
} | ||
} | ||
|
||
#pragma mark - Views setup | ||
|
||
- (void)updateScrollViewContentSize | ||
|
@@ -355,9 +361,7 @@ - (void)loadDefaultMenuItemsIfNeeded | |
void(^failureBlock)(NSError *) = ^(NSError * __unused error) { | ||
weakSelf.itemsLoadingLabel.text = NSLocalizedString(@"An error occurred loading the menu, please check your internet connection.", @"Menus error message seen when an error occurred loading a specific menu."); | ||
}; | ||
[self.menusService generateDefaultMenuItemsForBlog:self.blog | ||
success:successBlock | ||
failure:failureBlock]; | ||
self.fetchAllPagesCancellationHandler = [self fetchAllPagesWithSuccess:successBlock failure:failureBlock]; | ||
} | ||
} | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
import Foundation | ||
|
||
extension MenusViewController { | ||
|
||
/// Fetch all pages from the site. | ||
/// | ||
/// - Returns: A block that can be used to cancel the fetching. | ||
@objc func fetchAllPages(success: @escaping ([MenuItem]) -> Void, failure: @escaping (Error) -> Void) -> () -> Void { | ||
let coreDataStack = ContextManager.shared | ||
let repository = PostRepository(coreDataStack: coreDataStack) | ||
let fetchAllPagesTask = repository.fetchAllPages(statuses: [.publish], in: TaggedManagedObjectID(blog)) | ||
|
||
// Wait for the fetching all pages task to complete and pass its result to the success or failure block. | ||
Task { [weak self] in | ||
let allPages: [TaggedManagedObjectID<Page>] | ||
do { | ||
allPages = try await fetchAllPagesTask.value | ||
} catch { | ||
failure(error) | ||
return | ||
} | ||
|
||
guard let menusService = self?.menusService else { return } | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I was going to suggest moving the check at the start of the |
||
|
||
await menusService.managedObjectContext.perform { | ||
let items = allPages.compactMap { | ||
menusService.createItem(withPageID: $0.objectID, in: menusService.managedObjectContext) | ||
} | ||
success(items) | ||
} | ||
} | ||
|
||
return fetchAllPagesTask.cancel | ||
} | ||
|
||
} |
Original file line number | Diff line number | Diff line change | ||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|
|
@@ -241,6 +241,65 @@ class PostRepositoryTests: CoreDataTestCase { | |||||||||||
} | ||||||||||||
} | ||||||||||||
|
||||||||||||
func testFetchAllPagesStopsOnEmptyAPIResponse() async throws { | ||||||||||||
// Given two pages of API result: first page returns 100 page instances, and the second page returns an empty result. | ||||||||||||
remoteMock.remotePostsToReturnOnSyncPostsOfType = [ | ||||||||||||
try (1...100).map { | ||||||||||||
let post = try XCTUnwrap(RemotePost(siteID: NSNumber(value: $0), status: "publish", title: "Post: Test", content: "This is a test post")) | ||||||||||||
post.type = "page" | ||||||||||||
return post | ||||||||||||
}, | ||||||||||||
[] | ||||||||||||
] | ||||||||||||
|
||||||||||||
let pages = try await repository.fetchAllPages(statuses: [.publish], in: blogID).value | ||||||||||||
XCTAssertEqual(pages.count, 100) | ||||||||||||
} | ||||||||||||
|
||||||||||||
func testFetchAllPagesStopsOnNonFullPageAPIResponse() async throws { | ||||||||||||
// Given two pages of API result: first page returns 100 page instances, and the second page returns 10 (any amount that's less than 100) page instances. | ||||||||||||
remoteMock.remotePostsToReturnOnSyncPostsOfType = [ | ||||||||||||
try (1...100).map { | ||||||||||||
let post = try XCTUnwrap(RemotePost(siteID: NSNumber(value: $0), status: "publish", title: "Post: Test", content: "This is a test post")) | ||||||||||||
post.type = "page" | ||||||||||||
return post | ||||||||||||
}, | ||||||||||||
try (1...10).map { | ||||||||||||
let post = try XCTUnwrap(RemotePost(siteID: NSNumber(value: $0), status: "publish", title: "Post: Test", content: "This is a test post")) | ||||||||||||
post.type = "page" | ||||||||||||
return post | ||||||||||||
}, | ||||||||||||
] | ||||||||||||
|
||||||||||||
let pages = try await repository.fetchAllPages(statuses: [.publish], in: blogID).value | ||||||||||||
XCTAssertEqual(pages.count, 110) | ||||||||||||
} | ||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Have you considered a test to explicitly check that no more requests are made after an empty or < page size response has been received? 🤔 I guess the fact that Not a must. If I had some time, I'd see if I could change the source code to make these tests pass with more requests, but I don't think I'll get to it any time soon. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
I think that's already covered here. If there are more requests fired, then the fake
That means the final total pages should be more than 110 and breaks the assertion or throw an error. Given the test passes, we should(?) be fairly certain that there no unnecessary requests are fired. I guess that's not entirely true. Because if the I guess we could to add a WordPress-iOS/WordPress/WordPressTest/PostRepositoryTests.swift Lines 350 to 354 in 5339b86
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I have added another test in ed781a5 to ensure when there is an HTTP API error, the fetchAllPages should throw, which means the error is not swallowed and at the same time ensure no extra requests are fired after an empty or < page size response. |
||||||||||||
|
||||||||||||
func testCancelFetchAllPages() async throws { | ||||||||||||
remoteMock.remotePostsToReturnOnSyncPostsOfType = try (1...10).map { pageNo in | ||||||||||||
try (1...100).map { | ||||||||||||
let post = try XCTUnwrap(RemotePost(siteID: NSNumber(value: pageNo * 100 + $0), status: "publish", title: "Post: Test", content: "This is a test post")) | ||||||||||||
post.type = "page" | ||||||||||||
return post | ||||||||||||
} | ||||||||||||
} | ||||||||||||
|
||||||||||||
let cancelled = expectation(description: "Fetching task returns cancellation error") | ||||||||||||
let task = repository.fetchAllPages(statuses: [.publish], in: blogID) | ||||||||||||
|
||||||||||||
DispatchQueue.global().asyncAfter(deadline: .now() + .microseconds(100)) { | ||||||||||||
task.cancel() | ||||||||||||
} | ||||||||||||
|
||||||||||||
do { | ||||||||||||
let _ = try await task.value | ||||||||||||
} catch is CancellationError { | ||||||||||||
cancelled.fulfill() | ||||||||||||
} | ||||||||||||
|
||||||||||||
await fulfillment(of: [cancelled], timeout: 0.3) | ||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is a neat test 🤩 |
||||||||||||
} | ||||||||||||
|
||||||||||||
} | ||||||||||||
|
||||||||||||
// These mock classes are copied from PostServiceWPComTests. We can't simply remove the `private` in the original class | ||||||||||||
|
@@ -265,7 +324,7 @@ private class PostServiceRESTMock: PostServiceRemoteREST { | |||||||||||
} | ||||||||||||
|
||||||||||||
var remotePostToReturnOnGetPostWithID: RemotePost? | ||||||||||||
var remotePostsToReturnOnSyncPostsOfType = [RemotePost]() | ||||||||||||
var remotePostsToReturnOnSyncPostsOfType = [[RemotePost]]() // Each element contains an array of RemotePost for one API request. | ||||||||||||
var remotePostToReturnOnUpdatePost: RemotePost? | ||||||||||||
var remotePostToReturnOnCreatePost: RemotePost? | ||||||||||||
|
||||||||||||
|
@@ -289,7 +348,15 @@ private class PostServiceRESTMock: PostServiceRemoteREST { | |||||||||||
} | ||||||||||||
|
||||||||||||
override func getPostsOfType(_ postType: String!, options: [AnyHashable: Any]! = [:], success: (([RemotePost]?) -> Void)!, failure: ((Error?) -> Void)!) { | ||||||||||||
success(self.remotePostsToReturnOnSyncPostsOfType) | ||||||||||||
guard !remotePostsToReturnOnSyncPostsOfType.isEmpty else { | ||||||||||||
failure(testError()) | ||||||||||||
return | ||||||||||||
} | ||||||||||||
|
||||||||||||
let result = remotePostsToReturnOnSyncPostsOfType.removeFirst() | ||||||||||||
DispatchQueue.main.asyncAfter(deadline: .now() + .microseconds(50)) { | ||||||||||||
success(result) | ||||||||||||
} | ||||||||||||
} | ||||||||||||
|
||||||||||||
override func update(_ post: RemotePost!, success: ((RemotePost?) -> Void)!, failure: ((Error?) -> Void)!) { | ||||||||||||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nitpick. There docs in the previous method has
@param
s. The new one doesn't. One could see this as a regression.At the same time, I personally prefer this style of documentation, which I interpret as "only document was is not obvious."
What do you think? Is it worth using this as an example to start a team-wide conversation to define a standard?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is exactly what I was thinking. I don't think those param docs in the original function is really useful.