feat: Implement caching for selectable products and enhance product filtering in admin panels
Test Suite / test (24.15.0) (push) Has been cancelled

This commit is contained in:
Nils-Johan Gynther
2026-05-11 18:42:35 +02:00
parent d75fd00666
commit a4f65c6065
10 changed files with 481 additions and 79 deletions
@@ -21,9 +21,18 @@ final adminRepositoryProvider = Provider<AdminRepository>((ref) {
class AdminRepository {
final ApiClient _apiClient;
final Ref _ref;
List<AdminProduct>? _selectableProductsCache;
DateTime? _selectableProductsCacheAt;
static const Duration _selectableProductsCacheTtl = Duration(seconds: 45);
AdminRepository(this._apiClient, this._ref);
void _invalidateSelectableProductsCache() {
_selectableProductsCache = null;
_selectableProductsCacheAt = null;
}
// ── Interna helpers ────────────────────────────────────────────────────────
Future<String?> _token() => _ref.read(authStateProvider.future);
@@ -202,6 +211,50 @@ class AdminRepository {
Future<List<PendingProduct>> listPrivateProducts() =>
_getList(ProductApiPaths.privateList, PendingProduct.fromJson);
Future<List<AdminProduct>> listSelectableProductsForAdmin({
bool forceRefresh = false,
}) async {
final now = DateTime.now();
final cached = _selectableProductsCache;
final cacheAt = _selectableProductsCacheAt;
if (!forceRefresh && cached != null && cacheAt != null) {
if (now.difference(cacheAt) <= _selectableProductsCacheTtl) {
return List<AdminProduct>.from(cached);
}
}
final results = await Future.wait<dynamic>([
listProducts(),
listPrivateProducts(),
]);
final globalProducts = results[0] as List<AdminProduct>;
final privateProducts = results[1] as List<PendingProduct>;
final merged = <int, AdminProduct>{
for (final product in globalProducts) product.id: product,
};
for (final product in privateProducts) {
merged[product.id] = AdminProduct(
id: product.id,
name: product.name,
canonicalName: product.canonicalName,
ownerId: product.ownerId,
categoryId: product.categoryId,
categoryPath: product.categoryPath,
status: 'private',
);
}
final list = merged.values.toList();
list.sort(
(a, b) => a.displayName.toLowerCase().compareTo(b.displayName.toLowerCase()),
);
_selectableProductsCache = List<AdminProduct>.from(list);
_selectableProductsCacheAt = now;
return list;
}
Future<List<AdminProduct>> listDeletedProducts() =>
_getList(ProductApiPaths.deleted, AdminProduct.fromJson);
@@ -209,36 +262,51 @@ class AdminRepository {
_getList(ProductApiPaths.pending, PendingProduct.fromJson);
Future<void> setProductStatus(int productId, String status) =>
_patchVoid(ProductApiPaths.setStatus(productId), {'status': status});
_patchVoid(ProductApiPaths.setStatus(productId), {'status': status}).then((_) {
_invalidateSelectableProductsCache();
});
Future<AdminProduct> promotePrivateProduct(int productId) =>
_post<AdminProduct>(
ProductApiPaths.promotePrivate(productId),
body: null,
parse: (d) => AdminProduct.fromJson(Map<String, dynamic>.from(d as Map)),
);
).then((value) {
_invalidateSelectableProductsCache();
return value;
});
Future<void> setProductCategory(int productId, {required int? categoryId}) =>
_patchVoid(ProductApiPaths.update(productId), {'categoryId': categoryId});
_patchVoid(ProductApiPaths.update(productId), {'categoryId': categoryId}).then((_) {
_invalidateSelectableProductsCache();
});
Future<void> removeProduct(int productId) =>
_deleteVoid(ProductApiPaths.remove(productId));
_deleteVoid(ProductApiPaths.remove(productId)).then((_) {
_invalidateSelectableProductsCache();
});
Future<void> restoreProduct(int productId) =>
_postVoid(ProductApiPaths.restore(productId));
_postVoid(ProductApiPaths.restore(productId)).then((_) {
_invalidateSelectableProductsCache();
});
// ── Product canonical name updates ────────────────────────────────────────
Future<void> updateCanonicalName(int productId, String canonicalName) =>
_patchVoid(
ProductApiPaths.canonicalName(productId),
{'canonicalName': canonicalName.trim()},
);
).then((_) {
_invalidateSelectableProductsCache();
});
Future<void> updateCanonicalNamePrivate(int productId, String canonicalName) =>
_patchVoid(
ProductApiPaths.canonicalNamePrivate(productId),
{'canonicalName': canonicalName.trim()},
);
).then((_) {
_invalidateSelectableProductsCache();
});
// ── Product merging ────────────────────────────────────────────────────────
Future<void> mergeProductsPrivate({
@@ -248,6 +316,8 @@ class AdminRepository {
_postVoid(ProductApiPaths.mergePrivate, {
'sourceProductId': sourceProductId,
'targetProductId': targetProductId,
}).then((_) {
_invalidateSelectableProductsCache();
});
/// Skapar en ny aktiv produkt (kräver admin). Returnerar `{id, name, categoryId?}`.
@@ -258,7 +328,10 @@ class AdminRepository {
'name': name.trim(),
if (categoryId != null) 'categoryId': categoryId,
},
);
).then((value) {
_invalidateSelectableProductsCache();
return value;
});
int _parseUpdatedCount(dynamic data) {
if (data is! Map) {
@@ -274,7 +347,10 @@ class AdminRepository {
ProductApiPaths.bulkUpdate,
body: {'ids': ids, 'categoryId': categoryId},
parse: _parseUpdatedCount,
);
).then((value) {
_invalidateSelectableProductsCache();
return value;
});
Future<void> mergeProducts({
required int sourceProductId,
@@ -283,6 +359,8 @@ class AdminRepository {
_postVoid(ProductApiPaths.merge, {
'sourceProductId': sourceProductId,
'targetProductId': targetProductId,
}).then((_) {
_invalidateSelectableProductsCache();
});
Future<Map<String, dynamic>> previewMerge({