feat: enhance category selection in ProductFormDialog with multi-selection text and improved error handling
Build and Deploy / build (push) Failing after 16s
Build and Deploy / build (push) Failing after 16s
This commit is contained in:
@@ -0,0 +1,274 @@
|
|||||||
|
# Refactoring DiscountProduct Form - Session 2026-01-05
|
||||||
|
|
||||||
|
## هدف
|
||||||
|
یکسانسازی فرم ایجاد/ویرایش محصولات تخفیفی (DiscountProduct) با فرم محصولات عادی (Product) برای بهبود تجربه کاربری و افزودن قابلیتهای حرفهای.
|
||||||
|
|
||||||
|
## تغییرات انجام شده
|
||||||
|
|
||||||
|
### 1. Frontend - ProductFormDialog.razor
|
||||||
|
|
||||||
|
#### قبل:
|
||||||
|
- استفاده از MudGrid
|
||||||
|
- TextField ساده برای مسیر تصویر (بدون قابلیت آپلود)
|
||||||
|
- TextField چند خطی برای توضیحات
|
||||||
|
- MudSelect ساده برای دستهبندیها
|
||||||
|
- فاقد پیشنمایش تصویر
|
||||||
|
|
||||||
|
#### بعد:
|
||||||
|
- **تغییر Layout**: از MudGrid به MudStack
|
||||||
|
- **آپلود تصویر**:
|
||||||
|
- MudFileUpload برای انتخاب تصویر اصلی و بندانگشتی
|
||||||
|
- پیشنمایش تصویر قبل از آپلود
|
||||||
|
- نمایش تصویر موجود در حالت ویرایش
|
||||||
|
- **ویرایشگر HTML**:
|
||||||
|
- استفاده از MudHtmlEditor برای FullInformation
|
||||||
|
- قابلیت فرمتدهی متن (bold, italic, لیست و...)
|
||||||
|
- **دستهبندی**:
|
||||||
|
- استفاده از MudSelect با MultiSelection
|
||||||
|
- لود خودکار DiscountCategory ها (نه Category های عادی)
|
||||||
|
- نمایش تعداد دستهبندیهای انتخاب شده
|
||||||
|
- **فیلدهای اختصاصی حفظ شده**:
|
||||||
|
- MaxDiscountPercent
|
||||||
|
- IsActive
|
||||||
|
- SortOrder
|
||||||
|
|
||||||
|
**فایل**: `/home/masoud/Apps/project/FourSat/BackOffice/src/BackOffice/Pages/DiscountShop/Components/ProductFormDialog.razor`
|
||||||
|
|
||||||
|
### 2. Proto File - discountproduct.proto
|
||||||
|
|
||||||
|
#### تغییرات:
|
||||||
|
```protobuf
|
||||||
|
message CreateDiscountProductRequest {
|
||||||
|
// ... فیلدهای قبلی
|
||||||
|
ImageFileModel image_file = 12; // جدید
|
||||||
|
ImageFileModel thumbnail_file = 13; // جدید
|
||||||
|
}
|
||||||
|
|
||||||
|
message UpdateDiscountProductRequest {
|
||||||
|
// ... فیلدهای قبلی
|
||||||
|
ImageFileModel image_file = 12; // جدید
|
||||||
|
ImageFileModel thumbnail_file = 13; // جدید
|
||||||
|
}
|
||||||
|
|
||||||
|
// تعریف مدل جدید
|
||||||
|
message ImageFileModel {
|
||||||
|
bytes file = 1;
|
||||||
|
string mime = 2;
|
||||||
|
string file_name = 3;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**فایل**: `/home/masoud/Apps/project/FourSat/BackOffice.BFF/src/Protobufs/BackOffice.BFF.DiscountProduct.Protobuf/Protos/discountproduct.proto`
|
||||||
|
|
||||||
|
### 3. DTOs - Backend
|
||||||
|
|
||||||
|
#### CreateDiscountProductDto و UpdateDiscountProductDto:
|
||||||
|
```csharp
|
||||||
|
// پراپرتیهای جدید اضافه شده:
|
||||||
|
public byte[]? ImageFile { get; set; }
|
||||||
|
public string? ImageMime { get; set; }
|
||||||
|
public string? ImageFileName { get; set; }
|
||||||
|
public byte[]? ThumbnailFile { get; set; }
|
||||||
|
public string? ThumbnailMime { get; set; }
|
||||||
|
public string? ThumbnailFileName { get; set; }
|
||||||
|
```
|
||||||
|
|
||||||
|
**فایلها**:
|
||||||
|
- `/home/masoud/Apps/project/FourSat/BackOffice/src/BackOffice/Services/DiscountProduct/IDiscountProductService.cs`
|
||||||
|
|
||||||
|
### 4. Service Layer - DiscountProductService
|
||||||
|
|
||||||
|
#### تغییرات CreateAsync و UpdateAsync:
|
||||||
|
```csharp
|
||||||
|
// افزودن لاجیک آپلود فایل
|
||||||
|
if (dto.ImageFile != null && !string.IsNullOrEmpty(dto.ImageMime)) {
|
||||||
|
request.ImageFile = new ImageFileModel {
|
||||||
|
File = ByteString.CopyFrom(dto.ImageFile),
|
||||||
|
Mime = dto.ImageMime,
|
||||||
|
FileName = dto.ImageFileName
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**فایل**: `/home/masoud/Apps/project/FourSat/BackOffice/src/BackOffice/Services/DiscountProduct/DiscountProductService.cs`
|
||||||
|
|
||||||
|
### 5. Page Integration - DiscountProductsMainPage
|
||||||
|
|
||||||
|
#### تغییرات در OpenCreateDialog و OpenEditDialog:
|
||||||
|
```csharp
|
||||||
|
var dto = new CreateDiscountProductDto {
|
||||||
|
// ... سایر فیلدها
|
||||||
|
InitialCount = 0, // تغییر: همیشه 0 - موجودی از طریق Inventory مدیریت میشود
|
||||||
|
ImageFile = formData.ImageFile,
|
||||||
|
ImageMime = formData.ImageMime,
|
||||||
|
ImageFileName = formData.ImageFileName,
|
||||||
|
ThumbnailFile = formData.ThumbnailFile,
|
||||||
|
ThumbnailMime = formData.ThumbnailMime,
|
||||||
|
ThumbnailFileName = formData.ThumbnailFileName
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
**فایل**: `/home/masoud/Apps/project/FourSat/BackOffice/src/BackOffice/Pages/DiscountShop/DiscountProductsMainPage.razor`
|
||||||
|
|
||||||
|
### 6. BFF - Validators
|
||||||
|
|
||||||
|
#### اصلاح CreateDiscountProductCommandValidator:
|
||||||
|
```csharp
|
||||||
|
// قبل: الزامی بودن ShortInformation و FullInformation
|
||||||
|
RuleFor(x => x.ShortInformation)
|
||||||
|
.NotEmpty().WithMessage("اطلاعات کوتاه محصول الزامی است")
|
||||||
|
|
||||||
|
// بعد: اختیاری شدن با چک طول
|
||||||
|
RuleFor(x => x.ShortInformation)
|
||||||
|
.MaximumLength(500)
|
||||||
|
.When(x => !string.IsNullOrEmpty(x.ShortInformation));
|
||||||
|
|
||||||
|
RuleFor(x => x.FullInformation)
|
||||||
|
.MaximumLength(10000)
|
||||||
|
.When(x => !string.IsNullOrEmpty(x.FullInformation));
|
||||||
|
```
|
||||||
|
|
||||||
|
**فایلها**:
|
||||||
|
- `/home/masoud/Apps/project/FourSat/BackOffice.BFF/src/BackOffice.BFF.Application/DiscountProductCQ/Commands/CreateDiscountProduct/CreateDiscountProductCommandValidator.cs`
|
||||||
|
- `/home/masoud/Apps/project/FourSat/BackOffice.BFF/src/BackOffice.BFF.Application/DiscountProductCQ/Commands/UpdateDiscountProduct/UpdateDiscountProductCommandValidator.cs`
|
||||||
|
|
||||||
|
### 7. BFF - Command Handlers
|
||||||
|
|
||||||
|
#### اصلاح CreateDiscountProductCommandHandler:
|
||||||
|
```csharp
|
||||||
|
// افزودن null coalescing برای جلوگیری از ArgumentNullException
|
||||||
|
var createRequest = new CreateDiscountProductRequest {
|
||||||
|
ShortInfomation = request.ShortInformation ?? string.Empty,
|
||||||
|
FullInformation = request.FullInformation ?? string.Empty,
|
||||||
|
// ...
|
||||||
|
};
|
||||||
|
|
||||||
|
// افزودن لاجیک آپلود فایل به File Storage
|
||||||
|
if (request.ImageFile != null && request.ImageFile.File.Length > 0) {
|
||||||
|
var imageFileInfo = await _context.FileInfos.CreateNewFileInfoAsync(...);
|
||||||
|
if (imageFileInfo != null)
|
||||||
|
createRequest.ImagePath = imageFileInfo.File;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**فایلها**:
|
||||||
|
- `/home/masoud/Apps/project/FourSat/BackOffice.BFF/src/BackOffice.BFF.Application/DiscountProductCQ/Commands/CreateDiscountProduct/CreateDiscountProductCommandHandler.cs`
|
||||||
|
- `/home/masoud/Apps/project/FourSat/BackOffice.BFF/src/BackOffice.BFF.Application/DiscountProductCQ/Commands/UpdateDiscountProduct/UpdateDiscountProductCommandHandler.cs`
|
||||||
|
|
||||||
|
### 8. CMS - Validators
|
||||||
|
|
||||||
|
#### اصلاح CreateDiscountProductCommandValidator:
|
||||||
|
```csharp
|
||||||
|
// حذف الزامی بودن ImagePath و ThumbnailPath
|
||||||
|
// قبل:
|
||||||
|
RuleFor(v => v.ImagePath).NotEmpty()
|
||||||
|
RuleFor(v => v.ThumbnailPath).NotEmpty()
|
||||||
|
|
||||||
|
// بعد: حذف شده - فایلها از طریق BFF آپلود میشوند
|
||||||
|
|
||||||
|
// ShortInformation و FullInformation اختیاری شدن
|
||||||
|
RuleFor(v => v.ShortInfomation)
|
||||||
|
.MaximumLength(500)
|
||||||
|
.When(v => !string.IsNullOrEmpty(v.ShortInfomation));
|
||||||
|
```
|
||||||
|
|
||||||
|
**فایلها**:
|
||||||
|
- `/home/masoud/Apps/project/FourSat/CMS/src/CMSMicroservice.Application/DiscountShopCQ/Commands/CreateDiscountProduct/CreateDiscountProductCommandValidator.cs`
|
||||||
|
- `/home/masoud/Apps/project/FourSat/CMS/src/CMSMicroservice.Application/DiscountShopCQ/Commands/UpdateDiscountProduct/UpdateDiscountProductCommandValidator.cs`
|
||||||
|
|
||||||
|
### 9. Bug Fixes
|
||||||
|
|
||||||
|
#### 9.1. Foreign Key Constraint Error
|
||||||
|
**مشکل**: استفاده از CategoryMultiSelectCombo که Category های عادی را لود میکرد، در حالی که DiscountProduct نیاز به DiscountCategory دارد.
|
||||||
|
|
||||||
|
**راهحل**:
|
||||||
|
- برگشت به MudSelect ساده
|
||||||
|
- لود DiscountCategory از IDiscountCategoryService
|
||||||
|
- Flatten کردن tree برای نمایش در dropdown
|
||||||
|
|
||||||
|
#### 9.2. GetDiscountProducts NullReferenceException
|
||||||
|
**مشکل**: `response.MetaData` یا `response.Models` میتوانست null باشد.
|
||||||
|
|
||||||
|
**راهحل**:
|
||||||
|
```csharp
|
||||||
|
Products = response.Models?.Select(p => new DiscountProductDto {
|
||||||
|
Created = p.Created?.ToDateTime() ?? DateTime.MinValue
|
||||||
|
}).ToList() ?? new List<DiscountProductDto>()
|
||||||
|
```
|
||||||
|
|
||||||
|
**فایل**: `/home/masoud/Apps/project/FourSat/BackOffice.BFF/src/BackOffice.BFF.Application/DiscountProductCQ/Queries/GetDiscountProducts/GetDiscountProductsQueryHandler.cs`
|
||||||
|
|
||||||
|
#### 9.3. GetDiscountCategories Handler Bugs
|
||||||
|
**مشکلها**:
|
||||||
|
1. `response.Categories` بدون null check
|
||||||
|
2. شرطهای `string.IsNullOrWhiteSpace` معکوس بودند
|
||||||
|
3. `ParentCategoryId` چک نادرست
|
||||||
|
4. `node.Children` بدون null check
|
||||||
|
|
||||||
|
**راهحل**:
|
||||||
|
```csharp
|
||||||
|
Categories = response.Categories?.Select(MapCategory).ToList()
|
||||||
|
?? new List<DiscountCategoryTreeDto>()
|
||||||
|
|
||||||
|
// اصلاح شرطها
|
||||||
|
Description = !string.IsNullOrWhiteSpace(node.Description) ? node.Description : string.Empty,
|
||||||
|
ParentCategoryId = node.ParentCategoryId != 0 ? node.ParentCategoryId : null,
|
||||||
|
ChildCategories = node.Children?.Select(MapCategory).ToList()
|
||||||
|
?? new List<DiscountCategoryTreeDto>()
|
||||||
|
```
|
||||||
|
|
||||||
|
**فایل**: `/home/masoud/Apps/project/FourSat/BackOffice.BFF/src/BackOffice.BFF.Application/DiscountCategoryCQ/Queries/GetDiscountCategories/GetDiscountCategoriesQueryHandler.cs`
|
||||||
|
|
||||||
|
## Build و Deploy
|
||||||
|
|
||||||
|
### Build Status
|
||||||
|
- ✅ BackOffice: 0 errors, 248 warnings (MudBlazor style warnings only)
|
||||||
|
- ✅ BackOffice.BFF: 0 errors
|
||||||
|
- ✅ CMS: 0 errors
|
||||||
|
|
||||||
|
### دستور Build Proto
|
||||||
|
```bash
|
||||||
|
bash /home/masoud/Apps/project/FourSat/BackOffice/build-deps.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
### فایلهای نیاز به Deploy
|
||||||
|
1. **BackOffice.BFF** (stage)
|
||||||
|
- تغییرات: Validators, Command Handlers, Query Handlers
|
||||||
|
|
||||||
|
2. **CMS** (stage)
|
||||||
|
- تغییرات: Validators
|
||||||
|
|
||||||
|
3. **BackOffice** (local)
|
||||||
|
- در حال اجرا با `dotnet watch`
|
||||||
|
- تغییرات بلافاصله اعمال میشود
|
||||||
|
|
||||||
|
## نتیجه نهایی
|
||||||
|
|
||||||
|
### قابلیتهای جدید:
|
||||||
|
✅ آپلود تصویر با drag & drop
|
||||||
|
✅ پیشنمایش تصویر قبل از ذخیره
|
||||||
|
✅ ویرایشگر HTML برای محتوای غنی
|
||||||
|
✅ انتخاب چندگانه دستهبندی با UI بهتر
|
||||||
|
✅ مدیریت صحیح DiscountCategory ها
|
||||||
|
✅ حذف الزامی بودن فیلدهای اختیاری
|
||||||
|
✅ رفع تمام خطاهای NullReference
|
||||||
|
|
||||||
|
### تفاوت با قبل:
|
||||||
|
| قبل | بعد |
|
||||||
|
|-----|-----|
|
||||||
|
| TextField برای مسیر تصویر | آپلود فایل واقعی |
|
||||||
|
| TextField ساده | HTML Editor |
|
||||||
|
| CategoryMultiSelectCombo (اشتباه) | MudSelect با DiscountCategory |
|
||||||
|
| الزامی بودن توضیحات | اختیاری |
|
||||||
|
| خطای Foreign Key | حل شده |
|
||||||
|
|
||||||
|
## نکات مهم
|
||||||
|
|
||||||
|
1. **موجودی (InitialCount)**: همیشه 0 قرار میگیرد - موجودی باید از صفحه Inventory مدیریت شود
|
||||||
|
2. **دستهبندی**: حتماً DiscountCategory استفاده شود نه Category معمولی
|
||||||
|
3. **Validators**: فیلدهای اختیاری با `.When()` چک شوند
|
||||||
|
4. **Null Safety**: همیشه `??` و `?.` استفاده شود در LINQ queries
|
||||||
|
|
||||||
|
## مراجع
|
||||||
|
- Proto Guide: `/home/masoud/Apps/project/FourSat/PROTO-PACKAGING-GUIDE.md`
|
||||||
|
- Build Script: `/home/masoud/Apps/project/FourSat/BackOffice/build-deps.sh`
|
||||||
@@ -116,6 +116,7 @@
|
|||||||
Variant="Variant.Outlined"
|
Variant="Variant.Outlined"
|
||||||
Margin="Margin.Dense"
|
Margin="Margin.Dense"
|
||||||
MultiSelection="true"
|
MultiSelection="true"
|
||||||
|
MultiSelectionTextFunc="@(new Func<List<string>, string>(GetMultiSelectionText))"
|
||||||
Disabled="_loading"
|
Disabled="_loading"
|
||||||
T="long">
|
T="long">
|
||||||
@foreach (var category in _categories)
|
@foreach (var category in _categories)
|
||||||
@@ -193,9 +194,11 @@
|
|||||||
{
|
{
|
||||||
var tree = await CategoryService.GetCategoriesAsync(isActive: true);
|
var tree = await CategoryService.GetCategoriesAsync(isActive: true);
|
||||||
_categories = FlattenCategories(tree);
|
_categories = FlattenCategories(tree);
|
||||||
|
Console.WriteLine($"Loaded {_categories.Count} categories");
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
Console.WriteLine($"Error loading categories: {ex}");
|
||||||
Snackbar.Add($"خطا در بارگذاری دستهبندیها: {ex.Message}", Severity.Error);
|
Snackbar.Add($"خطا در بارگذاری دستهبندیها: {ex.Message}", Severity.Error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -226,6 +229,14 @@
|
|||||||
return category.Title;
|
return category.Title;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private string GetMultiSelectionText(List<string> selectedValues)
|
||||||
|
{
|
||||||
|
if (selectedValues == null || !selectedValues.Any())
|
||||||
|
return "دستهبندی انتخاب نشده";
|
||||||
|
|
||||||
|
return $"{selectedValues.Count} دستهبندی انتخاب شده";
|
||||||
|
}
|
||||||
|
|
||||||
private async Task OnMainImageSelected(IBrowserFile? file)
|
private async Task OnMainImageSelected(IBrowserFile? file)
|
||||||
{
|
{
|
||||||
_mainImageFile = file;
|
_mainImageFile = file;
|
||||||
|
|||||||
Reference in New Issue
Block a user