diff --git a/README.md b/README.md
index 2a1ff1b..3214ddc 100644
--- a/README.md
+++ b/README.md
@@ -87,7 +87,7 @@ By incorporating robust offline capabilities, CleanAspire empowers developers to
version: '3.8'
services:
apiservice:
- image: blazordevlab/cleanaspire-api:0.0.49
+ image: blazordevlab/cleanaspire-api:0.0.50
environment:
- ASPNETCORE_ENVIRONMENT=Development
- AllowedHosts=*
@@ -108,7 +108,7 @@ services:
webfrontend:
- image: blazordevlab/cleanaspire-clientapp:0.0.49
+ image: blazordevlab/cleanaspire-clientapp:0.0.50
ports:
- "8016:80"
- "8017:443"
diff --git a/src/CleanAspire.Api/CleanAspire.Api.csproj b/src/CleanAspire.Api/CleanAspire.Api.csproj
index 83eb66c..187a1f4 100644
--- a/src/CleanAspire.Api/CleanAspire.Api.csproj
+++ b/src/CleanAspire.Api/CleanAspire.Api.csproj
@@ -16,7 +16,7 @@
-
+
diff --git a/src/CleanAspire.Application/Common/QueryableExtensions/QueryableExtensions.cs b/src/CleanAspire.Application/Common/QueryableExtensions/QueryableExtensions.cs
index 3160010..87698a6 100644
--- a/src/CleanAspire.Application/Common/QueryableExtensions/QueryableExtensions.cs
+++ b/src/CleanAspire.Application/Common/QueryableExtensions/QueryableExtensions.cs
@@ -70,7 +70,7 @@ public static async Task> ProjectToPaginatedDataAsync
-
+
diff --git a/src/CleanAspire.ClientApp/Pages/Products/Components/NewProductDialog.razor b/src/CleanAspire.ClientApp/Pages/Products/Components/NewProductDialog.razor
index 585ca50..fefdd87 100644
--- a/src/CleanAspire.ClientApp/Pages/Products/Components/NewProductDialog.razor
+++ b/src/CleanAspire.ClientApp/Pages/Products/Components/NewProductDialog.razor
@@ -78,9 +78,14 @@
MudDialog.Close(DialogResult.Ok(true));
_saving = false;
},
+ invalid =>
+ {
+ Snackbar.Add(invalid.Message ?? L["Failed validation"], Severity.Error);
+ _saving = false;
+ },
error =>
{
- Snackbar.Add(L["Failed to create product."], Severity.Error);
+ Snackbar.Add(error.Message ?? L["Failed to create product."], Severity.Error);
_saving = false;
}
);
diff --git a/src/CleanAspire.ClientApp/Pages/Products/Edit.razor b/src/CleanAspire.ClientApp/Pages/Products/Edit.razor
index 9bf3a03..c2dd9a2 100644
--- a/src/CleanAspire.ClientApp/Pages/Products/Edit.razor
+++ b/src/CleanAspire.ClientApp/Pages/Products/Edit.razor
@@ -102,17 +102,11 @@
}
private async Task Save()
{
- var online = await OnlineStatusInterop.GetOnlineStatusAsync();
- if (!online)
- {
- Snackbar.Add(L["You are offline. Please check your internet connection."], Severity.Error);
- return;
- }
editForm?.Validate();
if (success == true)
{
_saving = true;
- var result = await ApiClientService.ExecuteAsync(() => ApiClient.Products.PutAsync(model));
+ var result = await ProductServiceProxy.UpdateProductAsync(model);
_saving = result.Match(
ok =>
{
@@ -121,12 +115,12 @@
},
invalid =>
{
- Snackbar.Add(invalid.Message, Severity.Error);
+ Snackbar.Add(invalid.Message ?? L["Failed validation"], Severity.Error);
return false;
},
error =>
{
- Snackbar.Add(error.Message, Severity.Error);
+ Snackbar.Add(error.Message ?? L["Failed to save."], Severity.Error);
return false;
}
diff --git a/src/CleanAspire.ClientApp/Pages/Products/Index.razor b/src/CleanAspire.ClientApp/Pages/Products/Index.razor
index 8c9a9a8..83b8b17 100644
--- a/src/CleanAspire.ClientApp/Pages/Products/Index.razor
+++ b/src/CleanAspire.ClientApp/Pages/Products/Index.razor
@@ -120,12 +120,6 @@
}
private async Task Delete()
{
- var online = await OnlineStatusInterop.GetOnlineStatusAsync();
- if (!online)
- {
- Snackbar.Add(L["You are offline. Please check your internet connection."], Severity.Error);
- return;
- }
await DialogServiceHelper.ShowConfirmationDialog("delete confirm", L["Are you sure you want to delete the selected items?"], async () =>
{
if (_selectedItems.Any())
@@ -142,7 +136,7 @@
},
error =>
{
- Snackbar.Add(L["Failed to delete selected items."], Severity.Error);
+ Snackbar.Add(error.Message??L["Failed to delete selected items."], Severity.Error);
});
}
diff --git a/src/CleanAspire.ClientApp/Services/Proxies/ProductServiceProxy.cs b/src/CleanAspire.ClientApp/Services/Proxies/ProductServiceProxy.cs
index deb43fc..25b9ded 100644
--- a/src/CleanAspire.ClientApp/Services/Proxies/ProductServiceProxy.cs
+++ b/src/CleanAspire.ClientApp/Services/Proxies/ProductServiceProxy.cs
@@ -16,6 +16,8 @@ namespace CleanAspire.ClientApp.Services.Proxies;
public class ProductServiceProxy
{
private const string OFFLINECREATECOMMANDCACHEKEY = "OfflineCreateCommand:Product";
+ private const string OFFLINEUPDATECOMMANDCACHEKEY = "OfflineUpdateCommand:Product";
+ private const string OFFLINEDELETECOMMANDCACHEKEY = "OfflineDeleteCommand:Product";
private readonly NavigationManager _navigationManager;
private readonly WebpushrService _webpushrService;
private readonly ApiClient _apiClient;
@@ -120,7 +122,7 @@ public async Task> GetProductByIdAsync(s
}
}
- public async Task> CreateProductAsync(CreateProductCommand command)
+ public async Task> CreateProductAsync(CreateProductCommand command)
{
var isOnline = await _onlineStatusInterop.GetOnlineStatusAsync();
if (isOnline)
@@ -134,12 +136,24 @@ public async Task> CreateProductAsync(CreateProd
return response;
}
+ catch (HttpValidationProblemDetails ex)
+ {
+ return new ApiClientValidationError(ex.Detail, ex);
+ }
+ catch (ProblemDetails ex)
+ {
+ return new ApiClientError(ex.Detail, ex);
+ }
catch (ApiException ex)
{
- return ex;
+ return new ApiClientError(ex.Message, ex);
+ }
+ catch (Exception ex)
+ {
+ return new ApiClientError(ex.Message, ex);
}
}
- else
+ else if(_offlineModeState.Enabled)
{
var cachedCommands = await _indexedDbCache.GetDataAsync>(IndexedDbCache.DATABASENAME, OFFLINECREATECOMMANDCACHEKEY)
?? new List();
@@ -172,9 +186,80 @@ public async Task> CreateProductAsync(CreateProd
}
return productDto;
}
+ return new ApiClientError("Offline mode is disabled. Please enable offline mode to create products in offline mode.", new Exception("Offline mode is disabled."));
}
-
- public async Task> DeleteProductsAsync(List productIds)
+ public async Task> UpdateProductAsync(UpdateProductCommand command)
+ {
+ var isOnline = await _onlineStatusInterop.GetOnlineStatusAsync();
+ if (isOnline)
+ {
+ try
+ {
+ var response = await _apiClient.Products.PutAsync(command);
+ return true;
+ }
+ catch (HttpValidationProblemDetails ex)
+ {
+ return new ApiClientValidationError(ex.Detail, ex);
+ }
+ catch (ProblemDetails ex)
+ {
+ return new ApiClientError(ex.Detail, ex);
+ }
+ catch (ApiException ex)
+ {
+ return new ApiClientError(ex.Message, ex);
+ }
+ catch (Exception ex)
+ {
+ return new ApiClientError(ex.Message, ex);
+ }
+ }
+ else if (_offlineModeState.Enabled)
+ {
+ var cachedCommands = await _indexedDbCache.GetDataAsync>(IndexedDbCache.DATABASENAME, OFFLINEUPDATECOMMANDCACHEKEY)
+ ?? new List();
+ cachedCommands.Add(command);
+ await _indexedDbCache.SaveDataAsync(IndexedDbCache.DATABASENAME, OFFLINEUPDATECOMMANDCACHEKEY, cachedCommands, new[] { "product_commands" });
+ var productDto = new ProductDto()
+ {
+ Id = command.Id,
+ Category = command.Category,
+ Currency = command.Currency,
+ Description = command.Description,
+ Name = command.Name,
+ Price = command.Price,
+ Sku = command.Sku,
+ Uom = command.Uom
+ };
+ var productCacheKey = GenerateProductCacheKey(productDto.Id);
+ await _indexedDbCache.SaveDataAsync(IndexedDbCache.DATABASENAME, productCacheKey, productDto, new[] { "product" });
+ var cachedPaginatedProducts = await _indexedDbCache.GetDataByTagsAsync(IndexedDbCache.DATABASENAME, new[] { "products_pagination" });
+ if (cachedPaginatedProducts != null && cachedPaginatedProducts.Any())
+ {
+ foreach (var dic in cachedPaginatedProducts)
+ {
+ var key = dic.Key;
+ var paginatedProducts = dic.Value;
+ var item = paginatedProducts.Items.FirstOrDefault(x => x.Id == productDto.Id);
+ if (item != null)
+ {
+ item.Category = productDto.Category;
+ item.Currency = productDto.Currency;
+ item.Description = productDto.Description;
+ item.Name = productDto.Name;
+ item.Price = productDto.Price;
+ item.Sku = productDto.Sku;
+ item.Uom = productDto.Uom;
+ }
+ await _indexedDbCache.SaveDataAsync(IndexedDbCache.DATABASENAME, key, paginatedProducts, new[] { "products_pagination" });
+ }
+ }
+ return true;
+ }
+ return new ApiClientError("Offline mode is disabled. Please enable offline mode to update products in offline mode.", new Exception("Offline mode is disabled."));
+ }
+ public async Task> DeleteProductsAsync(List productIds)
{
var isOnline = await _onlineStatusInterop.GetOnlineStatusAsync();
if (isOnline)
@@ -182,48 +267,155 @@ public async Task> DeleteProductsAsync(List pro
try
{
await _apiClient.Products.DeleteAsync(new DeleteProductCommand() { Ids = productIds });
- await _indexedDbCache.DeleteDataByTagsAsync(IndexedDbCache.DATABASENAME, new[] { "products_pagination","product" });
+ await _indexedDbCache.DeleteDataByTagsAsync(IndexedDbCache.DATABASENAME, new[] { "products_pagination", "product" });
return true;
}
+ catch (ProblemDetails ex)
+ {
+ return new ApiClientError(ex.Detail, ex);
+ }
catch (ApiException ex)
{
- return ex;
+ return new ApiClientError(ex.Message, ex);
}
}
- return true;
+ else if (_offlineModeState.Enabled)
+ {
+ var cachedDeleteCommands = await _indexedDbCache
+ .GetDataAsync>(IndexedDbCache.DATABASENAME, OFFLINEDELETECOMMANDCACHEKEY)
+ ?? new List();
+
+ cachedDeleteCommands.Add(new DeleteProductCommand() { Ids = productIds });
+ await _indexedDbCache.SaveDataAsync(
+ IndexedDbCache.DATABASENAME,
+ OFFLINEDELETECOMMANDCACHEKEY,
+ cachedDeleteCommands,
+ new[] { "product_commands" }
+ );
+
+ foreach (var productId in productIds)
+ {
+ var productCacheKey = GenerateProductCacheKey(productId);
+ await _indexedDbCache.DeleteDataAsync(IndexedDbCache.DATABASENAME, productCacheKey);
+ }
+ var cachedPaginatedProducts = await _indexedDbCache
+ .GetDataByTagsAsync(IndexedDbCache.DATABASENAME, new[] { "products_pagination" });
+
+ if (cachedPaginatedProducts != null && cachedPaginatedProducts.Any())
+ {
+ foreach (var dic in cachedPaginatedProducts)
+ {
+ var key = dic.Key;
+ var paginatedProducts = dic.Value;
+ paginatedProducts.Items = paginatedProducts.Items
+ .Where(product => !productIds.Contains(product.Id))
+ .ToList();
+
+ paginatedProducts.TotalItems = paginatedProducts.Items.Count;
+ await _indexedDbCache.SaveDataAsync(
+ IndexedDbCache.DATABASENAME,
+ key,
+ paginatedProducts,
+ new[] { "products_pagination" }
+ );
+ }
+ }
+ return true;
+ }
+ return new ApiClientError("Offline mode is disabled. Please enable offline mode to delete products in offline mode.", new Exception("Offline mode is disabled."));
}
public async Task SyncOfflineCachedDataAsync()
{
var cachedCreateProductCommands = await _indexedDbCache.GetDataAsync>(
IndexedDbCache.DATABASENAME,
OFFLINECREATECOMMANDCACHEKEY);
- if (cachedCreateProductCommands != null && cachedCreateProductCommands.Any())
+ var cachedUpdateProductCommands = await _indexedDbCache.GetDataAsync>(
+ IndexedDbCache.DATABASENAME,
+ OFFLINEUPDATECOMMANDCACHEKEY);
+ var cachedDeleteProductCommands = await _indexedDbCache.GetDataAsync>(
+ IndexedDbCache.DATABASENAME,
+ OFFLINEDELETECOMMANDCACHEKEY);
+ var totalCount = (cachedCreateProductCommands?.Count ?? 0) + (cachedUpdateProductCommands?.Count ?? 0) + (cachedDeleteProductCommands?.Count ?? 0);
+ var processedCount = 0;
+ if (totalCount > 0)
{
- var count = cachedCreateProductCommands.Count;
- var processedCount = 0;
- _offlineSyncService.SetSyncStatus(SyncStatus.Syncing, $"Starting sync: 0/{count} ...", count, processedCount);
+ _offlineSyncService.SetSyncStatus(SyncStatus.Syncing, $"Starting sync: 0/{totalCount} ...", totalCount, processedCount);
await Task.Delay(500);
- foreach (var command in cachedCreateProductCommands)
+
+ if (cachedCreateProductCommands != null && cachedCreateProductCommands.Any())
{
- var result = await CreateProductAsync(command);
- result.Switch(
- productDto =>
- {
- processedCount++;
- _offlineSyncService.SetSyncStatus(SyncStatus.Syncing, $"Syncing {processedCount}/{count} Success.", count, processedCount);
- },
- apiException =>
- {
- processedCount++;
- _offlineSyncService.SetSyncStatus(SyncStatus.Syncing, $"Syncing {processedCount}/{count} Failed ({apiException.Message}).", count, processedCount);
- });
- await Task.Delay(500);
- }
- _offlineSyncService.SetSyncStatus(SyncStatus.Completed, $"Sync completed: {processedCount}/{count} processed.", count, processedCount);
- await Task.Delay(1200);
- await _indexedDbCache.DeleteDataAsync(IndexedDbCache.DATABASENAME, OFFLINECREATECOMMANDCACHEKEY);
- _offlineSyncService.SetSyncStatus(SyncStatus.Idle, "", 0, 0);
+ foreach (var command in cachedCreateProductCommands)
+ {
+ var result = await CreateProductAsync(command);
+ result.Switch(
+ productDto =>
+ {
+ processedCount++;
+ _offlineSyncService.SetSyncStatus(SyncStatus.Syncing, $"Syncing {processedCount}/{totalCount} Success.", totalCount, processedCount);
+ },
+ invalid =>
+ {
+ processedCount++;
+ _offlineSyncService.SetSyncStatus(SyncStatus.Syncing, $"Syncing {processedCount}/{totalCount} Failed ({invalid.Message}).", totalCount, processedCount);
+ },
+ error =>
+ {
+ processedCount++;
+ _offlineSyncService.SetSyncStatus(SyncStatus.Syncing, $"Syncing {processedCount}/{totalCount} Failed ({error.Message}).", totalCount, processedCount);
+ });
+ await Task.Delay(500);
+ }
+ await _indexedDbCache.DeleteDataAsync(IndexedDbCache.DATABASENAME, OFFLINECREATECOMMANDCACHEKEY);
+ }
+ if (cachedUpdateProductCommands != null && cachedUpdateProductCommands.Any())
+ {
+ foreach (var command in cachedUpdateProductCommands)
+ {
+ var result = await UpdateProductAsync(command);
+ result.Switch(
+ productDto =>
+ {
+ processedCount++;
+ _offlineSyncService.SetSyncStatus(SyncStatus.Syncing, $"Syncing {processedCount}/{totalCount} Success.", totalCount, processedCount);
+ },
+ invalid =>
+ {
+ processedCount++;
+ _offlineSyncService.SetSyncStatus(SyncStatus.Syncing, $"Syncing {processedCount}/{totalCount} Failed ({invalid.Message}).", totalCount, processedCount);
+ },
+ error =>
+ {
+ processedCount++;
+ _offlineSyncService.SetSyncStatus(SyncStatus.Syncing, $"Syncing {processedCount}/{totalCount} Failed ({error.Message}).", totalCount, processedCount);
+ });
+ await Task.Delay(500);
+ }
+ await _indexedDbCache.DeleteDataAsync(IndexedDbCache.DATABASENAME, OFFLINEUPDATECOMMANDCACHEKEY);
+ }
+ if (cachedDeleteProductCommands != null && cachedDeleteProductCommands.Any())
+ {
+ foreach (var command in cachedDeleteProductCommands)
+ {
+ var result = await DeleteProductsAsync(command.Ids);
+ result.Switch(
+ ok =>
+ {
+ processedCount++;
+ _offlineSyncService.SetSyncStatus(SyncStatus.Syncing, $"Syncing {processedCount}/{totalCount} Success.", totalCount, processedCount);
+ },
+ error =>
+ {
+ processedCount++;
+ _offlineSyncService.SetSyncStatus(SyncStatus.Syncing, $"Syncing {processedCount}/{totalCount} Failed ({error.Message}).", totalCount, processedCount);
+ });
+ await Task.Delay(500);
+ }
+ await _indexedDbCache.DeleteDataAsync(IndexedDbCache.DATABASENAME, OFFLINEDELETECOMMANDCACHEKEY);
+ }
}
+ _offlineSyncService.SetSyncStatus(SyncStatus.Completed, $"Sync completed: {processedCount}/{totalCount} processed.", totalCount, processedCount);
+ await Task.Delay(1200);
+ _offlineSyncService.SetSyncStatus(SyncStatus.Idle, "", 0, 0);
}
private string GeneratePaginationCacheKey(ProductsWithPaginationQuery query)
diff --git a/src/CleanAspire.ClientApp/wwwroot/appsettings.Development.json b/src/CleanAspire.ClientApp/wwwroot/appsettings.Development.json
index c32edd8..fa07868 100644
--- a/src/CleanAspire.ClientApp/wwwroot/appsettings.Development.json
+++ b/src/CleanAspire.ClientApp/wwwroot/appsettings.Development.json
@@ -7,7 +7,7 @@
},
"ClientAppSettings": {
"AppName": "Progressive Web Application",
- "Version": "v0.0.49",
+ "Version": "v0.0.50",
"ServiceBaseUrl": "https://localhost:7341"
}
}
diff --git a/src/CleanAspire.ClientApp/wwwroot/appsettings.json b/src/CleanAspire.ClientApp/wwwroot/appsettings.json
index 1b52b80..89d5b6b 100644
--- a/src/CleanAspire.ClientApp/wwwroot/appsettings.json
+++ b/src/CleanAspire.ClientApp/wwwroot/appsettings.json
@@ -7,7 +7,7 @@
},
"ClientAppSettings": {
"AppName": "Progressive Web Application",
- "Version": "v0.0.49",
+ "Version": "v0.0.50",
"ServiceBaseUrl": "https://apiservice.blazorserver.com"
}
}