From 3922c4653911fc36466fc36d574555bd46f7a14e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sedat=20=C3=96ZT=C3=9CRK?= <76204082+iamsedatozturk@users.noreply.github.com> Date: Wed, 7 Oct 2026 21:08:10 +0300 Subject: [PATCH] =?UTF-8?q?Grid,=20Tree,=20FormDevexpress=20komponentlerin?= =?UTF-8?q?=20yetenekleri=20e=C5=9Fitlendi?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/instructions/lowcode.instructions.md | 25 +- README.md | 6 +- claude.md | 5 + .../5d11851e-5f00-4a0b-b5a3-b8211e56c8b5.json | 4 +- ui/src/views/form/FormButtons.tsx | 2 +- ui/src/views/form/FormDevExpress.tsx | 70 ++--- .../form/editors/GridBoxEditorComponent.tsx | 8 +- .../editors/ImageUploadEditorComponent.tsx | 62 +++- .../form/editors/TagBoxEditorComponent.tsx | 6 - ui/src/views/form/shared/formItems.ts | 29 +- ui/src/views/form/useLookupDataSource.ts | 8 +- ui/src/views/list/CardView.tsx | 3 +- ui/src/views/list/Grid.tsx | 96 +++--- ui/src/views/list/Tree.tsx | 95 +++--- .../list/editors/GridBoxEditorComponent.tsx | 30 -- .../editors/ImageUploadEditorComponent.tsx | 273 ------------------ .../editors/ImageViewerEditorComponent.tsx | 114 -------- .../list/editors/TagBoxEditorComponent.tsx | 42 --- ui/src/views/list/editors/cellEditors.tsx | 172 +++++++++++ ui/src/views/list/shared/cascade.ts | 221 +++++++++----- ui/src/views/list/shared/editPopup.ts | 2 +- ui/src/views/list/shared/editingForm.ts | 88 ++++-- .../views/list/shared/editingFormScripts.ts | 183 ++++++++++-- ui/src/views/list/shared/imageValue.ts | 4 +- ui/src/views/list/shared/workflowRows.tsx | 2 +- ui/src/views/list/useListFormColumns.ts | 16 +- ui/src/views/shared/MenuAddDialog.tsx | 2 +- ui/src/views/shared/SharedGridBoxEditor.tsx | 16 +- ui/src/views/shared/htmlEditorOptions.ts | 2 +- ui/src/views/shared/lookupColor.tsx | 2 +- 30 files changed, 784 insertions(+), 804 deletions(-) delete mode 100644 ui/src/views/list/editors/GridBoxEditorComponent.tsx delete mode 100644 ui/src/views/list/editors/ImageUploadEditorComponent.tsx delete mode 100644 ui/src/views/list/editors/ImageViewerEditorComponent.tsx delete mode 100644 ui/src/views/list/editors/TagBoxEditorComponent.tsx create mode 100644 ui/src/views/list/editors/cellEditors.tsx diff --git a/.github/instructions/lowcode.instructions.md b/.github/instructions/lowcode.instructions.md index 307cbea4..9f38d394 100644 --- a/.github/instructions/lowcode.instructions.md +++ b/.github/instructions/lowcode.instructions.md @@ -1490,9 +1490,28 @@ cascade eksik yetenek değildir ve analizde `Ready to Start`'ı engellemez. Tam | `CascadeFilterOperator` | `StaticData` cascade filtresinin operatörü; varsayılan `=` | | `CascadeEmptyFields` | **Ebeveyn alanda** doldurulur; bu alan değişince boşaltılacak çocuk alanlar, virgülle. Ara halka (İl) hem çocuk hem ebeveyn olduğu için iki tarafı birden taşır | -Cascade liste popup'ında ve form sayfasında (`FormDevExpress`) aynı davranır. Form sayfasında çocuk +Cascade liste popup'ında (Grid, Tree), satır içi düzenlemede ve form sayfasında (`FormDevExpress`) +aynı kuralla çalışır; kural tek yerdedir (`list/shared/cascade.ts`, `getCascadeClearFields`): bir alan +değişince ebeveyni o alan olan çocuklar **ve** alanın `CascadeEmptyFields` listesi boşaltılır. Kilit +ve temizleme platform editörlerinde (TagBox, GridBox, ImageUpload) de uygulanır. Form sayfasında çocuk yalnızca ebeveynin değeri **gerçekten değiştiğinde** temizlenir ve listesi yeni ebeveyn değeriyle yeniden kurulur; formun aynı değeri tekrar bildirmesi (veri forma geri verilince) yok sayılır. + +**Liste popup'ı ↔ form sayfası editör eşliği.** İkisi aynı ListForm tanımından beslendiği için +editör yetenekleri ortak koddan gelir; bir tarafa yeni yetenek ortak yardımcıya eklenerek verilir: + +| Yetenek | Ortak kaynak | +| --- | --- | +| `EditorOptions` varsayılanları (tarih maskesi/biçimi, HtmlEditor, telefon) | `withEditorTypeDefaults` (`list/shared/editingForm.ts`) | +| Lookup varsayılanları (`valueExpr`, `displayExpr`, `searchEnabled`) | `getLookupEditorDefaults` | +| 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 | + +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 +`createCustomEditorCellAdapter` ile verilir. Adres zincirinin hazır örneği Kiracılar ekranıdır: Ülke › İl › İlçe › Mahalle; mahalle lookup'ı `PostalCode`'u `group` olarak döndürür ve `copy("Group", "PostalCode")` tarifi posta kodunu doldurur. @@ -2842,7 +2861,7 @@ Preset mevcut JSON ile **birleşir**, diğer ayarları silmez; aynı anahtar var | Çoklu görsel | `width`, `height`, `multiple`, `accept:image/*` | | HTML tam araç çubuğu | `htmlEditorOptions.ts` varsayılanı: `toolbar.items` (tam liste), `toolbar.multiline`, `mediaResizing`, `imageUpload` (`tabs: file/url`, base64), `tableResizing`, `tableContextMenu` — boş `editorOptions` da aynı varsayılanı alır | | Yükseklik 100 / 200 | `{"height":100}` · `{"height":200}` | -| Barkod okutma butonu | `buttons[]` — kamera ikonlu buton; `onClick` = `"@SCAN_BARCODE"` token'ı. Yalnız `dxTextBox`. Token'ı `editingForm.ts` kamera diyaloğuna bağlar, okunan kod alana yazılır. HTTPS şart. | +| Barkod okutma butonu | `buttons[]` — kamera ikonlu buton; `onClick` = `"@SCAN_BARCODE"` token'ı. Yalnız `dxTextBox`. Token'ı `resolveEditorButtons` (`editingForm.ts`) kamera diyaloğuna bağlar, okunan kod alana yazılır; liste popup'ı ve form sayfası aynı. HTTPS şart. Fonksiyon metni yazılan `onClick` olayla birlikte `formData`, `fieldName`, `mode` alır (popup'ta ek olarak `rowKey`, `rowIndex`). | | Kamerayla foto çek | `{"accept":"image/*","capture":"environment"}` — yalnız `dxImageUpload`. Mobilde dosya seçici yerine **yalnızca** kamera açılır (`user` = ön kamera); masaüstünde yok sayılır. Ayarlanmazsa dokunmatik cihazda dosya seçicinin yanına ayrı bir kamera butonu gelir, yani ikisi birden kullanılabilir — bu kalıp kamerayı **zorunlu** kılmak içindir. | #### 6.4.1 Telefon biçimleri (`format` token'ı) @@ -2867,7 +2886,7 @@ varsayılanı ezer. - Ülke alanı olan her ekranda yeni kayıt Türkiye ile açılır: `FormFieldsDefaultValueJson` → `{ FieldName: "Country", FieldDbType: String, Value: "Türkiye", CustomValueType: Value }`. - Ülke alanı değişince maske yenilenir: Card/Scheduler/Form sayfasında formun `fieldDataChanged` olayıyla, - Grid/Tree'de `onEditorPreparing`'deki `attachFieldValueBroadcast` olayıyla (DataGrid formu `fieldDataChanged` + Grid/Tree'de `onEditorPreparing`'deki (`prepareEditingEditor`) `attachFieldValueBroadcast` olayıyla (DataGrid formu `fieldDataChanged` yayınlamaz; `editorOptions.onValueChanged` da kullanılamaz — editör fabrikası onu kendi değer yazan işleyicisinin üstüne birleştirir). Satır içi düzenlemede (form yok) maske satırın ülkesiyle açılır, değişikliği izlemez. - `phoneCountry` ekranında sabit uzunluk `pattern`'ı (`^[0-9]{10}$`) yazılmaz; uzunluğu maske denetler, diff --git a/README.md b/README.md index d300db38..1116c845 100644 --- a/README.md +++ b/README.md @@ -446,9 +446,13 @@ ve atanan kullanıcı alanlarından kurulur. `dxDropDownBox`, `dxGridBox`, `dxAutocomplete`, `dxColorBox`, `dxHtmlEditor`, `dxSlider`, `dxRangeSlider` — ayrıca görsel yükleme/görüntüleme editörleri. - **Lookup kaynakları:** `StaticData`, `Query`, `WebService`; ebeveyn–çocuk (cascade) desteklidir. - Cascade liste popup'ında ve form sayfasında (`/admin/form/...`, `FormDevExpress`) aynı çalışır. + 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` + listesi boşaltılır. Lookup'lı seçim kutusunda yazarak arama ikisinde de varsayılan açıktır; alanın `EditorOptions`'ında `{"searchEnabled": false}` kapatır. +- **Popup ↔ form sayfası eşliği:** `EditorOptions` varsayılanları, butonlar, `EditorScript`, cascade ve + platform editörleri (TagBox, GridBox, görsel) iki yerde ortak koddan gelir; tablo `lowcode.instructions.md` §4.3.4. - **Doğrulama:** `ValidationRuleJson` ile required/range/pattern/custom kuralları. Kurallar liste popup'ında ve form sayfasında (`/admin/form/...`) aynı uygulanır; maske (`mask`) `EditorOptions` ile verilir. - **Editör script:** `EditorScript` alanı ile alan değişiminde çalışan JS (örn. tarih farkından gün hesaplama, miktar × birim fiyat toplamı). Çalıştırma `utils/editorScriptRuntime.ts` üzerinden yapılır. diff --git a/claude.md b/claude.md index 33a8907d..f85a9554 100644 --- a/claude.md +++ b/claude.md @@ -231,6 +231,11 @@ Bir çelişki varsa sıra: `ai.instructions.md` → `dotnet.instructions.md` → - **`phoneFormat.ts` ↔ `DynamicReportPhoneFormatter.cs`** — telefon token'larının (`phoneGlobal`, `phoneTR`, `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 + 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. - **`htmlEditorOptions.ts` ↔ `EditorOptions.Html`** — HtmlEditor varsayılanı iki yerde yazılır ve bloklar birebir aynıdır. İstemci iç içe blokları tek seviye birleştirdiği için seed edilen `toolbar` varsayılanın yerine geçer; biri değişip diğeri değişmezse seed'li ekranlar eski araç diff --git a/configs/seeds/data/App.DevRequests/5d11851e-5f00-4a0b-b5a3-b8211e56c8b5.json b/configs/seeds/data/App.DevRequests/5d11851e-5f00-4a0b-b5a3-b8211e56c8b5.json index b8f65781..6d1b4980 100644 --- a/configs/seeds/data/App.DevRequests/5d11851e-5f00-4a0b-b5a3-b8211e56c8b5.json +++ b/configs/seeds/data/App.DevRequests/5d11851e-5f00-4a0b-b5a3-b8211e56c8b5.json @@ -18,9 +18,9 @@ "Priority": "5", "Completed": false, "SortOrder": 2, - "Timesheets": "[{\"id\":\"6a817f32-69b6-6e06-3cc4-3a2428dfe74d\",\"date\":\"2026-10-07\",\"userName\":\"system@sozsoft.com\",\"description\":\"Talep analiz edildi\",\"duration\":0.0167},{\"id\":\"9c434c1b-9451-efc8-02b1-3a2428e13282\",\"date\":\"2026-10-07\",\"userName\":\"system@sozsoft.com\",\"description\":\"Inv_T_ProductCategory tablosu olu\\u015Fturuldu\",\"duration\":0.0167},{\"id\":\"30fb4513-f16d-bb46-f408-3a2428e14ab8\",\"date\":\"2026-10-07\",\"userName\":\"system@sozsoft.com\",\"description\":\"Inv_T_Attribute tablosu olu\\u015Fturuldu\",\"duration\":0.0167},{\"id\":\"1dc7152e-2ff6-f6de-9205-3a2428e1655b\",\"date\":\"2026-10-07\",\"userName\":\"system@sozsoft.com\",\"description\":\"Inv_T_AttributeValue tablosu olu\\u015Fturuldu\",\"duration\":0.0167},{\"id\":\"dd3504f3-51d0-042b-a5b5-3a2428e19b77\",\"date\":\"2026-10-07\",\"userName\":\"system@sozsoft.com\",\"description\":\"Inv_T_Product tablosu olu\\u015Fturuldu\",\"duration\":0.0167},{\"id\":\"560153a2-526d-c786-f5b5-3a2428e2bfab\",\"date\":\"2026-10-07\",\"userName\":\"system@sozsoft.com\",\"description\":\"Inv_T_ProductAttributeLine tablosu olu\\u015Fturuldu\",\"duration\":0.0167},{\"id\":\"3414b481-2140-b3e6-dab3-3a2428e2d9b2\",\"date\":\"2026-10-07\",\"userName\":\"system@sozsoft.com\",\"description\":\"Inv_T_ProductVariant tablosu olu\\u015Fturuldu\",\"duration\":0.0167},{\"id\":\"a715a8e5-cb36-311c-2aa2-3a2428e2fa1a\",\"date\":\"2026-10-07\",\"userName\":\"system@sozsoft.com\",\"description\":\"Inv_T_ProductSupplier tablosu olu\\u015Fturuldu\",\"duration\":0.0167},{\"id\":\"8d745d78-25bf-b82a-c2f4-3a2428e31cbc\",\"date\":\"2026-10-07\",\"userName\":\"system@sozsoft.com\",\"description\":\"Inv_T_WorkCenter tablosu olu\\u015Fturuldu\",\"duration\":0.0167},{\"id\":\"c493b41a-a80e-e72d-2f9f-3a2428e333d3\",\"date\":\"2026-10-07\",\"userName\":\"system@sozsoft.com\",\"description\":\"Inv_T_WorkCenterCapacity tablosu olu\\u015Fturuldu\",\"duration\":0.0167},{\"id\":\"01397c97-65b8-c5e8-5766-3a2428e35783\",\"date\":\"2026-10-07\",\"userName\":\"system@sozsoft.com\",\"description\":\"Inv_T_Operation tablosu olu\\u015Fturuldu\",\"duration\":0.0167},{\"id\":\"a57fcb7d-12e9-9ca2-5a55-3a2428e36d82\",\"date\":\"2026-10-07\",\"userName\":\"system@sozsoft.com\",\"description\":\"Inv_T_OperationInstruction tablosu olu\\u015Fturuldu\",\"duration\":0.0167},{\"id\":\"4df60eac-980d-a497-93c4-3a2428e38bd9\",\"date\":\"2026-10-07\",\"userName\":\"system@sozsoft.com\",\"description\":\"Inv_T_Bom tablosu olu\\u015Fturuldu\",\"duration\":0.0167},{\"id\":\"6a68db4d-bb53-33b9-cf0b-3a2428e3a1bb\",\"date\":\"2026-10-07\",\"userName\":\"system@sozsoft.com\",\"description\":\"Inv_T_BomOperation tablosu olu\\u015Fturuldu\",\"duration\":0.0167},{\"id\":\"fb6aea44-6996-80d1-8d19-3a2428e3ba5a\",\"date\":\"2026-10-07\",\"userName\":\"system@sozsoft.com\",\"description\":\"Inv_T_BomLine tablosu olu\\u015Fturuldu\",\"duration\":0.0167},{\"id\":\"ab475620-374d-e4a1-dc1b-3a2428e40761\",\"date\":\"2026-10-07\",\"userName\":\"system@sozsoft.com\",\"description\":\"App.DeveloperKit.CustomEndpoints ekran\\u0131na sat\\u0131r eklendi\",\"duration\":0.0167},{\"id\":\"52e08632-e712-8432-977e-3a2428e48174\",\"date\":\"2026-10-07\",\"userName\":\"system@sozsoft.com\",\"description\":\"ProductCategory.json wizard\\u0027\\u0131 deploy edildi (2 demo sat\\u0131r)\",\"duration\":0.0167},{\"id\":\"4e8bdcfc-9621-2861-1954-3a2428e4e6a6\",\"date\":\"2026-10-07\",\"userName\":\"system@sozsoft.com\",\"description\":\"AttributeValue.json wizard\\u0027\\u0131 deploy edildi (0 demo sat\\u0131r)\",\"duration\":0.0167},{\"id\":\"c007dd02-6b11-d995-c720-3a2428e56195\",\"date\":\"2026-10-07\",\"userName\":\"system@sozsoft.com\",\"description\":\"Attribute.json wizard\\u0027\\u0131 deploy edildi (3 demo sat\\u0131r)\",\"duration\":0.0167},{\"id\":\"4fee692c-bce3-3c65-80ad-3a2428e5c017\",\"date\":\"2026-10-07\",\"userName\":\"system@sozsoft.com\",\"description\":\"WorkCenterCapacity.json wizard\\u0027\\u0131 deploy edildi (0 demo sat\\u0131r)\",\"duration\":0.0167},{\"id\":\"da9fe300-0f4e-f900-b08d-3a2428e64100\",\"date\":\"2026-10-07\",\"userName\":\"system@sozsoft.com\",\"description\":\"WorkCenterGeneral.json wizard\\u0027\\u0131 deploy edildi (0 demo sat\\u0131r)\",\"duration\":0.0167},{\"id\":\"4bbfcb44-0ddb-a68e-5a7c-3a2428e7a2f3\",\"date\":\"2026-10-07\",\"userName\":\"system@sozsoft.com\",\"description\":\"WorkCenter.json wizard\\u0027\\u0131 deploy edildi (1 demo sat\\u0131r)\",\"duration\":0.0333},{\"id\":\"4f66f26a-ee05-4a6a-847f-3a2428e809f9\",\"date\":\"2026-10-07\",\"userName\":\"system@sozsoft.com\",\"description\":\"OperationInstruction.json wizard\\u0027\\u0131 deploy edildi (0 demo sat\\u0131r)\",\"duration\":0.0167},{\"id\":\"913a0473-6e15-797a-7160-3a2428e868cd\",\"date\":\"2026-10-07\",\"userName\":\"system@sozsoft.com\",\"description\":\"OperationSettings.json wizard\\u0027\\u0131 deploy edildi (0 demo sat\\u0131r)\",\"duration\":0.0167},{\"id\":\"85b8537e-a7ab-54ef-bb16-3a2428e8d1b9\",\"date\":\"2026-10-07\",\"userName\":\"system@sozsoft.com\",\"description\":\"Operation.json wizard\\u0027\\u0131 deploy edildi (0 demo sat\\u0131r)\",\"duration\":0.0167},{\"id\":\"ca24a627-11b3-f8db-6f13-3a2428e934d0\",\"date\":\"2026-10-07\",\"userName\":\"system@sozsoft.com\",\"description\":\"BomOperation.json wizard\\u0027\\u0131 deploy edildi (0 demo sat\\u0131r)\",\"duration\":0.0167},{\"id\":\"6c0040f0-b981-9546-22ab-3a2428e9ac84\",\"date\":\"2026-10-07\",\"userName\":\"system@sozsoft.com\",\"description\":\"BomLine.json wizard\\u0027\\u0131 deploy edildi (0 demo sat\\u0131r)\",\"duration\":0.0167},{\"id\":\"037a0ab6-d2b5-5a38-a030-3a2428eaa433\",\"date\":\"2026-10-07\",\"userName\":\"system@sozsoft.com\",\"description\":\"Bom.json wizard\\u0027\\u0131 deploy edildi (0 demo sat\\u0131r)\",\"duration\":0.0167},{\"id\":\"fa6d1319-7742-f551-e75e-3a2428eb27cc\",\"date\":\"2026-10-07\",\"userName\":\"system@sozsoft.com\",\"description\":\"ProductVariant.json wizard\\u0027\\u0131 deploy edildi (0 demo sat\\u0131r)\",\"duration\":0.0167},{\"id\":\"62798abf-b710-74a7-006e-3a2428ecadd4\",\"date\":\"2026-10-07\",\"userName\":\"system@sozsoft.com\",\"description\":\"ProductGeneral.json wizard\\u0027\\u0131 deploy edildi (0 demo sat\\u0131r)\",\"duration\":0.0333},{\"id\":\"93b9f36f-5e8c-8607-373e-3a2428ed1027\",\"date\":\"2026-10-07\",\"userName\":\"system@sozsoft.com\",\"description\":\"ProductAttributeLine.json wizard\\u0027\\u0131 deploy edildi (0 demo sat\\u0131r)\",\"duration\":0.0167},{\"id\":\"c7fe1df4-3b68-222c-3a4b-3a2428ed62bd\",\"date\":\"2026-10-07\",\"userName\":\"system@sozsoft.com\",\"description\":\"ProductPurchasing.json wizard\\u0027\\u0131 deploy edildi (0 demo sat\\u0131r)\",\"duration\":0.0167},{\"id\":\"d6c003ce-d518-45df-94a2-3a2428ee01c8\",\"date\":\"2026-10-07\",\"userName\":\"system@sozsoft.com\",\"description\":\"ProductSupplier.json wizard\\u0027\\u0131 deploy edildi (0 demo sat\\u0131r)\",\"duration\":0.0167},{\"id\":\"d6b290e4-6dc9-7830-f0f9-3a2428ee7bc4\",\"date\":\"2026-10-07\",\"userName\":\"system@sozsoft.com\",\"description\":\"ProductStock.json wizard\\u0027\\u0131 deploy edildi (0 demo sat\\u0131r)\",\"duration\":0.0167},{\"id\":\"bc64a3db-7a2f-5e01-effa-3a2428eedc11\",\"date\":\"2026-10-07\",\"userName\":\"system@sozsoft.com\",\"description\":\"ProductAccounting.json wizard\\u0027\\u0131 deploy edildi (0 demo sat\\u0131r)\",\"duration\":0.0167},{\"id\":\"d8959a4c-3c31-d2a3-8ca4-3a2428ef93c6\",\"date\":\"2026-10-07\",\"userName\":\"system@sozsoft.com\",\"description\":\"Product.json wizard\\u0027\\u0131 deploy edildi (6 demo sat\\u0131r)\",\"duration\":0.0167},{\"id\":\"e06847ef-a0b7-d8c5-7b14-3a2428f2c514\",\"date\":\"2026-10-07\",\"userName\":\"system@sozsoft.com\",\"description\":\"BomOverview.json wizard\\u0027\\u0131 deploy edildi (0 demo sat\\u0131r)\",\"duration\":0.05}]", + "Timesheets": null, "SubTasks": null, - "Analysis": "

1. Amaç

\n

Envanter ve üretimin ana verisini kurmak: ürün kartı (satış/masraf/satınalma bayrakları, mal/hizmet, envanter takibi), nitelik–değer–varyant yapısı, tedarikçi fiyatları, iş merkezleri, operasyonlar, ürün ve varyant bazında reçeteler (BOM) ve iç içe açılan reçete genel bakışı (ağaç). Kullanıcılar stok, satınalma ve üretim planlama personelidir; sonraki satış/satınalma/üretim belgeleri bu ekranları lookup kaynağı olarak kullanır.

\n\n

2. Görsel analizi

\n\n\n

3. Çözüm yolu

\n

Wizard List ekranları + alt formlar. Kontaklar deseni: ana liste yalnız istenen kolonları taşır, diğer alanlar aynı tabloyu okuyan TabType: Form alt formlarda (ilişki Id → Id, yalnız düzenleme). Varyant üretimi Custom Endpoint Inventory.GenerateVariants (mevcut kayıt korunur); tetikleyici Ürün › Genel Bilgi sekmesinin açılış EditorScript'i (api()). İş merkezi çalışma saati için GET Custom Endpoint Inventory.WeeklyWorkingHours (ayar istemciye açık değil). Reçete genel bakışı Wizard List, Query kaynağı, Tree görünümü (RowId / ParentRowId); Query türetilmiş tabloya sarıldığından recursive CTE kullanılamaz, açılım CROSS APPLY ile sabit 4 seviye. Custom component gerekmez.

\n\n

4. Menü yeri

\n

Envanter › Tanımlamalar › {ekran}

\n\n\n\n\n\n\n\n\n\n\n\n\n
SeviyeKodAd (TR / EN)İkonDurum
KökApp.InventoryEnvanter / InventoryFcPackageYeni
AraApp.Inventory.DefinitionsTanımlamalar / DefinitionsFcSettingsYeni
EkranApp.Inventory.ProductCategoryÜrün Kategorileri / Product CategoriesFcFolderTreeYeni
EkranApp.Inventory.AttributeNitelikler / AttributesFcListYeni
EkranApp.Inventory.WorkCenterİş Merkezleri / Work CentersFcFactoryYeni
EkranApp.Inventory.OperationOperasyonlar / OperationsFcProcessYeni
EkranApp.Inventory.BomÜrün Reçeteleri / Bills of MaterialsFcDataSheetYeni
EkranApp.Inventory.ProductVariantÜrün Varyantları / Product VariantsFcGridYeni
EkranApp.Inventory.ProductÜrünler / ProductsFcPackageYeni
EkranApp.Inventory.BomOverviewReçete Genel Bakış / BoM OverviewFcTreeStructureYeni
\n

ModuleId: Inv · İzin grubu: App.Inventory · Route: /admin/list/{ListFormCode}. Menü sırası deploy sırasını (bağımlılık sırası) izler.
\nYetki: App.Inventory › App.Inventory.Definitions › {ListFormCode}; menüsüz alt formlar taşıyıcı listenin altında.

\n\n

5. Veri modeli

\n

14 tablonun hepsi yeni (create_table; Id PK, TenantId, tam audit + soft delete otomatik; decimal = 18,4). UomId nvarchar(64) = Adm_T_Uom.Id, lookup host + kiracı birimleri, DbSourceType 16.

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
TabloTanım tablosuKolonlar (* NOT NULL, = varsayılan)Lookup
Inv_T_ProductCategoryEvetName* (128), ParentId, IsActive* =1ParentId → kendisi
Inv_T_AttributeEvetName* (128), DisplayType* (16) ='Radio', VariantCreation* (16) ='Always', IsActive* =1static
Inv_T_AttributeValueHayırAttributeId*, Name* (128), IsCustom* =0, DefaultExtraPrice* =0, ColorCode (16), Image MAX, SortOrder* int =0Attribute
Inv_T_ProductEvetName* (256), Image MAX, IsFavorite* =0, CanBeSold* =1, CanBeExpensed* =0, CanBePurchased* =1, ProductType* (16) ='Goods', IsInventoryTracked* =0, TrackingType (16), SalesPrice* =0, SalesVatRate* =20, Cost* =0, PurchaseVatRate* =20, CategoryId, UomId* (64) ='Adet', InternalReference (64), Barcode (64), GtipCode (32), InternalNote MAX, PurchaseControlPolicy* (16) ='Received', PurchaseDescription MAX, ResponsibleUserId, Weight, Volume, OriginCountry (128), LotValuation* =0, CustomLotSerialFormat (64), ReceiptNote/DeliveryNote/InternalTransferNote MAX, IncomeAccount (32), ExpenseAccount (32), HasVariants* =0, IsActive* =1ProductCategory, Adm_T_Uom, AbpUsers, Sas_H_Country
Inv_T_ProductAttributeLineHayırProductId*, AttributeId*, ValueIds* MAX (çoklu seçim)Attribute, AttributeValue (cascade)
Inv_T_ProductVariantEvetProductId*, CombinationKey* (900), ValueText (512), InternalReference (64), Barcode (64), ExtraPrice* =0, Cost* =0, IsActive* =1Product
Inv_T_ProductSupplierHayırProductId*, ProductVariantId, ContactId*, MinQuantity* =1, UomId* (64) ='Adet', UnitPrice* =0, Currency* (16) ='TRY', LeadTimeDays* int =0Adm_T_Contact, ProductVariant, Adm_T_Uom, Sas_H_Currency
Inv_T_WorkCenterEvetName* (128), Code (32), Tags (256), AlternativeWorkCenterIds MAX, WeeklyWorkingHours decimal, TimeEfficiency* =100, OeeTarget* =90, SetupTimeMin* =0, CleanupTimeMin* =0, CostPerHour* =0, CostPerEmployeeHour* =0, ExpenseAccount (32), AllowedUserIds MAX, Description MAX, IsActive* =1WorkCenter, AbpUsers
Inv_T_WorkCenterCapacityHayırWorkCenterId*, ProductId*, Capacity* =1, SetupTimeMin, CleanupTimeMinProduct
Inv_T_OperationEvetName* (128), WorkCenterId*, ApplyOnVariantValueIds MAX, CostBasis* (16) ='Actual', DurationMode* (16) ='Fixed', DefaultDurationMin* =60, IsActive* =1WorkCenter, AttributeValue
Inv_T_OperationInstructionHayırOperationId*, Title* (256), Type* (16) ='Text', Reference MAX, SortOrder* int =0static
Inv_T_BomEvetProductId*, ProductVariantId, Quantity* =1, UomId* (64) ='Adet', Reference (64), ReadinessPolicy* (16) ='AllComponents', FlexibleConsumption* (16) ='Warning', OperationType (64), ManufacturingLeadDays* int =0, PreparationDays* int =0, UseBatchSize* =0, BatchSize, IsActive* =1 — BomType yokProduct, ProductVariant, Adm_T_Uom
Inv_T_BomOperationHayırBomId*, OperationId*, WorkCenterId*, DurationMin* =0, SortOrder* int =0Operation, WorkCenter
Inv_T_BomLineHayırBomId*, ComponentProductId*, ComponentVariantId, Quantity* =1, UomId* (64) ='Adet', BomOperationId, ApplyOnVariantValueIds MAXProduct, ProductVariant, Adm_T_Uom, BomOperation
\n

Static kümeler (TR/EN): DisplayType Radio/Pills/Select/Color/Multi/Image · VariantCreation Always/Dynamic/Never · ProductType Goods/Service/Combo · TrackingType Serial/Lot/Quantity · PurchaseControlPolicy Ordered/Received · ReadinessPolicy AllComponents/FirstOperation · FlexibleConsumption Allowed/Warning/Blocked · CostBasis Actual/Theoretical · DurationMode Fixed/Computed · Instruction Type Text/Pdf/Image/Link.

\n

BomOverview Query: TableName \"Q\", KeyFieldName RowId (DbSourceType 16), ParentRowId ağaç ebeveyni; Inv_T_Bom'un TenantId ve IsDeleted'ı seçilir. Kök satır (Level 0, ParentRowId NULL) = reçete; bileşen ve operasyon satırları ebeveynlerinin altında; bileşenin aktif reçetesi (varyantı eşleşen, yoksa varyantsız, en yeni) varsa satırları o bileşenin altında açılır, 4 seviyeye kadar. Miktar = Π(üst satır miktarı ÷ alt reçete miktarı) × satır miktarı; bileşen maliyeti = miktar × (varyant maliyeti, yoksa ürün maliyeti); operasyon maliyeti = süre/60 × CostPerHour; kök ve üst bileşen ürünleri yeniden açılmaz. Kolonlar: RowId, ParentRowId, BomId, RootProduct, Level, RowType, ItemName, Quantity, Uom, UnitCost, TotalCost.

\n\n

6. Alt form ağacı

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
Taşıyıcı › SekmeHedefTabTypeMenüİlişkiEkle/Düzenle/Sil
Nitelik › Nitelik DeğerleriApp.Inventory.AttributeValueListMenüsüzId → AttributeId✓/✓/✓
Ürün › Genel BilgiApp.Inventory.ProductGeneralFormMenüsüzId → Id–/✓/–
Ürün › Nitelik & VaryantlarApp.Inventory.ProductAttributeLineListMenüsüzId → ProductId✓/✓/✓
Ürün › VaryantlarApp.Inventory.ProductVariantListMenülüId → ProductId–/✓/–
Ürün › SatınalmaApp.Inventory.ProductPurchasingFormMenüsüzId → Id–/✓/–
Ürün › TedarikçilerApp.Inventory.ProductSupplierListMenüsüzId → ProductId✓/✓/✓
Ürün › StokApp.Inventory.ProductStockFormMenüsüzId → Id–/✓/–
Ürün › MuhasebeApp.Inventory.ProductAccountingFormMenüsüzId → Id–/✓/–
Ürün › Ürün ReçeteleriApp.Inventory.BomListMenülüId → ProductId✓/✓/✓
Varyant › Varyant ReçeteleriApp.Inventory.BomListMenülüId → ProductVariantId, ProductId → ProductId✓/✓/✓
İş Merkezi › Genel BilgiApp.Inventory.WorkCenterGeneralFormMenüsüzId → Id–/✓/–
İş Merkezi › Ürün KapasitesiApp.Inventory.WorkCenterCapacityListMenüsüzId → WorkCenterId✓/✓/✓
Operasyon › AçıklamalarApp.Inventory.OperationInstructionListMenüsüzId → OperationId✓/✓/✓
Operasyon › AyarlarApp.Inventory.OperationSettingsFormMenüsüzId → Id–/✓/–
Reçete › BileşenlerApp.Inventory.BomLineListMenüsüzId → BomId✓/✓/✓
Reçete › OperasyonlarApp.Inventory.BomOperationListMenüsüzId → BomId✓/✓/✓
\n\n

7. Etkilenen kısımlar

\n\n\n

8. Görünümler ve davranış

\n\n\n

9. Kararlar

\n\n\n

10. Üretim planı

\n
    \n
  1. create_table: Inv_T_ProductCategory
  2. \n
  3. create_table: Inv_T_Attribute
  4. \n
  5. create_table: Inv_T_AttributeValue
  6. \n
  7. create_table: Inv_T_Product
  8. \n
  9. create_table: Inv_T_ProductAttributeLine
  10. \n
  11. create_table: Inv_T_ProductVariant
  12. \n
  13. create_table: Inv_T_ProductSupplier
  14. \n
  15. create_table: Inv_T_WorkCenter
  16. \n
  17. create_table: Inv_T_WorkCenterCapacity
  18. \n
  19. create_table: Inv_T_Operation
  20. \n
  21. create_table: Inv_T_OperationInstruction
  22. \n
  23. create_table: Inv_T_Bom
  24. \n
  25. create_table: Inv_T_BomOperation
  26. \n
  27. create_table: Inv_T_BomLine
  28. \n
  29. insert_row: App.DeveloperKit.CustomEndpoints (Inventory.WeeklyWorkingHours)
  30. \n
  31. deploy_wizard: ProductCategory.json
  32. \n
  33. deploy_wizard: AttributeValue.json
  34. \n
  35. deploy_wizard: Attribute.json
  36. \n
  37. deploy_wizard: WorkCenterCapacity.json
  38. \n
  39. deploy_wizard: WorkCenterGeneral.json
  40. \n
  41. deploy_wizard: WorkCenter.json
  42. \n
  43. deploy_wizard: OperationInstruction.json
  44. \n
  45. deploy_wizard: OperationSettings.json
  46. \n
  47. deploy_wizard: Operation.json
  48. \n
  49. deploy_wizard: BomOperation.json
  50. \n
  51. deploy_wizard: BomLine.json
  52. \n
  53. deploy_wizard: Bom.json
  54. \n
  55. deploy_wizard: ProductVariant.json
  56. \n
  57. deploy_wizard: ProductGeneral.json
  58. \n
  59. deploy_wizard: ProductAttributeLine.json
  60. \n
  61. deploy_wizard: ProductPurchasing.json
  62. \n
  63. deploy_wizard: ProductSupplier.json
  64. \n
  65. deploy_wizard: ProductStock.json
  66. \n
  67. deploy_wizard: ProductAccounting.json
  68. \n
  69. deploy_wizard: Product.json
  70. \n
  71. deploy_wizard: BomOverview.json
  72. \n
  73. save_module_doc: App.Inventory.Definitions
  74. \n
\n\n

11. Üretime hazır

\n

Evet

" + "Analysis": "" } ] } diff --git a/ui/src/views/form/FormButtons.tsx b/ui/src/views/form/FormButtons.tsx index 419b89ef..9bce8f0c 100644 --- a/ui/src/views/form/FormButtons.tsx +++ b/ui/src/views/form/FormButtons.tsx @@ -19,7 +19,7 @@ import FormFieldOrderPanel from './FormFieldOrderPanel' import type { FormFieldOrder } from './shared/formItems' /** Form sayfasının kullanıcı bazlı alan sırası (`useGridData`). */ -export type FormFieldOrderControl = { +type FormFieldOrderControl = { value?: FormFieldOrder preview: (order: FormFieldOrder | undefined) => void save: (order: FormFieldOrder | undefined) => Promise diff --git a/ui/src/views/form/FormDevExpress.tsx b/ui/src/views/form/FormDevExpress.tsx index cebdb3cb..3dc66e8b 100644 --- a/ui/src/views/form/FormDevExpress.tsx +++ b/ui/src/views/form/FormDevExpress.tsx @@ -23,7 +23,9 @@ import { setFormEditorReadOnly, shouldRunEditorScriptOnContentReady, translateCaption, + withButtonContext, } from '../list/shared/editingForm' +import { getCascadeClearFields, isCascadeParentEmpty } from '../list/shared/cascade' import { isTouchLikeDevice } from '../list/shared/editPopup' import { flattenFormGroups, getCascadeParentFields } from './shared/formItems' @@ -164,11 +166,27 @@ const FormDevExpress = (props: { } } + /** + * Alan değeri değişince cascade kuralının boşalttığı alanları uygular: ebeveyni bu alan olan + * çocuklar ve alanın `CascadeEmptyFields` listesi (liste popup'ıyla aynı kural). + */ + const withCascadeCleared = (data: Record, dataField: string) => { + const columns = flattenFormGroups(formItemsRef.current).map((item) => item.colData) + getCascadeClearFields(columns, dataField).forEach((field) => { + data[field] = null + }) + return data + } + /** Özel editörlerin ortak değer değişimi akışı. */ const handleCustomEditorChange = (formItem: SimpleItemWithColData, value: any) => { if (!formItem.dataField) return if (isSameValue(formDataRef.current?.[formItem.dataField], value)) return - formDataRef.current = { ...formDataRef.current, [formItem.dataField]: value } + formDataRef.current = withCascadeCleared( + { ...formDataRef.current, [formItem.dataField]: value }, + formItem.dataField, + ) + formDataSnapshotRef.current = { ...formDataRef.current } setFormData(formDataRef.current) runEditorScript(formItem, value) } @@ -182,9 +200,6 @@ const FormDevExpress = (props: { ...(index !== undefined && mode !== 'view' && !isTouchLikeDevice() ? { autoFocus: index === 1 } : {}), - ...(formItem.editorType === 'dxDateBox' - ? { useMaskBehavior: true, openOnFieldClick: true, showClearButton: true } - : {}), ...(formItem.colData?.placeHolder ? { placeholder: translate('::' + formItem.colData.placeHolder) } : {}), @@ -202,27 +217,14 @@ const FormDevExpress = (props: { }, } : {}), + // onClick metni item üretilirken çözülür (`resolveEditorButtons`); burada kayıt bağlamı eklenir. ...(configuredButtons?.length ? { - buttons: configuredButtons.map((button: any) => { - // Yapılandırmadan string olarak gelen onClick bir kez fonksiyona çevrilir. - if (typeof button?.options?.onClick !== 'string') return button - - const originalClick = eval(`(${button.options.onClick})`) - return { - ...button, - options: { - ...button.options, - onClick: (e: any) => - originalClick({ - ...e, - formData: formDataRef.current, - fieldName: formItem.dataField, - mode, - }), - }, - } - }), + buttons: withButtonContext(configuredButtons, () => ({ + formData: formDataRef.current, + fieldName: formItem.dataField, + mode, + })), } : {}), } @@ -266,7 +268,7 @@ const FormDevExpress = (props: { editor.option( 'disabled', item.editorOptions?.disabled === true || - parentFields.some((parentField) => !formDataRef.current?.[parentField]), + isCascadeParentEmpty(parentFields, formDataRef.current), ) } catch (err) { console.debug('Cascade disabled update skipped for', item.dataField, err) @@ -350,7 +352,15 @@ const FormDevExpress = (props: { /** Platform editörünün render fonksiyonunu döner. */ const renderCustomEditor = (formItem: SimpleItemWithColData): ReactNode => { const value = formData[formItem.dataField!] - const editorOptions = getEditorOptions(formItem) + const parentFields = getCascadeParentFields(formItem) + const editorOptions = { + ...getEditorOptions(formItem), + // DevExtreme editörlerinde updateCascadeDisabledStates yapar; platform editörleri + // form.getEditor ile bulunamadığı için kilit burada verilir. + ...(mode !== 'view' && parentFields.length && isCascadeParentEmpty(parentFields, formData) + ? { disabled: true } + : {}), + } const onChange = (next: any) => handleCustomEditorChange(formItem, next) switch (formItem.editorType2) { @@ -359,7 +369,6 @@ const FormDevExpress = (props: { getCascadeParentFields(item).includes(e.dataField!)) - .forEach((child) => { - newFormData[child.dataField!] = null - }) + const newFormData = withCascadeCleared({ ...formData, [e.dataField]: e.value }, e.dataField) formDataSnapshotRef.current = { ...newFormData } formDataRef.current = newFormData diff --git a/ui/src/views/form/editors/GridBoxEditorComponent.tsx b/ui/src/views/form/editors/GridBoxEditorComponent.tsx index 6742f271..1a977c66 100644 --- a/ui/src/views/form/editors/GridBoxEditorComponent.tsx +++ b/ui/src/views/form/editors/GridBoxEditorComponent.tsx @@ -2,6 +2,7 @@ import { ColumnFormatDto, GridBoxOptionsDto } from '@/proxy/form/models' import { ReactElement } from 'react' import { SharedGridBoxEditor } from '@/views/shared/SharedGridBoxEditor' +/** Form sayfası ve liste düzenleme hücresinin ortak GridBox editörü. */ const GridBoxEditorComponent = ({ value, options, @@ -11,7 +12,6 @@ const GridBoxEditorComponent = ({ }: { value: any options?: GridBoxOptionsDto - values: any // all row data onValueChanged: (e: any) => void col?: ColumnFormatDto editorOptions: any @@ -22,9 +22,13 @@ const GridBoxEditorComponent = ({ ) diff --git a/ui/src/views/form/editors/ImageUploadEditorComponent.tsx b/ui/src/views/form/editors/ImageUploadEditorComponent.tsx index 7d72d27b..c60c2145 100644 --- a/ui/src/views/form/editors/ImageUploadEditorComponent.tsx +++ b/ui/src/views/form/editors/ImageUploadEditorComponent.tsx @@ -13,6 +13,51 @@ import { normalizeImageSize } from '../../list/shared/imageValue' import Input from '@/components/ui/Input' import { supportsPhotoCapture } from '@/utils/device' +/** + * Yüklenen değerin ham biçimini korur; ayraçlara göre bölmez. + * (Görüntüleme tarafındaki `normalizeImageValue` ile bilinçli olarak farklıdır: + * burada kullanıcı tarafından yazılmış virgüllü URL'ler bozulmamalıdır.) + */ +const normalizeUploadValue = (value: unknown): string[] => { + if (!value) return [] + if (Array.isArray(value)) return value.flatMap(normalizeUploadValue) + + if (typeof value === 'string') { + const trimmed = value.trim() + if (!trimmed) return [] + + if (trimmed.startsWith('[')) { + try { + const parsed = JSON.parse(trimmed) + if (Array.isArray(parsed)) return parsed.flatMap(normalizeUploadValue) + } catch { + return [trimmed] + } + } + + return [trimmed] + } + + return [String(value)].filter(Boolean) +} + +const resolveUploadedUrl = (data: unknown) => { + if (typeof data === 'string') return data + if (!data || typeof data !== 'object') return '' + + const record = data as Record + return String(record.url ?? record.fileUrl ?? record.path ?? '').trim() +} + +const toBase64 = (file: File): Promise => + new Promise((resolve, reject) => { + const reader = new FileReader() + reader.onload = () => resolve(reader.result as string) + reader.onerror = reject + reader.readAsDataURL(file) + }) + +/** Form sayfası ve liste düzenleme hücresinin ortak görsel yükleme editörü. */ const ImageUploadEditorComponent = ({ value, options, @@ -25,7 +70,7 @@ const ImageUploadEditorComponent = ({ editorOptions?: any }): ReactElement => { const { translate } = useLocalization() - const readOnly: boolean = editorOptions?.readOnly ?? false + const readOnly: boolean = editorOptions?.readOnly === true || editorOptions?.disabled === true // options önce prop olarak gelen ImageUploadOptionsDto'dan, sonra editorOptions JSON'undan okunur const resolvedOptions: ImageUploadOptionsDto = options ?? editorOptions ?? {} @@ -40,10 +85,7 @@ const ImageUploadEditorComponent = ({ // aksi halde dokunmatik cihazda dosya seciciye ek olarak kamera sunulur. const showCameraButton = !resolvedOptions.capture && supportsPhotoCapture() - const valueUrls = useMemo( - () => (Array.isArray(value) ? value : [value]).filter(Boolean) as string[], - [value], - ) + const valueUrls = useMemo(() => normalizeUploadValue(value), [value]) const [urls, setUrls] = useState(valueUrls) const [uploading, setUploading] = useState(false) @@ -63,14 +105,6 @@ const ImageUploadEditorComponent = ({ onValueChanged(newValue) } - const toBase64 = (file: File): Promise => - new Promise((resolve, reject) => { - const reader = new FileReader() - reader.onload = () => resolve(reader.result as string) - reader.onerror = reject - reader.readAsDataURL(file) - }) - const handleFiles = async (files: FileList | null) => { if (!files?.length) return @@ -87,7 +121,7 @@ const ImageUploadEditorComponent = ({ const res = await fetch(resolvedOptions.uploadUrl, { method: 'POST', body: formData }) if (!res.ok) throw new Error(`Upload failed: ${res.status}`) const data = await res.json() - uploadedUrls.push(String(data?.url ?? data?.fileUrl ?? data?.path ?? data ?? '').trim()) + uploadedUrls.push(resolveUploadedUrl(data)) } else { // uploadUrl yoksa base64 olarak sakla const base64 = await toBase64(file) diff --git a/ui/src/views/form/editors/TagBoxEditorComponent.tsx b/ui/src/views/form/editors/TagBoxEditorComponent.tsx index 6c9bc7a5..353dfce1 100644 --- a/ui/src/views/form/editors/TagBoxEditorComponent.tsx +++ b/ui/src/views/form/editors/TagBoxEditorComponent.tsx @@ -14,7 +14,6 @@ const TagBoxEditorComponent = ({ value: any setDefaultValue: boolean options?: TagBoxOptionsDto - values: any // all row data onValueChanged: (e: any) => void col?: ColumnFormatDto editorOptions: any @@ -27,11 +26,6 @@ const TagBoxEditorComponent = ({ [lookupDto?.displayExpr], ) - // const dataSource = - // typeof lookup?.dataSource === 'function' - // ? lookup.dataSource(values) - // : (lookup?.dataSource ?? []) - return ( +const flattenFormItems = (items: any[] = []): SimpleItemWithColData[] => items.flatMap((item) => [ ...(item?.dataField ? [item] : []), ...flattenFormItems(item?.items || []), @@ -30,10 +33,7 @@ export const flattenFormGroups = (groups: GroupItem[] = []): SimpleItemWithColDa /** Bir alanın cascade parent alan adlarını döner. */ export const getCascadeParentFields = (item: SimpleItemWithColData): string[] => - item.colData?.lookupDto?.cascadeParentFields - ?.split(',') - .map((field: string) => field.trim()) - .filter(Boolean) ?? [] + getCascadeParentFieldsOf(item.colData) type BuildFormItemsParams = { gridDto: GridDto @@ -77,16 +77,18 @@ const toFormItem = ( console.error('EditorOptions parse error:', colFormat.fieldName, err) } + // Liste popup'ıyla aynı lookup varsayılanları; veri kaynağını form sayfası kendisi kurar. const lookupEditorOptions = colFormat.lookupDto?.dataSourceType ? { + ...getLookupEditorDefaults(colFormat), dataSource: getLookupDataSource(colFormat, formData), - valueExpr: colFormat.lookupDto?.valueExpr?.toLowerCase(), - displayExpr: colFormat.lookupDto?.displayExpr?.toLowerCase(), - // Grid popup'ındaki lookup kolonu aramayı varsayılan açar; form sayfası aynı davranır. - searchEnabled: true, } : {} + if (parsedEditorOptions.buttons) { + parsedEditorOptions.buttons = resolveEditorButtons(parsedEditorOptions.buttons) + } + const item: SimpleItemWithColData = { canRead: colFormat.canRead ?? false, canUpdate: colFormat.canUpdate ?? false, @@ -99,10 +101,11 @@ const toFormItem = ( editorType2: colFormat.editorType2, editorType: resolveEditorType(colFormat.editorType2), colSpan: colFormat.colSpan, - editorOptions: withEditorTypeDefaults(colFormat.editorType2, { - ...lookupEditorOptions, - ...parsedEditorOptions, - }), + editorOptions: withEditorTypeDefaults( + colFormat.editorType2, + { ...lookupEditorOptions, ...parsedEditorOptions }, + colFormat.sourceDbType, + ), // DevExtreme item seçeneklerini yerinde (deep extend) günceller; ekran tanımı (gridDto) paylaşılırsa // sıra değişince bir alanın tanımı diğerininkiyle ezilir. Item'a tanımın kopyası verilir. colData: structuredClone(colFormat), diff --git a/ui/src/views/form/useLookupDataSource.ts b/ui/src/views/form/useLookupDataSource.ts index fcbf15b5..fad31b64 100644 --- a/ui/src/views/form/useLookupDataSource.ts +++ b/ui/src/views/form/useLookupDataSource.ts @@ -28,7 +28,7 @@ const isEmptyValue = (value: unknown) => value === undefined || value === null | * StaticData için öğelerdeki `CascadeRelationField` alanı (varsayılan `group`) ilk ebeveynin * değeriyle karşılaştırılır; ebeveyn boşsa süzülmez. */ -export const buildCascadeFilters = (lookupDto: any, rowData: any): any[] | null => { +const buildCascadeFilters = (lookupDto: any, rowData: any): any[] | null => { const parentFields = splitFields(lookupDto?.cascadeParentFields) if (!parentFields.length) return null @@ -46,7 +46,7 @@ export const buildCascadeFilters = (lookupDto: any, rowData: any): any[] | null return parentFields.map((field) => (isEmptyValue(rowData?.[field]) ? null : rowData[field])) } -export const createLookupStaticDataSource = ( +const createLookupStaticDataSource = ( load: () => any, filter: any[] | null, cacheKey: string, @@ -65,7 +65,7 @@ export const createLookupStaticDataSource = ( filter, }) -export const createLookupQueryDataSource = ( +const createLookupQueryDataSource = ( listFormCode?: string, listFormFieldName?: string, filters?: any[] | null, @@ -106,7 +106,7 @@ export const createLookupQueryDataSource = ( /** Gövde JSON şablonudur; değer tırnak içine yerleştiği için JSON kaçışlı yazılır. */ const toJsonText = (value: unknown) => (value === null ? 'null' : JSON.stringify(String(value)).slice(1, -1)) -export const createLookupApiDataSource = ( +const createLookupApiDataSource = ( listFormCode?: string, lookupQuery?: string, filters?: any[] | null, diff --git a/ui/src/views/list/CardView.tsx b/ui/src/views/list/CardView.tsx index 1150f300..862feb66 100644 --- a/ui/src/views/list/CardView.tsx +++ b/ui/src/views/list/CardView.tsx @@ -41,8 +41,7 @@ import { NotePanel } from '../form/notes/NotePanel' import SubForms from '../form/SubForms' import GridFilterDialogs from './GridFilterDialogs' import { GridExtraFilterToolbar } from './GridExtraFilterToolbar' -import { ImageUploadEditorComponent } from './editors/ImageUploadEditorComponent' -import { ImageViewerEditorComponent } from './editors/ImageViewerEditorComponent' +import { ImageUploadEditorComponent, ImageViewerEditorComponent } from './editors/cellEditors' import { autoNumber, extractSearchParamsFields, diff --git a/ui/src/views/list/Grid.tsx b/ui/src/views/list/Grid.tsx index 618264f6..6a8bdf64 100644 --- a/ui/src/views/list/Grid.tsx +++ b/ui/src/views/list/Grid.tsx @@ -37,10 +37,7 @@ import SubForms from '../form/SubForms' import { RowMode } from '../form/types' import { GridColumnData } from './GridColumnData' import GridFilterDialogs from './GridFilterDialogs' -import { GridBoxEditorComponent } from './editors/GridBoxEditorComponent' -import { ImageUploadEditorComponent } from './editors/ImageUploadEditorComponent' -import { ImageViewerEditorComponent } from './editors/ImageViewerEditorComponent' -import { TagBoxEditorComponent } from './editors/TagBoxEditorComponent' +import { GridBoxEditorComponent, ImageUploadEditorComponent, ImageViewerEditorComponent, TagBoxEditorComponent } from './editors/cellEditors' import { useFilters } from './useFilters' import { updateWorkflowApprovalToolbarItems, useToolbar } from './useToolbar' import { ImportDashboard } from '@/components/importManager/ImportDashboard' @@ -63,16 +60,14 @@ import { buildUpdatePayload, createFormItemMapper, expandJsonFormFields, - getActiveEditingForm, getValueByField, } from './shared/editingForm' import { - attachEditorButtonContext, - attachEditorValueChangedScript, + createCustomEditorCellAdapter, createEditingFormHandlers, + prepareEditingEditor, } from './shared/editingFormScripts' -import { attachFieldValueBroadcast } from '@/utils/phoneFormat' -import { attachCascadeBehavior, createCascadeMaps } from './shared/cascade' +import { createCascadeMaps } from './shared/cascade' import { createConditionalCellStyleHandler } from './shared/cellStyles' import { useExtraFilters, @@ -343,61 +338,27 @@ const Grid = (props: GridProps) => { const onEditorPreparing = useCallback( (editor: DataGridTypes.EditorPreparingEvent) => { - if (editor.parentType !== 'dataRow' || !editor.dataField || !gridDto) return - - const columnFormat = gridDto.columnFormats.find( - (column) => column.fieldName === editor.dataField, - ) - const isNewRow = Boolean((editor as any).row?.isNewRow) || mode === 'new' - - if (columnFormat?.editorOptions) { - try { - editor.editorOptions = { - ...editor.editorOptions, - ...JSON.parse(columnFormat.editorOptions), - } - } catch (err) { - console.error('EditorOptions parse error:', columnFormat.fieldName, err) - } - } - - const hasReadOnlyEditorOption = editor.editorOptions?.readOnly === true - const hasDisabledEditorOption = editor.editorOptions?.disabled === true - - if ( - (isNewRow && columnFormat?.allowAdding === false) || - (!isNewRow && columnFormat?.allowEditing === false) - ) { - editor.editorOptions.readOnly = true - } else if (isNewRow && columnFormat?.allowAdding === true) { - if (!hasReadOnlyEditorOption) editor.editorOptions.readOnly = false - if (!hasDisabledEditorOption) editor.editorOptions.disabled = false - } - - const cascadeInfo = cascadeMaps.cascadeFieldsMap.get(editor.dataField) - if (cascadeInfo) { - attachCascadeBehavior({ editor, gridDto, cascadeInfo, maps: cascadeMaps }) - } - - // phoneCountry maskesi ülke alanını bu olaydan izler; DataGrid formu fieldDataChanged yayınlamaz. - attachFieldValueBroadcast(editor) - - // Yalnızca düzenleme formunda yer alan alanların script'i çalıştırılır. - if (columnFormat?.editOrderNo != null && columnFormat.editorScript) { - attachEditorValueChangedScript({ - editor, - editorScript: columnFormat.editorScript, - fieldName: columnFormat.fieldName, - editingFormInstanceRef, - getActiveEditingForm, - }) - } - - attachEditorButtonContext(editor) + if (!gridDto) return + prepareEditingEditor({ editor, gridDto, mode, cascadeMaps, editingFormInstanceRef }) }, [gridDto, cascadeMaps, mode], ) + // Şablon fonksiyonları DevExtreme'de bir kez bağlanır; mod ref üzerinden okunur. + const modeRef = useRef(mode) + modeRef.current = mode + + const adaptCustomEditorCell = useMemo( + () => + createCustomEditorCellAdapter({ + getGridDto: () => gridDto, + getMode: () => modeRef.current, + cascadeMaps, + editingFormInstanceRef, + }), + [gridDto, cascadeMaps], + ) + /* ======================= COLUMNS & DATASOURCE ======================= */ @@ -665,9 +626,18 @@ const Grid = (props: GridProps) => { items: editingFormItems, }} /> -