diff --git a/.github/instructions/ai.instructions.md b/.github/instructions/ai.instructions.md index ab1a1e52..2cc1d46f 100644 --- a/.github/instructions/ai.instructions.md +++ b/.github/instructions/ai.instructions.md @@ -53,26 +53,28 @@ System nature: 3. Every proposal must include tenant and permission design. 4. Never bypass platform authorization patterns. 5. Never hardcode secrets, tenant ids, or connection strings. -6. **Stay inside the framework; ask before stepping out.** The platform may extend itself through - its own mechanisms (seed artifacts; adding a missing event, prop, or small backward-compatible - capability to an existing UI component — when Developer Kit tooling builds ~70% of the - requested component, the rest is completed with such small additions, never with a new - component; backward-compatible additions to script recipes / editor options / tokens). - Anything beyond that — a new React component or page, a new toolbox family, core engine changes (ListForm - runtime, seeders, code generator, auth, DataSourceManager, dispatcher/compiler), new ABP - entity/module/migration, platform table schema changes, new NuGet/npm dependencies, the - `App.Menus.Routes.json` seed data (except the `Seo*` columns of an existing row, which are part - of ordinary production — lowcode §0.9), new external integrations — is **out of frame**: finish everything that - fits inside the frame first, then ask the user with a single question (why configuration is - insufficient, the smallest change that works, files touched, rollback) and proceed **only with - explicit approval**. Approval is per request and does not carry over. Full lists: +6. **No new Core feature without the command.** Core = all source code under `api/` and `ui/`; + the frame = artifacts generated under `configs/seeds/` (wizard, SQL objects, `crud/`, custom + components, Custom Endpoints, Dynamic Services, workers, notifications, language/permission + rows, SEO, module docs). A new Core feature — however small or backward compatible: a DTO + field, a prop/event on an existing UI component, a script recipe, an editor option key, a + default-value token, a new React component/page, engine/seeder/MCP changes, new entity/ + migration, platform table schema, NuGet/npm packages, `App.Menus.Routes.json` rows (except the + `Seo*` columns of an existing row — lowcode §0.9), external integrations — is written **only + when the user gives the command "core sisteme ekle"** (or an equivalent that names the Core + change). Before concluding Core is needed, scan the full ListForm capability inventory + (`lowcode-reference.instructions.md` §0–§9, lowcode §4–§9). Without the command: finish what + fits in the frame and end with a single "Core gerektiren kısım" note (missing capability, why + the inventory falls short, smallest change, files, rollback) — do not offer Core as an + option to pick. Bug fixes that restore documented behaviour without new settings are not new + features. The command covers only the named change and does not carry over. Full lists: `lowcode.instructions.md` §0.8. Exception policy: -- Code-level React or backend development can be considered only if user explicitly requests code implementation and configuration path is insufficient. -- If exception is required, AI must explain why configuration-based options are not enough. -- Never say "cannot be done": the answer is either an in-frame build or an approval question. +- Code-level React or backend development happens only on the user's explicit command ("core sisteme ekle" or a request naming the code change). +- When Core would be needed, explain in a sentence or two why the configuration inventory is not enough. +- Never say "cannot be done": the answer is an in-frame build plus, if needed, a "Core gerektiren kısım" note. --- @@ -461,8 +463,8 @@ When configuration cannot satisfy requirement: 1. Try SQL-based design 2. Try Custom Endpoint 3. Try Dynamic Service -4. Propose minimal code change with explicit justification — this step is out of frame: - **ask and wait for approval before touching code** (rule 3.6). +4. Describe the minimal code change with explicit justification — this step is Core: + **do not touch code until the user says "core sisteme ekle"** (rule 3.6). --- diff --git a/.github/instructions/lowcode-reference.instructions.md b/.github/instructions/lowcode-reference.instructions.md index 1289638a..18dc406e 100644 --- a/.github/instructions/lowcode-reference.instructions.md +++ b/.github/instructions/lowcode-reference.instructions.md @@ -29,8 +29,9 @@ sorusunda buraya bak. `ColumnFormatDto` (alan) `*Json` kolonlarının ham metnini `[JsonIgnore]` ile gizler ve istemciye yalnızca tipli DTO'ya (`SchedulerOptionJson` → `SchedulerOptionDto`) çözülmüş hâlini yollar. Dolayısıyla: - - DTO'da olmayan bir DevExtreme seçeneğini `*Json`'a yazmak **işe yaramaz**; kalıcı olarak - gerekiyorsa önce ilgili DTO'ya alan eklenir (ve bu dosya güncellenir). + - DTO'da olmayan bir DevExtreme seçeneğini `*Json`'a yazmak **işe yaramaz**; DTO'ya alan + eklemek Core'dur ve yalnızca "core sisteme ekle" komutuyla yapılır (`lowcode` §0.8); eklenince + bu dosya da güncellenir. - Çözme `JsonSerializer`'ın varsayılan ayarlarıyla yapılır: anahtarlar **PascalCase ve büyük/küçük harfe duyarlıdır**. `"visible"` yazılan yerde `"Visible"` beklenir; küçük harfli anahtar hata vermez, sessizce düşer. İstemcinin gördüğü camelCase hâl API çıktısıdır, seed'e @@ -64,7 +65,7 @@ ama hiçbirini görmeden bırakma; kullanıcı "şunu da yapabilir miydik?" dedi | 10 | **Özet** | Toplam/adet satırı, grup özetleri, pivot | §9.5 · §6 | | 11 | **Biçimlendirme** | Koşullu satır/hücre rengi, CSS sınıfı | `lowcode` §4.3.7 | | 12 | **Düzenleme deneyimi** | Popup/satır/hücre/batch, form grupları, kopyalama, toplu silme | §3 | -| 13 | **Aksiyonlar** | Toolbar ve satır butonları: adres, diyalog, script, yetki, koşullu görünürlük | `lowcode` §4.9 | +| 13 | **Aksiyonlar** | Hazır toolbar/satır butonları, URL parametreleri, not/ek/geçmiş, içe-dışa aktarma; özel butonlar: adres, diyalog, script, yetki, koşullu görünürlük | §5.8 · `lowcode` §4.9 | | 14 | **Yazma mantığı** | Özel SQL, before/after kancaları, farklı servis adresi | §4 | | 15 | **Ana-detay** | Alt form sekmeleri ve ilişki alanları | §5.2 | | 16 | **Gösterge** | Üstte KPI kartları, grafik | §5.3 · §8 | @@ -105,10 +106,10 @@ ve çoğu isteğin cevabı aslında oradadır: | `SelectCommand` | — | Tablo/view adı, fonksiyon çağrısı ya da sorgu metni | | `TableName` | `SelectCommand` | Üretilen `SELECT`'te kolonların ve join'lerin önüne yazılan **alias**. Boşsa `SelectCommand` kullanılır. `4` Query'de **zorunludur**: sorgu `FROM (…) AS "{TableName}"` biçiminde türetilmiş tablo olarak sarılır ve boş bırakılırsa sorgu metni alias olur (SQL hatası). Wizard/MCP `deploy_wizard` Query ekranına `"Q"` (`WizardConsts.QuerySourceAlias`) yazar; fonksiyon çağrısında da kısa bir ad yaz | | `KeyFieldName` · `KeyFieldDbSourceType` | — · `16` String | Anahtar kolon ve `System.Data.DbType` karşılığı. `4` Query'de ikisi de **zorunludur** (kolonlar katalogdan okunamaz); wizard/`deploy_wizard` anahtarsız Query ekranını reddeder, tip boşsa (`0`) seçilen alanlardaki karşılığından tamamlar. Ana tablo (en dıştaki `FROM`) `IsDeleted` taşıyorsa `DefaultFilter` yazılır ve sorgu `IsDeleted`'i seçmek zorundadır | -| `SelectFieldsDefaultValueJson` | — | **Okuma** sorgusuna parametre olarak katılan değerler (`FieldsDefaultValueDto[]`) — `@USERID`, `@TENANTID` gibi | +| `SelectFieldsDefaultValueJson` | — | ListForm editöründe (Veritabanı › Select) düzenlenir ama runtime **okumaz** (`DefaultValueManager` Select'te `FormFieldsDefaultValueJson`'ı kullanır). Okuma koşulu `DefaultFilter`'a token'la yazılır (`lowcode` §4.3.5) | | `DefaultFilter` | — | Her sorgunun sonuna `AND` ile eklenen sabit `WHERE` koşulu. `DefaultValueHelper` üzerinden geçer, yani `@USERID`/`@TENANTID`/`@NOW` gibi token'lar çözülür. Soft delete'li tabloda (`IsDeletedField: true`) wizard buraya `"IsDeleted" = 'false'` yazar | | `SortMode` | — | **Metin**: `"single"` \| `"multiple"` \| `"none"` (`PlatformConsts.GridOptions.SortMode*`; wizard `single` yazar) | -| `PageSize` | — | Sayfa başına kayıt; `0` = tümü | +| `PageSize` | `50` (DB) | Sayfa başına kayıt; `0` = tümü. C# varsayılanı `0`, veritabanı varsayılanı `50`; boşsa grid `20` kullanır | | `Name` · `Title` · `Description` | — | Ekran adı, başlığı, açıklaması — **dil anahtarı** yazılır | | `IsTenant` · `IsBranch` · `IsOrganizationUnit` | `IsMultiTenant` · `false` · `false` | Otomatik kırılım filtresi; `IsTenant` ayrıca bağlantı çözümünü belirler (`lowcode` §9.3.3) | | `SeedFilePath` · `SeedSyncInsert` · `SeedSyncUpdate` · `SeedSyncDelete` | — · `false` | Ekranın verisini `data/` altındaki dosyaya aynalar; `.json` uzantılı değer tek dosya, uzantısız değer klasör (`lowcode` §9.15) | @@ -136,10 +137,11 @@ ve çoğu isteğin cevabı aslında oradadır: | `PivotOptionJson` | Pivot görünümü | §6 | | `TreeOptionJson` · `GanttOptionJson` · `SchedulerOptionJson` · `TodoOptionJson` | Ağaç/Gantt/Takvim/Kanban eşlemeleri | §7 | | `SeriesJson`, `CommonJson` ve diğer 19 grafik kolonu | Chart görünümü (21 kolon; `DataSourceJson` okunmaz) | §8 | +| `ExportJson` | Grafik dışında **Grid/Pivot dışa aktarmayı da** açar: `Enabled` + `AllowExportSelectedData`; Grid yalnız `pdf`/`xlsx`, Pivot yalnız `xlsx`. Grid düğmesi ayrıca `PermissionJson.E` ister | §8.4 · §5.8 | | `CustomJsSourcesJson` · `CustomStyleSourcesJson` | Sayfaya enjekte edilen JS/CSS | §5.5 | | `Width` · `Height` · `FullHeight` | Ekran boyutu. `Height > 0` sabit piksel yüksekliği verir. Grid/Tree/Gantt/Scheduler'da `FullHeight` bayrağı görünümü `calc(100vh - …)` ile sayfaya yayar; **TodoBoard** bayrağa bakmaz, `Height` verilmediyse panoyu her zaman sayfanın kalan yüksekliğine yayar (kolonlar kendi içinde kayar), alt form olarak açıldığında ise içeriği kadar yer kaplar. | — | | `ListFormType` · `IsSubForm` · `SubFormsListFormType` · `ShowNote` | Ekranın türü ve alt form davranışı | §5.6 | -| `CultureName` · `UserId` · `RoleId` | Aynı ekranın dil/kullanıcı/rol varyantı | §5.7 | +| `CultureName` · `UserId` · `RoleId` | Ekranın dil varyantı (`UserId`/`RoleId` okunmaz — alan seviyesinde çalışır) | §5.7 | --- @@ -182,18 +184,18 @@ Adı yalnızca sayfalamayı çağrıştırsa da bu blok **kaydırma ve yükleniy | --- | --- | --- | | `Visible` | `true` | Sayfalama şeridi | | `ShowPageSizeSelector` | `false` | Sayfa boyu seçici | -| `ShowInfo` | `false` | "Page 1 of 4 (37 items)" bilgisi | +| `ShowInfo` | `false` | "Page 1 of 4 (37 items)" bilgisi — **Grid okumaz** (sabit kapalı); Tree (boşsa `true`) ve Card okur | | `ShowNavigationButtons` | `true` | İleri/geri düğmeleri | | `AllowedPageSizes` | `"10,20,50,100"` | **Virgüllü metin**, dizi değil | -| `InfoText` | `"Page {0} of {1} ({2} items)"` | `{0}` sayfa · `{1}` toplam sayfa · `{2}` kayıt | +| `InfoText` | `"Page {0} of {1} ({2} items)"` | `{0}` sayfa · `{1}` toplam sayfa · `{2}` kayıt — yalnız Tree ve Card | | `DisplayMode` | `"adaptive"` | `adaptive` \| `compact` \| `full` | | `ScrollingMode` | `"standard"` | `standard` \| `virtual` \| `infinite` — uzun listelerde `virtual` | -| `LoadPanelEnabled` | `"auto"` | **Metin** alanıdır: `"auto"` \| `"true"` \| `"false"` | +| `LoadPanelEnabled` | `"auto"` | **Metin** alanıdır: `"auto"` \| `"true"` \| `"false"`. İstemci metni boolean'a çevirir (`resolveLoadPanelEnabled`); Grid, Tree, Pivot ve Card aynı kuralı kullanır | | `LoadPanelText` | `"Loading..."` | Yükleniyor metni | Sayfa başına kayıt sayısı ekranın `PageSize` alanındadır; `0` = tümü. `ScrollingMode` ile `PageSize` birlikte düşünülür: `virtual`/`infinite` seçilip `PageSize: 0` yazılırsa sayfalama -devre dışı kalır ve tüm küme tek istekte çekilir. +devre dışı kalır ve tüm küme tek istekte çekilir. `ScrollingMode` ve `LoadPanel*` Pivot'ta da okunur. ### 2.4 `SelectionJson` (`SelectionDto`) @@ -205,19 +207,25 @@ devre dışı kalır ve tüm küme tek istekte çekilir. | `ShowCheckBoxesMode` | `"always"` | `always` \| `none` \| `onClick` \| `onLongTap` | Toolbar butonları seçili satırlarla çalışır (`lowcode` §4.9); onay akışı varsa wizard `single` yazar. +Tree yalnız `Mode`'u okur (`recursive` `TreeOptionJson.RecursiveSelection`'dan gelir); `AllowSelectAll`, +`SelectAllMode`, `ShowCheckBoxesMode` Grid ve Card'da çalışır, `AllowSelectAll` Pivot'un başlık filtresinde +"tümünü seç"i de açar. `SubFormsListFormType: "List"` ekranında alt form sekmeleri seçili satırla +beslendiği için `Mode` `single`/`multiple` olmalıdır (§5.6). ### 2.5 `StateStoringJson` (`StateStoringDto`) | Anahtar | Varsayılan | Anlam | | --- | --- | --- | -| `Enabled` | `true` | Kullanıcının kolon/filtre/sıralama tercihleri saklansın mı | -| `Type` | `"custom"` | `custom` (sunucuda kullanıcı bazlı) \| `localStorage` \| `sessionStorage` | +| `Enabled` | `true` | Toolbar'daki **Grid durumunu kaydet / sıfırla** menüsünü açar. Kayıt **elle** yapılır (Grid/Tree/Pivot'ta otomatik kayıt kapalı); kayıtlı durum `Enabled`'dan bağımsız her açılışta yüklenir. Pivot'un kaydet/sıfırla düğmeleri her zaman görünür | +| `Type` | `"custom"` | **Okunmaz** — durum her zaman sunucuya (`ListFormCustomization`) yazılır; editör de yalnız `custom` sunar | | `StorageKey` | — | Anahtar; boşsa sunucu `listFormGridStorage-{ListFormCode}` üretir | -| `SavingTimeout` | `5000` | Değişiklikten kaç ms sonra yazılacağı — **`[Range(1500, 5000)]`**, dışına çıkan değer doğrulamada reddedilir | +| `SavingTimeout` | `5000` | Yalnız **TodoBoard**'da otomatik kayıt bekleme süresi — **`[Range(1500, 5000)]`**, dışına çıkan değer doğrulamada reddedilir | -`Type: "custom"` tercih edilir: tercih sunucuda `ListFormCustomization` kaydı olarak -(`ListFormCustomizationTypeEnum.GridState = 2`) kullanıcı bazlı saklanır, bu yüzden başka -tarayıcıdan girildiğinde de korunur. `localStorage`/`sessionStorage` yalnızca o tarayıcıdadır. +Durum sunucuda `ListFormCustomization` kaydı olarak (`ListFormCustomizationTypeEnum.GridState = 2`) +saklanır, başka tarayıcıdan girildiğinde de korunur. Kayıt **kullanıcı > rol > genel** önceliğiyle +çözülür: `UserId`/`RoleId` boş `GridState` herkese, `RoleId` dolu olan o role varsayılan düzen verir — +`UserUiFilter` (paylaşılan kayıtlı filtre) için de aynısı geçerlidir. Seçili görünüm (grid/card/…) ve +Card'ın kart başına kolon sayısı ise sunucuda değil **tarayıcının localStorage**'ındadır. Aynı `GridState` kaydı görünüm başına ayrı `FilterName` taşır: `list-`, `tree-`, `pivot-`, `todo-`, `gantt-` ve form sayfası için `form-{StorageKey}` (`useListFormStateStoring`). Form sayfasının state'i kullanıcının **alan sırasıdır** (`{"fieldOrder": {"Alan": 1, …}}`, grup içi sıra): araç çubuğundaki @@ -243,15 +251,19 @@ ekranın alanlarından, toplam türü `sum avg min max count`'tan seçilir. "Tü **`FilterRowJson`** (`GridFilterRowDto`): `Visible` (`false`), `ApplyFilter` (`auto` \| `onClick`), `AllowUserUiFilterSave` (varsayılan `true` — kullanıcı kendi filtresini kaydedebilir; -kayıt `ListFormCustomizationTypeEnum.UserUiFilter = 1` olarak tutulur). +kayıt `ListFormCustomizationTypeEnum.UserUiFilter = 1` olarak tutulur). `AllowUserUiFilterSave` aynı zamanda +toolbar'daki **Rapor › Liste** düğmesini ve kayıtlı filtre seçim kutusunu açar; `ApplyFilter: onClick` +filtre satırına **Filtre uygula** düğmesi ekler. Filtre uygulanınca başlık paneli renkli kenarlık alır +(kayıtlı durum varsa farklı renk). **`HeaderFilterJson`** (`GridHeaderFilterDto`): `Visible` (`false`), `AllowSearch` (`false`), -`Height` (`325`), `Width` (`252`), `SearchTimeout` (`500`). +`Height` (`325`), `Width` (`252`), `SearchTimeout` (`500`). Grid, Tree ve Gantt yalnız `Visible`'ı okur +(arama kolon bazında `ColumnHeaderJson.AllowSearch`'tedir); diğer dört anahtar yalnız Card ve Pivot'ta çalışır. **`FilterPanelJson`** (`FilterPanelDto`): `Visible` (`false`), `FilterEnabled` (`false`), `Texts.ClearFilter` (`"Clear"`) / `Texts.CreateFilter` (`"Create Filter"`) / -`Texts.FilterEnabledHint` (`"Enable the filter"`), `CustomizeText` (serbest JSON; filtre -metnini biçimlendirmek için — sözlükte tipi yoktur, olduğu gibi bileşene geçer). +`Texts.FilterEnabledHint` (`"Enable the filter"`), `CustomizeText`. Grid ve Tree yalnız `Visible`'ı okur; +`Texts` yalnız Card'da çalışır; `FilterEnabled` ve `CustomizeText` **hiçbir görünümde okunmaz**. **`SearchPanelJson`** (`SearchPanelDto`): `Visible` (`false`), `Width` (`160`). @@ -268,17 +280,17 @@ metnini biçimlendirmek için — sözlükte tipi yoktur, olduğu gibi bileşene | --- | --- | --- | | `Mode` | `"popup"` | `batch` \| `cell` \| `row` \| `form` \| `popup` | | `RefreshMode` | `"full"` | `full` \| `reshape` \| `repaint` | -| `AllowAdding` / `AllowUpdating` / `AllowDeleting` | `false` | Aksiyon izinleri (yetki ayrıca `PermissionJson`) | -| `AllowAllDeleting` | `false` | Toplu silme düğmesi | -| `AllowDuplicate` | `false` | Kaydı kopyalayarak yeni kayıt | -| `AllowDetail` | `false` | Satırda Detay düğmesi | +| `AllowAdding` / `AllowUpdating` / `AllowDeleting` | `false` | Aksiyon izinleri (yetki ayrıca `PermissionJson`: Ekle `C`, Düzenle `U`, Sil `D`). `AllowDeleting` + `D` toolbar'a **Seçilileri sil** düğmesi de ekler (2+ satır seçiliyken görünür). `Mode: batch` + `AllowUpdating` + `U` toolbar'a Kaydet/Geri al getirir | +| `AllowAllDeleting` | `false` | **Tümünü sil (filtreli)** düğmesi: filtredeki bütün kayıtları, önce sayıyı gösterip onayla siler; `AllowDeleting` + `D` de gerekir | +| `AllowDuplicate` | `false` | Satırda **Kopyala**: onay sorar, `list-form-data/duplicate` (`InsertCommand` + kancalar + `InsertFieldsDefaultValueJson`). Düğme **`C`** (Create) yetkisi ister | +| `AllowDetail` | `false` | Satırda Detay düğmesi — `/admin/form/{kod}/{id}` yeni sekmede; **`U`** yetkisi ister (`R` değil) | | `OpenDetailAfterInsert` | `false` | Popup'tan eklenen kayıt kaydedilince aynı sekmede kaydın form sayfasına (`/admin/form/{ListFormCode}/{Id}`) geçilir. Grid, Tree, Card ve Scheduler'da çalışır; anahtar insert yanıtından alınır, dönmezse geçilmez. Wizard cevabında da aynı adla vardır (`ListFormWizardDto.OpenDetailAfterInsert`); ne zaman açılacağı `lowcode` §4.2 Davranış tablosunda | | `ConfirmDelete` | `true` | Silme onayı | | `UseIcons` | `false` | Aksiyonlar ikon olarak | | `NewRowPosition` | `"viewportTop"` | `first` \| `last` \| `pageTop` \| `pageBottom` \| `viewportTop` \| `viewportBottom` | | `StartEditAction` | `"click"` | `click` \| `dblClick` | | `SelectTextOnEditStart` | `false` | Düzenlemeye girince metni seç | -| `EditColumnName` | — | Aksiyon sütununun başlığı | +| `EditColumnName` | — | **Ölü ayar** — editörde var ama komut sütununa başlık verilmez | | `SendOnlyChangedFormValuesUpdate` | `false` | Update'te yalnızca değişen alanları gönder | | `AddPageUrl` | — | Yeni kayıt popup yerine bu adreste açılır (`window.open`; PWA'da aynı sekme) | | `AddDialogName` | — | Yeni kayıt kayıtlı bir diyalogda açılır (`CommandColumnDto.DialogName` ile aynı liste). `AddPageUrl` ile birlikte verilirse **diyalog önceliklidir**; diyalog kapanınca liste yenilenir | @@ -290,6 +302,12 @@ metnini biçimlendirmek için — sözlükte tipi yoktur, olduğu gibi bileşene `DragEnabled` (`true`), `RestorePosition` (`true`), `Position` (`center` \| `top` \| `bottom` \| `left` \| `right`). +- `Title` dil anahtarıdır (`::` eklenip çevrilir; önüne ✚/🖊️ konur). +- `Height` sabit yükseklik değil **azami yüksekliktir** (`maxHeight`; boşsa `90vh`), popup içeriğe göre büyür. +- Mobilde (`<768px`, `pointer:coarse` ya da `hover:none`) popup tam genişlik/tam ekran açılır, form tek + kolona iner; tam ekran aç/kapa düğmesi her zaman vardır. +- `DragEnabled`, `RestorePosition`, `Position` çalışır ama ListForm editöründe alanı yoktur — seed ile yazılır. + ### 3.2 `EditingFormJson` (`EditingFormDto[]`) Düzenleme formundaki grupların listesi. Alanların hangi gruba düştüğü `ListFormField` üzerindeki @@ -321,18 +339,48 @@ olanlar dışarıda kalır. **Kancalar, kod yazmadan iş mantığı çalıştırmanın yeridir**: stok düşme, hareket kaydı, durum güncelleme, log, bildirim kuyruğuna satır ekleme. Karmaşıklaşınca gövdeyi bir stored procedure'e -taşı ve kancadan onu çağır (`EXEC Wms_S_AfterOrderInsert @Id, @USERID`). +taşı ve kancadan onu çağır (`EXEC Wms_S_AfterOrderInsert @Id, @LastModifierId`). + +- Kancalar ana komutla **aynı parametre sözlüğünü** kullanır. +- Insert'in ana komutu `ExecuteScalar` ile çalışır (SQL Server `OUTPUT Inserted.{Key}`, PG `RETURNING`); + özel `InsertCommand` anahtarı döndürmezse istemci yeni kaydın anahtarını alamaz (detaya gitme, alt form). +- Otomatik Update'te istekte `TenantId` varsa `WHERE`'e `AND TenantId` eklenir; otomatik Delete'te + **bütün parametreler** `WHERE` koşuludur. +- **Kopyala** (`list-form-data/duplicate`) `InsertCommand` + Insert kancaları + `InsertFieldsDefaultValueJson` + ile çalışır. +- Yazmadan sonra ekranın seed senkronu çalışır (`SeedSync*`, `lowcode` §9.15). ### 4.2 Komut içinde kullanılabilen anahtarlar -| Token | Değer | -| --- | --- | -| `@ID` | Kaydın anahtarı (Delete'te seçili anahtar kümesi) | -| `@USERID` · `@USERNAME` · `@ROLES` | Oturumdaki kullanıcı | -| `@FieldName` | Ekrandan gelen o alanın değeri — tablodaki her kolon için geçerlidir (`@OrderNo`, `@Amount`) | +Yazma komutlarında ve kancalarında **metin değiştirme yoktur** (`DefaultValueHelper` çalışmaz); yalnızca +Dapper parametreleri bağlanır: -`*FieldsDefaultValueJson` ile üretilen değerler (`@NOW`, `@NEWID`, `@TENANTID`, Sequence…) -komuta parametre olarak katılır (`lowcode` §4.3.5). **String birleştirme ile sorgu kurma.** +| Parametre | Kaynağı | +| --- | --- | +| `@{FieldName}` | İstekte gelen alan — Insert/Update'te yalnızca alan seviyesinde `C`/`U` yetkisi olanlar (Delete'te yetki süzgeci yok) | +| `@{FieldName}` | `*FieldsDefaultValueJson` öğesi — adı token değil **alan adıdır**: `@CreatorId`, `@CreationTime` (`@USERID` değil) | +| `@TenantId` | `IsTenant` açıksa | +| `@{KeyFieldName}` | Yalnızca **otomatik üretilen** Update/Delete'te eklenir. Özel `UpdateCommand`/`DeleteCommand`'da anahtar parametresi gelmez; wizard bunu `DeleteFieldsDefaultValueJson`'a `{FieldName:"Id", Value:"@ID", CustomValueType:2}` yazarak çözer (`DefaultDeleteCommand` → `… WHERE "Id" IN @Id`) | + +Bu yüzden `@USERID`, `@USERNAME`, `@ROLES`, `@ID` komut metninde **çalışmaz** ("Must declare scalar +variable"); kullanıcıya/zamana bağlı değer gerekiyorsa önce ilgili `*FieldsDefaultValueJson`'a alan adıyla +yazılır (`{FieldName:"LastModifierId", Value:"@USERID", CustomValueType:2}`), komutta `@LastModifierId` +kullanılır (`lowcode` §4.3.5). **String birleştirme ile sorgu kurma.** + +**Metin değiştirmenin çalıştığı yerler** (`DefaultValueHelper.GetDefaultValue`; `@USERID`, `@USERNAME`, +`@ROLES`, `@DATE`, `@NOW`, `@DAY`, `@MONTH`, `@YEAR`, `@TENANTID`): `SelectCommand`, `DefaultFilter`, +kullanıcı `ServerWhere` özelleştirmesi, widget `SqlQuery`, `LookupQuery`, `DbQuery` varsayılanının +`SqlQuery`'si, Custom Endpoint parametre varsayılanları, rapor lookup'ları. Host bağlamında +`= '@TENANTID'` → `IS NULL`, yalın `@TENANTID` → `NULL` olur; `@ROLES` `','` ile birleşir +(`IN ('@ROLES')` kalıbı). `@ID`, `@NEWID`, `@SELECTED_IDS`, `@AUTONUMBER` metinde çözülmez; +`@BRANCHID`/`@LANG` diye token yoktur. Çalışmadığı yerler: yazma komutları ve kancaları, `ServerJoin` +özelleştirmesi, `ExtraFilterJson` (istemci filtresi olarak parametreleşir), grafik özet sorgusu. + +**Örtük filtreler** (token değil, sorguya kendiliğinden eklenir): `IsTenant` → `TenantId = '{id}'`/`IS NULL`; +`IsBranch` → `BranchId IN (kullanıcının şubeleri)` — kullanıcının şubesi yoksa hiç satır gelmez; +`IsOrganizationUnit` → `OrganizationUnitId IN (birim + alt birimler)`; `ApprovalIsFilterUserName` → +onaycı filtresi. Widget SQL'i bu filtrelerden **geçmez** (kiracı koşulunu kendisi yazar). Yazma tarafında +şube/birim ataması yoktur — `BranchId` varsayılanla ya da formdan gelir. ### 4.3 `*ServiceAddress` @@ -341,6 +389,17 @@ komuta parametre olarak katılır (`lowcode` §4.3.5). **String birleştirme ile Service'e devretmek istiyorsan bu alanı o ucun adresiyle değiştirirsin — ekranın geri kalanı (form, doğrulama, yetki) aynı kalır. +- Çağrı **her zaman POST**'tur ve adres `'/api/app/' + adres` olarak kurulur (baştaki `/` silinir); + `http…` ile başlayan mutlak adres de önek alır ve çalışmaz — dış servis bir Custom Endpoint / Dynamic + Service arkasına konur. +- Gövdeler: insert `{data, listFormCode}` · update `{keys:[key], data, listFormCode}` (yalnızca değişen + alanlar) · delete `{keys:[key], listFormCode}`. +- **Adres boşsa işlem sessizce yapılmaz**; TodoBoard ekle/güncelle/sil düğmeleri kapanır, + `UpdateServiceAddress` boşsa iş akışı toolbar'ı tamamen gizlenir. Salt okunur ekran için adresleri + boşaltmak yerine `EditingOptionJson`'daki izin bayraklarını kapat. +- Yetki ucun kendisinde (Create/Update/Delete) denetlenir; bağlantı `DataSourceManager.GetDataSourceAsync(IsTenant, + DataSourceCode)` ile çözülür (`DataSourceType` `1` Mssql · `2` Postgresql). + **Çok değerli alanlar bu iki yolda farklı davranır.** Varsayılan uçta payload bir sözlüktür (`data: {...}`) ve `dxTagBox` dizisini `QueryHelper.GetFormattedValue` `|` ile birleştirir. Tipli bir DTO alan uçta (`list-form-dynamic-api/...` gibi) böyle bir adım yoktur: dizi `string` @@ -373,8 +432,8 @@ Alan seviyesindeki karşılığı `ListFormField.PermissionJson`'dır ve **şema | --- | --- | --- | | `Deny` | bool | `true` ise alan **hiç dönmez**; diğer anahtarlara bakılmaz | | `C` · `R` · `U` | string | Yaratma / okuma / güncelleme yetkisinin **adı**. Boşsa ekranın kendi Create/Read/Update yetkisi kullanılır | -| `E` | bool | Alan dışa aktarımda yer alsın mı | -| `I` | bool | Alan içe aktarımda kabul edilsin mi | +| `E` | bool | Alan dışa aktarımda yer alsın mı — **yazılmazsa `false`**: `PermissionJson`'ı olmayan alan dışa aktarılmaz | +| `I` | bool | Yalnızca istemcide: içe aktarma şablonunun kolonlarını seçer; sunucu yok sayar | ```json { "C": "App.Mrp.Orders.Create", "R": "App.Mrp.Orders", "U": "App.Mrp.Orders.Update", @@ -385,7 +444,8 @@ Alan seviyesindeki karşılığı `ListFormField.PermissionJson`'dır ve **şema **veritabanında yoktur** (`[NotMapped]`); `ListFormFieldManager.SetPermissionsAsync` bu JSON'u oturumdaki yetkilerle karşılaştırarak istek başına hesaplar. Üçü de `false` çıkan alan metadata'dan tamamen düşer — yani okuma yetkisi olmayan bir kolon sorguya bile girmez. -Wizard varsayılan olarak `DefaultFieldPermissionJson(permissionName)` üretir. +`C`/`U` yetkisi olmayan alan sunucuda insert/update verisinden atılır ve formda **salt okunur** +görünür. Wizard varsayılan olarak `DefaultFieldPermissionJson(permissionName)` üretir. ### 5.2 `SubFormsJson` (`SubFormDto[]`) @@ -394,12 +454,18 @@ Wizard varsayılan olarak `DefaultFieldPermissionJson(permissionName)` üretir. | `TabType` | Sekmenin görünümü — `ListFormTabTypeEnum`: `List` \| `Card` \| `Tree` \| `Gantt` \| `Scheduler` \| `Todo` \| `Form` \| `Chart` \| `Pivot`. `Form` ana kayda bağlı **tek** kaydı gösterir: kayıt yokken "Yeni" düğmesi (hedef ekranda `AllowAdding` açıksa), kayıt varken yalnızca düzenleme; ikinci kayıt açılmaz | | `TabTitle` | Dil anahtarı | | `Code` | Hedef `ListFormCode` | -| `IsRefresh` | Ana kayıt değişince yenilensin. MCP `deploy_wizard` yeni alt form bağlantısını `false` yazar; var olan bağlantıda önceki değer korunur | -| `Relation[]` | `ParentFieldName` → `ChildFieldName` + `DbType` (`System.Data.DbType`, **sayı**; bileşik anahtarda birden çok satır) | +| `IsRefresh` | `true` ise alt sekmede satır eklenip/değiştirilip/silindiğinde **ana form kaydı** yeniden çekilir (ana kayıt değişince alt sekme zaten her zaman yenilenir) — alt satırlardan hesaplanan başlık toplamı için açılır. MCP `deploy_wizard` yeni alt form bağlantısını `false` yazar; var olan bağlantıda önceki değer korunur | +| `Relation[]` | `ParentFieldName` → `ChildFieldName` + `DbType` (`System.Data.DbType`, **sayı**). İstemci yalnızca değeri dolu **ilk** satırı kullanır — bileşik anahtar desteklenmez | Alt formun kendisi de bir ListForm'dur: `IsSubForm = true` olmalı ve genellikle `CreateMenu: -false` ile menüsüz üretilir (`lowcode` §4.2). `TabType`, hedef ekranın `LayoutJson`'ında açık -olan bir görünüm olmalıdır; kapalı bir görünüm sekmede boş çizilir. +false` ile menüsüz üretilir (`lowcode` §4.2). `TabType` hedefin `LayoutJson` bayrağına bakmaz (Grid/Card/ +Pivot bayrak kapalı olsa da çizilir); Tree/Gantt/Scheduler/Todo hedefte ilgili `*Expr` alanlarını ister. + +- **`Form` sekmesi** kaydı ilişkiyle (`ChildFieldName = ana alanın değeri`) yükler; düzenlemede anahtar + yüklenen kayıttan okunur. Yeni kayıtta ilişki alanı ana kaydın değeriyle dolu ve kilitli açılır. +- **`Chart` sekmesi** ana ekranın grafik tanımını kullanır ve ilişkiyle süzülmez. +- Form sayfasında alt formlar görüntüle/düzenle modunda çizilir, yeni kayıtta çizilmez. Adres + `#{AltFormKodu}` ile açılırsa o sekme seçilir; sekme değişince hash yazılır. ### 5.3 `WidgetsJson` (`WidgetEditDto[]`) @@ -408,8 +474,8 @@ Ekranın üstündeki KPI şeridi. **Sözleşmesi sezgisel değildir, dikkatle ok | Anahtar | Ne yazılır | | --- | --- | | `SqlQuery` | Kartları besleyen sorgu. **Zorunludur** — boş olan widget hiç çizilmez | -| `Title` · `SubTitle` · `Value` · `Color` · `Icon` · `OnClick` | **Sorgu sonucundaki kolon adları** — sabit metin değil. Sunucu her satırda bu adlı kolonu okur; kolon yoksa boş string döner. `Title`/`SubTitle` kolonunun **değeri** ekranda dil anahtarı olarak çözülür (`WidgetGroup`, `::` öneki eklenerek); karşılığı olmayan metin aynen basılır, bu yüzden sorguya dil anahtarı yazılır | -| `ColSpan` · `ColGap` · `ClassName` | Şeritteki yerleşim (widget başına, karta değil) | +| `Title` · `SubTitle` · `Value` · `Color` · `Icon` · `OnClick` | **Sorgu sonucundaki kolon adları** — sabit metin değil. `OnClick` kolonunun **değeri JS olarak `eval` edilir** (adres değil): `'window.open(''/admin/list/X'')' AS Tikla`. Sunucu her satırda bu adlı kolonu okur; kolon yoksa boş string döner. `Title`/`SubTitle` kolonunun **değeri** ekranda dil anahtarı olarak çözülür (`WidgetGroup`, `::` öneki eklenerek); karşılığı olmayan metin aynen basılır, bu yüzden sorguya dil anahtarı yazılır | +| `ColSpan` · `ColGap` · `ClassName` | Şeritteki yerleşim (widget başına, karta değil): `ColSpan` 12 kolonluk ızgarada kart genişliği (mobilde 6'ya zorlanır), `ColGap` Tailwind `gap-{ColGap}` eki (`3` önerilir; `16` = 4rem) | | `ValueClassName` | Değerin CSS sınıfı, varsayılan `text-3xl` | | `IsActive` | **Varsayılanı `false`'tur** — yazılmazsa sorgu hiç çalışmaz ve şerit boş kalır; her widget'a `true` yaz | @@ -447,7 +513,8 @@ Sorgu ekranın bağlantısında (`IsTenant` + `DataSourceCode` ile çözülen, ` - Kartlar GridDto ile gelir; Grid, Tree, Card, Gantt ve Scheduler görünümünde Yenile düğmesi ve satır ekleme/güncelleme/silme kartları yeniden çeker (`useListFormWidgets`). Yalnızca widget listesi değişir, GridDto değiştirilmez — görünüm durumu (sayfa, seçim) korunur. Kart sorgusu - ekranın filtresine bağlı olmadığı için filtre değişikliği kartları yenilemez. + ekranın filtresine bağlı olmadığı için filtre değişikliği kartları yenilemez. Pivot'ta şerit çizilir + ama yenilenmez. - Kart sorgusu alt form sekmesinde `Relation` ile süzülmez; bu yüzden alt form olarak üretilen ekrana widget yazılmaz (menüsüz ya da menülü). Menüsüz (`CreateMenu: false`) wizard'ın `Widgets`'ı deploy'da ayrıca düşürülür; ekran alt form olarak (`SubForms` sekmesi, `isSubForm`) açıldığında @@ -469,6 +536,12 @@ değişir: UI wizard başlangıç değerleri (`Wizard.tsx`), `ListFormWizardDto` (§7); bayrak açık olsa bile alanlar boşsa istemci görünümü göstermez (`List.tsx` `isLayoutValid`) ve seeder o görünümün JSON'unu yazmaz. `DefaultLayout`, `true` olan bir görünümü göstermelidir. +- **`Grid` bayrağı ölüdür**: Grid düğmesi her zaman çizilir, kapatılamaz. +- `Card` bayrağı yalnız düğmeyi gizler; `DefaultLayout: "card"` bayrak kapalıyken de açılır. +- Düğme koşulu `isLayoutValid`'den gevşektir: Tree düğmesi yalnız `ParentIdExpr`, Gantt düğmesi + `ParentIdExpr` + `TitleExpr` ister. Geçersiz `DefaultLayout` ya da kayıtlı görünüm `grid`'e düşer. +- Kullanıcının seçtiği görünüm sunucuda değil tarayıcının localStorage'ında (`admin.lists.states`) durur. + ### 5.5 `CustomJsSourcesJson` / `CustomStyleSourcesJson` Her ikisi de **string dizisidir**. Bir eleman `http` ya da `/` ile başlıyorsa dosya olarak @@ -479,6 +552,9 @@ sayfaya eklenir (`