-
Notifications
You must be signed in to change notification settings - Fork 117
Dynamic Dashboard: Reviews Card Reload Functionality #12752
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
Changes from 21 commits
40184d4
ff9c78b
37193af
b61d592
2eea667
a47f3bb
616b88b
42da0b8
12d09c8
12708f7
cb58f27
4e8571f
2b2151b
3246160
34094ec
4fa99a1
4ad2e89
23839f6
3d9286c
9aec7bc
382f342
62be5a9
702678f
ae19a09
6cb627b
37fc482
40bb44a
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 |
---|---|---|
|
@@ -19,30 +19,174 @@ final class ReviewsDashboardCardViewModel: ObservableObject { | |
case approved | ||
} | ||
|
||
@Published private(set) var data: [ReviewViewModel] = [] | ||
private var reviewProductIDs: [Int64] = [] | ||
selanthiraiyan marked this conversation as resolved.
Show resolved
Hide resolved
|
||
private var reviewProducts: [Product] = [] | ||
@Published private(set) var syncingData = false | ||
@Published private(set) var syncingError: Error? | ||
|
||
private let siteID: Int64 | ||
private let stores: StoresManager | ||
private let storage: StorageManagerType | ||
private let storageManager: StorageManagerType | ||
private let analytics: Analytics | ||
|
||
public let siteID: Int64 | ||
public let filters: [ReviewsFilter] = [.all, .hold, .approved] | ||
|
||
init(siteID: Int64, | ||
stores: StoresManager = ServiceLocator.stores, | ||
storage: StorageManagerType = ServiceLocator.storageManager, | ||
storageManager: StorageManagerType = ServiceLocator.storageManager, | ||
analytics: Analytics = ServiceLocator.analytics) { | ||
self.siteID = siteID | ||
self.stores = stores | ||
self.storage = storage | ||
self.storageManager = storageManager | ||
self.analytics = analytics | ||
|
||
configureProductReviewsResultsController() | ||
} | ||
|
||
/// ResultsController for ProductReview | ||
private lazy var productReviewsResultsController: ResultsController<StorageProductReview> = { | ||
let sortDescriptor = NSSortDescriptor(keyPath: \StorageProductReview.dateCreated, ascending: false) | ||
return ResultsController<StorageProductReview>(storageManager: storageManager, | ||
matching: sitePredicate(), | ||
fetchLimit: Constants.numberOfItems, | ||
sortedBy: [sortDescriptor]) | ||
}() | ||
|
||
/// ResultsController for Product | ||
private lazy var productsResultsController: ResultsController<StorageProduct> = { | ||
let predicate = NSPredicate(format: "siteID == %lld AND productID IN %@", siteID, reviewProductIDs) | ||
selanthiraiyan marked this conversation as resolved.
Show resolved
Hide resolved
|
||
return ResultsController<StorageProduct>(storageManager: storageManager, matching: predicate, sortedBy: []) | ||
}() | ||
|
||
func dismissReviews() { | ||
// TODO: add tracking | ||
onDismiss?() | ||
} | ||
|
||
@MainActor | ||
func reloadData() async { | ||
syncingData = true | ||
syncingError = nil | ||
do { | ||
// Ignoring the result from remote as we're using storage as the single source of truth | ||
_ = try await loadReviews() | ||
} catch { | ||
syncingError = error | ||
} | ||
syncingData = false | ||
} | ||
} | ||
|
||
// MARK: - Private helpers | ||
private extension ReviewsDashboardCardViewModel { | ||
/// Predicate to entities that belong to the current store | ||
/// | ||
func sitePredicate() -> NSPredicate { | ||
return NSPredicate(format: "siteID == %lld", siteID) | ||
} | ||
|
||
@MainActor | ||
func loadReviews() async throws -> [ProductReview] { | ||
try await withCheckedThrowingContinuation { continuation in | ||
stores.dispatch(ProductReviewAction.synchronizeProductReviews(siteID: siteID, | ||
pageNumber: 1, | ||
pageSize: Constants.numberOfItems) { result in | ||
continuation.resume(with: result) | ||
}) | ||
} | ||
} | ||
|
||
@MainActor | ||
func retrieveProducts(for reviewProductIDs: [Int64]) async throws -> (products: [Product], hasNextPage: Bool) { | ||
try await withCheckedThrowingContinuation { continuation in | ||
stores.dispatch(ProductAction.retrieveProducts(siteID: siteID, | ||
productIDs: reviewProductIDs | ||
) { result in | ||
continuation.resume(with: result) | ||
}) | ||
} | ||
} | ||
|
||
/// Performs initial fetch from storage and updates results. | ||
func configureProductReviewsResultsController() { | ||
productReviewsResultsController.onDidChangeContent = { [weak self] in | ||
guard let self else { return } | ||
Task { | ||
await self.updateReviewsResults() | ||
} | ||
} | ||
productReviewsResultsController.onDidResetContent = { [weak self] in | ||
guard let self else { return } | ||
Task { | ||
await self.updateReviewsResults() | ||
} | ||
} | ||
|
||
do { | ||
try productReviewsResultsController.performFetch() | ||
} catch { | ||
ServiceLocator.crashLogging.logError(error) | ||
} | ||
} | ||
|
||
/// Updates data | ||
@MainActor | ||
func updateReviewsResults() async { | ||
let reviews = productReviewsResultsController.fetchedObjects.prefix(Constants.numberOfItems) | ||
reviewProductIDs = reviews.map { $0.productID } | ||
|
||
// Load products that matches the review product IDs | ||
if reviewProductIDs.isNotEmpty { | ||
do { | ||
try await fetchProducts() | ||
} catch { | ||
ServiceLocator.crashLogging.logError(error) | ||
} | ||
} | ||
|
||
data = reviews | ||
.map { review in | ||
let product = reviewProducts.first { $0.productID == review.productID } | ||
// TODO: also fetch notification | ||
return ReviewViewModel(review: review, product: product, notification: nil) | ||
} | ||
} | ||
|
||
/// Get products from storage if available, if not then fetch remotely. | ||
/// | ||
@MainActor | ||
private func fetchProducts() async throws { | ||
// Prefer to fetch locally first, if not available then fetch remotely | ||
try productsResultsController.performFetch() | ||
reviewProducts = productsResultsController.fetchedObjects | ||
|
||
if reviewProducts.isEmpty { | ||
selanthiraiyan marked this conversation as resolved.
Show resolved
Hide resolved
|
||
await loadReviewProducts(for: reviewProductIDs) | ||
try productsResultsController.performFetch() | ||
reviewProducts = productsResultsController.fetchedObjects | ||
} | ||
} | ||
|
||
@MainActor | ||
func loadReviewProducts(for reviewProductIDs: [Int64]) async { | ||
syncingData = true | ||
syncingError = nil | ||
|
||
do { | ||
// Ignoring the result from remote as we're using storage as the single source of truth | ||
_ = try await retrieveProducts(for: reviewProductIDs) | ||
} catch { | ||
syncingError = error | ||
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. Suggestion: I believe that we can display the reviews even if retrieving products fails. Could we ignore the error and just display the reviews without product info? If we think about it further, we could even load the products silently in the background and update the UI. What do you think? 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 suggestion and I can try adding it on the next PR that deals with loading states. I do notice that it can take a while waiting for products and notifications to be fetched, and showing partial result first can be more useful to users. 👍🏼 |
||
} | ||
syncingData = false | ||
} | ||
} | ||
|
||
private extension ReviewsDashboardCardViewModel { | ||
enum Constants { | ||
static let numberOfItems = 3 | ||
} | ||
} | ||
|
||
extension ReviewsDashboardCardViewModel.ReviewsFilter { | ||
|
Uh oh!
There was an error while loading. Please reload this page.