feat: enhance category selection in ProductFormDialog with multi-selection text and improved error handling
Build and Deploy / build (push) Failing after 16s

This commit is contained in:
masoodafar-web
2026-01-05 21:51:39 +03:30
parent 31226399ae
commit 21ee004869
2 changed files with 285 additions and 0 deletions
+274
View File
@@ -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"
Margin="Margin.Dense"
MultiSelection="true"
MultiSelectionTextFunc="@(new Func<List<string>, string>(GetMultiSelectionText))"
Disabled="_loading"
T="long">
@foreach (var category in _categories)
@@ -193,9 +194,11 @@
{
var tree = await CategoryService.GetCategoriesAsync(isActive: true);
_categories = FlattenCategories(tree);
Console.WriteLine($"Loaded {_categories.Count} categories");
}
catch (Exception ex)
{
Console.WriteLine($"Error loading categories: {ex}");
Snackbar.Add($"خطا در بارگذاری دسته‌بندی‌ها: {ex.Message}", Severity.Error);
}
}
@@ -226,6 +229,14 @@
return category.Title;
}
private string GetMultiSelectionText(List<string> selectedValues)
{
if (selectedValues == null || !selectedValues.Any())
return "دسته‌بندی انتخاب نشده";
return $"{selectedValues.Count} دسته‌بندی انتخاب شده";
}
private async Task OnMainImageSelected(IBrowserFile? file)
{
_mainImageFile = file;