Devexpress Grid kısmına Rating komponenti eklendi

This commit is contained in:
Sedat ÖZTÜRK 2026-10-09 22:40:51 +03:00
parent f341730b4a
commit f44308756c
33 changed files with 496 additions and 238 deletions

View file

@ -370,6 +370,7 @@ dörtlüyle üretilir (§9.8), görseldeki görünüm `DefaultLayout` olur.
| Renk kutucuğu | `dxColorBox` | |
| Kalın/italik araç çubuklu alan | `dxHtmlEditor` | `toolbar.items` |
| Kaydırıcı | `dxSlider` / `dxRangeSlider` | `min`, `max` |
| Yıldızlı puan (1–5) | `dxRating`, kolon `int` | `count`, `allowClear` |
| Fotoğraf / görsel alanı (seçilen, çekilen ya da başka kayıttan kopyalanan) | `dxImageUpload`, kolon `nvarchar(MAX)` (base64); salt okunur gösterilecekse `EditorOptions: {"readOnly": true}` — `dxImageViewer` değil | `accept`, `maxFileSize`, `capture` |
| Gri, tıklanamaz alan | ilgili editör + `EditorOptions: {"readOnly": true}` | |
| Kırmızı yıldızlı etiket | `IsRequired: true` | |
@ -1072,12 +1073,12 @@ yazılır:
| 25 | Xml | `xml` |
| 27 | DateTimeOffset | `datetimeoffset` |
**Editör tipleri (`EditorType2`)** — 21 tip, alana **metin** olarak yazılır. DevExtreme'den
**Editör tipleri (`EditorType2`)** — 22 tip, alana **metin** olarak yazılır. DevExtreme'den
gelenler: `dxAutocomplete`, `dxCalendar`, `dxCheckBox`, `dxColorBox`, `dxDateBox`,
`dxDateRangeBox`, `dxDropDownBox`, `dxHtmlEditor`, `dxLookup`, `dxNumberBox`, `dxRadioGroup`,
`dxRangeSlider`, `dxSelectBox`, `dxSlider`, `dxSwitch`, `dxTagBox`, `dxTextArea`, `dxTextBox`.
Platformun kendi editörleri (`PlatformEditorTypes`): `dxGridBox`, `dxImageViewer`,
`dxImageUpload` (+ `dxTagBox` platform tarafında da özel işlenir).
`dxImageUpload`, `dxRating` (+ `dxTagBox` platform tarafında da özel işlenir).
> **Tuzak:** `PlatformConsts.EditorTypes.dxLookup` sabiti bozuk bir metin taşır
> (`"dxHtmlEdidxLookuptor"`). Hiçbir yerde kullanılmadığı için etkisi yoktur, ama C# seeder
@ -1412,6 +1413,7 @@ anahtarları listeler; ortak anahtarlar (§4.3.3) her editörde geçerlidir.
| `dxColorBox` | Renk | String (16) | `editAlphaChannel`, `keyStep` |
| `dxSlider` | Tek değerli kaydırıcı | Sayı | `min`, `max`, `tooltip.showMode` |
| `dxRangeSlider` | Aralık kaydırıcı | Sayı | `min`, `max`, `showRange` |
| `dxRating` | Yıldızlı puan (platform). Tıklanan yıldızın sırası (1..`count`) tam sayı olarak kaydedilir; seçili yıldıza yeniden tıklamak değeri boşaltır (`allowClear: false` kapatır). Liste/kart hücresinde yıldız olarak çizilir; yıldız şablonu olmayan görünümde (Scheduler) sayı kutusudur | Int32 (11) | `count` (varsayılan 5), `allowClear` |
| `dxHtmlEditor` | Zengin metin | String (16) | `toolbar.items`, `toolbar.multiline`, `valueType` (`html`/`markdown`), `mediaResizing.enabled`, `imageUpload.tabs`, `imageUpload.uploadUrl`, `imageUpload.fileUploadMode`, `tableResizing.enabled`, `tableContextMenu.enabled`. Boş `editorOptions` da tam araç çubuğu, dosyadan/URL'den resim, resim boyutlandırma ve tablo araçlarıyla gelir (`ui/src/views/shared/htmlEditorOptions.ts`); yazılan değer varsayılanın üstüne tek seviye birleştirilir, yalnızca farkı yaz. |
| `dxImageUpload` | Görsel yükleme (platform); görsel tutan her kolonun editörü. `accept` belge türlerini de içerirse (`application/pdf,.doc,.docx` — CV, imzalı form) görsel olmayan `data:` değeri listede ve formda belge ikonuyla çizilir, tıklanınca blob olarak yeni sekmede açılır (`imageHoverPreview.thumbnailSource`). `uploadUrl` yoksa dosya **base64** metin olarak kolona yazılır → kolon `nvarchar(MAX)`; sınırlı boylu kolonla deploy reddedilir. `uploadUrl` varsa kolona dönen adres yazılır | String (16) | `uploadUrl`\*, `accept`\*, `capture`\*, `multiple`\*, `maxFileSize`\*, `width`\*, `height`\* |
| `dxImageViewer` | Görsel gösterimi (platform), yükleme yapmaz. Tablo kolonuna bağlı alanda kullanılmaz — görsel kolonu `dxImageUpload`'dır, deploy `dxImageViewer`'ı reddeder | String (16) | `width`, `height` |
@ -1536,7 +1538,7 @@ editör yetenekleri ortak koddan gelir; bir tarafa yeni yetenek ortak yardımcı
| Buton `onClick` (fonksiyon metni, `@SCAN_BARCODE`) ve bağlamı | `resolveEditorButtons` + `withButtonContext` |
| Cascade temizleme/kilit | `list/shared/cascade.ts` |
| Grid/Tree `onEditorPreparing` gövdesi | `prepareEditingEditor` (`list/shared/editingFormScripts.ts`) |
| TagBox, GridBox, ImageUpload, ImageViewer | Tek uygulama `form/editors/*`; liste hücresi `list/editors/cellEditors.tsx` adaptörüyle bağlanır |
| TagBox, GridBox, ImageUpload, ImageViewer, Rating | Tek uygulama `form/editors/*`; liste hücresi `list/editors/cellEditors.tsx` adaptörüyle bağlanır |
DataGrid/TreeList platform editörlerini `editCellTemplate` ile çizer ve `onEditorPreparing` onlar için
çalışmaz; salt-okunur/devre dışı kararı, cascade ve `EditorScript` bu editörlere
@ -1913,7 +1915,8 @@ cevaba yazılmaz.
],
"WorkflowDto": { "ApprovalUserFieldName": "", "ApprovalDateFieldName": "",
"ApprovalStatusFieldName": "", "ApprovalDescriptionFieldName": "",
"ApprovalIsFilterUserName": false, "ApprovalIsResetWorkflow": false, "Criteria": [] }
"ApprovalIsFilterUserName": false, "ApprovalIsResetWorkflow": false,
"ApprovalIsManualStart": false, "Criteria": [] }
}
}
```
@ -2288,6 +2291,7 @@ wizard'ınkinden **küçük** olmalıdır (§0.2).
| `ApprovalDescriptionFieldName` | Onay/ret açıklaması |
| `ApprovalIsFilterUserName` | Liste yalnızca kullanıcının onayına düşenleri göstersin |
| `ApprovalIsResetWorkflow` | Kayıt güncellenince akış başa dönsün |
| `ApprovalIsManualStart` | Yeni kayıt akışı kendiliğinden başlatmasın; kayıt taslak kalır, akış toolbar'daki başlat düğmesiyle başlar. Onaya gönderilmiş başlığa satır eklemeyi engelleyen alt formu olan ekranlarda (talep + satırlar) açılır; kapalıyken Grid/Tree yeni kaydın ardından akışı başlatır |
`Criteria` düğüm grafiğidir. `Kind` değerleri: **`Start`**, **`Compare`**, **`Approval`**,
**`Inform`**, **`End`**.
@ -2627,7 +2631,7 @@ Builder'ı (§5) açar — çıktıları `EditorOptions`/`EditorScript` string'l
**Adım 7 — İş Akışı.** Zorunlu ikili: `ApprovalUserFieldName` + `ApprovalStatusFieldName`;
opsiyonel `ApprovalDateFieldName`, `ApprovalDescriptionFieldName`, `ApprovalIsFilterUserName`
(liste yalnız sıradaki onaycıya düşsün), `ApprovalIsResetWorkflow` (kayıt değişince akış başa
dönsün). Onay düğümleri (`Criteria`) görsel akış tasarımcısında kurulur ve `ListFormWorkflow`
dönsün), `ApprovalIsManualStart` (yeni kayıt taslak kalsın, akış başlat düğmesiyle başlasın). Onay düğümleri (`Criteria`) görsel akış tasarımcısında kurulur ve `ListFormWorkflow`
kayıtlarına gider (§4.7).
**Adım 12 — Deploy.** Sol tarafta tüm adımların özeti, sağda üretim log'u. Log, deploy'un seed
@ -2986,7 +2990,7 @@ Dizi elemanları yaprak sayılmaz; `toolbar.items` tek bir `json` alanıdır.
| TagBox | `showSelectionControls`\*, `maxDisplayedTags`\*, `showMultiTagOnly`\*, `applyValueMode`\*, `multiline`, `hideSelectedItems` | `dxTagBox` |
| GridBox | `columns`\*, `selectionMode`\*, `filterRowVisible`\* | `dxGridBox` |
| Görsel | `uploadUrl`\*, `fileFieldName`, `accept`\*, `capture`\*, `multiple`\*, `maxFileSize`\* | `dxImageUpload`, `dxImageViewer` |
| Seçim | `text` (`dxCheckBox`), `switchedOnText`/`switchedOffText` (`dxSwitch`), `layout` (`dxRadioGroup`), `editAlphaChannel`/`keyStep` (`dxColorBox`) | ilgili editör |
| Seçim | `text` (`dxCheckBox`), `switchedOnText`/`switchedOffText` (`dxSwitch`), `layout` (`dxRadioGroup`), `editAlphaChannel`/`keyStep` (`dxColorBox`), `count`/`allowClear` (`dxRating`) | ilgili editör |
| Kaydırıcı | `min`, `max`, `step`, `tooltip.enabled`, `tooltip.showMode`, `showRange` | `dxSlider`, `dxRangeSlider` |
| HTML | `valueType`, `toolbar.multiline`, `toolbar.items`, `mediaResizing.enabled`, `imageUpload.tabs`, `imageUpload.fileUploadMode`, `imageUpload.uploadUrl`, `tableResizing.enabled`, `tableContextMenu.enabled` | `dxHtmlEditor` |
| Liste sütunu | `format` (metin hâli), `encodeHtml`, `buttons` | hepsi — bu üçü **grid sütununu** da etkiler |

View file

@ -444,7 +444,7 @@ ve atanan kullanıcı alanlarından kurulur.
- **Editör tipleri:** `dxTextBox`, `dxTextArea`, `dxNumberBox`, `dxDateBox`, `dxDateRangeBox`,
`dxCalendar`, `dxCheckBox`, `dxSwitch`, `dxRadioGroup`, `dxSelectBox`, `dxTagBox`, `dxLookup`,
`dxDropDownBox`, `dxGridBox`, `dxAutocomplete`, `dxColorBox`, `dxHtmlEditor`, `dxSlider`,
`dxRangeSlider` — ayrıca görsel yükleme/görüntüleme editörleri.
`dxRangeSlider` — ayrıca görsel yükleme/görüntüleme editörleri ve yıldızlı puan (`dxRating`).
- **Lookup kaynakları:** `StaticData`, `Query`, `WebService`; ebeveyn–çocuk (cascade) desteklidir.
Cascade liste popup'ında, satır içi düzenlemede ve form sayfasında (`/admin/form/...`, `FormDevExpress`)
aynı kuralla çalışır: alan değişince ebeveyni o alan olan çocuklar ve alanın `CascadeEmptyFields`

View file

@ -11,5 +11,11 @@ public class WorkflowDto
public bool ApprovalIsFilterUserName { get; set; }
public bool ApprovalIsResetWorkflow { get; set; }
/// <summary>
/// Açıksa yeni kayıt akışı kendiliğinden başlatmaz: kayıt taslak kalır, alt form satırları
/// onaydan önce girilebilir ve akış toolbar'daki başlat düğmesiyle başlar.
/// </summary>
public bool ApprovalIsManualStart { get; set; }
public List<ListFormWorkflowCriteriaDto> Criteria { get; set; } = [];
}

View file

@ -1023,6 +1023,7 @@ public static class PlatformConsts
public const string dxTextBox = "dxTextBox";
public const string dxImageViewer = "dxImageViewer";
public const string dxImageUpload = "dxImageUpload";
public const string dxRating = "dxRating";
}
public static class CustomEndpointConsts

View file

@ -11,6 +11,7 @@ public class Workflow : ValueObject
public string ApprovalDescriptionFieldName { get; set; }
public bool ApprovalIsFilterUserName { get; set; }
public bool ApprovalIsResetWorkflow { get; set; }
public bool ApprovalIsManualStart { get; set; }
protected override IEnumerable<object> GetAtomicValues()
{
@ -20,6 +21,7 @@ public class Workflow : ValueObject
yield return ApprovalDescriptionFieldName;
yield return ApprovalIsFilterUserName;
yield return ApprovalIsResetWorkflow;
yield return ApprovalIsManualStart;
}
}

View file

@ -272,7 +272,7 @@ Bir çelişki varsa sıra: `ai.instructions.md` → `dotnet.instructions.md` →
`phoneIntl`, `phoneCountry`) görüntü kuralı iki yerde yazılır: liste/kart istemcide, dinamik rapor sunucuda.
Biri değişirse diğeri de değişir; yoksa liste ile rapor aynı numarayı farklı yazar.
- **Liste popup'ı (Grid/Tree) ↔ form sayfası (`FormDevExpress`)** — editör yetenekleri ortak yardımcılardadır
(`list/shared/editingForm.ts`, `editingFormScripts.ts`, `cascade.ts`); TagBox/GridBox/ImageUpload/ImageViewer'ın
(`list/shared/editingForm.ts`, `editingFormScripts.ts`, `cascade.ts`); TagBox/GridBox/ImageUpload/ImageViewer/Rating'in
tek uygulaması `form/editors/*`, liste hücresi yalnızca `list/editors/cellEditors.tsx` adaptörüdür. Bir tarafa
yetenek eklenirken ortak yardımcıya eklenir; tek tarafa yazılan yetenek eşliği bozar. Tablo:
`lowcode.instructions.md` §4.3.4.

View file

@ -45770,6 +45770,20 @@
"Key": "App.ListFormEditWorkflow.ApprovalIsResetWorkflow",
"Value": "İş Akışı Sıfırlama?"
},
{
"Id": "15424420-968a-5f95-a216-0457cdecd257",
"CultureName": "en",
"ResourceName": "Platform",
"Key": "App.ListFormEditWorkflow.ApprovalIsManualStart",
"Value": "Start Workflow Manually?"
},
{
"Id": "ee2f23f5-49c8-5f81-a72c-10b4595443f4",
"CultureName": "tr",
"ResourceName": "Platform",
"Key": "App.ListFormEditWorkflow.ApprovalIsManualStart",
"Value": "Akış Elle Başlatılsın mı?"
},
{
"Id": "b49643d5-247b-586b-a640-6c2ebf3fdcf4",
"CultureName": "en",

View file

@ -78,7 +78,7 @@ Departmanların iç ve dış ihtiyaç duydukları malzemeleri satınalmaya iletm
- Wizard List ekranları + menüsüz satır alt formu + Grid Workflow. Beş liste aynı tabloyu (`Prc_T_MaterialRequest`) filtreyle gösterir.
- Belge no numaratörden: `MaterialRequest` (MT-YYYY-00001, yıllık sıfırlanır). Şirket alanı açılmadı (kiracı kapsamı).
- Departman onaycısı: `Adm_T_Approver`'da Onay Yeri = Satınalma (`Purchase`) ve talebin departmanı. Satınalma onaycısı: Onaycılar'a eklenen yeni Onay Yeri **Satınalma Masası** (`PurchaseDesk`) satırları, departmandan bağımsız. Herhangi bir onaycının onayı yeterli.
- Taslak aşaması: kayıt Taslak açılır; akış platformun Onaya Gönder işlemiyle başlar.
- Taslak aşaması: kayıt Taslak açılır; akış platformun Onaya Gönder işlemiyle başlar (`WorkflowDto.ApprovalIsManualStart: true` — yoksa yeni kayıt akışı kendiliğinden başlatır ve satır girilemez).
- Görseldeki "Kendi Depomda" ve "Tüm Depolar" stok kolonları üretilmedi: uygulamada stok miktarı/hareket tablosu yok; stok modülü gelince ayrı talep.
- Karşılama Şekli: Satınalma / Depodan Transfer.
- Onay akışının "onaylayan" alanı platformda kullanıcı adı (metin) yazdığı için akışa bağlanmadı (`ApprovalUserFieldName` boş); departman/satınalma onaylayanı kayıt sonrası prosedürde kaydı son güncelleyen kullanıcıdan yazılır. `ApproverId` kolonu kullanılmaz.
@ -295,7 +295,7 @@ Yönetim › Tanımlamalar › Onaycılar (onay yerleri Satınalma ve Satınalma
| Alan | Başlık | Editör | Zorunlu | Formda | Kaynak |
| --- | --- | --- | --- | --- | --- |
| `RequestNo` | Talep No | dxTextBox | | ✓ | |
| `Priority` | Öncelik (0-3) | dxNumberBox | ✓ | ✓ | |
| `Priority` | Öncelik (0-3) | dxRating | ✓ | ✓ | |
| `RequesterUserId` | Talep Eden | dxSelectBox | ✓ | ✓ | sorgu: AbpUsers · script |
| `DepartmentId` | Departman | dxSelectBox | ✓ | ✓ | sorgu: Hr_T_Department |
| `WarehouseId` | Depo | dxSelectBox | ✓ | ✓ | sorgu: Inv_T_Warehouse · script |
@ -340,7 +340,7 @@ Yönetim › Tanımlamalar › Onaycılar (onay yerleri Satınalma ve Satınalma
| Alan | Başlık | Editör | Zorunlu | Formda | Kaynak |
| --- | --- | --- | --- | --- | --- |
| `RequestNo` | Talep No | dxTextBox | | ✓ | |
| `Priority` | Öncelik (0-3) | dxNumberBox | ✓ | ✓ | |
| `Priority` | Öncelik (0-3) | dxRating | ✓ | ✓ | |
| `RequesterUserId` | Talep Eden | dxSelectBox | ✓ | ✓ | sorgu: AbpUsers |
| `DepartmentId` | Departman | dxSelectBox | ✓ | ✓ | sorgu: Hr_T_Department |
| `WarehouseId` | Depo | dxSelectBox | ✓ | ✓ | sorgu: Inv_T_Warehouse |
@ -378,7 +378,7 @@ Yönetim › Tanımlamalar › Onaycılar (onay yerleri Satınalma ve Satınalma
| Alan | Başlık | Editör | Zorunlu | Formda | Kaynak |
| --- | --- | --- | --- | --- | --- |
| `RequestNo` | Talep No | dxTextBox | | ✓ | |
| `Priority` | Öncelik (0-3) | dxNumberBox | ✓ | ✓ | |
| `Priority` | Öncelik (0-3) | dxRating | ✓ | ✓ | |
| `RequesterUserId` | Talep Eden | dxSelectBox | ✓ | ✓ | sorgu: AbpUsers |
| `DepartmentId` | Departman | dxSelectBox | ✓ | ✓ | sorgu: Hr_T_Department |
| `WarehouseId` | Depo | dxSelectBox | ✓ | ✓ | sorgu: Inv_T_Warehouse |
@ -418,7 +418,7 @@ Yönetim › Tanımlamalar › Onaycılar (onay yerleri Satınalma ve Satınalma
| Alan | Başlık | Editör | Zorunlu | Formda | Kaynak |
| --- | --- | --- | --- | --- | --- |
| `RequestNo` | Talep No | dxTextBox | | ✓ | |
| `Priority` | Öncelik (0-3) | dxNumberBox | ✓ | ✓ | |
| `Priority` | Öncelik (0-3) | dxRating | ✓ | ✓ | |
| `RequesterUserId` | Talep Eden | dxSelectBox | ✓ | ✓ | sorgu: AbpUsers |
| `DepartmentId` | Departman | dxSelectBox | ✓ | ✓ | sorgu: Hr_T_Department |
| `WarehouseId` | Depo | dxSelectBox | ✓ | ✓ | sorgu: Inv_T_Warehouse |
@ -456,7 +456,7 @@ Yönetim › Tanımlamalar › Onaycılar (onay yerleri Satınalma ve Satınalma
| Alan | Başlık | Editör | Zorunlu | Formda | Kaynak |
| --- | --- | --- | --- | --- | --- |
| `RequestNo` | Talep No | dxTextBox | | ✓ | |
| `Priority` | Öncelik (0-3) | dxNumberBox | ✓ | ✓ | |
| `Priority` | Öncelik (0-3) | dxRating | ✓ | ✓ | |
| `RequesterUserId` | Talep Eden | dxSelectBox | ✓ | ✓ | sorgu: AbpUsers · script |
| `DepartmentId` | Departman | dxSelectBox | ✓ | ✓ | sorgu: Hr_T_Department |
| `WarehouseId` | Depo | dxSelectBox | ✓ | ✓ | sorgu: Inv_T_Warehouse · script |

View file

@ -656,6 +656,7 @@
"ApprovalDescriptionFieldName": "ApprovalNote",
"ApprovalIsFilterUserName": false,
"ApprovalIsResetWorkflow": true,
"ApprovalIsManualStart": false,
"Criteria": [
{
"ListFormCode": "App.Prc.MaterialRequest",
@ -1385,7 +1386,7 @@
"Format": null,
"EditOrderNo": 2,
"EditGroupOrderNo": 1,
"EditorType2": "dxNumberBox",
"EditorType2": "dxRating",
"ColSpan": 1,
"EditorOptions": "{\"min\":0,\"max\":3,\"showSpinButtons\":true}",
"GridBoxOptions": null,

View file

@ -509,8 +509,8 @@
"Order": 1,
"ItemType": "group",
"Caption": "",
"ColCount": 2,
"ColSpan": 2
"ColCount": 4,
"ColSpan": 4
}
],
"PermissionDto": {
@ -575,6 +575,7 @@
"ApprovalDescriptionFieldName": "ApprovalNote",
"ApprovalIsFilterUserName": true,
"ApprovalIsResetWorkflow": true,
"ApprovalIsManualStart": false,
"Criteria": [
{
"ListFormCode": "App.Prc.MaterialRequestDeptApproval",
@ -1036,7 +1037,7 @@
"GroupInterval": "",
"WordWrapEnabled": false
},
"Alignment": null,
"Alignment": "left",
"Format": null,
"EditOrderNo": null,
"EditGroupOrderNo": null,
@ -1162,7 +1163,7 @@
"GroupInterval": "",
"WordWrapEnabled": false
},
"Alignment": null,
"Alignment": "left",
"Format": null,
"EditOrderNo": 1,
"EditGroupOrderNo": 1,
@ -1300,11 +1301,11 @@
"GroupInterval": "",
"WordWrapEnabled": false
},
"Alignment": null,
"Alignment": "left",
"Format": null,
"EditOrderNo": 2,
"EditGroupOrderNo": 1,
"EditorType2": "dxNumberBox",
"EditorType2": "dxRating",
"ColSpan": 1,
"EditorOptions": "{\"readOnly\":true}",
"GridBoxOptions": null,
@ -1438,7 +1439,7 @@
"GroupInterval": "",
"WordWrapEnabled": false
},
"Alignment": null,
"Alignment": "left",
"Format": null,
"EditOrderNo": 3,
"EditGroupOrderNo": 1,
@ -1576,7 +1577,7 @@
"GroupInterval": "",
"WordWrapEnabled": false
},
"Alignment": null,
"Alignment": "left",
"Format": null,
"EditOrderNo": 4,
"EditGroupOrderNo": 1,
@ -1714,7 +1715,7 @@
"GroupInterval": "",
"WordWrapEnabled": false
},
"Alignment": null,
"Alignment": "left",
"Format": null,
"EditOrderNo": 5,
"EditGroupOrderNo": 1,
@ -1852,7 +1853,7 @@
"GroupInterval": "",
"WordWrapEnabled": false
},
"Alignment": null,
"Alignment": "left",
"Format": null,
"EditOrderNo": 6,
"EditGroupOrderNo": 1,
@ -1990,7 +1991,7 @@
"GroupInterval": "",
"WordWrapEnabled": false
},
"Alignment": null,
"Alignment": "left",
"Format": null,
"EditOrderNo": 7,
"EditGroupOrderNo": 1,
@ -2128,7 +2129,7 @@
"GroupInterval": "",
"WordWrapEnabled": false
},
"Alignment": null,
"Alignment": "left",
"Format": null,
"EditOrderNo": 8,
"EditGroupOrderNo": 1,
@ -2254,7 +2255,7 @@
"GroupInterval": "",
"WordWrapEnabled": false
},
"Alignment": null,
"Alignment": "left",
"Format": null,
"EditOrderNo": 9,
"EditGroupOrderNo": 1,
@ -2380,12 +2381,12 @@
"GroupInterval": "",
"WordWrapEnabled": false
},
"Alignment": null,
"Alignment": "left",
"Format": null,
"EditOrderNo": 10,
"EditGroupOrderNo": 1,
"EditorType2": "dxSelectBox",
"ColSpan": 1,
"ColSpan": 3,
"EditorOptions": "{\"showClearButton\":true}",
"GridBoxOptions": null,
"TagBoxOptions": null,
@ -2506,12 +2507,12 @@
"GroupInterval": "",
"WordWrapEnabled": false
},
"Alignment": null,
"Alignment": "left",
"Format": null,
"EditOrderNo": 11,
"EditGroupOrderNo": 1,
"EditorType2": "dxTextArea",
"ColSpan": 2,
"ColSpan": 4,
"EditorOptions": "{\"readOnly\":true}",
"GridBoxOptions": null,
"TagBoxOptions": null,
@ -2632,7 +2633,7 @@
"GroupInterval": "",
"WordWrapEnabled": false
},
"Alignment": null,
"Alignment": "left",
"Format": null,
"EditOrderNo": null,
"EditGroupOrderNo": null,
@ -2758,7 +2759,7 @@
"GroupInterval": "",
"WordWrapEnabled": false
},
"Alignment": null,
"Alignment": "left",
"Format": null,
"EditOrderNo": null,
"EditGroupOrderNo": null,
@ -2884,7 +2885,7 @@
"GroupInterval": "",
"WordWrapEnabled": false
},
"Alignment": null,
"Alignment": "left",
"Format": null,
"EditOrderNo": null,
"EditGroupOrderNo": null,
@ -3010,7 +3011,7 @@
"GroupInterval": "",
"WordWrapEnabled": false
},
"Alignment": null,
"Alignment": "left",
"Format": null,
"EditOrderNo": null,
"EditGroupOrderNo": null,

View file

@ -586,6 +586,7 @@
"ApprovalDescriptionFieldName": null,
"ApprovalIsFilterUserName": false,
"ApprovalIsResetWorkflow": false,
"ApprovalIsManualStart": false,
"Criteria": []
},
"UserId": null,
@ -1224,7 +1225,7 @@
"Format": null,
"EditOrderNo": 2,
"EditGroupOrderNo": 1,
"EditorType2": "dxNumberBox",
"EditorType2": "dxRating",
"ColSpan": 1,
"EditorOptions": "{\"readOnly\":true}",
"GridBoxOptions": null,

View file

@ -604,6 +604,7 @@
"ApprovalDescriptionFieldName": "ApprovalNote",
"ApprovalIsFilterUserName": true,
"ApprovalIsResetWorkflow": true,
"ApprovalIsManualStart": false,
"Criteria": [
{
"ListFormCode": "App.Prc.MaterialRequestPurchaseApproval",
@ -1065,7 +1066,7 @@
"GroupInterval": "",
"WordWrapEnabled": false
},
"Alignment": null,
"Alignment": "left",
"Format": null,
"EditOrderNo": null,
"EditGroupOrderNo": null,
@ -1191,7 +1192,7 @@
"GroupInterval": "",
"WordWrapEnabled": false
},
"Alignment": null,
"Alignment": "left",
"Format": null,
"EditOrderNo": 1,
"EditGroupOrderNo": 1,
@ -1329,11 +1330,11 @@
"GroupInterval": "",
"WordWrapEnabled": false
},
"Alignment": null,
"Alignment": "left",
"Format": null,
"EditOrderNo": 2,
"EditGroupOrderNo": 1,
"EditorType2": "dxNumberBox",
"EditorType2": "dxRating",
"ColSpan": 1,
"EditorOptions": "{\"readOnly\":true}",
"GridBoxOptions": null,
@ -1467,7 +1468,7 @@
"GroupInterval": "",
"WordWrapEnabled": false
},
"Alignment": null,
"Alignment": "left",
"Format": null,
"EditOrderNo": 3,
"EditGroupOrderNo": 1,
@ -1605,7 +1606,7 @@
"GroupInterval": "",
"WordWrapEnabled": false
},
"Alignment": null,
"Alignment": "left",
"Format": null,
"EditOrderNo": 4,
"EditGroupOrderNo": 1,
@ -1743,7 +1744,7 @@
"GroupInterval": "",
"WordWrapEnabled": false
},
"Alignment": null,
"Alignment": "left",
"Format": null,
"EditOrderNo": 5,
"EditGroupOrderNo": 1,
@ -1881,7 +1882,7 @@
"GroupInterval": "",
"WordWrapEnabled": false
},
"Alignment": null,
"Alignment": "left",
"Format": null,
"EditOrderNo": 6,
"EditGroupOrderNo": 1,
@ -2019,7 +2020,7 @@
"GroupInterval": "",
"WordWrapEnabled": false
},
"Alignment": null,
"Alignment": "left",
"Format": null,
"EditOrderNo": 7,
"EditGroupOrderNo": 1,
@ -2157,7 +2158,7 @@
"GroupInterval": "",
"WordWrapEnabled": false
},
"Alignment": null,
"Alignment": "left",
"Format": null,
"EditOrderNo": 8,
"EditGroupOrderNo": 1,
@ -2283,7 +2284,7 @@
"GroupInterval": "",
"WordWrapEnabled": false
},
"Alignment": null,
"Alignment": "left",
"Format": null,
"EditOrderNo": 9,
"EditGroupOrderNo": 1,
@ -2409,7 +2410,7 @@
"GroupInterval": "",
"WordWrapEnabled": false
},
"Alignment": null,
"Alignment": "left",
"Format": null,
"EditOrderNo": 10,
"EditGroupOrderNo": 1,
@ -2535,7 +2536,7 @@
"GroupInterval": "",
"WordWrapEnabled": false
},
"Alignment": null,
"Alignment": "left",
"Format": null,
"EditOrderNo": 11,
"EditGroupOrderNo": 1,
@ -2661,7 +2662,7 @@
"GroupInterval": "",
"WordWrapEnabled": false
},
"Alignment": null,
"Alignment": "left",
"Format": null,
"EditOrderNo": null,
"EditGroupOrderNo": null,
@ -2787,7 +2788,7 @@
"GroupInterval": "",
"WordWrapEnabled": false
},
"Alignment": null,
"Alignment": "left",
"Format": null,
"EditOrderNo": null,
"EditGroupOrderNo": null,
@ -2913,7 +2914,7 @@
"GroupInterval": "",
"WordWrapEnabled": false
},
"Alignment": null,
"Alignment": "left",
"Format": null,
"EditOrderNo": null,
"EditGroupOrderNo": null,
@ -3039,7 +3040,7 @@
"GroupInterval": "",
"WordWrapEnabled": false
},
"Alignment": null,
"Alignment": "left",
"Format": null,
"EditOrderNo": null,
"EditGroupOrderNo": null,
@ -3165,7 +3166,7 @@
"GroupInterval": "",
"WordWrapEnabled": false
},
"Alignment": null,
"Alignment": "left",
"Format": null,
"EditOrderNo": 1,
"EditGroupOrderNo": 2,
@ -3291,7 +3292,7 @@
"GroupInterval": "",
"WordWrapEnabled": false
},
"Alignment": null,
"Alignment": "left",
"Format": null,
"EditOrderNo": 2,
"EditGroupOrderNo": 2,
@ -3417,7 +3418,7 @@
"GroupInterval": "",
"WordWrapEnabled": false
},
"Alignment": null,
"Alignment": "left",
"Format": null,
"EditOrderNo": 3,
"EditGroupOrderNo": 2,

View file

@ -670,12 +670,13 @@
"DefaultLayout": "grid"
},
"WorkflowDto": {
"ApprovalUserFieldName": null,
"ApprovalUserFieldName": "ApproverId",
"ApprovalDateFieldName": "ApprovalDate",
"ApprovalStatusFieldName": "ApprovalStatus",
"ApprovalDescriptionFieldName": "ApprovalNote",
"ApprovalIsFilterUserName": false,
"ApprovalIsResetWorkflow": true,
"ApprovalIsManualStart": false,
"Criteria": [
{
"ListFormCode": "App.Prc.MyMaterialRequest",
@ -1405,7 +1406,7 @@
"Format": null,
"EditOrderNo": 2,
"EditGroupOrderNo": 1,
"EditorType2": "dxNumberBox",
"EditorType2": "dxRating",
"ColSpan": 1,
"EditorOptions": "{\"min\":0,\"max\":3,\"showSpinButtons\":true}",
"GridBoxOptions": null,

View file

@ -1,15 +1,9 @@
import React, {
forwardRef,
useState,
useCallback,
useRef,
type KeyboardEvent,
type ReactNode,
} from 'react'
import React, { forwardRef, useState, useCallback, type KeyboardEvent, type ReactNode } from 'react'
import classNames from 'classnames'
import { useConfig } from '../ConfigProvider'
import type { CommonProps } from '../@types/common'
import { useLocalization } from '@/utils/hooks/useLocalization'
import { RATE_STAR_POINTS } from './starShape'
export interface RateProps extends CommonProps {
/** Toplam yıldız sayısı. Varsayılan: 5 */
@ -59,7 +53,7 @@ const StarIcon = ({ filled, half, size }: { filled: boolean; half: boolean; size
</defs>
)}
<polygon
points="12,2 15.09,8.26 22,9.27 17,14.14 18.18,21.02 12,17.77 5.82,21.02 7,14.14 2,9.27 8.91,8.26"
points={RATE_STAR_POINTS}
fill={half ? 'url(#rate-half)' : filled ? 'currentColor' : 'none'}
stroke="currentColor"
strokeWidth="1.5"
@ -95,7 +89,6 @@ const Rate = forwardRef<HTMLDivElement, RateProps>((props, ref) => {
const isControlled = valueProp !== undefined
const [internalValue, setInternalValue] = useState(defaultValue)
const [hoverValue, setHoverValue] = useState<number | null>(null)
const lastClickedRef = useRef<number | null>(null)
const value = isControlled ? valueProp! : internalValue
const displayValue = hoverValue !== null ? hoverValue : value
@ -139,18 +132,14 @@ const Rate = forwardRef<HTMLDivElement, RateProps>((props, ref) => {
const offsetX = e.clientX - rect.left
const clickedVal = getStarValue(index, offsetX, rect.width)
let newVal = clickedVal
if (allowClear && clickedVal === lastClickedRef.current) {
newVal = 0
lastClickedRef.current = null
} else {
lastClickedRef.current = clickedVal
}
// Seçili yıldıza yeniden tıklamak değeri sıfırlar; karşılaştırma son tıklamayla değil
// güncel değerle yapılır, yoksa kontrollü kullanımda dışarıdan gelen değer temizlenemez.
const newVal = allowClear && clickedVal === value ? 0 : clickedVal
if (!isControlled) setInternalValue(newVal)
onChange?.(newVal)
},
[disabled, readOnly, allowClear, allowHalf, isControlled, onChange],
[disabled, readOnly, allowClear, allowHalf, value, isControlled, onChange],
)
const handleKeyDown = useCallback(

View file

@ -0,0 +1,3 @@
/** Yıldız çokgeni; `Rate` ve liste hücresi (DOM şablonu) aynı şekli çizer. */
export const RATE_STAR_POINTS =
'12,2 15.09,8.26 22,9.27 17,14.14 18.18,21.02 12,17.77 5.82,21.02 7,14.14 2,9.27 8.91,8.26'

View file

@ -871,6 +871,7 @@ export enum PlatformEditorTypes {
dxGridBox = 'dxGridBox',
dxImageViewer = 'dxImageViewer',
dxImageUpload = 'dxImageUpload',
dxRating = 'dxRating',
}
export enum GanttScaleTypeEnum {
@ -948,6 +949,7 @@ export interface WorkflowDto {
approvalDescriptionFieldName: string
approvalIsFilterUserName: boolean
approvalIsResetWorkflow: boolean
approvalIsManualStart: boolean
criteria: ListFormWorkflowCriteriaDto[]
}

View file

@ -319,14 +319,25 @@ export function FormTabWorkflow(
approvalDescriptionFieldName: string(),
approvalIsFilterUserName: bool(),
approvalIsResetWorkflow: bool(),
approvalIsManualStart: bool(),
}),
})
const initialValues = useStoreState((s) => s.admin.lists.values)
if (!initialValues) {
const listValues = useStoreState((s) => s.admin.lists.values)
if (!listValues) {
return null
}
// Seçenek eklenmeden önce kaydedilmiş WorkflowJson'da alan yoktur; değeri tanımsız checkbox'ı
// Formik dizi olarak yazar (["on"]) ve boolean doğrulaması düşer.
const initialValues = {
...listValues,
workflowDto: {
...listValues.workflowDto,
approvalIsManualStart: Boolean(listValues.workflowDto?.approvalIsManualStart),
},
}
return (
<div>
<Formik
@ -341,7 +352,7 @@ export function FormTabWorkflow(
<Form>
<FormContainer size="sm">
<Card className="my-2">
<div className="grid grid-cols-1 md:grid-cols-6 gap-2">
<div className="grid grid-cols-1 md:grid-cols-4 xl:grid-cols-7 gap-2">
<FormItem
asterisk
label={translate('::App.ListFormEditWorkflow.ApprovalUserFieldName')}
@ -468,6 +479,19 @@ export function FormTabWorkflow(
>
<Field name="workflowDto.approvalIsResetWorkflow" component={Checkbox} />
</FormItem>
<FormItem
label={translate('::App.ListFormEditWorkflow.ApprovalIsManualStart')}
invalid={
!!(
errors.workflowDto?.approvalIsManualStart &&
touched.workflowDto?.approvalIsManualStart
)
}
errorMessage={errors.workflowDto?.approvalIsManualStart as string}
>
<Field name="workflowDto.approvalIsManualStart" component={Checkbox} />
</FormItem>
</div>
<Button block size="sm" variant="solid" type="submit" loading={isSubmitting}>

View file

@ -448,7 +448,8 @@ function FormFields({
<Container className="h-full">
<AdaptableCard className="h-full" bodyClass="h-full">
<div className="grid 2xl:grid-cols-5 xl:grid-cols-3 lg:grid-cols-5 gap-4 h-full border-t-2">
<div className="2xl:col-span-2 xl:col-span-1 lg:col-span-2 border-r-2">
{/* Liste sayfayı uzatmaz: sütun ekran yüksekliğiyle sınırlanır, alanlar kendi içinde kayar. */}
<div className="2xl:col-span-2 xl:col-span-1 lg:col-span-2 border-r-2 flex flex-col lg:h-[calc(100vh-350px)] lg:min-h-[400px]">
<div className="flex justify-end px-2 py-2">
<Segment
size="xs"
@ -469,99 +470,101 @@ function FormFields({
</Segment.Item>
</Segment>
</div>
<DndContext
sensors={sensors}
collisionDetection={closestCenter}
onDragEnd={onDragEnd}
>
<SortableContext
items={orderedFields.map((f) => f.id!)}
strategy={verticalListSortingStrategy}
<div className="flex-1 min-h-0 overflow-y-auto">
<DndContext
sensors={sensors}
collisionDetection={closestCenter}
onDragEnd={onDragEnd}
>
<Table compact>
<THead>
<Tr>
<Th className="w-3/12">
<div>
{canCreate && (
<Button
shape="circle"
variant="plain"
type="button"
size="sm"
title="Add"
icon={<FaFileMedical />}
onClick={() => {
setIsNewField(true)
setRow({} as ColumnFormatEditDto)
}}
/>
)}
{canCreate && (
<Button
shape="circle"
variant="plain"
type="button"
size="sm"
title="Copy"
icon={<FaCopy />}
onClick={async (e) => {
e.preventDefault()
setIsCopyField(true)
}}
/>
)}
{canCreate && allDbColumns.length > 0 && (
<Button
shape="circle"
variant="plain"
type="button"
size="sm"
title={translate('::App.ListFormField.AddAllColumns')}
icon={<FaTable />}
loading={isAddingAllColumns}
onClick={addAllColumns}
/>
)}
</div>
</Th>
<Th className="w-3/12">Field</Th>
<Th className="w-2/12">Culture</Th>
<Th className="hidden sm:table-cell w-2/12">Role</Th>
<Th className="hidden sm:table-cell w-2/12">User</Th>
</Tr>
</THead>
<TBody>
{orderedFields.map((field) => (
<SortableFieldRow
key={field.id}
field={field}
isSelected={field.id === row?.id}
canDelete={canDelete}
dragDisabled={
!canUpdate ||
isSavingOrder ||
(orderMode === 'edit' && field.editOrderNo == null)
}
dragTitle={translate('::App.ListFormField.DragToReorder')}
roleLabel={
field.roleId
? (roleList.find((a) => a.value === field.roleId)?.label ?? 'X')
: undefined
}
userLabel={
field.userId
? (userList.find((a) => a.value === field.userId)?.label ?? 'X')
: undefined
}
onSelect={() => setRow(field)}
onDelete={() => field.id && setDeleteRowId(field.id)}
/>
))}
</TBody>
</Table>
</SortableContext>
</DndContext>
<SortableContext
items={orderedFields.map((f) => f.id!)}
strategy={verticalListSortingStrategy}
>
<Table compact>
<THead className="sticky top-0 z-10">
<Tr>
<Th className="w-3/12">
<div>
{canCreate && (
<Button
shape="circle"
variant="plain"
type="button"
size="sm"
title="Add"
icon={<FaFileMedical />}
onClick={() => {
setIsNewField(true)
setRow({} as ColumnFormatEditDto)
}}
/>
)}
{canCreate && (
<Button
shape="circle"
variant="plain"
type="button"
size="sm"
title="Copy"
icon={<FaCopy />}
onClick={async (e) => {
e.preventDefault()
setIsCopyField(true)
}}
/>
)}
{canCreate && allDbColumns.length > 0 && (
<Button
shape="circle"
variant="plain"
type="button"
size="sm"
title={translate('::App.ListFormField.AddAllColumns')}
icon={<FaTable />}
loading={isAddingAllColumns}
onClick={addAllColumns}
/>
)}
</div>
</Th>
<Th className="w-3/12">Field</Th>
<Th className="w-2/12">Culture</Th>
<Th className="hidden sm:table-cell w-2/12">Role</Th>
<Th className="hidden sm:table-cell w-2/12">User</Th>
</Tr>
</THead>
<TBody>
{orderedFields.map((field) => (
<SortableFieldRow
key={field.id}
field={field}
isSelected={field.id === row?.id}
canDelete={canDelete}
dragDisabled={
!canUpdate ||
isSavingOrder ||
(orderMode === 'edit' && field.editOrderNo == null)
}
dragTitle={translate('::App.ListFormField.DragToReorder')}
roleLabel={
field.roleId
? (roleList.find((a) => a.value === field.roleId)?.label ?? 'X')
: undefined
}
userLabel={
field.userId
? (userList.find((a) => a.value === field.userId)?.label ?? 'X')
: undefined
}
onSelect={() => setRow(field)}
onDelete={() => field.id && setDeleteRowId(field.id)}
/>
))}
</TBody>
</Table>
</SortableContext>
</DndContext>
</div>
</div>
<div className="2xl:col-span-3 lg:col-span-3 xl:col-span-2">
<FormFieldEdit

View file

@ -138,6 +138,7 @@ const DROPDOWN_EDITORS = [
const DATE_EDITORS = ['dxDateBox', 'dxCalendar', 'dxDateRangeBox']
const IMAGE_EDITORS = ['dxImageUpload', 'dxImageViewer']
const SLIDER_EDITORS = ['dxSlider', 'dxRangeSlider']
const RATING_EDITORS = ['dxRating']
/* ------------------------------------------------------------------ *
* Ayar sözlüğü
@ -855,6 +856,14 @@ export const optionSpecs: OptionSpec[] = [
editors: ['dxColorBox'],
},
{ path: 'keyStep', label: 'keyStep', type: 'number', group: 'choice', editors: ['dxColorBox'] },
{ path: 'count', label: 'count', type: 'number', group: 'choice', editors: RATING_EDITORS },
{
path: 'allowClear',
label: 'allowClear',
type: 'boolean',
group: 'choice',
editors: RATING_EDITORS,
},
// ── Kaydırıcı ────────────────────────────────────────────────────
{ path: 'min', label: 'min', type: 'number', group: 'slider', editors: SLIDER_EDITORS },

View file

@ -162,6 +162,7 @@ export const columnEditorTypeListOptions = [
{ value: PlatformEditorTypes.dxGridBox, label: PlatformEditorTypes.dxGridBox },
{ value: PlatformEditorTypes.dxImageViewer, label: PlatformEditorTypes.dxImageViewer },
{ value: PlatformEditorTypes.dxImageUpload, label: PlatformEditorTypes.dxImageUpload },
{ value: PlatformEditorTypes.dxRating, label: PlatformEditorTypes.dxRating },
{ value: 'dxHtmlEditor', label: 'dxHtmlEditor' },
{ value: 'dxLookup', label: 'dxLookup' },
{ value: 'dxNumberBox', label: 'dxNumberBox' },

View file

@ -155,6 +155,7 @@ const initialValues: ListFormWizardDto = {
approvalDescriptionFieldName: '',
approvalIsFilterUserName: false,
approvalIsResetWorkflow: false,
approvalIsManualStart: false,
criteria: [],
},
menuEn: '',
@ -445,6 +446,7 @@ const Wizard = () => {
approvalDescriptionFieldName: '',
approvalIsFilterUserName: false,
approvalIsResetWorkflow: false,
approvalIsManualStart: false,
criteria: [],
})
const [criteria, setCriteria] = useState<ListFormWorkflowCriteriaDto[]>([])

View file

@ -391,7 +391,7 @@ function WizardStepWorkflow({
<Form>
<Card className="mb-4">
<FormContainer>
<div className="grid grid-cols-1 md:grid-cols-6 gap-4">
<div className="grid grid-cols-1 md:grid-cols-4 xl:grid-cols-7 gap-4">
{[
['approvalUserFieldName', '::App.ListFormEditWorkflow.ApprovalUserFieldName'],
[
@ -484,6 +484,26 @@ function WizardStepWorkflow({
)}
</Field>
</FormItem>
<FormItem
label={translate('::App.ListFormEditWorkflow.ApprovalIsManualStart')}
>
<Field name="approvalIsManualStart">
{({ field, form }: FieldProps<boolean>) => (
<Checkbox
name={field.name}
checked={Boolean(values.approvalIsManualStart)}
onChange={(checked) => {
form.setFieldValue(field.name, checked)
onWorkflowChange({
...values,
approvalIsManualStart: checked,
criteria,
})
}}
/>
)}
</Field>
</FormItem>
</div>
</FormContainer>
</Card>

View file

@ -14,6 +14,7 @@ import { Dispatch, ReactNode, RefObject, useCallback, useEffect, useRef, useStat
import { GridBoxEditorComponent } from './editors/GridBoxEditorComponent'
import { ImageUploadEditorComponent } from './editors/ImageUploadEditorComponent'
import { ImageViewerEditorComponent } from './editors/ImageViewerEditorComponent'
import { RatingEditorComponent } from './editors/RatingEditorComponent'
import { TagBoxEditorComponent } from './editors/TagBoxEditorComponent'
import { RowMode, SimpleItemWithColData } from './types'
import { PlatformEditorTypes } from '@/proxy/form/models'
@ -47,6 +48,7 @@ const CUSTOM_EDITOR_TYPES = [
PlatformEditorTypes.dxGridBox,
PlatformEditorTypes.dxImageUpload,
PlatformEditorTypes.dxImageViewer,
PlatformEditorTypes.dxRating,
] as const
const FormDevExpress = (props: {
@ -402,6 +404,14 @@ const FormDevExpress = (props: {
editorOptions={editorOptions}
/>
)
case PlatformEditorTypes.dxRating:
return (
<RatingEditorComponent
value={value}
onValueChanged={onChange}
editorOptions={editorOptions}
/>
)
default:
return null
}
@ -437,12 +447,13 @@ const FormDevExpress = (props: {
)
}
// Görsel editörleri DevExtreme'in bilmediği alan tiplerini yaymamak için
// Görsel ve puan editörleri DevExtreme'in bilmediği alan tiplerini yaymamak için
// yalnızca gerekli alanlarla oluşturulur.
const isImageEditor =
const isStandaloneEditor =
formItem.editorType2 === PlatformEditorTypes.dxImageUpload ||
formItem.editorType2 === PlatformEditorTypes.dxImageViewer
const itemProps = isImageEditor
formItem.editorType2 === PlatformEditorTypes.dxImageViewer ||
formItem.editorType2 === PlatformEditorTypes.dxRating
const itemProps = isStandaloneEditor
? {
dataField: formItem.dataField,
name: formItem.name,

View file

@ -0,0 +1,44 @@
import { useEffect, useState, type ReactElement } from 'react'
import { Rate } from '@/components/ui'
import { normalizeRatingValue, resolveRatingCount } from '../../list/shared/ratingValue'
/** Form sayfası ve liste düzenleme hücresinin ortak yıldızlı puan editörü; değer 1..count tam sayıdır. */
const RatingEditorComponent = ({
value,
onValueChanged,
editorOptions,
}: {
value: any
onValueChanged: (value: number | null) => void
editorOptions?: any
}): ReactElement => {
const count = resolveRatingCount(editorOptions)
const valueRating = normalizeRatingValue(value, count)
// Grid/Tree hücre şablonu bir kez çizilir ve setValue sonrası `value` prop'u güncellenmez;
// seçim yerel durumda tutulmazsa yıldızlar tıklamadan sonra eski değere döner.
const [rating, setRating] = useState(valueRating)
useEffect(() => {
setRating(valueRating)
}, [valueRating])
return (
<Rate
allowClear={editorOptions?.allowClear !== false}
className="flex items-center min-h-[36px]"
count={count}
disabled={editorOptions?.disabled === true}
readOnly={editorOptions?.readOnly === true}
size={22}
value={rating}
onChange={(next) => {
setRating(next)
// Temizlenen puan 0 değil boş kaydedilir; 0 geçerli bir puan değildir.
onValueChanged(next || null)
}}
/>
)
}
export { RatingEditorComponent }

View file

@ -14,6 +14,7 @@ export type EditorType2 =
| PlatformEditorTypes.dxGridBox
| PlatformEditorTypes.dxImageViewer
| PlatformEditorTypes.dxImageUpload
| PlatformEditorTypes.dxRating
export type SimpleItemWithColData = Overwrite<
SimpleItem,
{

View file

@ -34,14 +34,18 @@ import DxCardView, {
} from 'devextreme-react/card-view'
import CustomStore from 'devextreme/data/custom_store'
import type { DataType } from 'devextreme/common'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { useCallback, useEffect, useMemo, useRef, useState, type ReactElement } from 'react'
import { createRoot } from 'react-dom/client'
import PageTitle from '@/components/shared/PageTitle'
import { NotePanel } from '../form/notes/NotePanel'
import SubForms from '../form/SubForms'
import GridFilterDialogs from './GridFilterDialogs'
import { GridExtraFilterToolbar } from './GridExtraFilterToolbar'
import { ImageUploadEditorComponent, ImageViewerEditorComponent } from './editors/cellEditors'
import {
ImageUploadEditorComponent,
ImageViewerEditorComponent,
RatingEditorComponent,
} from './editors/cellEditors'
import {
autoNumber,
extractSearchParamsFields,
@ -93,9 +97,11 @@ const toCardColumn = (
const dataField = column.dataField
const originalCustomizeText =
typeof column.customizeText === 'function' ? column.customizeText : undefined
const isImageColumn =
// Görsel ve puan kolonlarının hücre şablonu kartta da aynı çizimi yapar.
const hasCellTemplate =
typeof column.cellTemplate === 'function' &&
Object.prototype.hasOwnProperty.call(column.extras ?? {}, 'imageUploadOptions')
(Object.prototype.hasOwnProperty.call(column.extras ?? {}, 'imageUploadOptions') ||
column.colData?.editorType2 === PlatformEditorTypes.dxRating)
const isHtmlColumn = column.colData?.editorType2 === 'dxHtmlEditor'
return Object.fromEntries(
@ -129,27 +135,26 @@ const toCardColumn = (
dataType: column.dataType,
editorOptions: column.editorOptions,
filterValue: column.filterValue,
fieldValueTemplate:
isImageColumn && typeof column.cellTemplate === 'function'
fieldValueTemplate: hasCellTemplate
? (templateOptions: any) => {
const templateData = templateOptions?.model ?? templateOptions
const container = document.createElement('div')
column.cellTemplate(container, {
column,
data: templateData.field?.card?.data,
displayValue: templateData.field?.displayValue,
value: templateData.field?.value,
})
return container
}
: isHtmlColumn
? (templateOptions: any) => {
const templateData = templateOptions?.model ?? templateOptions
const container = document.createElement('div')
column.cellTemplate(container, {
column,
data: templateData.field?.card?.data,
displayValue: templateData.field?.displayValue,
value: templateData.field?.value,
})
container.innerHTML = String(templateData.field?.value ?? '')
return container
}
: isHtmlColumn
? (templateOptions: any) => {
const templateData = templateOptions?.model ?? templateOptions
const container = document.createElement('div')
container.innerHTML = String(templateData.field?.value ?? '')
return container
}
: undefined,
: undefined,
format: column.format,
headerFilter: column.headerFilter,
lookup: column.lookup,
@ -545,39 +550,43 @@ const CardView = (props: CardViewProps) => {
const commandButtonsRef = useRef(commandButtons)
commandButtonsRef.current = commandButtons
const renderImageUploadEditor = useCallback((templateData: any) => {
const configuredDataField =
templateData?.dataField ??
templateData?.item?.dataField ??
templateData?.editorOptions?.name ??
templateData?.name
const dataField = getEditingFieldName(configuredDataField)
const column = listColumnsCacheRef.current.columns.find(
(candidate) => candidate.dataField?.toLowerCase() === String(dataField ?? '').toLowerCase(),
)
const component = templateData?.component
const currentFormData = component?.option?.('formData') ?? formDataRef.current ?? {}
const editorValue = templateData?.editorOptions?.value
/** Değer yazan platform editörünü (görsel yükleme, puan) kart formuna bağlar. */
const renderValueEditor = useCallback(
(templateData: any, Editor: (cell: any) => ReactElement) => {
const configuredDataField =
templateData?.dataField ??
templateData?.item?.dataField ??
templateData?.editorOptions?.name ??
templateData?.name
const dataField = getEditingFieldName(configuredDataField)
const column = listColumnsCacheRef.current.columns.find(
(candidate) => candidate.dataField?.toLowerCase() === String(dataField ?? '').toLowerCase(),
)
const component = templateData?.component
const currentFormData = component?.option?.('formData') ?? formDataRef.current ?? {}
const editorValue = templateData?.editorOptions?.value
return (
<ImageUploadEditorComponent
{...templateData}
column={column}
value={
editorValue !== undefined ? editorValue : getValueByField(currentFormData, dataField)
}
setValue={(value: any) => {
if (!dataField) return
if (typeof templateData?.editorOptions?.onValueChanged === 'function') {
templateData.editorOptions.onValueChanged({ value })
} else {
component?.updateData?.(dataField, value)
return (
<Editor
{...templateData}
column={column}
value={
editorValue !== undefined ? editorValue : getValueByField(currentFormData, dataField)
}
formDataRef.current = { ...currentFormData, [dataField]: value }
}}
/>
)
}, [])
setValue={(value: any) => {
if (!dataField) return
if (typeof templateData?.editorOptions?.onValueChanged === 'function') {
templateData.editorOptions.onValueChanged({ value })
} else {
component?.updateData?.(dataField, value)
}
formDataRef.current = { ...currentFormData, [dataField]: value }
}}
/>
)
},
[],
)
const renderImageViewerEditor = useCallback(
(templateData: any) => (
@ -590,11 +599,22 @@ const CardView = (props: CardViewProps) => {
(templateOptions: any) => {
const container = document.createElement('div')
createRoot(container).render(
renderImageUploadEditor(templateOptions?.model ?? templateOptions),
renderValueEditor(templateOptions?.model ?? templateOptions, ImageUploadEditorComponent),
)
return container
},
[renderImageUploadEditor],
[renderValueEditor],
)
const ratingFormTemplate = useCallback(
(templateOptions: any) => {
const container = document.createElement('div')
createRoot(container).render(
renderValueEditor(templateOptions?.model ?? templateOptions, RatingEditorComponent),
)
return container
},
[renderValueEditor],
)
const imageViewerFormTemplate = useCallback(
@ -893,10 +913,19 @@ const CardView = (props: CardViewProps) => {
? imageUploadFormTemplate
: item.editorType2 === PlatformEditorTypes.dxImageViewer
? imageViewerFormTemplate
: undefined,
: item.editorType2 === PlatformEditorTypes.dxRating
? ratingFormTemplate
: undefined,
}
},
[gridDto, imageUploadFormTemplate, imageViewerFormTemplate, mode, translate],
[
gridDto,
imageUploadFormTemplate,
imageViewerFormTemplate,
ratingFormTemplate,
mode,
translate,
],
)
const editingFormItems = useMemo(() => {

View file

@ -37,7 +37,13 @@ import SubForms from '../form/SubForms'
import { RowMode } from '../form/types'
import { GridColumnData } from './GridColumnData'
import GridFilterDialogs from './GridFilterDialogs'
import { GridBoxEditorComponent, ImageUploadEditorComponent, ImageViewerEditorComponent, TagBoxEditorComponent } from './editors/cellEditors'
import {
GridBoxEditorComponent,
ImageUploadEditorComponent,
ImageViewerEditorComponent,
RatingEditorComponent,
TagBoxEditorComponent,
} from './editors/cellEditors'
import { useFilters } from './useFilters'
import { updateWorkflowApprovalToolbarItems, useToolbar } from './useToolbar'
import { ImportDashboard } from '@/components/importManager/ImportDashboard'
@ -638,6 +644,10 @@ const Grid = (props: GridProps) => {
name="cellEditImageUpload"
render={(cell: any) => ImageUploadEditorComponent(adaptCustomEditorCell(cell))}
/>
<Template
name="cellEditRating"
render={(cell: any) => RatingEditorComponent(adaptCustomEditorCell(cell))}
/>
<Template
name="cellEditImageViewer"
render={(data: any) => (

View file

@ -31,7 +31,13 @@ import PageTitle from '@/components/shared/PageTitle'
import { RowMode } from '../form/types'
import { GridColumnData } from './GridColumnData'
import GridFilterDialogs from './GridFilterDialogs'
import { GridBoxEditorComponent, ImageUploadEditorComponent, ImageViewerEditorComponent, TagBoxEditorComponent } from './editors/cellEditors'
import {
GridBoxEditorComponent,
ImageUploadEditorComponent,
ImageViewerEditorComponent,
RatingEditorComponent,
TagBoxEditorComponent,
} from './editors/cellEditors'
import { useFilters } from './useFilters'
import { updateWorkflowApprovalToolbarItems, useToolbar } from './useToolbar'
import WidgetGroup from '@/components/ui/Widget/WidgetGroup'
@ -627,6 +633,10 @@ const Tree = (props: TreeProps) => {
name="cellEditImageUpload"
render={(cell: any) => ImageUploadEditorComponent(adaptCustomEditorCell(cell))}
/>
<Template
name="cellEditRating"
render={(cell: any) => RatingEditorComponent(adaptCustomEditorCell(cell))}
/>
<Template
name="cellEditImageViewer"
render={(data: any) => (

View file

@ -3,6 +3,7 @@ import type { ImageUploadOptionsDto } from '@/proxy/form/models'
import { GridBoxEditorComponent as FormGridBoxEditor } from '../../form/editors/GridBoxEditorComponent'
import { ImageUploadEditorComponent as FormImageUploadEditor } from '../../form/editors/ImageUploadEditorComponent'
import { ImageViewerEditorComponent as FormImageViewerEditor } from '../../form/editors/ImageViewerEditorComponent'
import { RatingEditorComponent as FormRatingEditor } from '../../form/editors/RatingEditorComponent'
import { TagBoxEditorComponent as FormTagBoxEditor } from '../../form/editors/TagBoxEditorComponent'
import { parseJsonObject } from '../shared/imageValue'
@ -12,7 +13,6 @@ import { parseJsonObject } from '../shared/imageValue'
* `form/editors/*`tir; burada yalnızca hücre verisi bileşen prop'larına çevrilir.
*/
/**
* Ekranın editorOptions'ı + hücre bağlamının salt-okunur/devre dışı kararı. Bağlamın geri
* kalanı (`value`, `onValueChanged`…) editörün kendi değer akışını ezeceği için alınmaz.
@ -104,6 +104,15 @@ const ImageUploadEditorComponent = (cellElement: any): ReactElement => {
)
}
/** Liste düzenleme hücresi (Grid, Tree, CardView); editör form sayfasıyla aynı bileşendir. */
const RatingEditorComponent = (cellElement: any): ReactElement => (
<FormRatingEditor
value={cellElement.value}
editorOptions={getCellEditorOptions(cellElement)}
onValueChanged={(value) => cellElement.setValue(value)}
/>
)
const resolveTemplateValue = (templateData: any) => {
const dataField =
templateData?.dataField ||
@ -168,5 +177,6 @@ export {
GridBoxEditorComponent,
ImageUploadEditorComponent,
ImageViewerEditorComponent,
RatingEditorComponent,
TagBoxEditorComponent,
}

View file

@ -468,6 +468,8 @@ const getDateEditorDefaults = (sourceDbType?: DbTypeEnum) => {
/** PlatformEditorTypes -> DevExtreme editorType eşlemesi. */
export const resolveEditorType = (editorType2?: EditorType2) => {
if (editorType2 === PlatformEditorTypes.dxGridBox) return 'dxDropDownBox'
// Yıldız şablonu olmayan görünümlerde (Scheduler…) puan sayı olarak girilir.
if (editorType2 === PlatformEditorTypes.dxRating) return 'dxNumberBox'
if (
editorType2 === PlatformEditorTypes.dxImageUpload ||
editorType2 === PlatformEditorTypes.dxImageViewer

View file

@ -0,0 +1,35 @@
import { RATE_STAR_POINTS } from '@/components/ui/Rate/starShape'
const DEFAULT_RATING_COUNT = 5
/** Yıldız sayısı editorOptions.count'tan gelir; geçersizse 5. */
export const resolveRatingCount = (editorOptions?: { count?: unknown }) => {
const count = Number(editorOptions?.count)
return Number.isInteger(count) && count > 0 ? count : DEFAULT_RATING_COUNT
}
/** Kolon değeri (sayı ya da metin) -> 0..count arası tam puan; boş değer 0. */
export const normalizeRatingValue = (value: unknown, count: number) => {
const rating = Math.round(Number(value))
return Number.isFinite(rating) ? Math.min(Math.max(rating, 0), count) : 0
}
const STAR_FILLED_COLOR = '#fbbf24'
const STAR_EMPTY_COLOR = '#d1d5db'
/**
* Liste hücresi (Grid, Tree, Card) DOM ile çizilir; React kökü açmamak için yıldızlar
* `Rate` bileşeniyle aynı çokgenle SVG olarak yazılır.
*/
export const renderRatingCell = (cellElement: HTMLElement, value: unknown, count: number) => {
const rating = normalizeRatingValue(value, count)
const stars = Array.from({ length: count }, (_, index) => {
const color = index < rating ? STAR_FILLED_COLOR : STAR_EMPTY_COLOR
const fill = index < rating ? color : 'none'
return `<svg width="16" height="16" viewBox="0 0 24 24" style="display:inline-block;flex-shrink:0"><polygon points="${RATE_STAR_POINTS}" fill="${fill}" stroke="${color}" stroke-width="1.5" stroke-linejoin="round"/></svg>`
})
cellElement.style.cssText += 'display:flex;align-items:center;gap:2px;'
cellElement.innerHTML = stars.join('')
cellElement.title = rating ? `${rating} / ${count}` : ''
}

View file

@ -26,7 +26,8 @@ const showWorkflowMessages = (messages: string[] = []) => {
/**
* Liste formunda workflow onay alanı tanımlıysa eklenen kayıt için
* workflow'u başlatır ve görünümü yeniler.
* workflow'u başlatır ve görünümü yeniler. `approvalIsManualStart` açıksa kayıt
* taslak kalır; alt form satırları girildikten sonra akış başlat düğmesiyle başlar.
*/
export const startWorkflowForInsertedRow = (
event: any,
@ -34,7 +35,8 @@ export const startWorkflowForInsertedRow = (
listFormCode: string,
refresh: () => void,
) => {
if (!gridDto.gridOptions.workflowDto?.approvalStatusFieldName) return
const workflowDto = gridDto.gridOptions.workflowDto
if (!workflowDto?.approvalStatusFieldName || workflowDto.approvalIsManualStart) return
const insertedKey = getPersistedInsertedKey(event, gridDto.gridOptions.keyFieldName)
if (insertedKey === undefined) return

View file

@ -32,6 +32,7 @@ import {
showImageHoverPreview,
thumbnailSource,
} from '../form/editors/imageHoverPreview'
import { renderRatingCell, resolveRatingCount } from './shared/ratingValue'
import { translateWorkflowLabel } from '@/utils/workflow/workflowHelpers'
import { findLookupColor, getLookupItemColor, tagBadgeColors } from '../shared/lookupColor'
import { applyPhoneColumnFormat, needsPhoneCountries } from './shared/columnFormatters'
@ -158,6 +159,14 @@ const cellTemplateImage = (
})
}
const cellTemplateRating = (
cellElement: HTMLElement,
cellInfo: DataGridTypes.ColumnCellTemplateData<any, any>,
) => {
const column = cellInfo?.column as GridColumnData | undefined
renderRatingCell(cellElement, cellInfo?.value, resolveRatingCount(column?.extras?.editorOptions))
}
function calculateFilterExpressionMultiValue(
this: DataGridTypes.Column,
filterValue: any,
@ -810,6 +819,16 @@ const useListFormColumns = ({
}
// #endregion image upload editor
if (
colData.editorType2 === PlatformEditorTypes.dxRating &&
!colData.lookupDto?.dataSourceType
) {
column.extras = { multiValue: false, editorOptions }
column.editCellTemplate = 'cellEditRating'
column.cellTemplate = cellTemplateRating as any
column.alignment = 'left'
}
if (colData.validationRuleDto) {
// for server side validation : https://js.devexpress.com/Demos/WidgetsGallery/Demo/DataGrid/DataValidation/jQuery/Light/
column.validationRules = colData.validationRuleDto as ValidationRule[]