EditorScript düzenlemeleri

This commit is contained in:
Sedat ÖZTÜRK 2026-10-08 01:07:28 +03:00
parent dea283e7cf
commit 1ede045bac
38 changed files with 837 additions and 447 deletions

View file

@ -546,7 +546,9 @@ eklenen dosya) bu satırlar ekranla birlikte seed edilir; migrate çalıştığ
1. `deploy_wizard` cevabına `SeedData: true` yaz, satırların hepsini aynı çağrıda `seedRows` ile
gönder (alan adından değere nesne dizisi). Ekranın `SeedFilePath`'i `data/{ListFormCode}.json`
olur, `SeedSyncInsert/Update/Delete` açılır (§9.15); satırlar deploy anında bu dosyaya yazılır.
olur; satırlar deploy anında bu dosyaya yazılır. `SeedSyncInsert/Update/Delete` (§9.15) demo veriyle
**açılmaz** — varsayılan `false`'tur; yalnızca istek "ekrandan girilen kayıtlar seed dosyasına
yansısın" diyorsa wizard cevabına `SeedSync: true` yazılır (wizard ekranında da aynı kural).
`Guid` anahtarı sunucu üretir; `TenantId` ve audit alanlarını migrate doldurur — yazma. Aynı
içerikli satır dosyada varsa (tekrar deploy) yeniden eklenmez.
2. DB Migrate'i bekleme. Migrate wizard seeder ile ekranı kurar, ardından `ListFormSeedDataApplier`
@ -1275,7 +1277,8 @@ atanmaz (admin dahil); ekranı görecek roller yetkiyi rol ekranından alır.
| --- | --- |
| `IsTenant` / `IsBranch` / `IsOrganizationUnit` | `[ListForm]` Otomatik kırılım filtresi. Tabloda `TenantId` varsa `IsTenant: true` **olmalı**. |
| `AllowAdding` / `AllowUpdating` / `AllowDeleting` / `ConfirmDelete` | `[ListForm]` Toolbar ve satır aksiyonları (`EditingOptionDto`). |
| `SeedData` | `[ListForm]` `true` ise `SeedFilePath = data/{ListFormCode}.json` ve `SeedSyncInsert/Update/Delete` açık üretilir; ekrandan girilen satır dosyaya yazılır, migrate geri yükler. Wizard ekranında sorulmaz; demo veri taşıyan MCP üretimi için (§0.10.1). Varsayılan `false`. |
| `SeedData` | `[ListForm]` `true` ise `SeedFilePath = data/{ListFormCode}.json` üretilir; migrate dosyadaki satırları yükler. Wizard ekranında sorulmaz; demo veri taşıyan MCP üretimi için (§0.10.1). Varsayılan `false`. |
| `SeedSync` | `[ListForm]` `SeedData` açıkken `SeedSyncInsert/Update/Delete`'i açar: ekrandan eklenen/değiştirilen/silinen kayıt dosyaya yansır. **Varsayılan `false`**; demo veri bunu açmaz, yalnızca istekte açıkça istenirse `true` yazılır. Okurken üç bayraktan biri açıksa `true`; tek tek ayar `ListForm` bölümünde yapılır. |
| `AllowDetail` | `[ListForm]` Satırda **Detay** düğmesi çıkar; `/admin/form/{ListFormCode}/{anahtar}` adresini yeni sekmede açar — aynı ekranın tek kayıtlık form görünümüdür. `KeyFieldName` boşsa çalışmaz. **`SubForms` doluysa cevaptan bağımsız `true` üretilir** (`ListFormWizardDto.BuildListForm`): wizard alt formları `Form` tipinde kurar ve sekmeler yalnızca Detay görünümünde açılır. Elle yazılan wizard dosyasında da alt formu olan ekranın `EditingOptionDto.AllowDetail` değeri `true` yazılır. **Alt formu olmayan listede `false` kalır** — MCP/AI üretiminde Detay düğmesi yalnızca `SubForms` taşıyan listeye açılır; kullanıcı açıkça istemedikçe tek tablolu liste ya da alt form ekranının kendisi (`PerformancePeriodSkill` gibi) Detay almaz. |
| `DefaultLayout` | `[ListForm]` `"grid"`, `"card"`, `"pivot"`, `"chart"`, `"tree"`, `"gantt"`, `"scheduler"`, `"todo"`. |
| `Grid` `Card` `Pivot` `Chart` `Tree` `Gantt` `Scheduler` `Todo` | `[ListForm]` Hangi görünümlerin açık olduğu (`LayoutDto`). **Liste ekranında `Grid`/`Card`/`Pivot`/`Chart` varsayılan `true`**, diğer dördü `false` (§9.8). Açtığın her görünümün option bloğunu da doldur. |
@ -1876,6 +1879,11 @@ bölümlerine düşer (§4.1). `SeededAt` ilk deploy anında sunucu tarafından
> tutarlı** olmalıdır: açılışta çalışan bir kural varsa (hesap tarifleri varsayılan `both`)
> `// @runOnOpen` satırı gelir ve metin literalleri çift tırnaklıdır (`JSON.stringify`). Elle yazmak yerine C# tarafında `EditorScript.Build(...)` kullan ya da
> Script Builder'da üret; uyuşmazsa dialog script'i "elle düzenlenmiş" sayar.
> **Wizard/MCP deploy'u script'i kurallardan yeniden üretir** (`EditorScript.Rebuild`): `// @builder`
> başlıklı script'in gövdesi atılıp kurallardan yazılır; `EditorScript` alanına yalnızca kural JSON'u
> (`{"v":1,"rules":[…]}`) da verilebilir. MCP script'i elle yazmaz, kuralları verir — elle yazılan
> gövde (yanlış yardımcı kullanımı) çalışmayan script üretiyordu. Başlıksız (tamamen elle yazılmış)
> script olduğu gibi kalır.
### 4.4.1 Deploy'un ve seeder'ın senin yerine ürettikleri — cevaba yazma
@ -2658,7 +2666,7 @@ bağlıdır.
| calc | `days` | start, end, target | `set('T', days('Bas', 'Bit'))` — bitiş günü **dahil** |
| calc | `hours` | start, end, target | `set('T', hours('Bas', 'Bit'))` — bitiş küçükse ertesi güne taşar |
| data | `copy` | source (seçili kayıttaki kolon/yol), target | `copy('unitPrice', 'UnitPrice')` |
| data | `setValue` | target, text | `set('T', tpl('{Musteri} - {value}'))` |
| data | `setValue` | target, text | `set('T', tpl('{Musteri} - {value}'))`; `text` `true`/`false` ise mantıksal sabit yazılır: `set('T', false)` — onay kutusuna metin `'false'` dolu sayılırdı. Runtime da mevcut değeri mantıksal olan alana gelen `'true'/'false'/'1'/'0'`'ı mantıksala çevirir |
| data | `clear` | sources[] | `clear('Il', 'Ilce')` |
| view | `readOnly` | target, invert? | `readOnly('Alan', <koşul>)` · invert → `readOnly('Alan', !(<koşul>))` · koşulsuz → `readOnly('Alan', true)` |
| interaction | `notify` | message | `notify('Limit aşıldı')` — mesajda `{Alan}` şablonu geçerli |
@ -2726,14 +2734,14 @@ Script `AsyncFunction` ile derlenir, aşağıdaki adlar parametre olarak enjekte
| `data` | Script'in üzerinde çalıştığı canlı form verisi (kopya) |
| `isReady` | `true` ise script form açılışında çalışıyor |
| `get('Alan')` | Form verisinden okur. **Nokta yolu** (`Musteri.Unvan`) ve **büyük/küçük harf duyarsız** eşleşme destekler (SQL kolon adları tutarsız gelebiliyor) |
| `num()` · `str()` · `bool()` | Tip dönüşümü. `bool` için `true`/`'true'`/`1`/`'1'` doğrudur |
| `num()` · `str()` · `bool()` | **Alan adı** alır, değeri değil: `str('ProductType')`. `str(get('X'))` ya da `str(value)` değeri alan adı sanıp boş döner — koşul hiç tutmaz. Değişen alanın değeri için `str(field)` ya da doğrudan `value` kullanılır. `bool` için `true`/`'true'`/`1`/`'1'` doğrudur |
| `dateOf('Alan')` | `Date` ya da `null` |
| `pick(nesne, 'a.b')` | Herhangi bir nesneden yol okuma |
| `set('Alan', deger)` · `set({A:1, B:2})` | Yazar. Değişmeyen değer yazılmaz; yazmalar biriktirilip **tek flush** ile forma ve düzenlenen grid hücresine uygulanır |
| `clear('A','B')` | Verilen alanları `null` yapar |
| `copy('kolon','Alan')` | Lookup/GridBox'ta **seçili kayıttan** taşır; hedef verilmezse yolun son parçası hedef olur. Seçili kayıt, olay formdan gelse de (popup form, form sayfası) değişen alanın editöründen okunur; lookup sorgusu taşınacak kolonları döndürmelidir (ör. `SELECT Id AS [Key], …, DepartmentId, JobPositionId`) |
| `selected('Yol')` · `snum()` · `sstr()` | Seçili kaydın tamamı / sayı / metin hâli |
| `readOnly('Alan', kosul)` | Formdaki **herhangi bir** editörü kilitler/açar (yalnızca script'in bağlı olduğu alanı değil). Grid popup'ında açılış çalışması (`isReady`) editörler her yeniden çizildiğinde tekrarlanır ve form verisi o anda güncel değildir; koşul başka bir alana bakıyorsa değer `editor.component.cellValue(satır, 'Alan')` ile okunur (örnek: `App.Hr.md` › İzin Yönetimi §5.1) |
| `readOnly('Alan', kosul)` | Formdaki **herhangi bir** editörü kilitler/açar (yalnızca script'in bağlı olduğu alanı değil). Grid popup'ında açılış çalışması (`isReady`) form her yeniden çizildiğinde (her `cellValue` yazımı) tekrarlanır; veri grid'in düzenlenen satırından güncel okunur (`getEditingRow`), `str('Alan')` yeterlidir |
| `round(x, 2)` · `sum('A','B')` | Matematik |
| `days('Bas','Bit')` · `hours('Bas','Bit')` | Gün farkı (bitiş dahil) · saat farkı (negatifse +24) |
| `tpl('{Alan} - {value}')` | Şablon: `{Alan}`, `{value}`, `{selected.Yol}` |
@ -2750,6 +2758,37 @@ Eski (uzun) script'ler bozulmasın diye geriye dönük uyum vardır: script içi
(`formData, e, editor, runtimeSetEditorReadOnly, setFormData`) derlenir ve **yardımcılar
tanımsız kalır**. Yardımcı adlarını yeniden tanımlama.
**Zincir ve kalıcı readOnly (Grid/Tree popup).** Script'in `set` ile yazdığı alanın kendi script'i de
çalışır — kullanıcı o alanı değiştirmiş gibi (`runChainedScripts`; derinlik sınırı yoktur, zincir yazılan kadar ilerler; `ProductType` →
`TrackInventory = false` → `TrackingMethod` salt okunur). Grid'e `cellValue` ile yazılan değer editörün
değişiklik olayını tetiklemediği için zincir olmadan bağımlı kural ancak kullanıcı alanı elle değiştirince
çalışırdı. Yazma popup formunu yeniden çizer (`editForm.repaint`) ve açılış script'leri yeniden çalışır;
formun `formData`'sı açılıştaki nesne olduğundan veri grid'in düzenlenen satırından okunur
(`getEditingRow`, `list/shared/editingForm.ts` — yeni satırın anahtarı nesnedir, `row.key ===` eşleşmez).
Eski veriyle çalışan script kendi `readOnly` kararını geri alırdı. Popup formu
`getController('editing')._editForm`'dadır (`getActiveEditingForm`). Form sayfası `updateData` ile alan
olayını zaten tetikler.
Zincirde bir alanın aynı değerle ikinci kez tetiklenmesi döngüdür (A → B → A) ve durdurulur; değeri her turda
değişen döngüyü 1000 adımlık emniyet sınırı yakalar. İkisi de kullanıcıya hata olarak gösterilir.
**Hatalı script kullanıcıya gösterilir** — derleme ya da çalışma hatası konsola düşmekle kalmaz, ekranda
`App.EditorScript.Error` bildirimi çıkar (alan adı + hata; `reportEditorScriptError`,
`utils/editorScriptRuntime.ts`). Form her yeniden çizildiğinde aynı hata 5 saniyede bir kez gösterilir.
**Derlenemeyen script hiç çalışmaz** — bir satırdaki söz dizimi hatası (eksik parantez) diğer kuralları da
durdurur. Script Builder kaydetmeden önce metni derler ve hatada kaydetmez (`App.ScriptBuilder.SyntaxError`).
**Script Builder ile MCP aynı kodu üretir.** Kod `// @builder` başlığındaki kurallardan üretilir; MCP/wizard
deploy'u gövdeyi atıp kuralları `EditorScript.Rebuild` ile yeniden üretir (C# çıktısı `scriptRecipes.ts` ile
birebir). Onay kutusu/anahtar alanına değer yazan `setValue` kuralında dialog serbest metin yerine Evet/Hayır
sectirir (`getParamChoices`); deploy aynı alana gelen `0`/`1`/`evet`/`hayır` değerini `true`/`false`'a çevirir.
Kod editöründe kurallarla eşleşmeyecek şekilde elle değiştirilen script'te dialog uyarır
(`App.ScriptBuilder.StaleHeader`) ve kaydederken bayat başlığı siler; başlık kalsaydı deploy gövdeyi başlıktan
yeniden üretip elle yapılan düzeltmeyi geri alırdı.
`readOnly` tarifinde `invert: "true"` koşulu tersler: "koşul sağlanmıyorsa salt okunur". "X seçilince kilitle"
için `invert` boş bırakılır.
### 5.7 Yetenek sınırları — script ile yapılamayanlar
- **Alan gizleme yok.** Runtime'da `visible` yardımcısı yoktur; koşullu görünürlük yerine

View file

@ -1563,7 +1563,7 @@ liste süzgeci ve çağrı kapısı aynı kuralı kullanır.
| `save_sql_object` | View/procedure/function scriptini `sql/object/{Ad}.sql` ve `postgres/object/{Ad}.sql` dosyalarına yazar (`McpSqlObjectWriter`); veritabanında çalıştırılmaz, nesneyi DB Migrate kurar. Gövde diyalektler arasında güvenle çevrilemediği için iki script de istemciden gelir ve aynı nesneyi tanımlamalıdır (`SqlObjectInvalid`); ad scriptten okunur, başlık `CREATE OR ALTER` / `CREATE OR REPLACE`'e çevrilir. Tablo scriptinin üstüne yazmaz. `.Write` + `App.SqlQueryManager` ister. |
| `generate_crud_endpoints` | SQL Query Manager CRUD diyaloğuyla aynı endpoint kümesini (`CrudEndpointSeedFileDto.Generate` — diyaloğun servisi de bunu kullanır) yalnızca `crud/{Entity}.json` dosyasına yazar; kayıtları DB Migrate ekler (`CrudEndpointSeedApplier`). `.Write` + `App.SqlQueryManager.CrudEndpoints` ister. |
| `list_wizards` / `get_wizard` | Wizard File Manager dosyaları; `get_wizard` seed dosyasını diskteki biçimiyle döndürür. |
| `deploy_wizard` | Wizard'ı Wizard ekranıyla aynı yoldan (`IListFormWizardAppService.Create`) deploy eder. Girdi seed dosyası biçimi (`{"Wizard": …, "ListForm": …, "Fields": […]}`) ya da düz cevaplardır; ikisi de `WizardSeedFileDto.FromDeployJson`'dan geçer: Wizard bölümünde yazılan cevap korunur, yazılmayan ekran alanı `ListForm`/`Fields`'tan tamamlanır, iki bölüm de ekrana katman olarak uygulanır (`WizardScreenOverlay`, üç yönlü: bölümdeki değer wizard'ın kendi ürettiğiyle aynıysa cevap kazanır, farklıysa editör ayarıdır ve korunur; kimlik alanları wizard'dan gelir, tanınmayan/hesaplanan anahtar reddedilir). Böylece MCP, ListForm editörünün yapabildiği her ayarı wizard dosyasına yazar ve `replaceFileName` ile güncelleme editör ayarlarını silmez. Yeni kaydın form varsayılanı kolonun tablodaki sabit `DEFAULT`'udur (katalog ya da seed scripti, `SqlSeedColumn.ConstantValue`; IsActive → true, UomId → Adet, para birimi → TRY tablonun `DEFAULT`'larından gelir); alana özel değer öğenin `DefaultValue`'su ile verilir (`lowcode.instructions.md` §4.3.5.1). Girdide sözleşmede olmayan anahtar adıyla reddedilir, sessizce yok sayılmaz. `StaticData` lookup seçeneklerinin düz metni `App.StaticLookup.*` dil anahtarına çevrilir ve EN + TR satırı yazılır (`LocalizeStaticLookupsAsync`, `lowcode.instructions.md` §4.3.4.1). Dosyaya kullanıcının etkin talebi `DevRequest` olarak yazılır (sunucu doldurur). Wizard ekranı gibi yalnızca seed dosyalarını yazar; ekran DB Migrate ile kurulur ve çıkıştaki `isPending` o zamana kadar `true` kalır (model migrate'i beklemez; `verify_wizard` bekleyen dosyada `install` kontrolünde durur). `replaceFileName` doluysa güncellemedir (dosyalar yeniden yazılır, önceki kayıtları migrate kaldırır). İstek demo veri taşıyorsa (prompt'taki satırlar, ekran görüntüsü) wizard `SeedData: true` ile deploy edilir — ekranın `SeedFilePath`'i `data/{ListFormCode}.json` olur ve `SeedSync*` bayrakları açılır — satırlar aynı çağrıda `seedRows` ile bu dosyaya yazılır (alan adları doğrulanır, `Guid` anahtarı sunucu üretir); migrate ekranı kurduktan sonra satırları ekler (`lowcode.instructions.md` §0.10.1). Widget sorgularındaki her dil anahtarı tanımlı olmalı ya da wizard'ın `Language.Texts` alanında EN + TR verilmeli, yoksa deploy adlarıyla reddeder (`ValidateWidgetLanguageKeysAsync`). `uploadUrl`'süz `dxImageUpload` alanı base64 sakladığı için kolonu sınırlı boyluysa (katalogdaki karakter sınırı) deploy reddedilir; tablo kolonuna bağlı `dxImageViewer` alanı da reddedilir — görsel kolonunun editörü her zaman `dxImageUpload`'dır (`ValidateImageColumnLengths`). `MenuOrder` yok sayılır: yeni ekran kardeşlerinin sonuna, güncelleme mevcut yerine yazılır — lookup kaynağı ekran önce deploy edildiği için onu kullanan ekranın üstünde kalır. |
| `deploy_wizard` | Wizard'ı Wizard ekranıyla aynı yoldan (`IListFormWizardAppService.Create`) deploy eder. Girdi seed dosyası biçimi (`{"Wizard": …, "ListForm": …, "Fields": […]}`) ya da düz cevaplardır; ikisi de `WizardSeedFileDto.FromDeployJson`'dan geçer: Wizard bölümünde yazılan cevap korunur, yazılmayan ekran alanı `ListForm`/`Fields`'tan tamamlanır, iki bölüm de ekrana katman olarak uygulanır (`WizardScreenOverlay`, üç yönlü: bölümdeki değer wizard'ın kendi ürettiğiyle aynıysa cevap kazanır, farklıysa editör ayarıdır ve korunur; kimlik alanları wizard'dan gelir, tanınmayan/hesaplanan anahtar reddedilir). Böylece MCP, ListForm editörünün yapabildiği her ayarı wizard dosyasına yazar ve `replaceFileName` ile güncelleme editör ayarlarını silmez. Yeni kaydın form varsayılanı kolonun tablodaki sabit `DEFAULT`'udur (katalog ya da seed scripti, `SqlSeedColumn.ConstantValue`; IsActive → true, UomId → Adet, para birimi → TRY tablonun `DEFAULT`'larından gelir); alana özel değer öğenin `DefaultValue`'su ile verilir (`lowcode.instructions.md` §4.3.5.1). Girdide sözleşmede olmayan anahtar adıyla reddedilir, sessizce yok sayılmaz. `StaticData` lookup seçeneklerinin düz metni `App.StaticLookup.*` dil anahtarına çevrilir ve EN + TR satırı yazılır (`LocalizeStaticLookupsAsync`, `lowcode.instructions.md` §4.3.4.1). Dosyaya kullanıcının etkin talebi `DevRequest` olarak yazılır (sunucu doldurur). Wizard ekranı gibi yalnızca seed dosyalarını yazar; ekran DB Migrate ile kurulur ve çıkıştaki `isPending` o zamana kadar `true` kalır (model migrate'i beklemez; `verify_wizard` bekleyen dosyada `install` kontrolünde durur). `replaceFileName` doluysa güncellemedir (dosyalar yeniden yazılır, önceki kayıtları migrate kaldırır). İstek demo veri taşıyorsa (prompt'taki satırlar, ekran görüntüsü) wizard `SeedData: true` ile deploy edilir — ekranın `SeedFilePath`'i `data/{ListFormCode}.json` olur (`SeedSync*` bayrakları demo veriyle açılmaz; yalnızca istekte açıkça istenirse wizard'ın `SeedSync: true` cevabıyla) — satırlar aynı çağrıda `seedRows` ile bu dosyaya yazılır (alan adları doğrulanır, `Guid` anahtarı sunucu üretir); migrate ekranı kurduktan sonra satırları ekler (`lowcode.instructions.md` §0.10.1). Widget sorgularındaki her dil anahtarı tanımlı olmalı ya da wizard'ın `Language.Texts` alanında EN + TR verilmeli, yoksa deploy adlarıyla reddeder (`ValidateWidgetLanguageKeysAsync`). `uploadUrl`'süz `dxImageUpload` alanı base64 sakladığı için kolonu sınırlı boyluysa (katalogdaki karakter sınırı) deploy reddedilir; tablo kolonuna bağlı `dxImageViewer` alanı da reddedilir — görsel kolonunun editörü her zaman `dxImageUpload`'dır (`ValidateImageColumnLengths`). `MenuOrder` yok sayılır: yeni ekran kardeşlerinin sonuna, güncelleme mevcut yerine yazılır — lookup kaynağı ekran önce deploy edildiği için onu kullanan ekranın üstünde kalır. |
| `delete_wizard` | Wizard Manager'daki silmeyle aynı: `InsertedRecords` + ekranın veri aynası, bağımlı wizard varsa reddedilir. `dropTables: true` ekranın tablosunu (yalnızca tasarımcı/`create_table` ile kurulmuş ve başka yerde kullanılmayan) CRUD endpoint'leri, `sql/object` ve `crud` dosyalarıyla düşürür; model bunu kullanıcı onayı olmadan göndermez (`WizardTableCleaner`, `lowcode.instructions.md` §4.5). |
| `verify_wizard` | Deploy edilmiş ekranı veri yazmadan doğrular: menü kaydı ve URL'si, menünün istediği yetki tanımı (`AbpPermissions`), List yolunda ekran sorgusu (`GetSelectAsync`, tek satır, çağıranın yetkisiyle), Custom yolunda bileşen (etkin, rota = menü URL'si) ve seed kapanışı (`ExportAnalysis` uyarıları; engellemez). İsteğe bağlıdır, migrate sonrasında kullanılır; `App.DevRequests` döngüsünün adımı değildir — döngü `Testing`'de biter, kullanıcı elle test edip `Staging`'e alır (`lowcode.instructions.md` §0.11). |
| `start_dev_request` | Geliştirme talebini Id ya da tam başlıkla üstlenir: yalnızca `Ready to Start` durumundakini `In Progress` durumuna alır, diğerlerini reddeder; talebin metnini döndürür. Açıklamaya yapıştırılmış base64 görseller (`data:image/png|jpeg|gif|webp`) metinden ayıklanır (`McpDevRequestImages`), yerlerinde `[image-N]` kalır ve her biri ayrı MCP resim içeriği (`ImageContentBlock`) olarak döner — model görseli resim olarak görür, görsel hiçbir yere yüklenmez. Sonuç istemcinin 1 MB sınırına sığdırılır: büyük görseller küçültülüp JPEG'e çevrilir (`IMcpImageShrinker`), sığmayan gönderilmez ve metinde bildirilir. `.Write` + `App.DevRequests.Update` ister. |

View file

@ -950,9 +950,9 @@ public class McpDevelopAppService(
return (string.IsNullOrWhiteSpace(json) ? null : WizardSeedFileDto.FromDeployJson(json))
?? throw new UserFriendlyException(L[McpErrorCodes.InvalidWizard]);
}
catch (JsonException ex)
catch (JsonException)
{
throw new UserFriendlyException(L[McpErrorCodes.InvalidWizardJson, ex.Message]);
throw new UserFriendlyException(L[McpErrorCodes.InvalidWizard]);
}
}

View file

@ -97,7 +97,7 @@ public sealed class McpDevelopTools(IMcpDevelopAppService developAppService, IMc
=> await developAppService.GetWizardAsync(fileName);
[McpServerTool(Name = McpToolNames.DeployWizard, Destructive = true, Idempotent = false)]
[Description("Wizard'i deploy eder: ListForm ekrani (ComponentKind 0) ya da bir custom component'i menuye baglayan kayit (ComponentKind 1), yetkileri, dil anahtarlari ve menusu seed dosyalarina yazilir. Deploy veritabanina yazmaz: ekran, menu ve yetkiler kullanici Wizard Manager'da DB Migrate calistirinca olusur; cikistaki isPending true iken ekran henuz yoktur; migrate'i bekleme, isini bitir ve kullaniciya yalnizca DB Migrate calistirmasini soyle. Eksik modul, ust menu, izin grubu, yetki ve dil satirlari da deploy'da dosyalara yazilir ve migrate onlari dogru sirayla ekler; bunlarin veritabaninda olmamasi beklenen durumdur, uyari olarak raporlama ve kullanicidan elle eklemesini isteme. Ust menu zinciri istenen derinlikte kurulur: MenuParentCode ekranin dogrudan ust menusu, Menu.MenuParentPath onun ustundeki seviyeler (kokten asagi, her biri {Code, Icon, En, Tr}); eksik her seviye bir ustunun altina kurulur, var olana baglanilir. Liste ekraninda Grid, Card, Pivot ve Chart gorunumleri varsayilan aciktir (DefaultLayout grid); kullanici bir gorunumu istemedigini soylemedikce bu bayraklari false yazma. Hedef tablo onceden var olmalidir; yoksa once create_table ile olustur. Istek ornek/demo veri tasiyorsa (prompt'taki tablo/liste ya da ekran goruntusunde gorunen satirlar) SeedData true yaz ve satirlarin hepsini ayni cagrida seedRows ile gonder: satirlar ekranin data/{ListFormCode}.json dosyasina yazilir, migrate ekrani kurduktan sonra ekler. Satirlar icin migrate'i bekleyip insert_row kullanma. replaceFileName verilirse o wizard'in dosyalari yeniden yazilir, onceki kayitlarin yerini migrate'te yenileri alir. Wizard cevaplarinin uretmedigi ekran ayarlari (FormFieldsDefaultValueDto/InsertFieldsDefaultValueDto gibi varsayilan degerler, ValidationRuleDto, Format, Alignment, Width, Visible, BandName, ozet/gruplama, ExtraFilter, Insert/Update/Delete komutlari, PageSize, CommandColumnDto …) ayni JSON'un ListForm ve Fields bolumlerine get_wizard ciktisindaki adlarla yazilir. Yeni kaydin form degeri (Aktif = true, Birim = Adet, Kur = TRY, Sure = 60) ayri yazilmaz: deploy kolonun tablodaki sabit DEFAULT degerini okur; tablodan farkli deger gerekiyorsa alan ogesinde DefaultValue yaz (Groups[].Items[].DefaultValue). StaticData lookup seceneklerinde (RadioGroup, SelectBox, TagBox) name dil anahtaridir (App.StaticLookup.{Key}); EN + TR metni bilinen secenegi Language.Texts'te ver, duz metin yazilirsa deploy onu anahtara cevirip TR'yi metinden yazar (get_dev_guide 4.3.4.1). Wizard girdisinde taninmayan anahtar adiyla reddedilir, baska adla tekrar deneme; bolumler kismi olabilir (yalnizca degisen anahtarlar), Fields ogeleri FieldName ile eslesir, *Json adlari (FormFieldsDefaultValueJson) *Dto karsiligina cevrilir. Guncellemede get_wizard ciktisini butunuyle degistirip gonder: tasarimci ayarlari korunur; Wizard bolumunde yazdigin cevap ayni ayarin ListForm'daki eski degerine ustun gelir. Kimlik alanlari (ListFormCode, Name, Title, Description, PermissionDto, FieldName) wizard'dan gelir. Fields'taki her alan Wizard Groups'ta da bulunmalidir; tanınmayan ya da hesaplanan bir anahtar reddedilir. Liste ekraninda DescTr ve DescEn ayrintili olmali (en az 3 satir, 150 karakter): ekran basligindaki bilgi simgesinde gosterilir; ne ise yaradigi, menude nerede ya da hangi ekranin sekmesi oldugu, ana alanlar, otomatik hesaplar ve iliskili ekranlar yazilir (get_dev_guide 4.4.2); kisa aciklama reddedilir. Deploy modulun dokumanini (docs/modules) acar ya da envanterini yeniler; elle yazilan bolumleri save_module_doc ile doldur. Ne yazacagini get_dev_guide 4. bolumunden ogren.")]
[Description("Wizard'i deploy eder: ListForm ekrani (ComponentKind 0) ya da bir custom component'i menuye baglayan kayit (ComponentKind 1), yetkileri, dil anahtarlari ve menusu seed dosyalarina yazilir. Deploy veritabanina yazmaz: ekran, menu ve yetkiler kullanici Wizard Manager'da DB Migrate calistirinca olusur; cikistaki isPending true iken ekran henuz yoktur; migrate'i bekleme, isini bitir ve kullaniciya yalnizca DB Migrate calistirmasini soyle. Eksik modul, ust menu, izin grubu, yetki ve dil satirlari da deploy'da dosyalara yazilir ve migrate onlari dogru sirayla ekler; bunlarin veritabaninda olmamasi beklenen durumdur, uyari olarak raporlama ve kullanicidan elle eklemesini isteme. Ust menu zinciri istenen derinlikte kurulur: MenuParentCode ekranin dogrudan ust menusu, Menu.MenuParentPath onun ustundeki seviyeler (kokten asagi, her biri {Code, Icon, En, Tr}); eksik her seviye bir ustunun altina kurulur, var olana baglanilir. Liste ekraninda Grid, Card, Pivot ve Chart gorunumleri varsayilan aciktir (DefaultLayout grid); kullanici bir gorunumu istemedigini soylemedikce bu bayraklari false yazma. Hedef tablo onceden var olmalidir; yoksa once create_table ile olustur. Istek ornek/demo veri tasiyorsa (prompt'taki tablo/liste ya da ekran goruntusunde gorunen satirlar) SeedData true yaz ve satirlarin hepsini ayni cagrida seedRows ile gonder: satirlar ekranin data/{ListFormCode}.json dosyasina yazilir, migrate ekrani kurduktan sonra ekler. SeedData ekrandan girilen kayitlari dosyaya yansitmaz (SeedSyncInsert/Update/Delete kapali); bunu yalnizca istek acikca isterse SeedSync true yaz. Satirlar icin migrate'i bekleyip insert_row kullanma. replaceFileName verilirse o wizard'in dosyalari yeniden yazilir, onceki kayitlarin yerini migrate'te yenileri alir. Wizard cevaplarinin uretmedigi ekran ayarlari (FormFieldsDefaultValueDto/InsertFieldsDefaultValueDto gibi varsayilan degerler, ValidationRuleDto, Format, Alignment, Width, Visible, BandName, ozet/gruplama, ExtraFilter, Insert/Update/Delete komutlari, PageSize, CommandColumnDto …) ayni JSON'un ListForm ve Fields bolumlerine get_wizard ciktisindaki adlarla yazilir. Yeni kaydin form degeri (Aktif = true, Birim = Adet, Kur = TRY, Sure = 60) ayri yazilmaz: deploy kolonun tablodaki sabit DEFAULT degerini okur; tablodan farkli deger gerekiyorsa alan ogesinde DefaultValue yaz (Groups[].Items[].DefaultValue). StaticData lookup seceneklerinde (RadioGroup, SelectBox, TagBox) name dil anahtaridir (App.StaticLookup.{Key}); EN + TR metni bilinen secenegi Language.Texts'te ver, duz metin yazilirsa deploy onu anahtara cevirip TR'yi metinden yazar (get_dev_guide 4.3.4.1). EditorScript'i elle yazma: Script Builder kurallarini ({v:1, rules:[...]}, get_dev_guide 5.3) ver, deploy kodu Script Builder dialogu ile birebir ayni uretir; govde yazsan da atilir. Onay kutusu alanina setValue text 'true'/'false' (0/1 cevrilir). set ile yazilan alanin kurallari da sirayla calisir (derinlik siniri yok); A->B->A dongusu kuran kural yazma, calisma aninda hata olarak gosterilir. Wizard girdisinde taninmayan anahtar adiyla reddedilir, baska adla tekrar deneme; bolumler kismi olabilir (yalnizca degisen anahtarlar), Fields ogeleri FieldName ile eslesir, *Json adlari (FormFieldsDefaultValueJson) *Dto karsiligina cevrilir. Guncellemede get_wizard ciktisini butunuyle degistirip gonder: tasarimci ayarlari korunur; Wizard bolumunde yazdigin cevap ayni ayarin ListForm'daki eski degerine ustun gelir. Kimlik alanlari (ListFormCode, Name, Title, Description, PermissionDto, FieldName) wizard'dan gelir. Fields'taki her alan Wizard Groups'ta da bulunmalidir; tanınmayan ya da hesaplanan bir anahtar reddedilir. Liste ekraninda DescTr ve DescEn ayrintili olmali (en az 3 satir, 150 karakter): ekran basligindaki bilgi simgesinde gosterilir; ne ise yaradigi, menude nerede ya da hangi ekranin sekmesi oldugu, ana alanlar, otomatik hesaplar ve iliskili ekranlar yazilir (get_dev_guide 4.4.2); kisa aciklama reddedilir. Deploy modulun dokumanini (docs/modules) acar ya da envanterini yeniler; elle yazilan bolumleri save_module_doc ile doldur. Ne yazacagini get_dev_guide 4. bolumunden ogren.")]
public async Task<WizardFileInfoDto?> DeployWizardAsync(
[Description("Wizard JSON'u: seed dosyasi bicimi ({\"Wizard\": {...}, \"ListForm\": {...}, \"Fields\": [...]}, get_wizard cikisi gibi) ya da duz wizard cevaplari. ListForm/Fields istege baglidir ve kismi olabilir. Enum alanlari sayi olarak yazilir.")]
string wizard,

View file

@ -100,12 +100,19 @@ public class ListFormWizardDto
public bool ConfirmDelete { get; set; }
/// <summary>
/// Ekranin verisi seed klasorundeki <c>data/{ListFormCode}.json</c> dosyasina aynalanir
/// (<c>SeedFilePath</c> + ekle/guncelle/sil senkronizasyonu). Istekle gelen ornek/demo satirlar
/// ekrandan girildiginde dosyaya yazilir ve migrate ile geri yuklenir.
/// Ekranin demo verisi seed klasorundeki <c>data/{ListFormCode}.json</c> dosyasindan migrate ile
/// yuklenir (<c>SeedFilePath</c>). Ekrandan yapilan kayitlarin dosyaya yansimasi ayri karardir
/// (<see cref="SeedSync"/>).
/// </summary>
public bool SeedData { get; set; }
/// <summary>
/// Ekrandan yapilan ekle/guncelle/sil islemleri veri dosyasina yansisin mi (<c>SeedSyncInsert/Update/Delete</c>).
/// Demo veri (<see cref="SeedData"/>) bunu acmaz: varsayilan kapali, yalnizca istekte acikca
/// istenirse true yazilir; aksi halde kullanicinin girdigi her kayit seed dosyasina karisirdi.
/// </summary>
public bool SeedSync { get; set; }
// Liste ekranı varsayılan olarak dört analiz görünümüyle gelir (UI wizard ve LayoutDto ile aynı);
// MCP/prompt ile gelen cevapta bayrak yazılmazsa görünüm sessizce kapanmasın diye.
public string DefaultLayout { get; set; } = "grid";
@ -279,9 +286,9 @@ public class ListFormWizardDto
// Query'de SelectCommand tablo adi degildir; silme komutu elle yazilir.
DeleteCommand = isDeletedField && SelectCommandType != SelectCommandTypeEnum.Query ? WizardConsts.DefaultDeleteCommand(SelectCommand) : null,
SeedFilePath = SeedData ? WizardConsts.SeedDataFilePath(listFormCode) : null,
SeedSyncInsert = SeedData,
SeedSyncUpdate = SeedData,
SeedSyncDelete = SeedData,
SeedSyncInsert = SeedData && SeedSync,
SeedSyncUpdate = SeedData && SeedSync,
SeedSyncDelete = SeedData && SeedSync,
};
// Varsayilanlar WizardConsts uzerinden JSON olarak kurulur; DTO'nun nesne karsiliklari
@ -450,6 +457,7 @@ public class ListFormWizardDto
IsBranch = listForm.IsBranch;
IsOrganizationUnit = listForm.IsOrganizationUnit;
SeedData = !string.IsNullOrWhiteSpace(listForm.SeedFilePath);
SeedSync = listForm.SeedSyncInsert || listForm.SeedSyncUpdate || listForm.SeedSyncDelete;
var layout = listForm.LayoutDto;
DefaultLayout = layout.DefaultLayout;

View file

@ -84,7 +84,7 @@ public class WizardSeedFileDto
[
nameof(ListFormWizardDto.IsTenant), nameof(ListFormWizardDto.IsBranch), nameof(ListFormWizardDto.IsOrganizationUnit),
nameof(ListFormWizardDto.AllowAdding), nameof(ListFormWizardDto.AllowUpdating), nameof(ListFormWizardDto.AllowDeleting),
nameof(ListFormWizardDto.AllowDetail), nameof(ListFormWizardDto.ConfirmDelete), nameof(ListFormWizardDto.SeedData),
nameof(ListFormWizardDto.AllowDetail), nameof(ListFormWizardDto.ConfirmDelete), nameof(ListFormWizardDto.SeedData), nameof(ListFormWizardDto.SeedSync),
nameof(ListFormWizardDto.DefaultLayout), nameof(ListFormWizardDto.Grid), nameof(ListFormWizardDto.Card),
nameof(ListFormWizardDto.Pivot), nameof(ListFormWizardDto.Tree), nameof(ListFormWizardDto.Chart),
nameof(ListFormWizardDto.Gantt), nameof(ListFormWizardDto.Scheduler), nameof(ListFormWizardDto.Todo),

View file

@ -348,6 +348,15 @@ public class ListFormWizardAppService(
ValidateImageColumnLengths(input, tableColumns);
await LocalizeStaticLookupsAsync(input, fieldDtos, rows, owned, inserted);
// Script Builder kurali tasiyan script'in kodu kurallardan uretilir; MCP'nin elle yazdigi
// govdeye guvenilmez (lowcode §4.4, §5.6).
var booleanFields = fieldDtos
.Where(f => f.SourceDbType == System.Data.DbType.Boolean || f.EditorType2 is "dxCheckBox" or "dxSwitch")
.Select(f => f.FieldName)
.ToHashSet(StringComparer.OrdinalIgnoreCase);
foreach (var field in fieldDtos)
field.EditorScript = Sozsoft.Platform.Editors.EditorScript.Rebuild(field.EditorScript, booleanFields.Contains);
foreach (var item in input.Groups.SelectMany(g => g.Items))
await WriteLanguageRowsAsync(rows, item.CaptionName, item.EnglishCaption, item.TurkishCaption, owned, inserted);
}

View file

@ -194,6 +194,56 @@ public static class EditorScript
return string.Join("\n", lines);
}
/// <summary>
/// Script Builder kurallarini tasiyan script'in kodunu kurallardan yeniden uretir. Girdi
/// <c>// @builder {...}</c> basligiyla baslayan bir script ya da yalnizca kural JSON'u
/// (<c>{"v":1,"rules":[...]}</c>) olabilir; basligin altindaki elle yazilmis govde atilir —
/// yanlis yardimci kullanimi (<c>str(get('X'))</c>) calismayan script uretirdi. Kural tasimayan
/// script oldugu gibi doner. <paramref name="isBooleanField"/> verilirse mantiksal alana deger yazan
/// kuralin degeri dialogdaki Evet/Hayir seciminin karsiligina (<c>true</c>/<c>false</c>) cevrilir.
/// </summary>
public static string Rebuild(string script, System.Func<string, bool> isBooleanField = null)
{
var text = script?.Trim();
if (string.IsNullOrEmpty(text))
{
return script;
}
string json;
if (text.StartsWith(BuilderHeader, System.StringComparison.Ordinal))
{
var lineEnd = text.IndexOf('\n');
json = (lineEnd < 0 ? text : text[..lineEnd])[BuilderHeader.Length..];
}
else if (text.StartsWith('{'))
{
json = text;
}
else
{
return script;
}
try
{
using var document = System.Text.Json.JsonDocument.Parse(json);
if (!document.RootElement.TryGetProperty("rules", out var rules) || rules.ValueKind != System.Text.Json.JsonValueKind.Array)
{
return script;
}
var built = Build(rules.EnumerateArray()
.Select(EditorScriptRule.FromJson)
.Select(rule => isBooleanField == null ? rule : rule.WithBooleanValue(isBooleanField)));
return built.Length == 0 ? script : built;
}
catch (System.Text.Json.JsonException)
{
return script;
}
}
/* ---------------------------------------------------------------- *
* Ortak yardimcilar (scriptRecipes.ts portu)
* ---------------------------------------------------------------- */

View file

@ -45,6 +45,10 @@ public sealed class EditorScriptCondition
internal string Value { get; }
/// <summary>Kural JSON'undaki kosulu geri okur (<see cref="EditorScriptRule.FromJson"/>).</summary>
internal static EditorScriptCondition Of(string @operator, string source, string value)
=> new EditorScriptCondition(@operator, source, value);
/// <summary>Kosulsuz - kural her zaman calisir.</summary>
public static EditorScriptCondition Always()
=> new EditorScriptCondition("always", string.Empty, string.Empty);

View file

@ -114,6 +114,76 @@ public sealed class EditorScriptRule
return this;
}
/// <summary>
/// <see cref="ToJson"/> ciktisini (dialogun <c>@builder</c> basligindaki kural) geri okur; bilinmeyen
/// tarif <see cref="IsComplete"/>'ten gecmez ve uretimde atlanir.
/// </summary>
internal static EditorScriptRule FromJson(System.Text.Json.JsonElement json)
{
static string Text(System.Text.Json.JsonElement element, string name) =>
element.ValueKind == System.Text.Json.JsonValueKind.Object && element.TryGetProperty(name, out var value)
? value.ValueKind == System.Text.Json.JsonValueKind.String ? value.GetString() : value.ToString()
: null;
static EditorScriptCondition Condition(System.Text.Json.JsonElement element) =>
EditorScriptCondition.Of(Text(element, "operator") ?? "always", Text(element, "source"), Text(element, "value"));
var rule = new EditorScriptRule(Text(json, "recipe") ?? string.Empty).WithId(Text(json, "id"));
if (json.TryGetProperty("params", out var parameters) && parameters.ValueKind == System.Text.Json.JsonValueKind.Object)
{
foreach (var parameter in parameters.EnumerateObject())
rule.Set(parameter.Name, Text(parameters, parameter.Name));
}
if (json.TryGetProperty("fields", out var fields) && fields.ValueKind == System.Text.Json.JsonValueKind.Array)
rule.SetFields(fields.EnumerateArray().Select(field => field.ToString()));
var conditions = new List<EditorScriptCondition>();
if (json.TryGetProperty("condition", out var condition))
conditions.Add(Condition(condition));
if (json.TryGetProperty("conditions", out var more) && more.ValueKind == System.Text.Json.JsonValueKind.Array)
conditions.AddRange(more.EnumerateArray().Select(Condition));
// Kosulsuz kural "always" ile yazilir; ToJson onu kendisi ekler.
rule.AddConditions([.. conditions.Where(c => c.Operator != "always")], Text(json, "join") == "or");
return rule.WithTrigger(Text(json, "trigger") switch
{
"change" => EditorScriptTrigger.Change,
"open" => EditorScriptTrigger.Open,
"both" => EditorScriptTrigger.Both,
_ => EditorScriptTrigger.Default
});
}
/// <summary>
/// Mantiksal alana yazan <c>setValue</c> kuralinin degerini <c>true</c>/<c>false</c>'a cevirir; dialog bu
/// alanlarda yalnizca Evet/Hayir sectirir. <c>tpl("0")</c> metni onay kutusunu dolu sayardi.
/// </summary>
internal EditorScriptRule WithBooleanValue(System.Func<string, bool> isBooleanField)
{
if (_recipe != "setValue" || !isBooleanField(Param("target")))
{
return this;
}
var flag = Param("text").Trim().ToLowerInvariant() switch
{
"true" or "1" or "yes" or "evet" or "on" => "true",
"false" or "0" or "no" or "hayir" or "hayır" or "off" => "false",
_ => null
};
var index = _params.FindIndex(pair => pair.Key == "text");
if (flag != null && index >= 0)
{
_params[index] = new KeyValuePair<string, string>("text", flag);
}
return this;
}
/// <summary>Tek kurallik script metni. Birden fazla kural icin <see cref="EditorScript.Build(EditorScriptRule[])"/> kullanin.</summary>
public string ToScript() => EditorScript.Build(this);
@ -352,7 +422,7 @@ public sealed class EditorScriptRule
return "copy(" + q(Param("source")) + ", " + q(Param("target")) + ")";
case "setValue":
return "set(" + q(Param("target")) + ", tpl(" + q(Param("text")) + "))";
return "set(" + q(Param("target")) + ", " + ValueLiteral(Param("text")) + ")";
case "clear":
return "clear(" + string.Join(", ", _fields.Select(q)) + ")";
@ -396,6 +466,18 @@ public sealed class EditorScriptRule
}
}
/// <summary>
/// <c>true</c>/<c>false</c> mantiksal sabit olarak yazilir; metin olarak (<c>tpl('false')</c>) yazilsa
/// onay kutusu dolu sayardi. Diger degerler sablondur. scriptRecipes.ts <c>valueLiteral</c> ile ayni.
/// </summary>
private static string ValueLiteral(string text)
{
var trimmed = (text ?? string.Empty).Trim();
return trimmed.Equals("true", System.StringComparison.OrdinalIgnoreCase) || trimmed.Equals("false", System.StringComparison.OrdinalIgnoreCase)
? trimmed.ToLowerInvariant()
: "tpl(" + EditorScript.Quote(text) + ")";
}
private static string TriggerName(EditorScriptTrigger trigger)
{
switch (trigger)

View file

@ -232,6 +232,13 @@ Bir çelişki varsa sıra: `ai.instructions.md` → `dotnet.instructions.md` →
`name` düz metin değil `App.StaticLookup.*` dil anahtarıdır; EN + TR satırı dil dosyasına yazılır.
Deploy düz metni kendisi anahtara çevirir (aynı TR metinli anahtar varsa onu kullanır) —
`ListFormWizardAppService.LocalizeStaticLookupsAsync`, `lowcode.instructions.md` §4.3.4.1.
20. **EditorScript** → MCP/wizard script'i elle yazmaz, Script Builder kurallarını verir; deploy kodu
kurallardan üretir (`EditorScript.Rebuild`). `str/num/bool` alan **adı** alır (`str('X')`, değeri
değil). `setValue`'da `true`/`false` mantıksal yazılır (onay kutusu alanında `0`/`1` deploy'da çevrilir).
MCP ve Script Builder aynı kuralları aynı koda çevirir; `set` zinciri derinlik sınırsızdır, döngü ve
script hatası kullanıcıya bildirilir. Ayrıntı: `lowcode.instructions.md` §4.4, §5.3, §5.6.
21. **Demo veri ≠ seed senkronu** → `SeedData: true` yalnızca `SeedFilePath` açar;
`SeedSyncInsert/Update/Delete` istekte açıkça istenmedikçe `false`'tur (wizard cevabı `SeedSync`).
### Cross-cutting: touching one thing means touching these too
@ -335,7 +342,8 @@ Bir çelişki varsa sıra: `ai.instructions.md` → `dotnet.instructions.md` →
adım ekleme. Ayrıntı: `lowcode.instructions.md` §4.0.
- **MCP ile demo veri** → geliştirme isteği örnek satır taşıyorsa (prompt'taki tablo/liste, ekran
görüntüsü) ekran `deploy_wizard`'a `SeedData: true` ile gider (`SeedFilePath = data/{ListFormCode}.json`
+ `SeedSync*` açık, `ListFormWizardDto.BuildListForm`) ve satırlar aynı çağrıda `seedRows` ile
; `SeedSync*` bayrakları **kapalı** kalır — yalnızca istek ekrandan girilen kayıtların dosyaya
yansımasını açıkça isterse `SeedSync: true`, wizard ekranında da aynı; `ListFormWizardDto.BuildListForm`) ve satırlar aynı çağrıda `seedRows` ile
`configs/seeds/data/{ListFormCode}.json` dosyasına yazılır (`McpDevelopAppService`; alan adları
ekrana göre doğrulanır, `Guid` anahtarı sunucu üretir). Migrate ekranı kurar, ardından satırları
ekler; MCP migrate'i beklemez. Talep döngüsü `Testing`'de biter, `Staging`'e kullanıcı alır. Dosya elle yazılmaz, görselden okunamayan değer uydurulmaz.

File diff suppressed because one or more lines are too long

View file

@ -53,6 +53,25 @@
"Cost": 0,
"IsActive": true,
"Id": "4de8b328-a0bf-3b36-96d9-3a2429f022bf"
},
{
"Id": "eb0ce687-3014-4f8b-9482-469f3e6d3993",
"Name": "Deneme",
"IsFavorite": false,
"CanBeSold": true,
"CanBeExpensed": false,
"CanBePurchased": false,
"ProductType": "Service",
"CategoryId": "eb9eed73-a7e1-5b5b-c31b-3a2429dfe211",
"TrackInventory": false,
"TrackingMethod": "Quantity",
"SalesPrice": 0.0000,
"Cost": 0.0000,
"InternalReference": null,
"UomId": "Adet",
"PurchaseUomId": "Adet",
"IsActive": true,
"Image": null
}
]
}

View file

@ -59742,6 +59742,48 @@
"Key": "App.ScriptBuilder.AddOption",
"Value": "+ ekle"
},
{
"Id": "92147417-0362-5de6-9fdd-8c45da7e3857",
"CultureName": "en",
"ResourceName": "Platform",
"Key": "App.ScriptBuilder.SyntaxError",
"Value": "The script has a syntax error and was not saved: {message}"
},
{
"Id": "cd1e1fe4-af2d-5042-b258-ae16236da342",
"CultureName": "tr",
"ResourceName": "Platform",
"Key": "App.ScriptBuilder.SyntaxError",
"Value": "Script'te söz dizimi hatası var, kaydedilmedi: {message}"
},
{
"Id": "e0f30407-7383-5a15-ad1d-7beb2c8a264c",
"CultureName": "en",
"ResourceName": "Platform",
"Key": "App.EditorScript.Error",
"Value": "The editor script of field '{field}' is faulty and did not run: {message}"
},
{
"Id": "cf351e16-422f-565e-ae84-ff72de0f8102",
"CultureName": "tr",
"ResourceName": "Platform",
"Key": "App.EditorScript.Error",
"Value": "'{field}' alanının editör script'i hatalı, çalışmadı: {message}"
},
{
"Id": "cf45e07a-0a2e-50bc-92d2-af9850cd22d7",
"CultureName": "en",
"ResourceName": "Platform",
"Key": "App.ScriptBuilder.StaleHeader",
"Value": "The code no longer matches the wizard rules. On save the rules header is removed and the code is kept as hand-written; use Regenerate to return to the rules."
},
{
"Id": "7c7d420a-df72-5d21-b2e4-5f9f95bfe873",
"CultureName": "tr",
"ResourceName": "Platform",
"Key": "App.ScriptBuilder.StaleHeader",
"Value": "Kod sihirbaz kurallarıyla artık eşleşmiyor. Kaydedince kural başlığı kaldırılır ve kod elle yazılmış olarak kalır; kurallara dönmek için Yeniden üret'i kullanın."
},
{
"Id": "5c33f8a3-775e-50ba-9e9b-97e0a4032e7a",
"CultureName": "en",

View file

@ -252,7 +252,6 @@ Dış entegrasyon yok. Ölçü Birimleri, Kontaklar, Para Birimleri, Ülkeler ve
| `InternalReference` | İç Referans | dxTextBox | | ✓ | |
| `ProductName` | Adı | dxTextBox | | ✓ | |
| `VariantName` | Varyant Değerleri | dxTextBox | | ✓ | |
| `VariantKey` | Varyant Anahtarı | dxTextBox | | | |
| `SalesPrice` | Satış Fiyatı | dxNumberBox | | ✓ | |
| `Cost` | Maliyet | dxNumberBox | | ✓ | |
| `UomId` | Birim | dxSelectBox | | ✓ | sorgu: Adm_T_Uom |

View file

@ -82,9 +82,9 @@
"SelectCommandType": 1,
"SelectFieldsDefaultValueDto": [],
"SeedFilePath": "data/App.Inventory.Attribute.json",
"SeedSyncInsert": true,
"SeedSyncUpdate": true,
"SeedSyncDelete": true,
"SeedSyncInsert": false,
"SeedSyncUpdate": false,
"SeedSyncDelete": false,
"DeleteCommand": "UPDATE \"Inv_T_Attribute\" SET \"DeleterId\"=@DeleterId, \"DeletionTime\"=CURRENT_TIMESTAMP, \"IsDeleted\"='true' WHERE \"Id\" IN @Id",
"DeleteBeforeCommand": null,
"DeleteAfterCommand": null,

View file

@ -70,9 +70,9 @@
"SelectCommandType": 1,
"SelectFieldsDefaultValueDto": [],
"SeedFilePath": "data/App.Prc.DeliveryMethod.json",
"SeedSyncInsert": true,
"SeedSyncUpdate": true,
"SeedSyncDelete": true,
"SeedSyncInsert": false,
"SeedSyncUpdate": false,
"SeedSyncDelete": false,
"DeleteCommand": "UPDATE \"Prc_T_DeliveryMethod\" SET \"DeleterId\"=@DeleterId, \"DeletionTime\"=CURRENT_TIMESTAMP, \"IsDeleted\"='true' WHERE \"Id\" IN @Id",
"DeleteBeforeCommand": null,
"DeleteAfterCommand": null,
@ -771,7 +771,7 @@
"GroupInterval": "",
"WordWrapEnabled": false
},
"Alignment": null,
"Alignment": "left",
"Format": null,
"EditOrderNo": null,
"EditGroupOrderNo": null,
@ -909,7 +909,7 @@
"GroupInterval": "",
"WordWrapEnabled": false
},
"Alignment": null,
"Alignment": "left",
"Format": null,
"EditOrderNo": 1,
"EditGroupOrderNo": 1,
@ -1047,7 +1047,7 @@
"GroupInterval": "",
"WordWrapEnabled": false
},
"Alignment": null,
"Alignment": "left",
"Format": null,
"EditOrderNo": 2,
"EditGroupOrderNo": 1,

View file

@ -76,9 +76,9 @@
"SelectCommandType": 1,
"SelectFieldsDefaultValueDto": [],
"SeedFilePath": "data/App.Prc.Incoterm.json",
"SeedSyncInsert": true,
"SeedSyncUpdate": true,
"SeedSyncDelete": true,
"SeedSyncInsert": false,
"SeedSyncUpdate": false,
"SeedSyncDelete": false,
"DeleteCommand": "UPDATE \"Prc_T_Incoterm\" SET \"DeleterId\"=@DeleterId, \"DeletionTime\"=CURRENT_TIMESTAMP, \"IsDeleted\"='true' WHERE \"Id\" IN @Id",
"DeleteBeforeCommand": null,
"DeleteAfterCommand": null,
@ -777,7 +777,7 @@
"GroupInterval": "",
"WordWrapEnabled": false
},
"Alignment": null,
"Alignment": "left",
"Format": null,
"EditOrderNo": null,
"EditGroupOrderNo": null,
@ -915,7 +915,7 @@
"GroupInterval": "",
"WordWrapEnabled": false
},
"Alignment": null,
"Alignment": "left",
"Format": null,
"EditOrderNo": 1,
"EditGroupOrderNo": 1,
@ -1053,7 +1053,7 @@
"GroupInterval": "",
"WordWrapEnabled": false
},
"Alignment": null,
"Alignment": "left",
"Format": null,
"EditOrderNo": 2,
"EditGroupOrderNo": 1,
@ -1191,7 +1191,7 @@
"GroupInterval": "",
"WordWrapEnabled": false
},
"Alignment": null,
"Alignment": "left",
"Format": null,
"EditOrderNo": 3,
"EditGroupOrderNo": 1,

View file

@ -76,9 +76,9 @@
"SelectCommandType": 1,
"SelectFieldsDefaultValueDto": [],
"SeedFilePath": "data/App.Prc.PaymentMethod.json",
"SeedSyncInsert": true,
"SeedSyncUpdate": true,
"SeedSyncDelete": true,
"SeedSyncInsert": false,
"SeedSyncUpdate": false,
"SeedSyncDelete": false,
"DeleteCommand": "UPDATE \"Prc_T_PaymentMethod\" SET \"DeleterId\"=@DeleterId, \"DeletionTime\"=CURRENT_TIMESTAMP, \"IsDeleted\"='true' WHERE \"Id\" IN @Id",
"DeleteBeforeCommand": null,
"DeleteAfterCommand": null,
@ -777,7 +777,7 @@
"GroupInterval": "",
"WordWrapEnabled": false
},
"Alignment": null,
"Alignment": "left",
"Format": null,
"EditOrderNo": null,
"EditGroupOrderNo": null,
@ -915,7 +915,7 @@
"GroupInterval": "",
"WordWrapEnabled": false
},
"Alignment": null,
"Alignment": "left",
"Format": null,
"EditOrderNo": 1,
"EditGroupOrderNo": 1,
@ -1041,7 +1041,7 @@
"GroupInterval": "",
"WordWrapEnabled": false
},
"Alignment": null,
"Alignment": "left",
"Format": null,
"EditOrderNo": 2,
"EditGroupOrderNo": 1,
@ -1179,7 +1179,7 @@
"GroupInterval": "",
"WordWrapEnabled": false
},
"Alignment": null,
"Alignment": "left",
"Format": null,
"EditOrderNo": 3,
"EditGroupOrderNo": 1,

View file

@ -82,9 +82,9 @@
"SelectCommandType": 1,
"SelectFieldsDefaultValueDto": [],
"SeedFilePath": "data/App.Prc.PaymentTerm.json",
"SeedSyncInsert": true,
"SeedSyncUpdate": true,
"SeedSyncDelete": true,
"SeedSyncInsert": false,
"SeedSyncUpdate": false,
"SeedSyncDelete": false,
"DeleteCommand": "UPDATE \"Prc_T_PaymentTerm\" SET \"DeleterId\"=@DeleterId, \"DeletionTime\"=CURRENT_TIMESTAMP, \"IsDeleted\"='true' WHERE \"Id\" IN @Id",
"DeleteBeforeCommand": null,
"DeleteAfterCommand": null,
@ -783,7 +783,7 @@
"GroupInterval": "",
"WordWrapEnabled": false
},
"Alignment": null,
"Alignment": "left",
"Format": null,
"EditOrderNo": null,
"EditGroupOrderNo": null,
@ -921,7 +921,7 @@
"GroupInterval": "",
"WordWrapEnabled": false
},
"Alignment": null,
"Alignment": "left",
"Format": null,
"EditOrderNo": 1,
"EditGroupOrderNo": 1,
@ -1047,7 +1047,7 @@
"GroupInterval": "",
"WordWrapEnabled": false
},
"Alignment": null,
"Alignment": "left",
"Format": null,
"EditOrderNo": 2,
"EditGroupOrderNo": 1,
@ -1185,7 +1185,7 @@
"GroupInterval": "",
"WordWrapEnabled": false
},
"Alignment": null,
"Alignment": "left",
"Format": null,
"EditOrderNo": 3,
"EditGroupOrderNo": 1,
@ -1311,7 +1311,7 @@
"GroupInterval": "",
"WordWrapEnabled": false
},
"Alignment": null,
"Alignment": "left",
"Format": null,
"EditOrderNo": 4,
"EditGroupOrderNo": 1,

View file

@ -171,9 +171,9 @@
"SelectCommandType": 1,
"SelectFieldsDefaultValueDto": [],
"SeedFilePath": "data/App.Inventory.Product.json",
"SeedSyncInsert": true,
"SeedSyncUpdate": true,
"SeedSyncDelete": true,
"SeedSyncInsert": false,
"SeedSyncUpdate": false,
"SeedSyncDelete": false,
"DeleteCommand": "UPDATE \"Inv_T_Product\" SET \"DeleterId\"=@DeleterId, \"DeletionTime\"=CURRENT_TIMESTAMP, \"IsDeleted\"='true' WHERE \"Id\" IN @Id",
"DeleteBeforeCommand": null,
"DeleteAfterCommand": null,
@ -1846,7 +1846,7 @@
"GridBoxOptions": null,
"TagBoxOptions": null,
"ImageUploadOptions": null,
"EditorScript": "// @runOnOpen\nreadOnly(\"TrackInventory\", str(get(\"ProductType\")) === \"Service\")\nif (!isReady && str(value) === \"Service\") { set(\"TrackInventory\", false); clear(\"TrackingMethod\"); readOnly(\"TrackingMethod\", true) }",
"EditorScript": "// @runOnOpen\nif (str(\"ProductType\") === \"Service\") set(\"TrackInventory\", false)\nreadOnly(\"TrackInventory\", str(\"ProductType\") === \"Service\")",
"LastModificationTime": null,
"LastModifierId": null,
"CreationTime": "0001-01-01T00:00:00",

View file

@ -763,7 +763,7 @@
"GroupInterval": "",
"WordWrapEnabled": false
},
"Alignment": null,
"Alignment": "left",
"Format": null,
"EditOrderNo": null,
"EditGroupOrderNo": null,
@ -889,7 +889,7 @@
"GroupInterval": "",
"WordWrapEnabled": false
},
"Alignment": null,
"Alignment": "left",
"Format": null,
"EditOrderNo": null,
"EditGroupOrderNo": null,
@ -1027,7 +1027,7 @@
"GroupInterval": "",
"WordWrapEnabled": false
},
"Alignment": null,
"Alignment": "left",
"Format": null,
"EditOrderNo": 1,
"EditGroupOrderNo": 1,
@ -1121,11 +1121,11 @@
"DataSourceType": 2,
"ValueExpr": "Key",
"DisplayExpr": "Name",
"CascadeRelationField": null,
"CascadeRelationField": "",
"CascadeFilterOperator": "=",
"CascadeParentFields": "AttributeId",
"CascadeEmptyFields": null,
"LookupQuery": "SELECT CAST(Id AS nvarchar(36)) AS [Key], Name AS [Name] FROM Inv_T_AttributeValue WHERE IsDeleted = 'false' AND IsActive = 1 AND (AttributeId = @param0 OR @param0 IS NULL) ORDER BY SortOrder, Name"
"CascadeEmptyFields": "",
"LookupQuery": "SELECT CAST(Id AS nvarchar(36)) AS [Key], Name AS [Name], ColorCode AS [Color] FROM Inv_T_AttributeValue WHERE IsDeleted = 'false' AND IsActive = 1 AND (AttributeId = @param0 OR @param0 IS NULL) ORDER BY SortOrder, Name"
},
"ValidationRuleDto": [
{
@ -1165,7 +1165,7 @@
"GroupInterval": "",
"WordWrapEnabled": false
},
"Alignment": null,
"Alignment": "left",
"Format": null,
"EditOrderNo": 2,
"EditGroupOrderNo": 1,

View file

@ -76,9 +76,9 @@
"SelectCommandType": 1,
"SelectFieldsDefaultValueDto": [],
"SeedFilePath": "data/App.Inventory.ProductCategory.json",
"SeedSyncInsert": true,
"SeedSyncUpdate": true,
"SeedSyncDelete": true,
"SeedSyncInsert": false,
"SeedSyncUpdate": false,
"SeedSyncDelete": false,
"DeleteCommand": "UPDATE \"Inv_T_ProductCategory\" SET \"DeleterId\"=@DeleterId, \"DeletionTime\"=CURRENT_TIMESTAMP, \"IsDeleted\"='true' WHERE \"Id\" IN @Id",
"DeleteBeforeCommand": null,
"DeleteAfterCommand": null,
@ -777,7 +777,7 @@
"GroupInterval": "",
"WordWrapEnabled": false
},
"Alignment": null,
"Alignment": "left",
"Format": null,
"EditOrderNo": null,
"EditGroupOrderNo": null,
@ -915,7 +915,7 @@
"GroupInterval": "",
"WordWrapEnabled": false
},
"Alignment": null,
"Alignment": "left",
"Format": null,
"EditOrderNo": 1,
"EditGroupOrderNo": 1,
@ -1041,7 +1041,7 @@
"GroupInterval": "",
"WordWrapEnabled": false
},
"Alignment": null,
"Alignment": "left",
"Format": null,
"EditOrderNo": 2,
"EditGroupOrderNo": 1,
@ -1179,7 +1179,7 @@
"GroupInterval": "",
"WordWrapEnabled": false
},
"Alignment": null,
"Alignment": "left",
"Format": null,
"EditOrderNo": 3,
"EditGroupOrderNo": 1,

View file

@ -68,12 +68,6 @@
"En": "Variant Values",
"Tr": "Varyant Değerleri"
},
{
"FieldName": "VariantKey",
"CaptionName": "App.Listform.ListformField.VariantKey",
"En": "Variant Key",
"Tr": "Varyant Anahtarı"
},
{
"FieldName": "SalesPrice",
"CaptionName": "App.Listform.ListformField.SalesPrice",
@ -789,7 +783,7 @@
"GroupInterval": "",
"WordWrapEnabled": false
},
"Alignment": null,
"Alignment": "left",
"Format": null,
"EditOrderNo": null,
"EditGroupOrderNo": null,
@ -915,7 +909,7 @@
"GroupInterval": "",
"WordWrapEnabled": false
},
"Alignment": null,
"Alignment": "left",
"Format": null,
"EditOrderNo": null,
"EditGroupOrderNo": null,
@ -1041,7 +1035,7 @@
"GroupInterval": "",
"WordWrapEnabled": false
},
"Alignment": null,
"Alignment": "left",
"Format": null,
"EditOrderNo": 1,
"EditGroupOrderNo": 1,
@ -1167,7 +1161,7 @@
"GroupInterval": "",
"WordWrapEnabled": false
},
"Alignment": null,
"Alignment": "left",
"Format": null,
"EditOrderNo": 2,
"EditGroupOrderNo": 1,
@ -1293,7 +1287,7 @@
"GroupInterval": "",
"WordWrapEnabled": false
},
"Alignment": null,
"Alignment": "left",
"Format": null,
"EditOrderNo": 3,
"EditGroupOrderNo": 1,
@ -1326,17 +1320,17 @@
"JoinFieldName2": null,
"BaseTableJoinFieldName2_OrValue": null
},
"FieldName": "VariantKey",
"CaptionName": "App.Listform.ListformField.VariantKey",
"FieldName": "SalesPrice",
"CaptionName": "App.Listform.ListformField.SalesPrice",
"PlaceHolder": null,
"ReadOnly": false,
"Visible": true,
"IsActive": true,
"Width": 0,
"ListOrderNo": 6,
"SourceDbType": 16,
"SourceDbType": 7,
"CultureName": "en",
"DataType": "string",
"DataType": "number",
"SortIndex": 0,
"SortDirection": null,
"AllowSearch": true,
@ -1419,13 +1413,13 @@
"GroupInterval": "",
"WordWrapEnabled": false
},
"Alignment": null,
"Alignment": "left",
"Format": null,
"EditOrderNo": null,
"EditGroupOrderNo": null,
"EditorType2": "dxTextBox",
"EditOrderNo": 4,
"EditGroupOrderNo": 1,
"EditorType2": "dxNumberBox",
"ColSpan": 1,
"EditorOptions": null,
"EditorOptions": "{\"format\":{\"type\":\"fixedPoint\",\"precision\":2}}",
"GridBoxOptions": null,
"TagBoxOptions": null,
"ImageUploadOptions": null,
@ -1452,8 +1446,8 @@
"JoinFieldName2": null,
"BaseTableJoinFieldName2_OrValue": null
},
"FieldName": "SalesPrice",
"CaptionName": "App.Listform.ListformField.SalesPrice",
"FieldName": "Cost",
"CaptionName": "App.Listform.ListformField.Cost",
"PlaceHolder": null,
"ReadOnly": false,
"Visible": true,
@ -1545,133 +1539,7 @@
"GroupInterval": "",
"WordWrapEnabled": false
},
"Alignment": null,
"Format": null,
"EditOrderNo": 4,
"EditGroupOrderNo": 1,
"EditorType2": "dxNumberBox",
"ColSpan": 1,
"EditorOptions": "{\"format\":{\"type\":\"fixedPoint\",\"precision\":2}}",
"GridBoxOptions": null,
"TagBoxOptions": null,
"ImageUploadOptions": null,
"EditorScript": null,
"LastModificationTime": null,
"LastModifierId": null,
"CreationTime": "0001-01-01T00:00:00",
"CreatorId": null,
"Id": "00000000-0000-0000-0000-000000000000"
},
{
"EditType": null,
"ListFormCode": "App.Inventory.ProductVariant",
"RoleId": null,
"UserId": null,
"ColumnJoinTableDto": {
"JoinType": null,
"TableName": null,
"JoinFieldName": null,
"BaseTableJoinFieldName": null,
"SelectFieldName": null,
"FieldNameAlias": null,
"JoinCondition2": "AND",
"JoinFieldName2": null,
"BaseTableJoinFieldName2_OrValue": null
},
"FieldName": "Cost",
"CaptionName": "App.Listform.ListformField.Cost",
"PlaceHolder": null,
"ReadOnly": false,
"Visible": true,
"IsActive": true,
"Width": 0,
"ListOrderNo": 8,
"SourceDbType": 7,
"CultureName": "en",
"DataType": "number",
"SortIndex": 0,
"SortDirection": null,
"AllowSearch": true,
"AllowEditing": true,
"AllowAdding": true,
"ColumnCssClass": null,
"ColumnCssValue": null,
"BandName": null,
"ColumnFilterDto": {
"AllowFiltering": true,
"FilterOperations": null,
"SelectedFilterOperation": null,
"FilterValue": null
},
"ColumnHeaderDto": {
"AllowHeaderFiltering": true,
"AllowSearch": false,
"DataSource": null,
"DataSourceString": ""
},
"ColumnGroupingDto": {
"AllowGrouping": true,
"GroupIndex": null,
"AutoExpandGroup": false
},
"ColumnCustomizationDto": {
"Fixed": false,
"FixedPosition": "left",
"AllowReordering": true
},
"ColumnTotalSummaryDto": {
"SummaryType": null,
"ValueFormat": null,
"ShowInGroupFooter": false,
"AlignByColumn": false,
"DisplayFormat": null,
"ShowInColumn": null
},
"ColumnGroupSummaryDto": {
"SummaryType": null,
"ValueFormat": null,
"ShowInGroupFooter": false,
"AlignByColumn": false,
"DisplayFormat": null,
"ShowInColumn": null
},
"LookupDto": {
"DataSourceType": 0,
"ValueExpr": "Key",
"DisplayExpr": "Name",
"CascadeRelationField": null,
"CascadeFilterOperator": "=",
"CascadeParentFields": null,
"CascadeEmptyFields": null,
"LookupQuery": null
},
"ValidationRuleDto": [],
"ColumnStylingDto": [],
"PermissionDto": {
"Deny": false,
"C": "App.Inventory.ProductVariant.Create",
"R": "App.Inventory.ProductVariant",
"U": "App.Inventory.ProductVariant.Update",
"E": true,
"I": true
},
"CanRead": false,
"CanCreate": false,
"CanUpdate": false,
"CanExport": false,
"DefaultValue": null,
"DefaultValueType": 0,
"PivotSettingsDto": {
"IsPivot": true,
"Expanded": false,
"SortOrder": "asc",
"Area": "filter",
"Format": null,
"SummaryType": null,
"GroupInterval": "",
"WordWrapEnabled": false
},
"Alignment": null,
"Alignment": "left",
"Format": null,
"EditOrderNo": 5,
"EditGroupOrderNo": 1,
@ -1711,7 +1579,7 @@
"Visible": true,
"IsActive": true,
"Width": 0,
"ListOrderNo": 9,
"ListOrderNo": 8,
"SourceDbType": 16,
"CultureName": "en",
"DataType": "string",
@ -1797,7 +1665,7 @@
"GroupInterval": "",
"WordWrapEnabled": false
},
"Alignment": null,
"Alignment": "left",
"Format": null,
"EditOrderNo": 6,
"EditGroupOrderNo": 1,
@ -1837,7 +1705,7 @@
"Visible": true,
"IsActive": true,
"Width": 0,
"ListOrderNo": 10,
"ListOrderNo": 9,
"SourceDbType": 9,
"CultureName": "en",
"DataType": "string",
@ -1923,7 +1791,7 @@
"GroupInterval": "",
"WordWrapEnabled": false
},
"Alignment": null,
"Alignment": "left",
"Format": null,
"EditOrderNo": 7,
"EditGroupOrderNo": 1,
@ -1963,6 +1831,132 @@
"Visible": true,
"IsActive": true,
"Width": 0,
"ListOrderNo": 10,
"SourceDbType": 3,
"CultureName": "en",
"DataType": "boolean",
"SortIndex": 0,
"SortDirection": null,
"AllowSearch": true,
"AllowEditing": true,
"AllowAdding": true,
"ColumnCssClass": null,
"ColumnCssValue": null,
"BandName": null,
"ColumnFilterDto": {
"AllowFiltering": true,
"FilterOperations": null,
"SelectedFilterOperation": null,
"FilterValue": null
},
"ColumnHeaderDto": {
"AllowHeaderFiltering": true,
"AllowSearch": false,
"DataSource": null,
"DataSourceString": ""
},
"ColumnGroupingDto": {
"AllowGrouping": true,
"GroupIndex": null,
"AutoExpandGroup": false
},
"ColumnCustomizationDto": {
"Fixed": false,
"FixedPosition": "left",
"AllowReordering": true
},
"ColumnTotalSummaryDto": {
"SummaryType": null,
"ValueFormat": null,
"ShowInGroupFooter": false,
"AlignByColumn": false,
"DisplayFormat": null,
"ShowInColumn": null
},
"ColumnGroupSummaryDto": {
"SummaryType": null,
"ValueFormat": null,
"ShowInGroupFooter": false,
"AlignByColumn": false,
"DisplayFormat": null,
"ShowInColumn": null
},
"LookupDto": {
"DataSourceType": 0,
"ValueExpr": "Key",
"DisplayExpr": "Name",
"CascadeRelationField": null,
"CascadeFilterOperator": "=",
"CascadeParentFields": null,
"CascadeEmptyFields": null,
"LookupQuery": null
},
"ValidationRuleDto": [],
"ColumnStylingDto": [],
"PermissionDto": {
"Deny": false,
"C": "App.Inventory.ProductVariant.Create",
"R": "App.Inventory.ProductVariant",
"U": "App.Inventory.ProductVariant.Update",
"E": true,
"I": true
},
"CanRead": false,
"CanCreate": false,
"CanUpdate": false,
"CanExport": false,
"DefaultValue": null,
"DefaultValueType": 0,
"PivotSettingsDto": {
"IsPivot": true,
"Expanded": false,
"SortOrder": "asc",
"Area": "filter",
"Format": null,
"SummaryType": null,
"GroupInterval": "",
"WordWrapEnabled": false
},
"Alignment": "left",
"Format": null,
"EditOrderNo": 8,
"EditGroupOrderNo": 1,
"EditorType2": "dxCheckBox",
"ColSpan": 1,
"EditorOptions": null,
"GridBoxOptions": null,
"TagBoxOptions": null,
"ImageUploadOptions": null,
"EditorScript": null,
"LastModificationTime": null,
"LastModifierId": null,
"CreationTime": "0001-01-01T00:00:00",
"CreatorId": null,
"Id": "00000000-0000-0000-0000-000000000000"
},
{
"EditType": null,
"ListFormCode": "App.Inventory.ProductVariant",
"RoleId": null,
"UserId": null,
"ColumnJoinTableDto": {
"JoinType": null,
"TableName": null,
"JoinFieldName": null,
"BaseTableJoinFieldName": null,
"SelectFieldName": null,
"FieldNameAlias": null,
"JoinCondition2": "AND",
"JoinFieldName2": null,
"BaseTableJoinFieldName2_OrValue": null
},
"FieldName": "IsDeleted",
"CaptionName": "App.Listform.ListformField.IsDeleted",
"PlaceHolder": null,
"ReadOnly": false,
"Visible": true,
"IsActive": true,
"Width": 0,
"ListOrderNo": 11,
"SourceDbType": 3,
"CultureName": "en",
@ -2049,133 +2043,7 @@
"GroupInterval": "",
"WordWrapEnabled": false
},
"Alignment": null,
"Format": null,
"EditOrderNo": 8,
"EditGroupOrderNo": 1,
"EditorType2": "dxCheckBox",
"ColSpan": 1,
"EditorOptions": null,
"GridBoxOptions": null,
"TagBoxOptions": null,
"ImageUploadOptions": null,
"EditorScript": null,
"LastModificationTime": null,
"LastModifierId": null,
"CreationTime": "0001-01-01T00:00:00",
"CreatorId": null,
"Id": "00000000-0000-0000-0000-000000000000"
},
{
"EditType": null,
"ListFormCode": "App.Inventory.ProductVariant",
"RoleId": null,
"UserId": null,
"ColumnJoinTableDto": {
"JoinType": null,
"TableName": null,
"JoinFieldName": null,
"BaseTableJoinFieldName": null,
"SelectFieldName": null,
"FieldNameAlias": null,
"JoinCondition2": "AND",
"JoinFieldName2": null,
"BaseTableJoinFieldName2_OrValue": null
},
"FieldName": "IsDeleted",
"CaptionName": "App.Listform.ListformField.IsDeleted",
"PlaceHolder": null,
"ReadOnly": false,
"Visible": true,
"IsActive": true,
"Width": 0,
"ListOrderNo": 12,
"SourceDbType": 3,
"CultureName": "en",
"DataType": "boolean",
"SortIndex": 0,
"SortDirection": null,
"AllowSearch": true,
"AllowEditing": true,
"AllowAdding": true,
"ColumnCssClass": null,
"ColumnCssValue": null,
"BandName": null,
"ColumnFilterDto": {
"AllowFiltering": true,
"FilterOperations": null,
"SelectedFilterOperation": null,
"FilterValue": null
},
"ColumnHeaderDto": {
"AllowHeaderFiltering": true,
"AllowSearch": false,
"DataSource": null,
"DataSourceString": ""
},
"ColumnGroupingDto": {
"AllowGrouping": true,
"GroupIndex": null,
"AutoExpandGroup": false
},
"ColumnCustomizationDto": {
"Fixed": false,
"FixedPosition": "left",
"AllowReordering": true
},
"ColumnTotalSummaryDto": {
"SummaryType": null,
"ValueFormat": null,
"ShowInGroupFooter": false,
"AlignByColumn": false,
"DisplayFormat": null,
"ShowInColumn": null
},
"ColumnGroupSummaryDto": {
"SummaryType": null,
"ValueFormat": null,
"ShowInGroupFooter": false,
"AlignByColumn": false,
"DisplayFormat": null,
"ShowInColumn": null
},
"LookupDto": {
"DataSourceType": 0,
"ValueExpr": "Key",
"DisplayExpr": "Name",
"CascadeRelationField": null,
"CascadeFilterOperator": "=",
"CascadeParentFields": null,
"CascadeEmptyFields": null,
"LookupQuery": null
},
"ValidationRuleDto": [],
"ColumnStylingDto": [],
"PermissionDto": {
"Deny": false,
"C": "App.Inventory.ProductVariant.Create",
"R": "App.Inventory.ProductVariant",
"U": "App.Inventory.ProductVariant.Update",
"E": true,
"I": true
},
"CanRead": false,
"CanCreate": false,
"CanUpdate": false,
"CanExport": false,
"DefaultValue": null,
"DefaultValueType": 0,
"PivotSettingsDto": {
"IsPivot": true,
"Expanded": false,
"SortOrder": "asc",
"Area": "filter",
"Format": null,
"SummaryType": null,
"GroupInterval": "",
"WordWrapEnabled": false
},
"Alignment": null,
"Alignment": "left",
"Format": null,
"EditOrderNo": null,
"EditGroupOrderNo": null,

View file

@ -94,9 +94,9 @@
"SelectCommandType": 1,
"SelectFieldsDefaultValueDto": [],
"SeedFilePath": "data/App.Inventory.WorkCenter.json",
"SeedSyncInsert": true,
"SeedSyncUpdate": true,
"SeedSyncDelete": true,
"SeedSyncInsert": false,
"SeedSyncUpdate": false,
"SeedSyncDelete": false,
"DeleteCommand": "UPDATE \"Inv_T_WorkCenter\" SET \"DeleterId\"=@DeleterId, \"DeletionTime\"=CURRENT_TIMESTAMP, \"IsDeleted\"='true' WHERE \"Id\" IN @Id",
"DeleteBeforeCommand": null,
"DeleteAfterCommand": null,

View file

@ -76,9 +76,9 @@
"SelectCommandType": 1,
"SelectFieldsDefaultValueDto": [],
"SeedFilePath": "data/App.Inventory.WorkCenterTag.json",
"SeedSyncInsert": true,
"SeedSyncUpdate": true,
"SeedSyncDelete": true,
"SeedSyncInsert": false,
"SeedSyncUpdate": false,
"SeedSyncDelete": false,
"DeleteCommand": "UPDATE \"Inv_T_WorkCenterTag\" SET \"DeleterId\"=@DeleterId, \"DeletionTime\"=CURRENT_TIMESTAMP, \"IsDeleted\"='true' WHERE \"Id\" IN @Id",
"DeleteBeforeCommand": null,
"DeleteAfterCommand": null,
@ -777,7 +777,7 @@
"GroupInterval": "",
"WordWrapEnabled": false
},
"Alignment": null,
"Alignment": "left",
"Format": null,
"EditOrderNo": null,
"EditGroupOrderNo": null,
@ -915,7 +915,7 @@
"GroupInterval": "",
"WordWrapEnabled": false
},
"Alignment": null,
"Alignment": "left",
"Format": null,
"EditOrderNo": 1,
"EditGroupOrderNo": 1,
@ -1041,7 +1041,7 @@
"GroupInterval": "",
"WordWrapEnabled": false
},
"Alignment": null,
"Alignment": "left",
"Format": null,
"EditOrderNo": 2,
"EditGroupOrderNo": 1,
@ -1179,7 +1179,7 @@
"GroupInterval": "",
"WordWrapEnabled": false
},
"Alignment": null,
"Alignment": "left",
"Format": null,
"EditOrderNo": 3,
"EditGroupOrderNo": 1,

View file

@ -79,8 +79,12 @@ function ScriptBuilderDialog({
const cancelText = cancelLabel ?? translate('::App.Platform.Cancel')
const [rules, setRules] = useState<ScriptRule[]>([])
const [code, setCode] = useState('')
const [syntaxError, setSyntaxError] = useState('')
// Sihirbaz bir butona basılarak açılır; varsayılan her zaman kod editörüdür.
const [view, setView] = useState<'code' | 'wizard'>('code')
// Önceki kaydetme denemesinin hatası, metin değişince ya da diyalog yeniden açılınca geçersizdir.
useEffect(() => setSyntaxError(''), [code, view, isOpen])
const [openRules, setOpenRules] = useState<Record<string, boolean>>({})
const [paletteOpen, setPaletteOpen] = useState(true)
const [helpOpen, setHelpOpen] = useState(true)
@ -102,6 +106,13 @@ function ScriptBuilderDialog({
const generatedScript = useMemo(() => dialect.buildScript(rules), [dialect, rules])
const completeRuleCount = rules.filter((rule) => dialect.isRuleComplete(rule)).length
const codeDiverged = code.trim() !== '' && code.trim() !== generatedScript.trim()
// Kural başlığı taşıyan ama elle değiştirilmiş kod: başlık artık gövdeyi anlatmaz. Kalırsa
// dialog ve sunucu (`EditorScript.Rebuild`) gövdeyi başlıktan yeniden üretip elle yapılan
// düzeltmeyi geri alır.
const staleHeader = useMemo(() => {
const parsed = dialect.parseScript(code)
return parsed.hasHeader && !parsed.inSync
}, [dialect, code])
useEffect(() => {
if (!isOpen) return
@ -284,9 +295,7 @@ function ScriptBuilderDialog({
menuPortalTarget={document.body}
options={options.map((option) => ({ value: option, label: option }))}
value={
options.includes(selectedValue)
? { value: selectedValue, label: selectedValue }
: null
options.includes(selectedValue) ? { value: selectedValue, label: selectedValue } : null
}
onChange={(option) => onChange(option?.value ?? '')}
/>
@ -341,6 +350,10 @@ function ScriptBuilderDialog({
}
const currentValue = rule.params[param.key] ?? ''
const ruleChoices =
param.type === 'text' ? dialect.getParamChoices?.(rule, param.key) : undefined
const choices = ruleChoices ?? param.choices
const inputType = ruleChoices ? 'select' : param.type
return (
<label key={param.key} className="min-w-0">
@ -359,24 +372,22 @@ function ScriptBuilderDialog({
(next) => updateParam(rule.id, param.key, next),
param.placeholder || translate('::App.ScriptBuilder.Choose'),
)}
{param.type === 'select' && (
{inputType === 'select' && (
<Select
size="sm"
className="w-full"
menuPortalTarget={document.body}
options={(param.choices ?? []).map((choice) => ({
options={(choices ?? []).map((choice) => ({
value: choice.value,
label: choice.label,
}))}
value={(param.choices ?? [])
value={(choices ?? [])
.filter((choice) => choice.value === currentValue)
.map((choice) => ({ value: choice.value, label: choice.label }))}
onChange={(option) =>
option && updateParam(rule.id, param.key, option.value)
}
onChange={(option) => option && updateParam(rule.id, param.key, option.value)}
/>
)}
{(param.type === 'text' || param.type === 'number') && (
{(inputType === 'text' || inputType === 'number') && (
<Input
unstyle
className={controlClass}
@ -635,9 +646,7 @@ function ScriptBuilderDialog({
value={triggerOptions.filter(
(option) => option.value === (trigger ?? dialect.triggers?.[0]?.value),
)}
onChange={(option) =>
option && updateRule(rule.id, { trigger: option.value })
}
onChange={(option) => option && updateRule(rule.id, { trigger: option.value })}
/>
</label>
)}
@ -836,6 +845,13 @@ function ScriptBuilderDialog({
</div>
)}
{view === 'code' && staleHeader && (
<div className="flex items-center gap-2 rounded-md border border-amber-200 bg-amber-50 px-3 py-2 text-xs text-amber-700 dark:border-amber-900 dark:bg-amber-950 dark:text-amber-200">
<FaExclamationTriangle className="shrink-0" />
<span className="flex-1">{translate('::App.ScriptBuilder.StaleHeader')}</span>
</div>
)}
{view === 'code' ? (
<div className="flex min-h-0 flex-1 gap-3">
<div className="flex min-h-0 min-w-0 flex-1 flex-col gap-2">
@ -966,6 +982,11 @@ function ScriptBuilderDialog({
</Dialog.Body>
<Dialog.Footer className="mt-1 flex justify-end gap-2 border-t border-gray-200 pt-3 dark:border-gray-700">
{syntaxError && (
<p className="mr-auto self-center text-xs text-red-600">
{translate('::App.ScriptBuilder.SyntaxError', { message: syntaxError })}
</p>
)}
<Button size="sm" type="button" variant="plain" icon={<FaTimes />} onClick={onClose}>
{cancelText}
</Button>
@ -975,7 +996,18 @@ function ScriptBuilderDialog({
variant="solid"
icon={<FaCheck />}
onClick={() => {
onApply(view === 'wizard' ? generatedScript : code)
const script =
view === 'wizard'
? generatedScript
: staleHeader
? code.trim().split('\n').slice(1).join('\n').trim()
: code
// Runtime script'i derleyemezse tamamı (diğer kurallar dahil) sessizce çalışmaz;
// bozuk metin kaydedilmeden önce yakalanır.
const error = findSyntaxError(script)
setSyntaxError(error)
if (error) return
onApply(script)
onClose()
}}
>
@ -986,4 +1018,18 @@ function ScriptBuilderDialog({
)
}
const AsyncFunction = Object.getPrototypeOf(async function noop() {}).constructor as new (
...args: string[]
) => unknown
/** Script runtime'ın derleyebileceği bir gövde mi; değilse derleyicinin mesajı. */
const findSyntaxError = (script: string) => {
try {
new AsyncFunction(script)
return ''
} catch (error) {
return error instanceof SyntaxError ? error.message : ''
}
}
export default ScriptBuilderDialog

View file

@ -151,6 +151,11 @@ export interface ScriptDialect {
getTrigger?: (rule: ScriptRule) => string
/** Kural kartında gösterilecek uyarı; örneğin eksik bağlam. */
getRuleWarning?: (rule: ScriptRule) => string
/**
* Serbest metin parametresinin kurala göre sabit seçenekleri (mantıksal alana yazılan değer için
* true/false). Seçenek dönerse dialog metin kutusu yerine seçici gösterir.
*/
getParamChoices?: (rule: ScriptRule, paramKey: string) => ScriptRecipeParam['choices']
}
/** Kuralın bütün koşulları; ilki her zaman `condition`. */

View file

@ -77,6 +77,8 @@ export interface ListFormWizardDto {
* Wizard adımlarında sorulmaz (MCP ile demo veri taşıyan ekranlar açar), düzenlemede korunur.
*/
seedData: boolean
/** Ekrandan yapılan kayıtlar veri dosyasına yansısın mı; varsayılan kapalı, düzenlemede korunur. */
seedSync: boolean
defaultLayout: ListViewLayoutType
grid: boolean

View file

@ -13,6 +13,9 @@
* yeniden derlenir.
*/
import { createElement } from 'react'
import Notification from '@/components/ui/Notification'
import toast from '@/components/ui/toast'
import apiService from '@/services/api.service'
import { getLocalization } from '@/services/localization.service'
import { store } from '@/store'
@ -69,9 +72,36 @@ const HELPER_PARAMS = [
* ------------------------------------------------------------------ */
/** Script runtime React ağacının dışındadır; dil metni store'dan okunur. */
const translateRuntime = (key: string) => {
const translateRuntime = (key: string, params?: Record<string, string | number>) => {
const { texts, config } = store.getState().abpConfig
return getLocalization(texts ?? {}, config?.localization?.defaultResourceName, key)
return getLocalization(texts ?? {}, config?.localization?.defaultResourceName, key, params)
}
/** Form her yeniden çizildiğinde aynı script tekrar çalışır; aynı hata kısa sürede bir kez gösterilir. */
const REPEATED_ERROR_WINDOW_MS = 5000
const lastReportedErrors = new Map<string, number>()
/**
* Script hatasını kullanıcıya gösterir. Konsola düşen hata kullanıcıya görünmez; bozuk bir kural
* sessizce çalışmadığında ekran "bozuk" görünür ama nedeni bilinmez.
*/
export const reportEditorScriptError = (field: string, error: unknown) => {
const message = error instanceof Error ? error.message : String(error)
console.error('Editor script error:', field, error)
const key = `${field}|${message}`
const now = Date.now()
if (now - (lastReportedErrors.get(key) ?? 0) < REPEATED_ERROR_WINDOW_MS) return
lastReportedErrors.set(key, now)
toast.push(
createElement(
Notification,
{ type: 'danger', duration: 6000 },
translateRuntime('::App.EditorScript.Error', { field, message }),
),
{ placement: 'top-end' },
)
}
/** Alan adını büyük/küçük harf duyarsız arar; SQL kolonları tutarsız gelebiliyor. */
@ -98,8 +128,7 @@ const toDate = (input: any): Date | null => {
return Number.isNaN(parsed.getTime()) ? null : parsed
}
const startOfDayUtc = (date: Date) =>
Date.UTC(date.getFullYear(), date.getMonth(), date.getDate())
const startOfDayUtc = (date: Date) => Date.UTC(date.getFullYear(), date.getMonth(), date.getDate())
/* ------------------------------------------------------------------ *
* Yardımcı API üretimi
@ -156,9 +185,15 @@ const buildHelpers = ({
queueMicrotask(flush)
}
const writeOne = (name: string, next: any) => {
const writeOne = (name: string, input: any) => {
if (!name) return
const key = resolveKey(data, name) ?? name
// Mantıksal alana şablondan gelen 'false'/'0' metni dolu sayılırdı; mevcut değer
// mantıksalsa yazılan değer de mantıksala çevrilir.
const next =
typeof data[key] === 'boolean' && /^(true|false|1|0)$/i.test(String(input))
? /^(true|1)$/i.test(String(input))
: input
// Değer değişmediyse yazmıyoruz. Form açılışında (contentReady) çalışan
// script'ler setFormData'yı tetikleyip yeniden contentReady doğurabildiği
// için bu koruma olmadan sonsuz döngü oluşur.
@ -191,8 +226,7 @@ const buildHelpers = ({
const clear = (...names: string[]) => set(Object.fromEntries(names.map((name) => [name, null])))
const readOnly = (name: string, flag: boolean = true) =>
runtimeSetEditorReadOnly?.(name, !!flag)
const readOnly = (name: string, flag: boolean = true) => runtimeSetEditorReadOnly?.(name, !!flag)
/** Lookup/GridBox ile seçilen kaydın bir kolonunu form alanına taşır. */
const copy = (sourcePath: string, targetName?: string) => {
@ -230,13 +264,14 @@ const buildHelpers = ({
const tpl = (text: string) =>
String(text ?? '')
.replace(/\{value\}/g, () => String(e?.value ?? ''))
.replace(/\{selected\.([^}]+)\}/g, (_, key: string) => String(pickPath(selectedItem, key) ?? ''))
.replace(/\{selected\.([^}]+)\}/g, (_, key: string) =>
String(pickPath(selectedItem, key) ?? ''),
)
.replace(/\{([^}]+)\}/g, (_, key: string) => String(get(key) ?? ''))
/** Geçerli alanı değişiklik öncesindeki değerine döndürür. */
const revert = () => {
const previous =
e?.previousValue ?? pickPath(formData, field) ?? null
const previous = e?.previousValue ?? pickPath(formData, field) ?? null
writeOne(field, previous)
const activeEditor = e?.component?.getEditor?.(field)
if (activeEditor?.option) activeEditor.option('value', previous)
@ -405,7 +440,15 @@ const compileScript = (script: string): CompiledScript => {
export const executeEditorScript = (script: string, context: EditorScriptRuntimeContext) => {
const { formData, e, editor, runtimeSetEditorReadOnly, setFormData } = context
const compiled = compileScript(script)
const scriptField = String(editor?.dataField ?? e?.dataField ?? '')
let compiled: CompiledScript
try {
compiled = compileScript(script)
} catch (error) {
reportEditorScriptError(scriptField, error)
return Promise.resolve()
}
const { args, flush } = buildHelpers(context)
const baseArgs = [formData, e, editor, runtimeSetEditorReadOnly, setFormData]
@ -415,8 +458,6 @@ export const executeEditorScript = (script: string, context: EditorScriptRuntime
flush()
return result
.catch((error: unknown) => {
console.error('Editor script error:', error)
})
.catch((error: unknown) => reportEditorScriptError(scriptField, error))
.finally(flush)
}

View file

@ -6,7 +6,7 @@ import { Field, FieldProps, Form, Formik } from 'formik'
import { bool, object, string } from 'yup'
import { columnEditorTypeListOptions, sortDirectionOptions } from '../options'
import { FormFieldEditProps } from './FormFields'
import { EditingFormDto, SortDirectionEnum } from '@/proxy/form/models'
import { DbTypeEnum, EditingFormDto, SortDirectionEnum } from '@/proxy/form/models'
import { FaCode } from 'react-icons/fa'
import { useState } from 'react'
import EditorScriptBuilderDialog from '../json-row-operations/EditorScriptBuilderDialog'
@ -52,6 +52,16 @@ function FormFieldTabOptions({
value: field.fieldName,
label: field.captionName || field.fieldName,
}))
const booleanFields = fields
.filter(
(field) =>
field.fieldName &&
(field.sourceDbType === DbTypeEnum.Boolean ||
field.dataType === 'boolean' ||
field.editorType2 === 'dxCheckBox' ||
field.editorType2 === 'dxSwitch'),
)
.map((field) => field.fieldName!)
return (
<Formik
@ -189,6 +199,7 @@ function FormFieldTabOptions({
value={values.editorScript}
currentField={values.fieldName}
fields={editorScriptFields}
booleanFields={booleanFields}
onClose={() => setIsEditorScriptDialogOpen(false)}
onApply={(val) => setFieldValue('editorScript', val)}
/>

View file

@ -9,6 +9,7 @@ type EditorScriptBuilderDialogProps = {
value?: string
currentField?: string
fields: SelectBoxOption[]
booleanFields?: string[]
onClose: () => void
onApply: (value: string) => void
}
@ -22,6 +23,7 @@ function EditorScriptBuilderDialog({
value,
currentField,
fields,
booleanFields,
onClose,
onApply,
}: EditorScriptBuilderDialogProps) {
@ -30,9 +32,16 @@ function EditorScriptBuilderDialog({
// Çağıran her render'da yeni bir dizi verebilir; lehçe alan adlarına göre
// sabitlenmezse editör yazarken sıfırlanırdı.
const fieldKey = fields.map((field) => String(field.value || field.label || '')).join('|')
const booleanKey = (booleanFields ?? []).join('|')
const dialect = useMemo(
() => createFormScriptDialect({ currentField, fields: fieldKey.split('|'), translate }),
[currentField, fieldKey, translate],
() =>
createFormScriptDialect({
currentField,
fields: fieldKey.split('|'),
booleanFields: booleanKey.split('|'),
translate,
}),
[currentField, fieldKey, booleanKey, translate],
)
return (

View file

@ -137,6 +137,8 @@ const snippets: ScriptSnippet[] = [
export type FormScriptDialectContext = {
/** Form kolonları. */
fields: string[]
/** Mantıksal (onay kutusu / anahtar) alanlar; değer yazan kurallarda true/false seçtirilir. */
booleanFields?: string[]
/** Script'in bağlı olduğu alan; başlıkta gösterilir. */
currentField?: string
/** `useLocalization` hook'undan gelen çeviri fonksiyonu. */
@ -150,6 +152,11 @@ export const createFormScriptDialect = (context: FormScriptDialectContext): Scri
)
const asFormRule = (rule: ScriptRule) => rule as unknown as FormScriptRule
const booleanFields = new Set((context.booleanFields ?? []).map((field) => field.toLowerCase()))
const booleanChoices = [
{ value: 'true', label: t('::App.Platform.Yes') },
{ value: 'false', label: t('::App.Platform.No') },
]
// Tarif listesi hem "Kural ekle" seçicisinde hem de kural kartında kullanılıyor.
// Tek bir çevrilmiş liste üretip ikisini de buradan besliyoruz; aksi halde kart
@ -241,11 +248,16 @@ export const createFormScriptDialect = (context: FormScriptDialectContext): Scri
// Kosulsuz kuralda "Her zaman ->" oneki bilgi tasimiyor, yalnizca gurultu.
if (!conditions.length) return action
const joiner =
rule.join === 'or'
? ` ${t('::App.Platform.Or')} `
: ` ${t('::App.Platform.And')} `
rule.join === 'or' ? ` ${t('::App.Platform.Or')} ` : ` ${t('::App.Platform.And')} `
return `${conditions.join(joiner)} → ${action}`
},
getTrigger: (rule) => effectiveTrigger(asFormRule(rule)),
// Onay kutusuna metin ('0', 'Hayır') yazmak dolu sayılırdı; değer mantıksal sabit olarak seçilir.
getParamChoices: (rule, paramKey) =>
rule.recipe === 'setValue' &&
paramKey === 'text' &&
booleanFields.has(String(rule.params.target ?? '').toLowerCase())
? booleanChoices
: undefined,
}
}

View file

@ -157,6 +157,15 @@ export type TranslateFn = (key: string, params?: Record<string, string | number>
const q = (input: string) => JSON.stringify(input ?? '')
/**
* `true`/`false` mantıksal sabit olarak yazılır; metin (`tpl('false')`) onay kutusunu dolu sayardı.
* Diğer değerler şablondur. `EditorScriptRule.ValueLiteral` (C#) ile aynı.
*/
const valueLiteral = (text: string) => {
const trimmed = String(text ?? '').trim()
return /^(true|false)$/i.test(trimmed) ? trimmed.toLowerCase() : `tpl(${q(text)})`
}
const label = (input: string, fallback: string) => input || fallback
const isNumericLiteral = (input: string) => /^-?\d+(\.\d+)?$/.test(String(input).trim())
@ -508,7 +517,7 @@ export const recipes: Recipe[] = [
help: 'App.ScriptBuilderText.Help',
},
],
build: ({ params }) => `set(${q(params.target)}, tpl(${q(params.text)}))`,
build: ({ params }) => `set(${q(params.target)}, ${valueLiteral(params.text)})`,
describe: ({ params }, t) =>
t('::App.ScriptBuilderDescribe.SetValue', {
target: label(params.target, t('::App.Listform.ListformField.Target')),

View file

@ -73,6 +73,7 @@ const initialValues: ListFormWizardDto = {
confirmDelete: true,
allowDetail: false,
seedData: false,
seedSync: false,
defaultLayout: 'grid',
grid: true,
card: true,
@ -715,6 +716,7 @@ const Wizard = () => {
confirmDelete: w.confirmDelete ?? true,
allowDetail: w.allowDetail ?? false,
seedData: w.seedData ?? false,
seedSync: w.seedSync ?? false,
defaultLayout: w.defaultLayout ?? 'grid',
grid: w.grid ?? initialValues.grid,
card: w.card ?? initialValues.card,

View file

@ -3,7 +3,7 @@
* Grid, Tree ve form sayfası aynı kuralları paylaşır.
*/
import type { ColumnFormatDto, GridDto } from '@/proxy/form/models'
import { getActiveEditingForm } from './editingForm'
import { getActiveEditingForm, getEditingRow } from './editingForm'
type CascadeInfo = {
parentFields: string[]
@ -99,16 +99,6 @@ export const hasCascadeBehavior = (maps: CascadeMaps, fieldName: string) =>
maps.parentToChildrenMap.has(fieldName) ||
maps.clearFieldsMap.has(fieldName)
const getEditingRow = (grid: any) => {
const rowKey = grid.option('editing.editRowKey')
return {
rowIndex: grid.getRowIndexByKey(rowKey) as number,
rowData: grid.getVisibleRows().find((row: any) => row.key === rowKey)?.data as
| Record<string, any>
| undefined,
}
}
/** Parent değiştikten sonra etkilenen çocuk editörlerin kilidini yeniler. */
const updateChildDisabledStates = (
grid: any,

View file

@ -222,11 +222,25 @@ export const setFormEditorReadOnly = (form: any, field: string, readOnly: boolea
return true
}
/** DataGrid/TreeList popup düzenleme formu; düzenleme denetleyicisinde yaşar. */
export const getActiveEditingForm = (component: any) => {
const editForm = component?.getView?.('editingView')?._editForm
const editForm = component?.getController?.('editing')?._editForm
return editForm?.getEditor || editForm?.itemOption ? editForm : undefined
}
/**
* Düzenlenen satırın güncel verisi (`cellValue` ile yazılanlar dahil). Yeni satırın anahtarı
* nesnedir; `row.key === rowKey` eşleşmez, satır grid'in değerle eşleyen indeksinden okunur.
*/
export const getEditingRow = (grid: any) => {
const rowKey = grid?.option?.('editing.editRowKey')
const rowIndex: number =
rowKey !== undefined && rowKey !== null ? (grid?.getRowIndexByKey?.(rowKey) ?? -1) : -1
const rowData: Record<string, any> | undefined =
rowIndex >= 0 ? grid?.getVisibleRows?.()?.[rowIndex]?.data : undefined
return { rowKey, rowIndex, rowData }
}
/**
* Form ilk açıldığında da çalışması gereken script'leri ayıklar.
*
@ -242,11 +256,11 @@ export const getActiveEditingForm = (component: any) => {
export const shouldRunEditorScriptOnContentReady = (script?: string) =>
Boolean(
script &&
(script.includes('@runOnOpen') ||
/\breadOnly\s*\(/.test(script) ||
/\bisReady\b/.test(script) ||
script.includes('setEditorReadOnly') ||
script.includes('runtimeSetEditorReadOnly')),
(script.includes('@runOnOpen') ||
/\breadOnly\s*\(/.test(script) ||
/\bisReady\b/.test(script) ||
script.includes('setEditorReadOnly') ||
script.includes('runtimeSetEditorReadOnly')),
)
/* ------------------------------------------------------------------ *
@ -537,7 +551,11 @@ export const createFormItemMapper = ({
defaultEditorOptions.placeholder = translate('::' + colFormat.placeHolder)
}
if (typeof colFormat.defaultValue === 'string' && colFormat.defaultValue === '@AUTONUMBER' && mode === 'new') {
if (
typeof colFormat.defaultValue === 'string' &&
colFormat.defaultValue === '@AUTONUMBER' &&
mode === 'new'
) {
defaultEditorOptions.value = autoNumber()
}

View file

@ -5,7 +5,7 @@
* birebir tekrar ediyordu; buradan tek noktadan sağlanır.
*/
import type { MutableRefObject } from 'react'
import { executeEditorScript } from '@/utils/editorScriptRuntime'
import { executeEditorScript, reportEditorScriptError } from '@/utils/editorScriptRuntime'
import { attachFieldValueBroadcast } from '@/utils/phoneFormat'
import type { GridDto } from '@/proxy/form/models'
import type { RowMode } from '../../form/types'
@ -19,6 +19,7 @@ import {
} from './cascade'
import {
getActiveEditingForm,
getEditingRow,
getValueByField,
resolveEditorButtons,
setFormEditorReadOnly,
@ -34,14 +35,6 @@ type EditingFormHandlerParams = {
editingFormInstanceRef: MutableRefObject<any>
}
const getEditingRowIndex = (grid: any) => {
const rowKey = grid?.option?.('editing.editRowKey')
return {
rowKey,
rowIndex: rowKey !== undefined ? (grid?.getRowIndexByKey?.(rowKey) ?? -1) : -1,
}
}
/**
* Grid hücrelerine script tarafından yazılan değerleri uygular.
* CardView gibi `cellValue` API'si olmayan bileşenlerde sessizce atlanır;
@ -52,6 +45,99 @@ const createGridCellWriter = (grid: any, rowIndex: number) => (newData: Record<s
Object.keys(newData).forEach((field) => grid.cellValue(rowIndex, field, newData[field]))
}
/**
* Zincirde bir alanın aynı değerle ikinci kez tetiklenmesi döngüdür (A → B → A). Derinlik sınırı
* yoktur; değeri her turda değişen döngüyü (sayaç gibi) bu emniyet sınırı yakalar.
*/
const MAX_CHAIN_STEPS = 1000
const chainStep = (field: string, value: unknown) => {
try {
return `${field.toLowerCase()}=${JSON.stringify(value ?? null)}`
} catch {
return `${field.toLowerCase()}=${String(value)}`
}
}
type ChainedScriptParams = {
gridDto: GridDto | undefined
grid: any
form: any
before: Record<string, any>
after: Record<string, any>
sourceField?: string
/** Zincirin bu ana kadar çalıştırdığı alan=değer adımları; ilk çağrıda verilmez. */
visited?: Set<string>
}
/**
* Script'in `set` ile yazdığı alanların kendi script'lerini çalıştırır; onların yazdıkları da aynı
* şekilde devam eder. Grid'e `cellValue` ile yazılan değer editörün değişiklik olayını tetiklemez;
* zincir olmadan "TrackInventory false olunca TrackingMethod salt okunur" gibi bağımlı kurallar
* kullanıcı alanı elle değiştirene kadar çalışmazdı. Form sayfası (`updateData`) olayı zaten
* tetiklediği için yalnızca Grid/Tree'de gerekir.
*/
const runChainedScripts = ({
gridDto,
grid,
form,
before,
after,
sourceField,
visited = new Set(sourceField ? [chainStep(sourceField, after[sourceField])] : []),
}: ChainedScriptParams) => {
if (!gridDto) return
const source = String(sourceField ?? '').toLowerCase()
const fields = getEditingFormFields(gridDto)
const changed = Object.keys(after).filter(
(key) => key.toLowerCase() !== source && !Object.is(before?.[key], after[key]),
)
changed.forEach((key) => {
const format = fields.find(
(field) => String(field.fieldName ?? '').toLowerCase() === key.toLowerCase(),
)
if (!format?.editorScript || !format.fieldName) return
const step = chainStep(format.fieldName, after[key])
if (visited.has(step) || visited.size >= MAX_CHAIN_STEPS) {
reportEditorScriptError(
format.fieldName,
new Error(`${[...visited].map((item) => item.split('=')[0]).join(' → ')} → ${key}`),
)
return
}
visited.add(step)
const writeToGrid = createGridCellWriter(grid, getEditingRow(grid).rowIndex)
executeEditorScript(format.editorScript, {
formData: { ...after },
e: {
component: form,
dataField: format.fieldName,
value: after[key],
scriptEvent: 'valueChanged',
},
editor: { dataField: format.fieldName, component: grid },
runtimeSetEditorReadOnly: (field: string, flag: boolean) =>
setFormEditorReadOnly(form, field, flag),
setFormData: (next: Record<string, any>) => {
writeToGrid(next)
runChainedScripts({
gridDto,
grid,
form,
before: after,
after: next,
sourceField: format.fieldName,
visited,
})
},
})
})
}
export const createEditingFormHandlers = ({
getGridDto,
getGridInstance,
@ -89,9 +175,13 @@ export const createEditingFormHandlers = ({
return values
}, {})
// Popup formu her `cellValue` yazımında yeniden çizilir ve bu olay yeniden gelir; formun
// `formData`'sı açılıştaki nesnedir, güncel değerler grid'in düzenlenen satırındadır. Eski
// değerle çalışan script kendi verdiği readOnly kararını geri alırdı.
const formData = {
...editingFormDataRef.current,
...(form?.option?.('formData') || {}),
...(getEditingRow(grid).rowData || {}),
...editorValues,
}
editingFormDataRef.current = { ...formData }
@ -99,7 +189,7 @@ export const createEditingFormHandlers = ({
const runtimeSetEditorReadOnly = (field: string, readOnly: boolean) =>
setFormEditorReadOnly(form, field, readOnly)
const { rowIndex } = getEditingRowIndex(grid)
const { rowIndex } = getEditingRow(grid)
const writeToGrid = createGridCellWriter(grid, rowIndex)
/**
@ -152,7 +242,7 @@ export const createEditingFormHandlers = ({
setFormData: applyFormData,
})
} catch (err) {
console.error('Script exec error on contentReady', field.fieldName, err)
reportEditorScriptError(field.fieldName!, err)
}
})
} finally {
@ -177,7 +267,7 @@ export const createEditingFormHandlers = ({
try {
const grid = getGridInstance()
const { rowIndex } = getEditingRowIndex(grid)
const { rowIndex } = getEditingRow(grid)
const formData = {
...(e.component?.option?.('formData') || {}),
[e.dataField]: e.value,
@ -196,10 +286,18 @@ export const createEditingFormHandlers = ({
e.component?.option?.('formData', newData)
editingFormDataRef.current = { ...newData }
writeToGrid(newData)
runChainedScripts({
gridDto,
grid,
form: e.component,
before: formData,
after: newData,
sourceField: e.dataField,
})
},
})
} catch (err) {
console.error('Script exec error', formField.fieldName, err)
reportEditorScriptError(formField.fieldName!, err)
}
}
@ -208,6 +306,7 @@ export const createEditingFormHandlers = ({
type EditorValueChangedScriptParams = {
editor: any
gridDto: GridDto
editorScript: string
fieldName?: string
editingFormInstanceRef: MutableRefObject<any>
@ -218,6 +317,7 @@ type EditorValueChangedScriptParams = {
*/
const attachEditorValueChangedScript = ({
editor,
gridDto,
editorScript,
fieldName,
editingFormInstanceRef,
@ -230,26 +330,31 @@ const attachEditorValueChangedScript = ({
try {
const grid = editor.component
const { rowKey, rowIndex } = getEditingRowIndex(grid)
const formData = {
...(grid.getVisibleRows().find((row: any) => row.key === rowKey)?.data || {}),
[editorDataField]: e.value,
}
const { rowIndex, rowData } = getEditingRow(grid)
const formData = { ...(rowData || {}), [editorDataField]: e.value }
const form = editingFormInstanceRef.current ?? getActiveEditingForm(grid)
const writeToGrid = createGridCellWriter(grid, rowIndex)
executeEditorScript(editorScript, {
formData,
e: { ...e, scriptEvent: 'valueChanged' },
editor,
runtimeSetEditorReadOnly: (field: string, readOnly: boolean) =>
setFormEditorReadOnly(
editingFormInstanceRef.current ?? getActiveEditingForm(grid),
field,
readOnly,
),
setFormData: createGridCellWriter(grid, rowIndex),
setFormEditorReadOnly(form, field, readOnly),
setFormData: (newData: Record<string, any>) => {
writeToGrid(newData)
runChainedScripts({
gridDto,
grid,
form,
before: formData,
after: newData,
sourceField: editorDataField,
})
},
})
} catch (err) {
console.error('Script exec error', fieldName, err)
reportEditorScriptError(fieldName ?? '', err)
}
}
}
@ -263,9 +368,8 @@ const attachEditorButtonContext = (editor: any, mode: RowMode) => {
editor.editorOptions.buttons = withButtonContext(editor.editorOptions.buttons, () => {
const grid = editor.component
const { rowKey, rowIndex } = getEditingRowIndex(grid)
const formData = grid.getVisibleRows().find((row: any) => row.key === rowKey)?.data || {}
return { formData, fieldName: editor.dataField, mode, rowKey, rowIndex }
const { rowKey, rowIndex, rowData } = getEditingRow(grid)
return { formData: rowData || {}, fieldName: editor.dataField, mode, rowKey, rowIndex }
})
}
@ -333,6 +437,7 @@ export const prepareEditingEditor = ({
if (columnFormat?.editOrderNo != null && columnFormat.editorScript) {
attachEditorValueChangedScript({
editor,
gridDto,
editorScript: columnFormat.editorScript,
fieldName: columnFormat.fieldName,
editingFormInstanceRef,
@ -395,19 +500,31 @@ export const createCustomEditorCellAdapter =
if (columnFormat?.editOrderNo == null || !columnFormat.editorScript) return
const { rowIndex } = getEditingRowIndex(grid)
const { rowIndex } = getEditingRow(grid)
const activeForm = editingFormInstanceRef.current ?? getActiveEditingForm(grid)
const formData = { ...rowData, [fieldName]: value }
const writeToGrid = createGridCellWriter(grid, rowIndex)
try {
executeEditorScript(columnFormat.editorScript, {
formData: { ...rowData, [fieldName]: value },
formData,
e: { component: activeForm, dataField: fieldName, value, scriptEvent: 'valueChanged' },
editor: { dataField: fieldName, component: grid },
runtimeSetEditorReadOnly: (field: string, flag: boolean) =>
setFormEditorReadOnly(activeForm, field, flag),
setFormData: createGridCellWriter(grid, rowIndex),
setFormData: (newData: Record<string, any>) => {
writeToGrid(newData)
runChainedScripts({
gridDto,
grid,
form: activeForm,
before: formData,
after: newData,
sourceField: fieldName,
})
},
})
} catch (err) {
console.error('Script exec error', fieldName, err)
reportEditorScriptError(fieldName ?? '', err)
}
}